diff options
Diffstat (limited to 'collections/luci-lib-docker/luasrc/docker.lua')
-rw-r--r-- | collections/luci-lib-docker/luasrc/docker.lua | 786 |
1 files changed, 435 insertions, 351 deletions
diff --git a/collections/luci-lib-docker/luasrc/docker.lua b/collections/luci-lib-docker/luasrc/docker.lua index 0361c05382..6b14626e5d 100644 --- a/collections/luci-lib-docker/luasrc/docker.lua +++ b/collections/luci-lib-docker/luasrc/docker.lua @@ -2,6 +2,7 @@ LuCI - Lua Configuration Interface Copyright 2019 lisaac <https://github.com/lisaac/luci-lib-docker> ]]-- + require "nixio.util" require "luci.util" local jsonc = require "luci.jsonc" @@ -14,301 +15,376 @@ local json_stringify = jsonc.stringify local json_parse = jsonc.parse local chunksource = function(sock, buffer) - buffer = buffer or "" - return function() - local output - local _, endp, count = buffer:find("^([0-9a-fA-F]+)\r\n") - if not count then - local newblock, code = sock:recv(1024) - if not newblock then return nil, code end - buffer = buffer .. newblock - _, endp, count = buffer:find("^([0-9a-fA-F]+)\r\n") - end - count = tonumber(count, 16) - if not count then - return nil, -1, "invalid encoding" - elseif count == 0 then -- finial - return nil - elseif count <= #buffer - endp then - --data >= count - output = buffer:sub(endp + 1, endp + count) - if count == #buffer - endp then -- [data] - buffer = buffer:sub(endp + count + 1) - count, code = sock:recvall(2) --read \r\n - if not count then return nil, code end - elseif count + 1 == #buffer - endp then -- [data]\r - buffer = buffer:sub(endp + count + 2) - count, code = sock:recvall(1) --read \n - if not count then return nil, code end - else -- [data]\r\n[count]\r\n[data]... - buffer = buffer:sub(endp + count + 3) -- cut buffer - end - return output - else - -- data < count - output = buffer:sub(endp + 1, endp + count) - buffer = buffer:sub(endp + count + 1) - local remain, code = sock:recvall(count - #output) --need read remaining - if not remain then return nil, code end - output = output .. remain - count, code = sock:recvall(2) --read \r\n - if not count then return nil, code end - return output - end - end + buffer = buffer or "" + + return function() + local output + local _, endp, count = buffer:find("^([0-9a-fA-F]+)\r\n") + + if not count then + local newblock, code = sock:recv(1024) + if not newblock then + return nil, code + end + buffer = buffer .. newblock + _, endp, count = buffer:find("^([0-9a-fA-F]+)\r\n") + end + + count = tonumber(count, 16) + if not count then + return nil, -1, "invalid encoding" + elseif count == 0 then -- finial + return nil + elseif count <= #buffer - endp then -- data >= count + output = buffer:sub(endp + 1, endp + count) + if count == #buffer - endp then -- [data] + buffer = buffer:sub(endp + count + 1) + count, code = sock:recvall(2) --read \r\n + if not count then + return nil, code + end + elseif count + 1 == #buffer - endp then -- [data]\r + buffer = buffer:sub(endp + count + 2) + count, code = sock:recvall(1) --read \n + if not count then + return nil, code + end + else -- [data]\r\n[count]\r\n[data]... + buffer = buffer:sub(endp + count + 3) -- cut buffer + end + return output + else -- data < count + output = buffer:sub(endp + 1, endp + count) + buffer = buffer:sub(endp + count + 1) + local remain, code = sock:recvall(count - #output) --need read remaining + if not remain then + return nil, code + end + output = output .. remain + count, code = sock:recvall(2) --read \r\n + if not count then + return nil, code + end + return output + end + end end local chunksink = function (sock) - return function(chunk, err) - if not chunk then - return sock:writeall("0\r\n\r\n") - else - return sock:writeall(("%X\r\n%s\r\n"):format(#chunk, tostring(chunk))) - end - end + return function(chunk, err) + if not chunk then + return sock:writeall("0\r\n\r\n") + else + return sock:writeall(("%X\r\n%s\r\n"):format(#chunk, tostring(chunk))) + end + end end local docker_stream_filter = function(buffer) - buffer = buffer or "" - if #buffer < 8 then - return "" - end - 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" - 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)) - if valid_length > #buffer + 8 then - return "" - end - return stream_type .. ": " .. string.sub(buffer, 9, valid_length + 8) - -- return string.sub(buffer, 9, valid_length + 8) + buffer = buffer or "" + if #buffer < 8 then + return "" + end + 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" + 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)) + if valid_length > #buffer + 8 then + return "" + end + return stream_type .. ": " .. string.sub(buffer, 9, valid_length + 8) end local open_socket = function(req_options) - local socket - if type(req_options) ~= "table" then return socket end - if req_options.socket_path then - socket = nixio.socket("unix", "stream") - if socket:connect(req_options.socket_path) ~= true then return nil end - elseif req_options.host and req_options.port then - socket = nixio.connect(req_options.host, req_options.port) - end - if socket then - return socket - else - return nil - end + local socket + if type(req_options) ~= "table" then + return socket + end + if req_options.socket_path then + socket = nixio.socket("unix", "stream") + if socket:connect(req_options.socket_path) ~= true then + return nil + end + elseif req_options.host and req_options.port then + socket = nixio.connect(req_options.host, req_options.port) + end + if socket then + return socket + else + return nil + end end local send_http_socket = function(docker_socket, req_header, req_body, callback) - if docker_socket:send(req_header) == 0 then - return { - headers={code=498,message="bad path", protocol="HTTP/1.1"}, - body={message="can\'t send data to socket"} - } - end - - if req_body and type(req_body) == "function" and req_header and req_header:match("chunked") then - -- chunked send - req_body(chunksink(docker_socket)) - elseif req_body and type(req_body) == "function" then - -- normal send by req_body function - req_body(docker_socket) - elseif req_body and type(req_body) == "table" then - -- json - docker_socket:send(json_stringify(req_body)) - if options.debug then io.popen("echo '".. json_stringify(req_body) .. "' >> " .. options.debug_path) end - elseif req_body then - docker_socket:send(req_body) - if options.debug then io.popen("echo '".. req_body .. "' >> " .. options.debug_path) end - end - - local linesrc = docker_socket:linesource() - -- read socket using source http://w3.impa.br/~diego/software/luasocket/ltn12.html - --http://lua-users.org/wiki/FiltersSourcesAndSinks - -- handle response header - local line = linesrc() - if not line then - docker_socket:close() - return { - headers = {code=499, message="bad socket path", protocol="HTTP/1.1"}, - body = {message="no data receive from socket"} - } - end - local response = {code = 0, headers = {}, body = {}} - - local p, code, msg = line:match("^([%w./]+) ([0-9]+) (.*)") - response.protocol = p - response.code = tonumber(code) - response.message = msg - line = linesrc() - while line and line ~= "" do - local key, val = line:match("^([%w-]+)%s?:%s?(.*)") - if key and key ~= "Status" then - if type(response.headers[key]) == "string" then - response.headers[key] = {response.headers[key], val} - elseif type(response.headers[key]) == "table" then - response.headers[key][#response.headers[key] + 1] = val - else - response.headers[key] = val - end - end - line = linesrc() - end - -- handle response body - local body_buffer = linesrc(true) - response.body = {} - if type(callback) ~= "function" then - if response.headers["Transfer-Encoding"] == "chunked" then - local source = chunksource(docker_socket, body_buffer) - code = ltn12.pump.all(source, (ltn12.sink.table(response.body))) and response.code or 555 - response.code = code - else - local body_source = ltn12.source.cat(ltn12.source.string(body_buffer), docker_socket:blocksource()) - code = ltn12.pump.all(body_source, (ltn12.sink.table(response.body))) and response.code or 555 - response.code = code - end - else - if response.headers["Transfer-Encoding"] == "chunked" then - local source = chunksource(docker_socket, body_buffer) - callback(response, source) - else - local body_source = ltn12.source.cat(ltn12.source.string(body_buffer), docker_socket:blocksource()) - callback(response, body_source) - end - end - docker_socket:close() - return response + if docker_socket:send(req_header) == 0 then + return { + headers={ + code=498, + message="bad path", + protocol="HTTP/1.1" + }, + body={ + message="can\'t send data to socket" + } + } + end + + if req_body and type(req_body) == "function" and req_header and req_header:match("chunked") then + -- chunked send + req_body(chunksink(docker_socket)) + elseif req_body and type(req_body) == "function" then + -- normal send by req_body function + req_body(docker_socket) + elseif req_body and type(req_body) == "table" then + -- json + docker_socket:send(json_stringify(req_body)) + if options.debug then + io.popen("echo '".. json_stringify(req_body) .. "' >> " .. options.debug_path) + end + elseif req_body then + docker_socket:send(req_body) + if options.debug then + io.popen("echo '".. req_body .. "' >> " .. options.debug_path) + end + end + + local linesrc = docker_socket:linesource() + -- read socket using source http://w3.impa.br/~diego/software/luasocket/ltn12.html + -- http://lua-users.org/wiki/FiltersSourcesAndSinks + -- handle response header + local line = linesrc() + if not line then + docker_socket:close() + return { + headers = { + code=499, + message="bad socket path", + protocol="HTTP/1.1" + }, + body = { + message="no data receive from socket" + } + } + end + + local response = { + code = 0, + headers = {}, + body = {} + } + + local p, code, msg = line:match("^([%w./]+) ([0-9]+) (.*)") + response.protocol = p + response.code = tonumber(code) + response.message = msg + line = linesrc() + + while line and line ~= "" do + local key, val = line:match("^([%w-]+)%s?:%s?(.*)") + if key and key ~= "Status" then + if type(response.headers[key]) == "string" then + response.headers[key] = { + response.headers[key], + val + } + elseif type(response.headers[key]) == "table" then + response.headers[key][#response.headers[key] + 1] = val + else + response.headers[key] = val + end + end + line = linesrc() + end + + -- handle response body + local body_buffer = linesrc(true) + response.body = {} + + if type(callback) ~= "function" then + if response.headers["Transfer-Encoding"] == "chunked" then + local source = chunksource(docker_socket, body_buffer) + code = ltn12.pump.all(source, (ltn12.sink.table(response.body))) and response.code or 555 + response.code = code + else + local body_source = ltn12.source.cat(ltn12.source.string(body_buffer), docker_socket:blocksource()) + code = ltn12.pump.all(body_source, (ltn12.sink.table(response.body))) and response.code or 555 + response.code = code + end + else + if response.headers["Transfer-Encoding"] == "chunked" then + local source = chunksource(docker_socket, body_buffer) + callback(response, source) + else + local body_source = ltn12.source.cat(ltn12.source.string(body_buffer), docker_socket:blocksource()) + callback(response, body_source) + end + end + docker_socket:close() + return response end local gen_header = function(options, http_method, api_group, api_action, name_or_id, request) - local header, query, path - name_or_id = (name_or_id ~= "") and name_or_id or nil - - if request and type(request.query) == "table" then - local k, v - for k, v in pairs(request.query) do - if type(v) == "table" then - query = (query and query .. "&" or "?") .. k .. "=" .. urlencode(json_stringify(v)) - elseif type(v) == "boolean" then - query = (query and query .. "&" or "?") .. k .. "=" .. (v and "true" or "false") - elseif type(v) == "number" or type(v) == "string" then - query = (query and query .. "&" or "?") .. k .. "=" .. v - end - end - end - path = (api_group and ("/" .. api_group) or "") .. (name_or_id and ("/" .. name_or_id) or "") .. (api_action and ("/" .. api_action) or "") .. (query or "") - header = (http_method or "GET") .. " " .. path .. " " .. options.protocol .. "\r\n" - header = header .. "Host: " .. options.host .. "\r\n" - header = header .. "User-Agent: " .. options.user_agent .. "\r\n" - header = header .. "Connection: close\r\n" - - if request and type(request.header) == "table" then - local k, v - for k, v in pairs(request.header) do - header = header .. k .. ": " .. v .. "\r\n" - end - end - - -- when requst_body is function, we need to custom header using custom header - if request and request.body and type(request.body) == "function" then - if not header:match("Content-Length:") then - header = header .. "Transfer-Encoding: chunked\r\n" - end - elseif http_method == "POST" and request and request.body and type(request.body) == "table" then - local conetnt_json = json_stringify(request.body) - header = header .. "Content-Type: application/json\r\n" - header = header .. "Content-Length: " .. #conetnt_json .. "\r\n" - elseif request and request.body and type(request.body) == "string" then - header = header .. "Content-Length: " .. #request.body .. "\r\n" - end - header = header .. "\r\n" - if options.debug then io.popen("echo '".. header .. "' >> " .. options.debug_path) end - return header + local header, query, path + name_or_id = (name_or_id ~= "") and name_or_id or nil + + if request and type(request.query) == "table" then + local k, v + for k, v in pairs(request.query) do + if type(v) == "table" then + query = (query and query .. "&" or "?") .. k .. "=" .. urlencode(json_stringify(v)) + elseif type(v) == "boolean" then + query = (query and query .. "&" or "?") .. k .. "=" .. (v and "true" or "false") + elseif type(v) == "number" or type(v) == "string" then + query = (query and query .. "&" or "?") .. k .. "=" .. v + end + end + end + + path = (api_group and ("/" .. api_group) or "") .. (name_or_id and ("/" .. name_or_id) or "") .. (api_action and ("/" .. api_action) or "") .. (query or "") + header = (http_method or "GET") .. " " .. path .. " " .. options.protocol .. "\r\n" + header = header .. "Host: " .. options.host .. "\r\n" + header = header .. "User-Agent: " .. options.user_agent .. "\r\n" + header = header .. "Connection: close\r\n" + + if request and type(request.header) == "table" then + local k, v + for k, v in pairs(request.header) do + header = header .. k .. ": " .. v .. "\r\n" + end + end + + -- when requst_body is function, we need to custom header using custom header + if request and request.body and type(request.body) == "function" then + if not header:match("Content-Length:") then + header = header .. "Transfer-Encoding: chunked\r\n" + end + elseif http_method == "POST" and request and request.body and type(request.body) == "table" then + local conetnt_json = json_stringify(request.body) + header = header .. "Content-Type: application/json\r\n" + header = header .. "Content-Length: " .. #conetnt_json .. "\r\n" + elseif request and request.body and type(request.body) == "string" then + header = header .. "Content-Length: " .. #request.body .. "\r\n" + end + + header = header .. "\r\n" + if options.debug then + io.popen("echo '".. header .. "' >> " .. options.debug_path) + end + + return header end local call_docker = function(options, http_method, api_group, api_action, name_or_id, request, callback) - local req_options = setmetatable({}, {__index = options}) - - local req_header = gen_header(req_options, http_method, api_group, api_action, name_or_id, request) - local req_body = request and request.body or nil - local docker_socket = open_socket(req_options) - - if docker_socket then - return send_http_socket(docker_socket, req_header, req_body, callback) - else - return { - headers = {code=497, message="bad socket path or host", protocol="HTTP/1.1"}, - body = {message="can\'t connect to socket"} - } - end + local req_options = setmetatable({}, { + __index = options + }) + + local req_header = gen_header(req_options, + http_method, + api_group, + api_action, + name_or_id, + request) + + local req_body = request and request.body or nil + local docker_socket = open_socket(req_options) + + if docker_socket then + return send_http_socket(docker_socket, req_header, req_body, callback) + else + return { + headers = { + code=497, + message="bad socket path or host", + protocol="HTTP/1.1" + }, + body = { + message="can\'t connect to socket" + } + } + end end local gen_api = function(_table, http_method, api_group, api_action) - local _api_action - if api_action == "get_archive" or api_action == "put_archive" then - _api_action = "archive" - elseif api_action == "df" then - _api_action = "system/df" - elseif api_action ~= "list" and api_action ~= "inspect" and api_action ~= "remove" then - _api_action = api_action - elseif (api_group == "containers" or api_group == "images" or api_group == "exec") and (api_action == "list" or api_action == "inspect") then - _api_action = "json" - end - - local fp = function(self, request, callback) - local name_or_id = request and (request.name or request.id or request.name_or_id) or nil - if api_action == "list" then - if (name_or_id ~= "" and name_or_id ~= nil) then - if api_group == "images" then - name_or_id = nil - else - request.query = request and request.query or {} - request.query.filters = request.query.filters or {} - request.query.filters.name = request.query.filters.name or {} - request.query.filters.name[#request.query.filters.name + 1] = name_or_id - name_or_id = nil - end - end - elseif api_action == "create" then - if (name_or_id ~= "" and name_or_id ~= nil) then - request.query = request and request.query or {} - request.query.name = request.query.name or name_or_id - name_or_id = nil - end - elseif api_action == "logs" then - local body_buffer = "" - local response = call_docker(self.options, http_method, api_group, _api_action, name_or_id, request, callback) - if response.code >= 200 and response.code < 300 then - for i, v in ipairs(response.body) do - body_buffer = body_buffer .. docker_stream_filter(response.body[i]) - end - response.body = body_buffer - end - return response - end - local response = call_docker(self.options, http_method, api_group, _api_action, name_or_id, request, callback) - if response.headers and response.headers["Content-Type"] == "application/json" then - if #response.body == 1 then - response.body = json_parse(response.body[1]) - else - local tmp = {} - for _, v in ipairs(response.body) do - tmp[#tmp+1] = json_parse(v) - end - response.body = tmp - end - end - return response - end - - if api_group then - _table[api_group][api_action] = fp - else - _table[api_action] = fp - end + local _api_action + + if api_action == "get_archive" or api_action == "put_archive" then + api_action = "archive" + elseif api_action == "df" then + _api_action = "system/df" + elseif api_action ~= "list" and api_action ~= "inspect" and api_action ~= "remove" then + _api_action = api_action + elseif (api_group == "containers" or api_group == "images" or api_group == "exec") and (api_action == "list" or api_action == "inspect") then + _api_action = "json" + end + + local fp = function(self, request, callback) + local name_or_id = request and (request.name or request.id or request.name_or_id) or nil + + if api_action == "list" then + if (name_or_id ~= "" and name_or_id ~= nil) then + if api_group == "images" then + name_or_id = nil + else + request.query = request and request.query or {} + request.query.filters = request.query.filters or {} + request.query.filters.name = request.query.filters.name or {} + request.query.filters.name[#request.query.filters.name + 1] = name_or_id + name_or_id = nil + end + end + elseif api_action == "create" then + if (name_or_id ~= "" and name_or_id ~= nil) then + request.query = request and request.query or {} + request.query.name = request.query.name or name_or_id + name_or_id = nil + end + elseif api_action == "logs" then + local body_buffer = "" + local response = call_docker(self.options, + http_method, + api_group, + _api_action, + name_or_id, + request, + callback) + if response.code >= 200 and response.code < 300 then + for i, v in ipairs(response.body) do + body_buffer = body_buffer .. docker_stream_filter(response.body[i]) + end + response.body = body_buffer + end + return response + end + + local response = call_docker(self.options, http_method, api_group, _api_action, name_or_id, request, callback) + + if response.headers and response.headers["Content-Type"] == "application/json" then + if #response.body == 1 then + response.body = json_parse(response.body[1]) + else + local tmp = {} + for _, v in ipairs(response.body) do + tmp[#tmp+1] = json_parse(v) + end + response.body = tmp + end + end + return response + end + + if api_group then + _table[api_group][api_action] = fp + else + _table[api_action] = fp + end end -local _docker = {containers = {}, exec = {}, images = {}, networks = {}, volumes = {}} +local _docker = { + containers = {}, + exec = {}, + images = {}, + networks = {}, + volumes = {} +} gen_api(_docker, "GET", "containers", "list") gen_api(_docker, "POST", "containers", "create") @@ -368,85 +444,93 @@ gen_api(_docker, "GET", nil, "_ping") gen_api(_docker, "GET", nil, "df") function _docker.new(options) - local docker = {} - local _options = options or {} - docker.options = { - socket_path = _options.socket_path or nil, - host = _options.socket_path and "localhost" or _options.host, - port = not _options.socket_path and _options.port or nil, - tls = _options.tls or nil, - tls_cacert = _options.tls and _options.tls_cacert or nil, - tls_cert = _options.tls and _options.tls_cert or nil, - tls_key = _options.tls and _options.tls_key or nil, - version = _options.version or "v1.40", - user_agent = _options.user_agent or "LuCI", - protocol = _options.protocol or "HTTP/1.1", - debug = _options.debug or false, - debug_path = _options.debug and _options.debug_path or nil - } - setmetatable( - docker, - { - __index = function(t, key) - if _docker[key] ~= nil then - return _docker[key] - else - return _docker.containers[key] - end - end - } - ) - setmetatable( - docker.containers, - { - __index = function(t, key) - if key == "options" then - return docker.options - end - end - } - ) - setmetatable( - docker.networks, - { - __index = function(t, key) - if key == "options" then - return docker.options - end - end - } - ) - setmetatable( - docker.images, - { - __index = function(t, key) - if key == "options" then - return docker.options - end - end - } - ) - setmetatable( - docker.volumes, - { - __index = function(t, key) - if key == "options" then - return docker.options - end - end - } - ) - setmetatable( - docker.exec, - { - __index = function(t, key) - if key == "options" then - return docker.options - end - end - } - ) - return docker + local docker = {} + local _options = options or {} + + docker.options = { + socket_path = _options.socket_path or nil, + host = _options.socket_path and "localhost" or _options.host, + port = not _options.socket_path and _options.port or nil, + tls = _options.tls or nil, + tls_cacert = _options.tls and _options.tls_cacert or nil, + tls_cert = _options.tls and _options.tls_cert or nil, + tls_key = _options.tls and _options.tls_key or nil, + version = _options.version or "v1.40", + user_agent = _options.user_agent or "LuCI", + protocol = _options.protocol or "HTTP/1.1", + debug = _options.debug or false, + debug_path = _options.debug and _options.debug_path or nil + } + + setmetatable( + docker, + { + __index = function(t, key) + if _docker[key] ~= nil then + return _docker[key] + else + return _docker.containers[key] + end + end + } + ) + + setmetatable( + docker.containers, + { + __index = function(t, key) + if key == "options" then + return docker.options + end + end + } + ) + + setmetatable( + docker.networks, + { + __index = function(t, key) + if key == "options" then + return docker.options + end + end + } + ) + + setmetatable( + docker.images, + { + __index = function(t, key) + if key == "options" then + return docker.options + end + end + } + ) + + setmetatable( + docker.volumes, + { + __index = function(t, key) + if key == "options" then + return docker.options + end + end + } + ) + + setmetatable( + docker.exec, + { + __index = function(t, key) + if key == "options" then + return docker.options + end + end + } + ) + + return docker end return _docker |