kong/spec/helpers/perf/drivers/docker.lua (493 lines of code) (raw):

local nkeys = require "table.nkeys" local perf = require("spec.helpers.perf") local tools = require("kong.tools.utils") local helpers local _M = {} local mt = {__index = _M} local UPSTREAM_PORT = 18088 local KONG_DEFAULT_HYBRID_CERT = "/etc/kong-hybrid-cert.pem" local KONG_DEFAULT_HYBRID_CERT_KEY = "/etc/kong-hybrid-key.pem" function _M.new(opts) return setmetatable({ opts = opts, log = perf.new_logger("[docker]"), psql_ct_id = nil, kong_ct_ids = {}, worker_ct_id = nil, daily_image_desc = nil, }, mt) end local function start_container(cid) if not cid then return false, "container does not exist" end local _, err = perf.execute("docker start " .. cid) if err then return false, "docker start:" .. err end local out, err = perf.execute("docker inspect --format='{{.State.Running}}' " .. cid) if err then return false, "docker inspect:" .. err end if out:gsub("\n", "") ~= "true" then local out, err = perf.execute("docker logs -n5 " .. cid) if err then return false, "docker logs:" .. err end return false, out end return true end local function create_container(self, args, img, cmd) local out, err = perf.execute("docker images --format '{{.Repository}}:{{.Tag}}' " .. img) -- plain pattern find if err or not out:find(img, nil, true) then local _, err = perf.execute("docker pull " .. img, { logger = self.log.log_exec }) if err then return false, err end end args = args or "" cmd = cmd or "" out, err = perf.execute("docker create " .. args .. " " .. img .. " " .. cmd) if err then return false, err end local cid = out:match("^[a-f0-9]+$") if not cid then return false, "invalid container ID: " .. out end return cid end local function get_container_port(cid, ct_port) local out, err = perf.execute( "docker inspect " .. "--format='{{range $p, $conf := .NetworkSettings.Ports}}" .. "{{if eq $p \"" .. ct_port .. "\" }}{{(index $conf 0).HostPort}}{{end}}" .. "{{end}}' " .. cid) if err then return false, "docker inspect:" .. err .. ": " .. (out or "nil") end return tonumber(out) end local function get_container_vip(cid) local out, err = perf.execute("docker inspect --format='{{.NetworkSettings.Networks.bridge.IPAddress}}' " .. cid) if err then return false, "docker inspect:" .. err .. ": " .. (out or "nil") end return out end function _M:teardown() self.setup_kong_called = false local ct_ids = {"worker_ct_id", "psql_ct_id" } for _, cid in ipairs(ct_ids) do if self[cid] then perf.execute("docker rm -f " .. self[cid], { logger = self.log.log_exec }) self[cid] = nil end end for conf_id, kong_ct_id in pairs(self.kong_ct_ids) do perf.execute("docker rm -f " .. kong_ct_id, { logger = self.log.log_exec }) self.kong_ct_ids[conf_id] = nil end perf.git_restore() return true end local function prepare_spec_helpers(self, use_git, version) local psql_port, err = get_container_port(self.psql_ct_id, "5432/tcp") if not psql_port then return false, "failed to get psql port: " .. (err or "nil") end -- wait if not perf.wait_output("docker logs -f " .. self.psql_ct_id, "is ready to accept connections") then return false, "timeout waiting psql to start (5s)" end self.log.info("psql is started to listen at port ", psql_port) perf.setenv("KONG_PG_PORT", ""..psql_port) ngx.sleep(3) -- TODO: less flaky if not use_git then local current_spec_helpers_version = perf.get_kong_version(true) if current_spec_helpers_version ~= version then self.log.info("Current spec helpers version " .. current_spec_helpers_version .. " doesn't match with version to be tested " .. version .. ", checking out remote version") version = version:match("%d+%.%d+%.%d+") perf.git_checkout(version) -- throws end end -- reload the spec.helpers module, since it may have been loaded with -- a different set of env vars perf.clear_loaded_package() -- just to let spec.helpers happy, we are not going to start kong locally require("kong.meta")._DEPENDENCIES.nginx = {"0.0.0.0", "9.9.9.9"} helpers = require("spec.helpers") package.loaded['kong.meta'] = nil require("kong.meta") perf.unsetenv("KONG_PG_PORT") helpers.admin_client = function(timeout) if nkeys(self.kong_ct_ids) < 1 then error("helpers.admin_client can only be called after perf.start_kong") end -- find all kong containers with first one that exposes admin port for _, kong_id in pairs(kong.ct_ids) do local admin_port, err = get_container_port(kong_id, "8001/tcp") if err then error("failed to get kong admin port: " .. (err or "nil")) end if admin_port then return helpers.http_client("127.0.0.1", admin_port, timeout or 60000) end -- not admin_port, it's fine, maybe it's a dataplane end error("failed to get kong admin port from all Kong containers") end return helpers end function _M:setup() if not self.psql_ct_id then local cid, err = create_container(self, "-p5432 " .. "-e POSTGRES_HOST_AUTH_METHOD=trust -e POSTGRES_DB=kong_tests " .. "-e POSTGRES_USER=kong ", "postgres:11", "postgres -N 2333") if err then return false, "error running docker create when creating kong container: " .. err end self.psql_ct_id = cid end self.log.info("psql container ID is ", self.psql_ct_id) local ok, err = start_container(self.psql_ct_id) if not ok then return false, "psql is not running: " .. err end return true end function _M:start_worker(conf, port_count) conf = conf or [[ location = /test { return 200; } ]] local listeners = {} for i=1,port_count do listeners[i] = ("listen %d reuseport;"):format(UPSTREAM_PORT+i-1) end listeners = table.concat(listeners, "\n") if not self.worker_ct_id then local _, err = perf.execute( "docker build --progress plain -t perf-test-upstream -", { logger = self.log.log_exec, stdin = ([[ FROM nginx:alpine RUN apk update && apk add wrk RUN echo -e '\ server {\ %s\ access_log off;\ location =/health { \ return 200; \ } \ %s \ }' > /etc/nginx/conf.d/perf-test.conf # copy paste ENTRYPOINT ["/docker-entrypoint.sh"] STOPSIGNAL SIGQUIT CMD ["nginx", "-g", "daemon off;"] ]]):format(listeners:gsub("\n", "\\n"), conf:gsub("\n", "\\n")) } ) if err then return false, err end local cid, err = create_container(self, "-p " .. UPSTREAM_PORT, "perf-test-upstream") if err then return false, "error running docker create when creating upstream: " .. err end self.worker_ct_id = cid end self.log.info("worker container ID is ", self.worker_ct_id) local ok, err = start_container(self.worker_ct_id) if not ok then return false, "worker is not running: " .. err end ngx.sleep(3) -- TODO: less flaky local worker_vip, err = get_container_vip(self.worker_ct_id) if err then return false, "unable to read worker container's private IP: " .. err end if not perf.wait_output("docker logs -f " .. self.worker_ct_id, " start worker process") then self.log.info("worker container logs:") perf.execute("docker logs " .. self.worker_ct_id, { logger = self.log.log_exec }) return false, "timeout waiting worker(nginx) to start (5s)" end self.log.info("worker is started") local uris = {} for i=1,port_count do uris[i] = "http://" .. worker_vip .. ":" .. UPSTREAM_PORT+i-1 end return uris end function _M:setup_kong(version) local ok, err = _M.setup(self) if not ok then return ok, err end local git_repo_path self.daily_image_desc = nil if version:startswith("git:") then git_repo_path = perf.git_checkout(version:sub(#("git:")+1)) version = perf.get_kong_version() if self.opts.use_daily_image then self.kong_image = "kong/kong:master-nightly-ubuntu" perf.execute("docker pull " .. self.kong_image, { logger = self.log.log_exec }) local manifest, err = perf.execute("docker inspect " .. self.kong_image) if err then return nil, "failed to inspect daily image: " .. err end local labels, err = perf.parse_docker_image_labels(manifest) if err then return nil, "failed to use parse daily image manifest: " .. err end self.log.debug("daily image " .. labels.version .." was pushed at ", labels.created) self.daily_image_desc = labels.version .. ", " .. labels.created else self.kong_image = "kong:" .. version end self.log.debug("current git hash resolves to docker version ", version) elseif version:match("rc") or version:match("beta") then self.kong_image = "kong/kong:" .. version else self.kong_image = "kong:" .. version end self.git_repo_path = git_repo_path local docker_args = "--link " .. self.psql_ct_id .. ":postgres " .. "-e KONG_PG_HOST=postgres " .. "-e KONG_PG_DATABASE=kong_tests " local _, err = perf.execute("docker run --rm " .. docker_args .. " " .. self.kong_image .. " kong migrations bootstrap", { logger = self.log.log_exec }) if err then return nil, "error running initial migration: " .. err end self.setup_kong_called = true return prepare_spec_helpers(self, git_repo_path, version) end function _M:start_kong(kong_conf, driver_conf) if not self.setup_kong_called then return false, "setup_kong() must be called before start_kong()" end local kong_name = driver_conf.name or 'default' if not driver_conf.ports then driver_conf.ports = { 8000 } end if self.kong_ct_ids[kong_name] == nil then if not kong_conf['cluster_cert'] then kong_conf['cluster_cert'] = KONG_DEFAULT_HYBRID_CERT kong_conf['cluster_cert_key'] = KONG_DEFAULT_HYBRID_CERT_KEY end local docker_args = "--name kong_perf_kong_$(date +%s)_" .. kong_name .. " " for k, v in pairs(kong_conf) do docker_args = docker_args .. string.format("-e KONG_%s=%s ", k:upper(), v) end docker_args = docker_args .. "-e KONG_PROXY_ACCESS_LOG=/dev/null " -- adds database configuration if kong_conf['database'] == nil then docker_args = docker_args .. "--link " .. self.psql_ct_id .. ":postgres " .. "-e KONG_PG_HOST=postgres " .. "-e KONG_PG_DATABASE=kong_tests " end -- link to other kong instances for name, ctid in pairs(self.kong_ct_ids) do docker_args = docker_args .. string.format("--link %s:%s ", ctid, name) end for _, port in ipairs(driver_conf.ports) do docker_args = docker_args .. string.format("-p %d ", port) end local cid, err = create_container(self, docker_args, self.kong_image, "/bin/bash -c 'kong migrations bootstrap; kong migrations up -y; kong migrations finish -y; /docker-entrypoint.sh kong docker-start'") if err then return false, "error running docker create when creating kong container: " .. err end self.kong_ct_ids[kong_name] = cid perf.execute("docker cp ./spec/fixtures/kong_clustering.crt " .. cid .. ":" .. KONG_DEFAULT_HYBRID_CERT) perf.execute("docker cp ./spec/fixtures/kong_clustering.key " .. cid .. ":" .. KONG_DEFAULT_HYBRID_CERT_KEY) if self.git_repo_path then perf.execute("docker exec --user=root " .. cid .. " find /usr/local/openresty/site/lualib/kong/ -name '*.ljbc' -delete; true") perf.execute("docker cp " .. self.git_repo_path .. "/kong " .. cid .. ":/usr/local/share/lua/5.1/") end end self.log.info("starting kong container \"" .. kong_name .. "\" with ID ", self.kong_ct_ids[kong_name]) local ok, err = start_container(self.kong_ct_ids[kong_name]) if not ok then return false, "kong is not running: " .. err end -- wait if not perf.wait_output("docker logs -f " .. self.kong_ct_ids[kong_name], " start worker process", 30) then self.log.info("kong container logs:") perf.execute("docker logs " .. self.kong_ct_ids[kong_name], { logger = self.log.log_exec }) return false, "timeout waiting kong to start (5s)" end local ports = driver_conf.ports local port_maps = {} for _, port in ipairs(ports) do local mport, err = get_container_port(self.kong_ct_ids[kong_name], port .. "/tcp") if not mport then return false, "can't find exposed port " .. port .. " for kong " .. self.kong_ct_ids[kong_name] .. " :" .. err end table.insert(port_maps, string.format("%s->%s/tcp", mport, port)) end self.log.info("kong container \"" .. kong_name .. "\" is started to listen at port ", table.concat(port_maps, ", ")) return self.kong_ct_ids[kong_name] end function _M:stop_kong() for conf_id, kong_ct_id in pairs(self.kong_ct_ids) do local _, err = perf.execute("docker stop " .. kong_ct_id) if err then return false end end return true end function _M:get_start_load_cmd(stub, script, uri, kong_name) if not self.worker_ct_id then return false, "worker container is not started, 'start_worker' must be called first" end local kong_id if not uri then if not kong_name then -- find all kong containers with first one that exposes proxy port for name, ct_id in pairs(self.kong_ct_ids) do local admin_port, err = get_container_port(ct_id, "8000/tcp") if err then -- this is fine, it means this kong doesn't have a proxy port self.log.debug("failed to get kong proxy port for " .. ct_id .. ": " .. (err or "nil")) elseif admin_port then kong_id = ct_id self.log.info("automatically picked kong container \"", name, "\" with ID " .. ct_id .. " for proxy port") break end end if not kong_id then return false, "failed to find kong proxy port" end else kong_id = self.kong_ct_ids[kong_name] if not kong_id then return false, "kong container \"" .. kong_name .. "\" is not found" end end local kong_vip, err = get_container_vip(kong_id) if err then return false, "unable to read kong container's private IP: " .. err end uri = string.format("http://%s:8000", kong_vip) end local script_path if script then script_path = string.format("/tmp/wrk-%s.lua", tools.random_string()) local out, err = perf.execute(string.format( "docker exec -i %s tee %s", self.worker_ct_id, script_path), { stdin = script, }) if err then return false, "failed to write script in " .. self.worker_ct_id .. " container: " .. (out or err) end end script_path = script_path and ("-s " .. script_path) or "" return "docker exec " .. self.worker_ct_id .. " " .. stub:format(script_path, uri) end function _M:get_admin_uri(kong_name) local kong_id if not kong_name then -- find all kong containers with first one that exposes admin port for name, ct_id in pairs(self.kong_ct_ids) do local admin_port, err = get_container_port(ct_id, "8001/tcp") if err then -- this is fine, it means this kong doesn't have an admin port self.log.warn("failed to get kong admin port for " .. ct_id .. ": " .. (err or "nil")) elseif admin_port then kong_id = ct_id self.log.info("automatically picked kong container \"", name, "\" with ID " .. ct_id .. " for admin port") break end end if not kong_id then return nil, "failed to find kong admin port" end else kong_id = self.kong_ct_ids[kong_name] if not kong_id then return false, "kong container \"" .. kong_name .. "\" is not found" end end local kong_vip, err = get_container_vip(kong_id) if err then return false, "unable to read kong container's private IP: " .. err end return string.format("http://%s:8001", kong_vip) end function _M:get_start_stapxx_cmd() error("SystemTap support not yet implemented in docker driver") end function _M:get_wait_stapxx_cmd() error("SystemTap support not yet implemented in docker driver") end function _M:generate_flamegraph() error("SystemTap support not yet implemented in docker driver") end function _M:save_error_log(path) for _, kong_ct_id in pairs(self.kong_ct_ids) do local _, err = perf.execute("docker logs " .. kong_ct_id .. " 2>'" .. path .. "-" .. kong_ct_id .. "'", { logger = self.log.log_exec }) if err then return false, "failed to save error log for kong " .. kong_ct_id .. ": " .. err end end return true end function _M:save_pgdump(path) if not self.psql_ct_id then return false, "postgres container not started" end return perf.execute("docker exec -i " .. self.psql_ct_id .. " pg_dump -Ukong kong_tests --data-only >'" .. path .. "'", { logger = self.log.log_exec }) end function _M:load_pgdump(path, dont_patch_service) if not self.psql_ct_id then return false, "postgres container not started" end local _, err = perf.execute("cat " .. path .. " |docker exec -i " .. self.psql_ct_id .. " psql -Ukong kong_tests", { logger = self.log.log_exec }) if err then return false, err end if dont_patch_service then return true end if not self.worker_ct_id then return false, "worker not started, can't patch_service; call start_worker first" end local worker_vip, err = get_container_vip(self.worker_ct_id) if err then return false, "unable to read worker container's private IP: " .. err end return perf.execute("echo \"UPDATE services set host='" .. worker_vip .. "', port=" .. UPSTREAM_PORT .. ", protocol='http';\" | docker exec -i " .. self.psql_ct_id .. " psql -Ukong kong_tests", { logger = self.log.log_exec }) end function _M:get_based_version() return self.daily_image_desc or perf.get_kong_version() end function _M:remote_execute(node_type, cmds, continue_on_error) local ct_id if node_type == "kong" then ct_id = self.kong_ct_ids[next(self.kong_ct_ids)] elseif node_type == "worker" then ct_id = self.worker_ct_id elseif node_type == "db" then ct_id = self.psql_ct_id else return false, "unknown node type: " .. node_type end for _, cmd in ipairs(cmds) do local c = string.gsub(cmd, "'", "'\\''") local out, err = perf.execute("docker exec -i " .. ct_id .. " '" .. c .. "'", { logger = self.log.log_exec }) if err and not continue_on_error then return false, "failed to execute command: " .. cmd .. ": " .. (out or err) end end return true end return _M