docker.lua 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. --[[
  2. LuCI - Lua Configuration Interface
  3. Copyright 2019 lisaac <https://github.com/lisaac/luci-lib-docker>
  4. ]]--
  5. require "nixio.util"
  6. require "luci.util"
  7. local jsonc = require "luci.jsonc"
  8. local nixio = require "nixio"
  9. local ltn12 = require "luci.ltn12"
  10. local fs = require "nixio.fs"
  11. local urlencode = luci.util.urlencode or luci.http and luci.http.protocol and luci.http.protocol.urlencode
  12. local json_stringify = jsonc.stringify
  13. local json_parse = jsonc.parse
  14. local chunksource = function(sock, buffer)
  15. buffer = buffer or ""
  16. return function()
  17. local output
  18. local _, endp, count = buffer:find("^([0-9a-fA-F]+)\r\n")
  19. if not count then
  20. local newblock, code = sock:recv(1024)
  21. if not newblock then return nil, code end
  22. buffer = buffer .. newblock
  23. _, endp, count = buffer:find("^([0-9a-fA-F]+)\r\n")
  24. end
  25. count = tonumber(count, 16)
  26. if not count then
  27. return nil, -1, "invalid encoding"
  28. elseif count == 0 then -- finial
  29. return nil
  30. elseif count <= #buffer - endp then
  31. --data >= count
  32. output = buffer:sub(endp + 1, endp + count)
  33. if count == #buffer - endp then -- [data]
  34. buffer = buffer:sub(endp + count + 1)
  35. count, code = sock:recvall(2) --read \r\n
  36. if not count then return nil, code end
  37. elseif count + 1 == #buffer - endp then -- [data]\r
  38. buffer = buffer:sub(endp + count + 2)
  39. count, code = sock:recvall(1) --read \n
  40. if not count then return nil, code end
  41. else -- [data]\r\n[count]\r\n[data]...
  42. buffer = buffer:sub(endp + count + 3) -- cut buffer
  43. end
  44. return output
  45. else
  46. -- data < count
  47. output = buffer:sub(endp + 1, endp + count)
  48. buffer = buffer:sub(endp + count + 1)
  49. local remain, code = sock:recvall(count - #output) --need read remaining
  50. if not remain then return nil, code end
  51. output = output .. remain
  52. count, code = sock:recvall(2) --read \r\n
  53. if not count then return nil, code end
  54. return output
  55. end
  56. end
  57. end
  58. local chunksink = function (sock)
  59. return function(chunk, err)
  60. if not chunk then
  61. return sock:writeall("0\r\n\r\n")
  62. else
  63. return sock:writeall(("%X\r\n%s\r\n"):format(#chunk, tostring(chunk)))
  64. end
  65. end
  66. end
  67. local docker_stream_filter = function(buffer)
  68. buffer = buffer or ""
  69. if #buffer < 8 then
  70. return ""
  71. end
  72. local stream_type = ((string.byte(buffer, 1) == 1) and "stdout") or ((string.byte(buffer, 1) == 2) and "stderr") or ((string.byte(buffer, 1) == 0) and "stdin") or "stream_err"
  73. local valid_length =
  74. tonumber(string.byte(buffer, 5)) * 256 * 256 * 256 + tonumber(string.byte(buffer, 6)) * 256 * 256 + tonumber(string.byte(buffer, 7)) * 256 + tonumber(string.byte(buffer, 8))
  75. if valid_length > #buffer + 8 then
  76. return ""
  77. end
  78. return stream_type .. ": " .. string.sub(buffer, 9, valid_length + 8)
  79. -- return string.sub(buffer, 9, valid_length + 8)
  80. end
  81. local open_socket = function(req_options)
  82. local socket
  83. if type(req_options) ~= "table" then return socket end
  84. if req_options.socket_path then
  85. socket = nixio.socket("unix", "stream")
  86. if socket:connect(req_options.socket_path) ~= true then return nil end
  87. elseif req_options.host and req_options.port then
  88. socket = nixio.connect(req_options.host, req_options.port)
  89. end
  90. if socket then
  91. return socket
  92. else
  93. return nil
  94. end
  95. end
  96. local send_http_socket = function(docker_socket, req_header, req_body, callback)
  97. if docker_socket:send(req_header) == 0 then
  98. return {
  99. headers={code=498,message="bad path", protocol="HTTP/1.1"},
  100. body={message="can\'t send data to socket"}
  101. }
  102. end
  103. if req_body and type(req_body) == "function" and req_header and req_header:match("chunked") then
  104. -- chunked send
  105. req_body(chunksink(docker_socket))
  106. elseif req_body and type(req_body) == "function" then
  107. -- normal send by req_body function
  108. req_body(docker_socket)
  109. elseif req_body and type(req_body) == "table" then
  110. -- json
  111. docker_socket:send(json_stringify(req_body))
  112. if options.debug then io.popen("echo '".. json_stringify(req_body) .. "' >> " .. options.debug_path) end
  113. elseif req_body then
  114. docker_socket:send(req_body)
  115. if options.debug then io.popen("echo '".. req_body .. "' >> " .. options.debug_path) end
  116. end
  117. local linesrc = docker_socket:linesource()
  118. -- read socket using source http://w3.impa.br/~diego/software/luasocket/ltn12.html
  119. --http://lua-users.org/wiki/FiltersSourcesAndSinks
  120. -- handle response header
  121. local line = linesrc()
  122. if not line then
  123. docker_socket:close()
  124. return {
  125. headers = {code=499, message="bad socket path", protocol="HTTP/1.1"},
  126. body = {message="no data receive from socket"}
  127. }
  128. end
  129. local response = {code = 0, headers = {}, body = {}}
  130. local p, code, msg = line:match("^([%w./]+) ([0-9]+) (.*)")
  131. response.protocol = p
  132. response.code = tonumber(code)
  133. response.message = msg
  134. line = linesrc()
  135. while line and line ~= "" do
  136. local key, val = line:match("^([%w-]+)%s?:%s?(.*)")
  137. if key and key ~= "Status" then
  138. if type(response.headers[key]) == "string" then
  139. response.headers[key] = {response.headers[key], val}
  140. elseif type(response.headers[key]) == "table" then
  141. response.headers[key][#response.headers[key] + 1] = val
  142. else
  143. response.headers[key] = val
  144. end
  145. end
  146. line = linesrc()
  147. end
  148. -- handle response body
  149. local body_buffer = linesrc(true)
  150. response.body = {}
  151. if type(callback) ~= "function" then
  152. if response.headers["Transfer-Encoding"] == "chunked" then
  153. local source = chunksource(docker_socket, body_buffer)
  154. code = ltn12.pump.all(source, (ltn12.sink.table(response.body))) and response.code or 555
  155. response.code = code
  156. else
  157. local body_source = ltn12.source.cat(ltn12.source.string(body_buffer), docker_socket:blocksource())
  158. code = ltn12.pump.all(body_source, (ltn12.sink.table(response.body))) and response.code or 555
  159. response.code = code
  160. end
  161. else
  162. if response.headers["Transfer-Encoding"] == "chunked" then
  163. local source = chunksource(docker_socket, body_buffer)
  164. callback(response, source)
  165. else
  166. local body_source = ltn12.source.cat(ltn12.source.string(body_buffer), docker_socket:blocksource())
  167. callback(response, body_source)
  168. end
  169. end
  170. docker_socket:close()
  171. return response
  172. end
  173. local gen_header = function(options, http_method, api_group, api_action, name_or_id, request)
  174. local header, query, path
  175. name_or_id = (name_or_id ~= "") and name_or_id or nil
  176. if request and type(request.query) == "table" then
  177. local k, v
  178. for k, v in pairs(request.query) do
  179. if type(v) == "table" then
  180. query = (query and query .. "&" or "?") .. k .. "=" .. urlencode(json_stringify(v))
  181. elseif type(v) == "boolean" then
  182. query = (query and query .. "&" or "?") .. k .. "=" .. (v and "true" or "false")
  183. elseif type(v) == "number" or type(v) == "string" then
  184. query = (query and query .. "&" or "?") .. k .. "=" .. v
  185. end
  186. end
  187. end
  188. path = (api_group and ("/" .. api_group) or "") .. (name_or_id and ("/" .. name_or_id) or "") .. (api_action and ("/" .. api_action) or "") .. (query or "")
  189. header = (http_method or "GET") .. " " .. path .. " " .. options.protocol .. "\r\n"
  190. header = header .. "Host: " .. options.host .. "\r\n"
  191. header = header .. "User-Agent: " .. options.user_agent .. "\r\n"
  192. header = header .. "Connection: close\r\n"
  193. if request and type(request.header) == "table" then
  194. local k, v
  195. for k, v in pairs(request.header) do
  196. header = header .. k .. ": " .. v .. "\r\n"
  197. end
  198. end
  199. -- when requst_body is function, we need to custom header using custom header
  200. if request and request.body and type(request.body) == "function" then
  201. if not header:match("Content-Length:") then
  202. header = header .. "Transfer-Encoding: chunked\r\n"
  203. end
  204. elseif http_method == "POST" and request and request.body and type(request.body) == "table" then
  205. local conetnt_json = json_stringify(request.body)
  206. header = header .. "Content-Type: application/json\r\n"
  207. header = header .. "Content-Length: " .. #conetnt_json .. "\r\n"
  208. elseif request and request.body and type(request.body) == "string" then
  209. header = header .. "Content-Length: " .. #request.body .. "\r\n"
  210. end
  211. header = header .. "\r\n"
  212. if options.debug then io.popen("echo '".. header .. "' >> " .. options.debug_path) end
  213. return header
  214. end
  215. local call_docker = function(options, http_method, api_group, api_action, name_or_id, request, callback)
  216. local req_options = setmetatable({}, {__index = options})
  217. local req_header = gen_header(req_options, http_method, api_group, api_action, name_or_id, request)
  218. local req_body = request and request.body or nil
  219. local docker_socket = open_socket(req_options)
  220. if docker_socket then
  221. return send_http_socket(docker_socket, req_header, req_body, callback)
  222. else
  223. return {
  224. headers = {code=497, message="bad socket path or host", protocol="HTTP/1.1"},
  225. body = {message="can\'t connect to socket"}
  226. }
  227. end
  228. end
  229. local gen_api = function(_table, http_method, api_group, api_action)
  230. local _api_action
  231. if api_action == "get_archive" or api_action == "put_archive" then
  232. _api_action = "archive"
  233. elseif api_action == "df" then
  234. _api_action = "system/df"
  235. elseif api_action ~= "list" and api_action ~= "inspect" and api_action ~= "remove" then
  236. _api_action = api_action
  237. elseif (api_group == "containers" or api_group == "images" or api_group == "exec") and (api_action == "list" or api_action == "inspect") then
  238. _api_action = "json"
  239. end
  240. local fp = function(self, request, callback)
  241. local name_or_id = request and (request.name or request.id or request.name_or_id) or nil
  242. if api_action == "list" then
  243. if (name_or_id ~= "" and name_or_id ~= nil) then
  244. if api_group == "images" then
  245. name_or_id = nil
  246. else
  247. request.query = request and request.query or {}
  248. request.query.filters = request.query.filters or {}
  249. request.query.filters.name = request.query.filters.name or {}
  250. request.query.filters.name[#request.query.filters.name + 1] = name_or_id
  251. name_or_id = nil
  252. end
  253. end
  254. elseif api_action == "create" then
  255. if (name_or_id ~= "" and name_or_id ~= nil) then
  256. request.query = request and request.query or {}
  257. request.query.name = request.query.name or name_or_id
  258. name_or_id = nil
  259. end
  260. elseif api_action == "logs" then
  261. local body_buffer = ""
  262. local response = call_docker(self.options, http_method, api_group, _api_action, name_or_id, request, callback)
  263. if response.code >= 200 and response.code < 300 then
  264. for i, v in ipairs(response.body) do
  265. body_buffer = body_buffer .. docker_stream_filter(response.body[i])
  266. end
  267. response.body = body_buffer
  268. end
  269. return response
  270. end
  271. local response = call_docker(self.options, http_method, api_group, _api_action, name_or_id, request, callback)
  272. if response.headers and response.headers["Content-Type"] == "application/json" then
  273. if #response.body == 1 then
  274. response.body = json_parse(response.body[1])
  275. else
  276. local tmp = {}
  277. for _, v in ipairs(response.body) do
  278. tmp[#tmp+1] = json_parse(v)
  279. end
  280. response.body = tmp
  281. end
  282. end
  283. return response
  284. end
  285. if api_group then
  286. _table[api_group][api_action] = fp
  287. else
  288. _table[api_action] = fp
  289. end
  290. end
  291. local _docker = {containers = {}, exec = {}, images = {}, networks = {}, volumes = {}}
  292. gen_api(_docker, "GET", "containers", "list")
  293. gen_api(_docker, "POST", "containers", "create")
  294. gen_api(_docker, "GET", "containers", "inspect")
  295. gen_api(_docker, "GET", "containers", "top")
  296. gen_api(_docker, "GET", "containers", "logs")
  297. gen_api(_docker, "GET", "containers", "changes")
  298. gen_api(_docker, "GET", "containers", "stats")
  299. gen_api(_docker, "POST", "containers", "resize")
  300. gen_api(_docker, "POST", "containers", "start")
  301. gen_api(_docker, "POST", "containers", "stop")
  302. gen_api(_docker, "POST", "containers", "restart")
  303. gen_api(_docker, "POST", "containers", "kill")
  304. gen_api(_docker, "POST", "containers", "update")
  305. gen_api(_docker, "POST", "containers", "rename")
  306. gen_api(_docker, "POST", "containers", "pause")
  307. gen_api(_docker, "POST", "containers", "unpause")
  308. gen_api(_docker, "POST", "containers", "update")
  309. gen_api(_docker, "DELETE", "containers", "remove")
  310. gen_api(_docker, "POST", "containers", "prune")
  311. gen_api(_docker, "POST", "containers", "exec")
  312. gen_api(_docker, "POST", "exec", "start")
  313. gen_api(_docker, "POST", "exec", "resize")
  314. gen_api(_docker, "GET", "exec", "inspect")
  315. gen_api(_docker, "GET", "containers", "get_archive")
  316. gen_api(_docker, "PUT", "containers", "put_archive")
  317. -- TODO: export,attch
  318. gen_api(_docker, "GET", "images", "list")
  319. gen_api(_docker, "POST", "images", "create")
  320. gen_api(_docker, "GET", "images", "inspect")
  321. gen_api(_docker, "GET", "images", "history")
  322. gen_api(_docker, "POST", "images", "tag")
  323. gen_api(_docker, "DELETE", "images", "remove")
  324. gen_api(_docker, "GET", "images", "search")
  325. gen_api(_docker, "POST", "images", "prune")
  326. gen_api(_docker, "GET", "images", "get")
  327. gen_api(_docker, "POST", "images", "load")
  328. gen_api(_docker, "GET", "networks", "list")
  329. gen_api(_docker, "GET", "networks", "inspect")
  330. gen_api(_docker, "DELETE", "networks", "remove")
  331. gen_api(_docker, "POST", "networks", "create")
  332. gen_api(_docker, "POST", "networks", "connect")
  333. gen_api(_docker, "POST", "networks", "disconnect")
  334. gen_api(_docker, "POST", "networks", "prune")
  335. gen_api(_docker, "GET", "volumes", "list")
  336. gen_api(_docker, "GET", "volumes", "inspect")
  337. gen_api(_docker, "DELETE", "volumes", "remove")
  338. gen_api(_docker, "POST", "volumes", "create")
  339. gen_api(_docker, "GET", nil, "events")
  340. gen_api(_docker, "GET", nil, "version")
  341. gen_api(_docker, "GET", nil, "info")
  342. gen_api(_docker, "GET", nil, "_ping")
  343. gen_api(_docker, "GET", nil, "df")
  344. function _docker.new(options)
  345. local docker = {}
  346. local _options = options or {}
  347. docker.options = {
  348. socket_path = _options.socket_path or nil,
  349. host = _options.socket_path and "localhost" or _options.host,
  350. port = not _options.socket_path and _options.port or nil,
  351. tls = _options.tls or nil,
  352. tls_cacert = _options.tls and _options.tls_cacert or nil,
  353. tls_cert = _options.tls and _options.tls_cert or nil,
  354. tls_key = _options.tls and _options.tls_key or nil,
  355. version = _options.version or "v1.40",
  356. user_agent = _options.user_agent or "LuCI",
  357. protocol = _options.protocol or "HTTP/1.1",
  358. debug = _options.debug or false,
  359. debug_path = _options.debug and _options.debug_path or nil
  360. }
  361. setmetatable(
  362. docker,
  363. {
  364. __index = function(t, key)
  365. if _docker[key] ~= nil then
  366. return _docker[key]
  367. else
  368. return _docker.containers[key]
  369. end
  370. end
  371. }
  372. )
  373. setmetatable(
  374. docker.containers,
  375. {
  376. __index = function(t, key)
  377. if key == "options" then
  378. return docker.options
  379. end
  380. end
  381. }
  382. )
  383. setmetatable(
  384. docker.networks,
  385. {
  386. __index = function(t, key)
  387. if key == "options" then
  388. return docker.options
  389. end
  390. end
  391. }
  392. )
  393. setmetatable(
  394. docker.images,
  395. {
  396. __index = function(t, key)
  397. if key == "options" then
  398. return docker.options
  399. end
  400. end
  401. }
  402. )
  403. setmetatable(
  404. docker.volumes,
  405. {
  406. __index = function(t, key)
  407. if key == "options" then
  408. return docker.options
  409. end
  410. end
  411. }
  412. )
  413. setmetatable(
  414. docker.exec,
  415. {
  416. __index = function(t, key)
  417. if key == "options" then
  418. return docker.options
  419. end
  420. end
  421. }
  422. )
  423. return docker
  424. end
  425. return _docker