docker.lua 13 KB

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