luci-app-dockerman: fix EOL issue
[oweals/luci.git] / applications / luci-app-dockerman / luasrc / model / cbi / dockerman / newcontainer.lua
1 --[[
2 LuCI - Lua Configuration Interface
3 Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
4 ]]--
5
6 require "luci.util"
7 local uci = luci.model.uci.cursor()
8 local docker = require "luci.model.docker"
9 local dk = docker.new()
10 local cmd_line = table.concat(arg, '/')
11 local create_body = {}
12
13 local images = dk.images:list().body
14 local networks = dk.networks:list().body
15 local containers = dk.containers:list({query = {all=true}}).body
16
17 local is_quot_complete = function(str)
18   require "math"
19   if not str then return true end
20   local num = 0, w
21   for w in str:gmatch("\"") do
22     num = num + 1
23   end
24   if math.fmod(num, 2) ~= 0 then return false end
25   num = 0
26   for w in str:gmatch("\'") do
27     num = num + 1
28   end
29   if math.fmod(num, 2) ~= 0 then return false end
30   return true
31 end
32
33 local resolve_cli = function(cmd_line)
34   local config = {advance = 1}
35   local key_no_val = '|t|d|i|tty|rm|read_only|interactive|init|help|detach|privileged|P|publish_all|'
36   local key_with_val = '|sysctl|add_host|a|attach|blkio_weight_device|cap_add|cap_drop|device|device_cgroup_rule|device_read_bps|device_read_iops|device_write_bps|device_write_iops|dns|dns_option|dns_search|e|env|env_file|expose|group_add|l|label|label_file|link|link_local_ip|log_driver|log_opt|network_alias|p|publish|security_opt|storage_opt|tmpfs|v|volume|volumes_from|blkio_weight|cgroup_parent|cidfile|cpu_period|cpu_quota|cpu_rt_period|cpu_rt_runtime|c|cpu_shares|cpus|cpuset_cpus|cpuset_mems|detach_keys|disable_content_trust|domainname|entrypoint|gpus|health_cmd|health_interval|health_retries|health_start_period|health_timeout|h|hostname|ip|ip6|ipc|isolation|kernel_memory|log_driver|mac_address|m|memory|memory_reservation|memory_swap|memory_swappiness|mount|name|network|no_healthcheck|oom_kill_disable|oom_score_adj|pid|pids_limit|restart|runtime|shm_size|sig_proxy|stop_signal|stop_timeout|ulimit|u|user|userns|uts|volume_driver|w|workdir|'
37   local key_abb = {net='network',a='attach',c='cpu-shares',d='detach',e='env',h='hostname',i='interactive',l='label',m='memory',p='publish',P='publish_all',t='tty',u='user',v='volume',w='workdir'}
38   local key_with_list = '|sysctl|add_host|a|attach|blkio_weight_device|cap_add|cap_drop|device|device_cgroup_rule|device_read_bps|device_read_iops|device_write_bps|device_write_iops|dns|dns_option|dns_search|e|env|env_file|expose|group_add|l|label|label_file|link|link_local_ip|log_driver|log_opt|network_alias|p|publish|security_opt|storage_opt|tmpfs|v|volume|volumes_from|'
39   local key = nil
40   local _key = nil
41   local val = nil
42   local is_cmd = false
43
44   cmd_line = cmd_line:match("^DOCKERCLI%s+(.+)")
45   for w in cmd_line:gmatch("[^%s]+") do
46     if w =='\\' then
47     elseif not key and not _key and not is_cmd then
48       --key=val
49       key, val = w:match("^%-%-([%lP%-]-)=(.+)")
50       if not key then
51         --key val
52         key = w:match("^%-%-([%lP%-]+)")
53         if not key then
54           -- -v val
55           key = w:match("^%-([%lP%-]+)")
56           if key then
57             -- for -dit
58             if key:match("i") or key:match("t") or key:match("d") then
59               if key:match("i") then
60                 config[key_abb["i"]] = true
61                 key:gsub("i", "")
62               end
63               if key:match("t") then
64                 config[key_abb["t"]] = true
65                 key:gsub("t", "")
66               end
67               if key:match("d") then
68                 config[key_abb["d"]] = true
69                 key:gsub("d", "")
70               end
71               if key:match("P") then
72                 config[key_abb["P"]] = true
73                 key:gsub("P", "")
74               end
75               if key == "" then key = nil end
76             end
77           end
78         end
79       end
80       if key then
81         key = key:gsub("-","_")
82         key = key_abb[key] or key
83         if key_no_val:match("|"..key.."|") then
84           config[key] = true
85           val = nil
86           key = nil
87         elseif key_with_val:match("|"..key.."|") then
88           -- if key == "cap_add" then config.privileged = true end
89         else
90           key = nil
91           val = nil
92         end
93       else
94         config.image = w
95         key = nil
96         val = nil
97         is_cmd = true
98       end
99     elseif (key or _key) and not is_cmd then
100       if key == "mount" then
101         -- we need resolve mount options here
102         -- type=bind,source=/source,target=/app
103         local _type = w:match("^type=([^,]+),") or "bind"
104         local source =  (_type ~= "tmpfs") and (w:match("source=([^,]+),") or  w:match("src=([^,]+),")) or ""
105         local target =  w:match(",target=([^,]+)") or  w:match(",dst=([^,]+)") or w:match(",destination=([^,]+)") or ""
106         local ro = w:match(",readonly") and "ro" or nil
107         if source and target then
108           if _type ~= "tmpfs" then
109             -- bind or volume
110             local bind_propagation = (_type == "bind") and w:match(",bind%-propagation=([^,]+)") or nil
111             val = source..":"..target .. ((ro or bind_propagation) and (":" .. (ro and ro or "") .. (((ro and bind_propagation) and "," or "") .. (bind_propagation and bind_propagation or ""))or ""))
112           else
113             -- tmpfs
114             local tmpfs_mode = w:match(",tmpfs%-mode=([^,]+)") or nil
115             local tmpfs_size = w:match(",tmpfs%-size=([^,]+)") or nil
116             key = "tmpfs"
117             val = target .. ((tmpfs_mode or tmpfs_size) and (":" .. (tmpfs_mode and ("mode=" .. tmpfs_mode) or "") .. ((tmpfs_mode and tmpfs_size) and "," or "") .. (tmpfs_size and ("size=".. tmpfs_size) or "")) or "")
118             if not config[key] then config[key] = {} end
119             table.insert( config[key], val )
120             key = nil
121             val = nil
122           end
123         end
124       else
125         val = w
126       end
127     elseif is_cmd then
128       config["command"] = (config["command"] and (config["command"] .. " " )or "")  .. w
129     end
130     if (key or _key) and val then
131       key = _key or key
132       if key_with_list:match("|"..key.."|") then
133         if not config[key] then config[key] = {} end
134         if _key then
135           config[key][#config[key]] = config[key][#config[key]] .. " " .. w
136         else
137           table.insert( config[key], val )
138         end
139         if is_quot_complete(config[key][#config[key]]) then
140           -- clear quotation marks
141           config[key][#config[key]] = config[key][#config[key]]:gsub("[\"\']", "")
142           _key = nil
143         else
144           _key = key
145         end
146       else
147         config[key] = (config[key] and (config[key] .. " ") or "") .. val
148         if is_quot_complete(config[key]) then
149           -- clear quotation marks
150           config[key] = config[key]:gsub("[\"\']", "")
151           _key = nil
152         else
153           _key = key
154         end
155       end
156       key = nil
157       val = nil
158     end
159   end
160   return config
161 end
162 -- reslvo default config
163 local default_config = {}
164 if cmd_line and cmd_line:match("^DOCKERCLI.+") then
165   default_config = resolve_cli(cmd_line)
166 elseif cmd_line and cmd_line:match("^duplicate/[^/]+$") then
167   local container_id = cmd_line:match("^duplicate/(.+)")
168   create_body = dk:containers_duplicate_config({id = container_id}) or {}
169   if not create_body.HostConfig then create_body.HostConfig = {} end
170   if next(create_body) ~= nil then
171     default_config.name = nil
172     default_config.image = create_body.Image
173     default_config.hostname = create_body.Hostname
174     default_config.tty = create_body.Tty and true or false
175     default_config.interactive = create_body.OpenStdin and true or false
176     default_config.privileged = create_body.HostConfig.Privileged and true or false
177     default_config.restart =  create_body.HostConfig.RestartPolicy and create_body.HostConfig.RestartPolicy.name or nil
178     -- default_config.network = create_body.HostConfig.NetworkMode == "default" and "bridge" or create_body.HostConfig.NetworkMode
179     -- if container has leave original network, and add new network, .HostConfig.NetworkMode is INcorrect, so using first child of .NetworkingConfig.EndpointsConfig
180     default_config.network = create_body.NetworkingConfig and create_body.NetworkingConfig.EndpointsConfig and next(create_body.NetworkingConfig.EndpointsConfig) or nil
181     default_config.ip = default_config.network and default_config.network ~= "bridge" and default_config.network ~= "host" and default_config.network ~= "null" and create_body.NetworkingConfig.EndpointsConfig[default_config.network].IPAMConfig and create_body.NetworkingConfig.EndpointsConfig[default_config.network].IPAMConfig.IPv4Address or nil
182     default_config.link = create_body.HostConfig.Links
183     default_config.env = create_body.Env
184     default_config.dns = create_body.HostConfig.Dns
185     default_config.volume = create_body.HostConfig.Binds
186     default_config.cap_add = create_body.HostConfig.CapAdd
187     default_config.publish_all = create_body.HostConfig.PublishAllPorts
188
189     if create_body.HostConfig.Sysctls and type(create_body.HostConfig.Sysctls) == "table" then
190       default_config.sysctl = {}
191       for k, v in pairs(create_body.HostConfig.Sysctls) do
192         table.insert( default_config.sysctl, k.."="..v )
193       end
194     end
195
196     if create_body.HostConfig.LogConfig and create_body.HostConfig.LogConfig.Config and type(create_body.HostConfig.LogConfig.Config) == "table" then
197       default_config.log_opt = {}
198       for k, v in pairs(create_body.HostConfig.LogConfig.Config) do
199         table.insert( default_config.log_opt, k.."="..v )
200       end
201     end
202
203     if create_body.HostConfig.PortBindings and type(create_body.HostConfig.PortBindings) == "table" then
204       default_config.publish = {}
205       for k, v in pairs(create_body.HostConfig.PortBindings) do
206         table.insert( default_config.publish, v[1].HostPort..":"..k:match("^(%d+)/.+").."/"..k:match("^%d+/(.+)") )
207       end
208     end
209
210     default_config.user = create_body.User or nil
211     default_config.command = create_body.Cmd and type(create_body.Cmd) == "table" and table.concat(create_body.Cmd, " ") or nil
212     default_config.advance = 1
213     default_config.cpus = create_body.HostConfig.NanoCPUs
214     default_config.cpu_shares =  create_body.HostConfig.CpuShares
215     default_config.memory = create_body.HostConfig.Memory
216     default_config.blkio_weight = create_body.HostConfig.BlkioWeight
217
218     if create_body.HostConfig.Devices and type(create_body.HostConfig.Devices) == "table" then
219       default_config.device = {}
220       for _, v in ipairs(create_body.HostConfig.Devices) do
221         table.insert( default_config.device, v.PathOnHost..":"..v.PathInContainer..(v.CgroupPermissions ~= "" and (":" .. v.CgroupPermissions) or "") )
222       end
223     end
224     if create_body.HostConfig.Tmpfs and type(create_body.HostConfig.Tmpfs) == "table" then
225       default_config.tmpfs = {}
226       for k, v in pairs(create_body.HostConfig.Tmpfs) do
227         table.insert( default_config.tmpfs, k .. (v~="" and ":" or "")..v )
228       end
229     end
230   end
231 end
232
233 local m = SimpleForm("docker", translate("Docker"))
234 m.redirect = luci.dispatcher.build_url("admin", "docker", "containers")
235 -- m.reset = false
236 -- m.submit = false
237 -- new Container
238
239 docker_status = m:section(SimpleSection)
240 docker_status.template = "dockerman/apply_widget"
241 docker_status.err=docker:read_status()
242 docker_status.err=docker_status.err and docker_status.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
243 if docker_status.err then docker:clear_status() end
244
245 local s = m:section(SimpleSection, translate("New Container"))
246 s.addremove = true
247 s.anonymous = true
248
249 local d = s:option(DummyValue,"cmd_line", translate("Resolve CLI"))
250 d.rawhtml  = true
251 d.template = "dockerman/newcontainer_resolve"
252
253 d = s:option(Value, "name", translate("Container Name"))
254 d.rmempty = true
255 d.default = default_config.name or nil
256
257 d = s:option(Flag, "interactive", translate("Interactive (-i)"))
258 d.rmempty = true
259 d.disabled = 0
260 d.enabled = 1
261 d.default = default_config.interactive and 1 or 0
262
263 d = s:option(Flag, "tty", translate("TTY (-t)"))
264 d.rmempty = true
265 d.disabled = 0
266 d.enabled = 1
267 d.default = default_config.tty and 1 or 0
268
269 d = s:option(Value, "image", translate("Docker Image"))
270 d.rmempty = true
271 d.default = default_config.image or nil
272 for _, v in ipairs (images) do
273   if v.RepoTags then
274     d:value(v.RepoTags[1], v.RepoTags[1])
275   end
276 end
277
278 d = s:option(Flag, "_force_pull", translate("Always pull image first"))
279 d.rmempty = true
280 d.disabled = 0
281 d.enabled = 1
282 d.default = 0
283
284 d = s:option(Flag, "privileged", translate("Privileged"))
285 d.rmempty = true
286 d.disabled = 0
287 d.enabled = 1
288 d.default = default_config.privileged and 1 or 0
289
290 d = s:option(ListValue, "restart", translate("Restart Policy"))
291 d.rmempty = true
292
293 d:value("no", "No")
294 d:value("unless-stopped", "Unless stopped")
295 d:value("always", "Always")
296 d:value("on-failure", "On failure")
297 d.default = default_config.restart or "unless-stopped"
298
299 local d_network = s:option(ListValue, "network", translate("Networks"))
300 d_network.rmempty = true
301 d_network.default = default_config.network or "bridge"
302
303 local d_ip = s:option(Value, "ip", translate("IPv4 Address"))
304 d_ip.datatype="ip4addr"
305 d_ip:depends("network", "nil")
306 d_ip.default = default_config.ip or nil
307
308 d = s:option(DynamicList, "link", translate("Links with other containers"))
309 d.placeholder = "container_name:alias"
310 d.rmempty = true
311 d:depends("network", "bridge")
312 d.default = default_config.link or nil
313
314 d = s:option(DynamicList, "dns", translate("Set custom DNS servers"))
315 d.placeholder = "8.8.8.8"
316 d.rmempty = true
317 d.default = default_config.dns or nil
318
319 d = s:option(Value, "user", translate("User(-u)"), translate("The user that commands are run as inside the container.(format: name|uid[:group|gid])"))
320 d.placeholder = "1000:1000"
321 d.rmempty = true
322 d.default = default_config.user or nil
323
324 d = s:option(DynamicList, "env", translate("Environmental Variable(-e)"), translate("Set environment variables to inside the container"))
325 d.placeholder = "TZ=Asia/Shanghai"
326 d.rmempty = true
327 d.default = default_config.env or nil
328
329 d = s:option(DynamicList, "volume", translate("Bind Mount(-v)"), translate("Bind mount a volume"))
330 d.placeholder = "/media:/media:slave"
331 d.rmempty = true
332 d.default = default_config.volume or nil
333
334 local d_publish = s:option(DynamicList, "publish", translate("Exposed Ports(-p)"), translate("Publish container's port(s) to the host"))
335 d_publish.placeholder = "2200:22/tcp"
336 d_publish.rmempty = true
337 d_publish.default = default_config.publish or nil
338
339 d = s:option(Value, "command", translate("Run command"))
340 d.placeholder = "/bin/sh init.sh"
341 d.rmempty = true
342 d.default = default_config.command or nil
343
344 d = s:option(Flag, "advance", translate("Advance"))
345 d.rmempty = true
346 d.disabled = 0
347 d.enabled = 1
348 d.default = default_config.advance or 0
349
350 d = s:option(Value, "hostname", translate("Host Name"), translate("The hostname to use for the container"))
351 d.rmempty = true
352 d.default = default_config.hostname or nil
353 d:depends("advance", 1)
354
355 d = s:option(Flag, "publish_all", translate("Exposed All Ports(-P)"), translate("Allocates an ephemeral host port for all of a container's exposed ports"))
356 d.rmempty = true
357 d.disabled = 0
358 d.enabled = 1
359 d.default = default_config.publish_all and 1 or 0
360 d:depends("advance", 1)
361
362 d = s:option(DynamicList, "device", translate("Device(--device)"), translate("Add host device to the container"))
363 d.placeholder = "/dev/sda:/dev/xvdc:rwm"
364 d.rmempty = true
365 d:depends("advance", 1)
366 d.default = default_config.device or nil
367
368 d = s:option(DynamicList, "tmpfs", translate("Tmpfs(--tmpfs)"), translate("Mount tmpfs directory"))
369 d.placeholder = "/run:rw,noexec,nosuid,size=65536k"
370 d.rmempty = true
371 d:depends("advance", 1)
372 d.default = default_config.tmpfs or nil
373
374 d = s:option(DynamicList, "sysctl", translate("Sysctl(--sysctl)"), translate("Sysctls (kernel parameters) options"))
375 d.placeholder = "net.ipv4.ip_forward=1"
376 d.rmempty = true
377 d:depends("advance", 1)
378 d.default = default_config.sysctl or nil
379
380 d = s:option(DynamicList, "cap_add", translate("CAP-ADD(--cap-add)"), translate("A list of kernel capabilities to add to the container"))
381 d.placeholder = "NET_ADMIN"
382 d.rmempty = true
383 d:depends("advance", 1)
384 d.default = default_config.cap_add or nil
385
386 d = s:option(Value, "cpus", translate("CPUs"), translate("Number of CPUs. Number is a fractional number. 0.000 means no limit"))
387 d.placeholder = "1.5"
388 d.rmempty = true
389 d:depends("advance", 1)
390 d.datatype="ufloat"
391 d.default = default_config.cpus or nil
392
393 d = s:option(Value, "cpu_shares", translate("CPU Shares Weight"), translate("CPU shares relative weight, if 0 is set, the system will ignore the value and use the default of 1024"))
394 d.placeholder = "1024"
395 d.rmempty = true
396 d:depends("advance", 1)
397 d.datatype="uinteger"
398 d.default = default_config.cpu_shares or nil
399
400 d = s:option(Value, "memory", translate("Memory"), translate("Memory limit (format: <number>[<unit>]). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M"))
401 d.placeholder = "128m"
402 d.rmempty = true
403 d:depends("advance", 1)
404 d.default = default_config.memory or nil
405
406 d = s:option(Value, "blkio_weight", translate("Block IO Weight"), translate("Block IO weight (relative weight) accepts a weight value between 10 and 1000"))
407 d.placeholder = "500"
408 d.rmempty = true
409 d:depends("advance", 1)
410 d.datatype="uinteger"
411 d.default = default_config.blkio_weight or nil
412
413 d = s:option(DynamicList, "log_opt", translate("Log driver options"), translate("The logging configuration for this container"))
414 d.placeholder = "max-size=1m"
415 d.rmempty = true
416 d:depends("advance", 1)
417 d.default = default_config.log_opt or nil
418
419 for _, v in ipairs (networks) do
420   if v.Name then
421     local parent = v.Options and v.Options.parent or nil
422     local ip = v.IPAM and v.IPAM.Config and v.IPAM.Config[1] and v.IPAM.Config[1].Subnet or nil
423     ipv6 =  v.IPAM and v.IPAM.Config and v.IPAM.Config[2] and v.IPAM.Config[2].Subnet or nil
424     local network_name = v.Name .. " | " .. v.Driver  .. (parent and (" | " .. parent) or "") .. (ip and (" | " .. ip) or "").. (ipv6 and (" | " .. ipv6) or "")
425     d_network:value(v.Name, network_name)
426
427     if v.Name ~= "none" and v.Name ~= "bridge" and v.Name ~= "host" then
428       d_ip:depends("network", v.Name)
429     end
430
431     if v.Driver == "bridge" then
432       d_publish:depends("network", v.Name)
433     end
434   end
435 end
436
437 m.handle = function(self, state, data)
438   if state ~= FORM_VALID then return end
439   local tmp
440   local name = data.name or ("luci_" .. os.date("%Y%m%d%H%M%S"))
441   local hostname = data.hostname
442   local tty = type(data.tty) == "number" and (data.tty == 1 and true or false) or default_config.tty or false
443   local publish_all = type(data.publish_all) == "number" and (data.publish_all == 1 and true or false) or default_config.publish_all or false
444   local interactive = type(data.interactive) == "number" and (data.interactive == 1 and true or false) or default_config.interactive or false
445   local image = data.image
446   local user = data.user
447   if image and not image:match(".-:.+") then
448     image = image .. ":latest"
449   end
450   local privileged = type(data.privileged) == "number" and (data.privileged == 1 and true or false) or default_config.privileged or false
451   local restart = data.restart
452   local env = data.env
453   local dns = data.dns
454   local cap_add = data.cap_add
455   local sysctl = {}
456   tmp = data.sysctl
457   if type(tmp) == "table" then
458     for i, v in ipairs(tmp) do
459       local k,v1 = v:match("(.-)=(.+)")
460       if k and v1 then
461         sysctl[k]=v1
462       end
463     end
464   end
465   local log_opt = {}
466   tmp = data.log_opt
467   if type(tmp) == "table" then
468     for i, v in ipairs(tmp) do
469       local k,v1 = v:match("(.-)=(.+)")
470       if k and v1 then
471         log_opt[k]=v1
472       end
473     end
474   end
475   local network = data.network
476   local ip = (network ~= "bridge" and network ~= "host" and network ~= "none") and data.ip or nil
477   local volume = data.volume
478   local memory = data.memory or 0
479   local cpu_shares = data.cpu_shares or 0
480   local cpus = data.cpus or 0
481   local blkio_weight = data.blkio_weight or 500
482
483   local portbindings = {}
484   local exposedports = {}
485   local tmpfs = {}
486   tmp = data.tmpfs
487   if type(tmp) == "table" then
488     for i, v in ipairs(tmp)do
489       local k= v:match("([^:]+)")
490       local v1 = v:match(".-:([^:]+)") or ""
491       if k then
492         tmpfs[k]=v1
493       end
494     end
495   end
496
497   local device = {}
498   tmp = data.device
499   if type(tmp) == "table" then
500     for i, v in ipairs(tmp) do
501       local t = {}
502       local _,_, h, c, p = v:find("(.-):(.-):(.+)")
503       if h and c then
504         t['PathOnHost'] = h
505         t['PathInContainer'] = c
506         t['CgroupPermissions'] = p or "rwm"
507       else
508         local _,_, h, c = v:find("(.-):(.+)")
509         if h and c then
510           t['PathOnHost'] = h
511           t['PathInContainer'] = c
512           t['CgroupPermissions'] = "rwm"
513         else
514           t['PathOnHost'] = v
515           t['PathInContainer'] = v
516           t['CgroupPermissions'] = "rwm"
517         end
518       end
519       if next(t) ~= nil then
520         table.insert( device, t )
521       end
522     end
523   end
524
525   tmp = data.publish or {}
526   for i, v in ipairs(tmp) do
527     for v1 ,v2 in string.gmatch(v, "(%d+):([^%s]+)") do
528       local _,_,p= v2:find("^%d+/(%w+)")
529       if p == nil then
530         v2=v2..'/tcp'
531       end
532       portbindings[v2] = {{HostPort=v1}}
533       exposedports[v2] = {HostPort=v1}
534     end
535   end
536
537   local link = data.link
538   tmp = data.command
539   local command = {}
540   if tmp ~= nil then
541     for v in string.gmatch(tmp, "[^%s]+") do
542       command[#command+1] = v
543     end 
544   end
545   if memory ~= 0 then
546     _,_,n,unit = memory:find("([%d%.]+)([%l%u]+)")
547     if n then
548       unit = unit and unit:sub(1,1):upper() or "B"
549       if  unit == "M" then
550         memory = tonumber(n) * 1024 * 1024
551       elseif unit == "G" then
552         memory = tonumber(n) * 1024 * 1024 * 1024
553       elseif unit == "K" then
554         memory = tonumber(n) * 1024
555       else
556         memory = tonumber(n)
557       end
558     end
559   end
560
561   create_body.Hostname = network ~= "host" and (hostname or name) or nil
562   create_body.Tty = tty and true or false
563   create_body.OpenStdin = interactive and true or false
564   create_body.User = user
565   create_body.Cmd = command
566   create_body.Env = env
567   create_body.Image = image
568   create_body.ExposedPorts = exposedports
569   create_body.HostConfig = create_body.HostConfig or {}
570   create_body.HostConfig.Dns = dns
571   create_body.HostConfig.Binds = volume
572   create_body.HostConfig.RestartPolicy = { Name = restart, MaximumRetryCount = 0 }
573   create_body.HostConfig.Privileged = privileged and true or false
574   create_body.HostConfig.PortBindings = portbindings
575   create_body.HostConfig.Memory = tonumber(memory)
576   create_body.HostConfig.CpuShares = tonumber(cpu_shares)
577   create_body.HostConfig.NanoCPUs = tonumber(cpus) * 10 ^ 9
578   create_body.HostConfig.BlkioWeight = tonumber(blkio_weight)
579   create_body.HostConfig.PublishAllPorts = publish_all
580   if create_body.HostConfig.NetworkMode ~= network then
581     -- network mode changed, need to clear duplicate config
582     create_body.NetworkingConfig = nil
583   end
584   create_body.HostConfig.NetworkMode = network
585   if ip then
586     if create_body.NetworkingConfig and create_body.NetworkingConfig.EndpointsConfig and type(create_body.NetworkingConfig.EndpointsConfig) == "table" then
587       -- ip + duplicate config
588       for k, v in pairs (create_body.NetworkingConfig.EndpointsConfig) do
589         if k == network and v.IPAMConfig and v.IPAMConfig.IPv4Address then
590           v.IPAMConfig.IPv4Address = ip
591         else
592           create_body.NetworkingConfig.EndpointsConfig = { [network] = { IPAMConfig = { IPv4Address = ip } } }
593         end
594         break
595       end
596     else
597       -- ip + no duplicate config
598       create_body.NetworkingConfig = { EndpointsConfig = { [network] = { IPAMConfig = { IPv4Address = ip } } } }
599     end
600   elseif not create_body.NetworkingConfig then
601     -- no ip + no duplicate config
602     create_body.NetworkingConfig = nil
603   end
604   create_body["HostConfig"]["Tmpfs"] = tmpfs
605   create_body["HostConfig"]["Devices"] = device
606   create_body["HostConfig"]["Sysctls"] = sysctl
607   create_body["HostConfig"]["CapAdd"] = cap_add
608   create_body["HostConfig"]["LogConfig"] = next(log_opt) ~= nil and { Config = log_opt } or nil
609
610   if network == "bridge" then
611     create_body["HostConfig"]["Links"] = link
612   end
613   local pull_image = function(image)
614     local json_stringify = luci.jsonc and luci.jsonc.stringify
615     docker:append_status("Images: " .. "pulling" .. " " .. image .. "...\n")
616     local res = dk.images:create({query = {fromImage=image}}, docker.pull_image_show_status_cb)
617     if res and res.code == 200 and (res.body[#res.body] and not res.body[#res.body].error and res.body[#res.body].status and (res.body[#res.body].status == "Status: Downloaded newer image for ".. image or res.body[#res.body].status == "Status: Image is up to date for ".. image)) then
618       docker:append_status("done\n")
619     else
620       res.code = (res.code == 200) and 500 or res.code
621       docker:append_status("code:" .. res.code.." ".. (res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message)).. "\n")
622       luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer"))
623     end
624   end
625   docker:clear_status()
626   local exist_image = false
627   if image then
628     for _, v in ipairs (images) do
629       if v.RepoTags and v.RepoTags[1] == image then
630         exist_image = true
631         break
632       end
633     end
634     if not exist_image then
635       pull_image(image)
636     elseif data._force_pull == 1 then
637       pull_image(image)
638     end
639   end
640
641   create_body = docker.clear_empty_tables(create_body)
642   docker:append_status("Container: " .. "create" .. " " .. name .. "...")
643   local res = dk.containers:create({name = name, body = create_body})
644   if res and res.code == 201 then
645     docker:clear_status()
646     luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers"))
647   else
648     docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
649     luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer"))
650   end
651 end
652
653 return m