docker.lua 15 KB

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