docker.lua 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. --[[
  2. LuCI - Lua Configuration Interface
  3. Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
  4. ]]--
  5. require "luci.util"
  6. local docker = require "luci.docker"
  7. local uci = (require "luci.model.uci").cursor()
  8. local _docker = {}
  9. --pull image and return iamge id
  10. local update_image = function(self, image_name)
  11. local json_stringify = luci.jsonc and luci.jsonc.stringify
  12. _docker:append_status("Images: " .. "pulling" .. " " .. image_name .. "...\n")
  13. local res = self.images:create({query = {fromImage=image_name}}, _docker.pull_image_show_status_cb)
  14. if res and res.code == 200 and (#res.body > 0 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_name)) then
  15. _docker:append_status("done\n")
  16. else
  17. res.body.message = res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message)
  18. end
  19. new_image_id = self.images:inspect({name = image_name}).body.Id
  20. return new_image_id, res
  21. end
  22. local table_equal = function(t1, t2)
  23. if not t1 then return true end
  24. if not t2 then return false end
  25. if #t1 ~= #t2 then return false end
  26. for i, v in ipairs(t1) do
  27. if t1[i] ~= t2[i] then return false end
  28. end
  29. return true
  30. end
  31. local table_subtract = function(t1, t2)
  32. if not t1 or next(t1) == nil then return nil end
  33. if not t2 or next(t2) == nil then return t1 end
  34. local res = {}
  35. for _, v1 in ipairs(t1) do
  36. local found = false
  37. for _, v2 in ipairs(t2) do
  38. if v1 == v2 then
  39. found= true
  40. break
  41. end
  42. end
  43. if not found then
  44. table.insert(res, v1)
  45. end
  46. end
  47. return next(res) == nil and nil or res
  48. end
  49. local map_subtract = function(t1, t2)
  50. if not t1 or next(t1) == nil then return nil end
  51. if not t2 or next(t2) == nil then return t1 end
  52. local res = {}
  53. for k1, v1 in pairs(t1) do
  54. local found = false
  55. for k2, v2 in ipairs(t2) do
  56. if k1 == k2 and luci.util.serialize_data(v1) == luci.util.serialize_data(v2) then
  57. found= true
  58. break
  59. end
  60. end
  61. if not found then
  62. res[k1] = v1
  63. -- if v1 and type(v1) == "table" then
  64. -- if next(v1) == nil then
  65. -- res[k1] = { k = 'v' }
  66. -- else
  67. -- res[k1] = v1
  68. -- end
  69. -- end
  70. end
  71. end
  72. return next(res) ~= nil and res or nil
  73. end
  74. _docker.clear_empty_tables = function ( t )
  75. local k, v
  76. if next(t) == nil then
  77. t = nil
  78. else
  79. for k, v in pairs(t) do
  80. if type(v) == 'table' then
  81. t[k] = _docker.clear_empty_tables(v)
  82. end
  83. end
  84. end
  85. return t
  86. end
  87. -- return create_body, extra_network
  88. local get_config = function(container_config, image_config)
  89. local config = container_config.Config
  90. local old_host_config = container_config.HostConfig
  91. local old_network_setting = container_config.NetworkSettings.Networks or {}
  92. if config.WorkingDir == image_config.WorkingDir then config.WorkingDir = "" end
  93. if config.User == image_config.User then config.User = "" end
  94. if table_equal(config.Cmd, image_config.Cmd) then config.Cmd = nil end
  95. if table_equal(config.Entrypoint, image_config.Entrypoint) then config.Entrypoint = nil end
  96. if table_equal(config.ExposedPorts, image_config.ExposedPorts) then config.ExposedPorts = nil end
  97. config.Env = table_subtract(config.Env, image_config.Env)
  98. config.Labels = table_subtract(config.Labels, image_config.Labels)
  99. config.Volumes = map_subtract(config.Volumes, image_config.Volumes)
  100. -- subtract ports exposed in image from container
  101. if old_host_config.PortBindings and next(old_host_config.PortBindings) ~= nil then
  102. config.ExposedPorts = {}
  103. for p, v in pairs(old_host_config.PortBindings) do
  104. config.ExposedPorts[p] = { HostPort=v[1] and v[1].HostPort }
  105. end
  106. end
  107. -- handle network config, we need only one network, extras need to network connect action
  108. local network_setting = {}
  109. local multi_network = false
  110. local extra_network = {}
  111. for k, v in pairs(old_network_setting) do
  112. if multi_network then
  113. extra_network[k] = v
  114. else
  115. network_setting[k] = v
  116. end
  117. multi_network = true
  118. end
  119. -- handle hostconfig
  120. local host_config = old_host_config
  121. -- if host_config.PortBindings and next(host_config.PortBindings) == nil then host_config.PortBindings = nil end
  122. -- host_config.LogConfig = nil
  123. host_config.Mounts = {}
  124. -- for volumes
  125. for i, v in ipairs(container_config.Mounts) do
  126. if v.Type == "volume" then
  127. table.insert(host_config.Mounts, {
  128. Type = v.Type,
  129. Target = v.Destination,
  130. Source = v.Source:match("([^/]+)\/_data"),
  131. BindOptions = (v.Type == "bind") and {Propagation = v.Propagation} or nil,
  132. ReadOnly = not v.RW
  133. })
  134. end
  135. end
  136. -- merge configs
  137. local create_body = config
  138. create_body["HostConfig"] = host_config
  139. create_body["NetworkingConfig"] = {EndpointsConfig = network_setting}
  140. create_body = _docker.clear_empty_tables(create_body) or {}
  141. extra_network = _docker.clear_empty_tables(extra_network) or {}
  142. return create_body, extra_network
  143. end
  144. local upgrade = function(self, request)
  145. _docker:clear_status()
  146. -- get image name, image id, container name, configuration information
  147. local container_info = self.containers:inspect({id = request.id})
  148. if container_info.code > 300 and type(container_info.body) == "table" then
  149. return container_info
  150. end
  151. local image_name = container_info.body.Config.Image
  152. if not image_name:match(".-:.+") then image_name = image_name .. ":latest" end
  153. local old_image_id = container_info.body.Image
  154. local container_name = container_info.body.Name:sub(2)
  155. local image_id, res = update_image(self, image_name)
  156. if res and res.code ~= 200 then return res end
  157. if image_id == old_image_id then
  158. return {code = 305, body = {message = "Already up to date"}}
  159. end
  160. _docker:append_status("Container: " .. "Stop" .. " " .. container_name .. "...")
  161. res = self.containers:stop({name = container_name})
  162. if res and res.code < 305 then
  163. _docker:append_status("done\n")
  164. else
  165. return res
  166. end
  167. _docker:append_status("Container: rename" .. " " .. container_name .. " to ".. container_name .. "_old ...")
  168. res = self.containers:rename({name = container_name, query = { name = container_name .. "_old" }})
  169. if res and res.code < 300 then
  170. _docker:append_status("done\n")
  171. else
  172. return res
  173. end
  174. -- handle config
  175. local image_config = self.images:inspect({id = old_image_id}).body.Config
  176. local create_body, extra_network = get_config(container_info.body, image_config)
  177. -- create new container
  178. _docker:append_status("Container: Create" .. " " .. container_name .. "...")
  179. create_body = _docker.clear_empty_tables(create_body)
  180. res = self.containers:create({name = container_name, body = create_body})
  181. if res and res.code > 300 then return res end
  182. _docker:append_status("done\n")
  183. -- extra networks need to network connect action
  184. for k, v in pairs(extra_network) do
  185. _docker:append_status("Networks: Connect" .. " " .. container_name .. "...")
  186. res = self.networks:connect({id = k, body = {Container = container_name, EndpointConfig = v}})
  187. if res.code > 300 then return res end
  188. _docker:append_status("done\n")
  189. end
  190. _docker:clear_status()
  191. return res
  192. end
  193. local duplicate_config = function (self, request)
  194. local container_info = self.containers:inspect({id = request.id})
  195. if container_info.code > 300 and type(container_info.body) == "table" then return nil end
  196. local old_image_id = container_info.body.Image
  197. local image_config = self.images:inspect({id = old_image_id}).body.Config
  198. return get_config(container_info.body, image_config)
  199. end
  200. _docker.new = function(option)
  201. local option = option or {}
  202. local remote = uci:get("dockerman", "local", "remote_endpoint")
  203. options = {
  204. host = (remote == "true") and (option.host or uci:get("dockerman", "local", "remote_host")) or nil,
  205. port = (remote == "true") and (option.port or uci:get("dockerman", "local", "remote_port")) or nil,
  206. debug = option.debug or uci:get("dockerman", "local", "debug") == 'true' and true or false,
  207. debug_path = option.debug_path or uci:get("dockerman", "local", "debug_path")
  208. }
  209. options.socket_path = (remote ~= "true" or not options.host or not options.port) and (option.socket_path or uci:get("dockerman", "local", "socket_path") or "/var/run/docker.sock") or nil
  210. local _new = docker.new(options)
  211. _new.options.status_path = uci:get("dockerman", "local", "status_path")
  212. _new.containers_upgrade = upgrade
  213. _new.containers_duplicate_config = duplicate_config
  214. return _new
  215. end
  216. _docker.options={}
  217. _docker.options.status_path = uci:get("dockerman", "local", "status_path")
  218. _docker.append_status=function(self,val)
  219. if not val then return end
  220. local file_docker_action_status=io.open(self.options.status_path, "a+")
  221. file_docker_action_status:write(val)
  222. file_docker_action_status:close()
  223. end
  224. _docker.write_status=function(self,val)
  225. if not val then return end
  226. local file_docker_action_status=io.open(self.options.status_path, "w+")
  227. file_docker_action_status:write(val)
  228. file_docker_action_status:close()
  229. end
  230. _docker.read_status=function(self)
  231. return nixio.fs.readfile(self.options.status_path)
  232. end
  233. _docker.clear_status=function(self)
  234. nixio.fs.remove(self.options.status_path)
  235. end
  236. local status_cb = function(res, source, handler)
  237. res.body = res.body or {}
  238. while true do
  239. local chunk = source()
  240. if chunk then
  241. --standard output to res.body
  242. table.insert(res.body, chunk)
  243. handler(chunk)
  244. else
  245. return
  246. end
  247. end
  248. end
  249. --{"status":"Pulling from library\/debian","id":"latest"}
  250. --{"status":"Pulling fs layer","progressDetail":[],"id":"50e431f79093"}
  251. --{"status":"Downloading","progressDetail":{"total":50381971,"current":2029978},"id":"50e431f79093","progress":"[==> ] 2.03MB\/50.38MB"}
  252. --{"status":"Download complete","progressDetail":[],"id":"50e431f79093"}
  253. --{"status":"Extracting","progressDetail":{"total":50381971,"current":17301504},"id":"50e431f79093","progress":"[=================> ] 17.3MB\/50.38MB"}
  254. --{"status":"Pull complete","progressDetail":[],"id":"50e431f79093"}
  255. --{"status":"Digest: sha256:a63d0b2ecbd723da612abf0a8bdb594ee78f18f691d7dc652ac305a490c9b71a"}
  256. --{"status":"Status: Downloaded newer image for debian:latest"}
  257. _docker.pull_image_show_status_cb = function(res, source)
  258. return status_cb(res, source, function(chunk)
  259. local json_parse = luci.jsonc.parse
  260. local step = json_parse(chunk)
  261. if type(step) == "table" then
  262. local buf = _docker:read_status()
  263. local num = 0
  264. local str = '\t' .. (step.id and (step.id .. ": ") or "") .. (step.status and step.status or "") .. (step.progress and (" " .. step.progress) or "").."\n"
  265. if step.id then buf, num = buf:gsub("\t"..step.id .. ": .-\n", str) end
  266. if num == 0 then
  267. buf = buf .. str
  268. end
  269. _docker:write_status(buf)
  270. end
  271. end)
  272. end
  273. --{"status":"Downloading from https://downloads.openwrt.org/releases/19.07.0/targets/x86/64/openwrt-19.07.0-x86-64-generic-rootfs.tar.gz"}
  274. --{"status":"Importing","progressDetail":{"current":1572391,"total":3821714},"progress":"[====================\u003e ] 1.572MB/3.822MB"}
  275. --{"status":"sha256:d5304b58e2d8cc0a2fd640c05cec1bd4d1229a604ac0dd2909f13b2b47a29285"}
  276. _docker.import_image_show_status_cb = function(res, source)
  277. return status_cb(res, source, function(chunk)
  278. local json_parse = luci.jsonc.parse
  279. local step = json_parse(chunk)
  280. if type(step) == "table" then
  281. local buf = _docker:read_status()
  282. local num = 0
  283. local str = '\t' .. (step.status and step.status or "") .. (step.progress and (" " .. step.progress) or "").."\n"
  284. if step.status then buf, num = buf:gsub("\t"..step.status .. " .-\n", str) end
  285. if num == 0 then
  286. buf = buf .. str
  287. end
  288. _docker:write_status(buf)
  289. end
  290. end
  291. )
  292. end
  293. -- _docker.print_status_cb = function(res, source)
  294. -- return status_cb(res, source, function(step)
  295. -- luci.util.perror(step)
  296. -- end
  297. -- )
  298. -- end
  299. _docker.create_macvlan_interface = function(name, device, gateway, subnet)
  300. if not nixio.fs.access("/etc/config/network") or not nixio.fs.access("/etc/config/firewall") then return end
  301. if uci:get("dockerman", "local", "remote_endpoint") == "true" then return end
  302. local ip = require "luci.ip"
  303. local if_name = "docker_"..name
  304. local dev_name = "macvlan_"..name
  305. local net_mask = tostring(ip.new(subnet):mask())
  306. local lan_interfaces
  307. -- add macvlan device
  308. uci:delete("network", dev_name)
  309. uci:set("network", dev_name, "device")
  310. uci:set("network", dev_name, "name", dev_name)
  311. uci:set("network", dev_name, "ifname", device)
  312. uci:set("network", dev_name, "type", "macvlan")
  313. uci:set("network", dev_name, "mode", "bridge")
  314. -- add macvlan interface
  315. uci:delete("network", if_name)
  316. uci:set("network", if_name, "interface")
  317. uci:set("network", if_name, "proto", "static")
  318. uci:set("network", if_name, "ifname", dev_name)
  319. uci:set("network", if_name, "ipaddr", gateway)
  320. uci:set("network", if_name, "netmask", net_mask)
  321. uci:foreach("firewall", "zone", function(s)
  322. if s.name == "lan" then
  323. local interfaces
  324. if type(s.network) == "table" then
  325. interfaces = table.concat(s.network, " ")
  326. uci:delete("firewall", s[".name"], "network")
  327. else
  328. interfaces = s.network and s.network or ""
  329. end
  330. interfaces = interfaces .. " " .. if_name
  331. interfaces = interfaces:gsub("%s+", " ")
  332. uci:set("firewall", s[".name"], "network", interfaces)
  333. end
  334. end)
  335. uci:commit("firewall")
  336. uci:commit("network")
  337. os.execute("ifup " .. if_name)
  338. end
  339. _docker.remove_macvlan_interface = function(name)
  340. if not nixio.fs.access("/etc/config/network") or not nixio.fs.access("/etc/config/firewall") then return end
  341. if uci:get("dockerman", "local", "remote_endpoint") == "true" then return end
  342. local if_name = "docker_"..name
  343. local dev_name = "macvlan_"..name
  344. uci:foreach("firewall", "zone", function(s)
  345. if s.name == "lan" then
  346. local interfaces
  347. if type(s.network) == "table" then
  348. interfaces = table.concat(s.network, " ")
  349. else
  350. interfaces = s.network and s.network or ""
  351. end
  352. interfaces = interfaces and interfaces:gsub(if_name, "")
  353. interfaces = interfaces and interfaces:gsub("%s+", " ")
  354. uci:set("firewall", s[".name"], "network", interfaces)
  355. end
  356. end)
  357. uci:commit("firewall")
  358. uci:delete("network", dev_name)
  359. uci:delete("network", if_name)
  360. uci:commit("network")
  361. os.execute("ip link del " .. if_name)
  362. end
  363. return _docker