local helpers = require "spec.helpers" local constants = require "kong.constants" local cjson = require "cjson" local lyaml = require "lyaml" local lfs = require "lfs" local shell = require "resty.shell" local function sort_by_name(a, b) return a.name < b.name end local function convert_yaml_nulls(tbl) for k,v in pairs(tbl) do if v == lyaml.null then tbl[k] = ngx.null elseif type(v) == "table" then convert_yaml_nulls(v) end end end describe("kong config", function() local bp, db lazy_setup(function() bp, db = helpers.get_db_utils(nil, {}) -- runs migrations end) after_each(function() helpers.kill_all() end) lazy_teardown(function() helpers.clean_prefix() end) it("config help", function() local _, stderr = helpers.kong_exec "config --help" assert.not_equal("", stderr) end) it("#db config imports a yaml file", function() assert(db.plugins:truncate()) assert(db.routes:truncate()) assert(db.services:truncate()) local dns_hostsfile = assert(os.tmpname() .. ".hosts") local fd = assert(io.open(dns_hostsfile, "w")) assert(fd:write("127.0.0.1 " .. constants.REPORTS.ADDRESS)) assert(fd:close()) local filename = helpers.make_yaml_file([[ _format_version: "1.1" services: - name: foo host: example.com protocol: https _comment: my comment _ignore: - foo: bar routes: - hosts: ['foo.test'] plugins: - name: key-auth _comment: my comment _ignore: - foo: bar - name: http-log config: http_endpoint: https://example.com - name: bar host: example.test port: 3000 routes: - hosts: ['bar.test'] plugins: - name: basic-auth - name: tcp-log config: port: 10000 host: 127.0.0.1 - name: rate-limiting config: minute: 200 policy: redis redis: host: 127.0.0.1 plugins: - name: correlation-id id: 467f719f-a544-4a8f-bc4b-7cd12913a9d4 config: header_name: null generator: "uuid" echo_downstream: false ]]) finally(function() os.remove(filename) os.remove(dns_hostsfile) end) assert(helpers.start_kong({ nginx_conf = "spec/fixtures/custom_nginx.template", dns_hostsfile = dns_hostsfile, resolver_hosts_file = dns_hostsfile, anonymous_reports = "on", })) local thread = helpers.tcp_server(constants.REPORTS.STATS_TLS_PORT, {tls=true}) assert(helpers.kong_exec("config db_import " .. filename, { prefix = helpers.test_conf.prefix, anonymous_reports = "on", })) local _, res = assert(thread:join()) assert.matches("signal=config-db-import", res, nil, true) -- it will be updated on-the-fly assert.matches("decl_fmt_version=3.0", res, nil, true) assert.matches("file_ext=.yml", res, nil, true) local client = helpers.admin_client() local res = client:get("/services/foo") assert.res_status(200, res) local res = client:get("/services/bar") assert.res_status(200, res) local res = client:get("/services/foo/plugins") local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equals(2, #json.data) local res = client:get("/services/bar/plugins") local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equals(3, #json.data) local res = client:get("/plugins/467f719f-a544-4a8f-bc4b-7cd12913a9d4") local body = assert.res_status(200, res) local json = cjson.decode(body) json.created_at = nil json.updated_at = nil json.protocols = nil assert.same({ name = "correlation-id", instance_name = ngx.null, id = "467f719f-a544-4a8f-bc4b-7cd12913a9d4", route = ngx.null, service = ngx.null, consumer = ngx.null, enabled = true, config = { header_name = ngx.null, generator = "uuid", echo_downstream = false, }, tags = ngx.null, }, json) assert(helpers.stop_kong()) end) pending("#db config db_import does not require Kong to be running", function() -- this actually sends data to the telemetry endpoint. TODO: how to avoid that? -- in this case we do not change the DNS hostsfile.. -- NetidState Recv-Q Send-Q Local Address:Port Peer Address:Port -- tcp ESTAB 0 216 172.23.0.4:35578 35.169.37.138:61830 -- this is the amazon splunk ip -- tcp ESTAB 0 0 172.23.0.4:40746 172.23.0.3:5432 local filename = helpers.make_yaml_file([[ _format_version: "1.1" services: - name: foobar host: example.com protocol: https _comment: my comment _ignore: - foo: bar routes: - hosts: ['foo.test'] plugins: - name: key-auth _comment: my comment _ignore: - foo: bar - name: http-log config: http_endpoint: https://example.com - name: bar host: example.test port: 3000 routes: - hosts: ['bar.test'] plugins: - name: basic-auth - name: tcp-log config: port: 10000 host: 127.0.0.1 ]]) assert(helpers.kong_exec("config db_import " .. filename, { prefix = helpers.test_conf.prefix, })) end) -- same as with "config db_import does not require Kong to be running" -- when no kong is present, we can't mock a response pending("#db config db_import deals with repeated targets", function() -- Since Kong 2.2.0 there's no more target history, but we must make sure -- that old configs still can be imported. local filename = helpers.make_yaml_file([[ _format_version: "1.1" _transform: false _format_version: '2.1' parameters: - created_at: ~ key: cluster_id value: 36ad7d46-b95c-44f6-a79e-edb1f33baaf7 upstreams: - hash_on_header: ~ algorithm: round-robin host_header: ~ hash_on_cookie: ~ created_at: 1618602527 hash_on_cookie_path: / hash_fallback: none hash_fallback_header: ~ healthchecks: active: https_verify_certificate: true http_path: / https_sni: ~ type: http concurrency: 10 healthy: interval: 0 http_statuses: - 200 - 302 successes: 0 unhealthy: http_failures: 0 http_statuses: - 429 - 404 - 500 - 501 - 502 - 503 - 504 - 505 interval: 0 tcp_failures: 0 timeouts: 0 timeout: 1 threshold: 0 passive: healthy: successes: 0 http_statuses: - 200 - 201 - 202 - 203 - 204 - 205 - 206 - 207 - 208 - 226 - 300 - 301 - 302 - 303 - 304 - 305 - 306 - 307 - 308 unhealthy: http_failures: 0 http_statuses: - 429 - 500 - 503 tcp_failures: 0 timeouts: 0 type: http slots: 10000 client_certificate: ~ name: upstreama hash_on: none tags: ~ id: ab0060c9-7830-415a-9a84-d2d5dd76a04c targets: - upstream: ab0060c9-7830-415a-9a84-d2d5dd76a04c target: 127.0.0.1:6664 created_at: 1618602543.967 weight: 50 tags: ~ id: d72fa60a-31d3-436a-a4cb-a35444618a7a - upstream: ab0060c9-7830-415a-9a84-d2d5dd76a04c target: 127.0.0.1:6664 created_at: 1618602544.967 weight: 100 tags: ~ id: d72fa60a-31d3-436a-a4cb-a35444618a7b - upstream: ab0060c9-7830-415a-9a84-d2d5dd76a04c target: 127.0.0.1:6661 created_at: 1618602534.682 weight: 100 tags: ~ id: fe590183-61a1-4b59-b77c-5d70835d9714 ]]) assert(helpers.kong_exec("config db_import " .. filename, { prefix = helpers.test_conf.prefix, })) end) it("#db config db_import catches errors in input", function() assert(helpers.start_kong({ nginx_conf = "spec/fixtures/custom_nginx.template", })) finally(function() helpers.stop_kong() end) local filename = helpers.make_yaml_file([[ _format_version: "1.1" services: - name: foobar host: [] port: -23 protocol: https _comment: my comment _ignore: - foo: bar routes: 123 ]]) local ok, err = helpers.kong_exec("config db_import " .. filename, { prefix = helpers.test_conf.prefix, }) assert.falsy(ok) assert.match("Error: Failed parsing:", err) assert.match("in 'host': expected a string", err) assert.match("in 'port': value should be between 0 and 65535", err) assert.match("in 'routes': expected an array", err) end) it("#db config db_import is idempotent based on endpoint_key and cache_key", function() assert(db.plugins:truncate()) assert(db.routes:truncate()) assert(db.services:truncate()) local filename = helpers.make_yaml_file([[ _format_version: "1.1" services: - name: foo url: http://example.com routes: - name: r1 hosts: ['foo.test'] plugins: - name: basic-auth - name: tcp-log config: port: 10000 host: 127.0.0.1 - name: bar url: https://example.org routes: - name: r2 hosts: ['bar.test'] plugins: - name: basic-auth - name: tcp-log config: port: 10000 host: 127.0.0.1 ]]) assert(helpers.start_kong({ nginx_conf = "spec/fixtures/custom_nginx.template", })) assert(helpers.kong_exec("config db_import " .. filename, { prefix = helpers.test_conf.prefix, })) local client = helpers.admin_client() local res = client:get("/routes") local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equals(2, #json.data) res = client:get("/services/foo") assert.res_status(200, res) res = client:get("/services/bar") assert.res_status(200, res) res = client:get("/services/foo/plugins") body = assert.res_status(200, res) json = cjson.decode(body) assert.equals(2, #json.data) res = client:get("/services/bar/plugins") body = assert.res_status(200, res) json = cjson.decode(body) assert.equals(2, #json.data) res = client:get("/plugins") body = assert.res_status(200, res) json = cjson.decode(body) assert.equals(4, #json.data) assert(helpers.kong_exec("config db_import " .. filename, { prefix = helpers.test_conf.prefix, })) client = helpers.admin_client() res = client:get("/routes") body = assert.res_status(200, res) json = cjson.decode(body) assert.equals(2, #json.data) res = client:get("/services/foo") assert.res_status(200, res) res = client:get("/services/bar") assert.res_status(200, res) res = client:get("/services/foo/plugins") body = assert.res_status(200, res) json = cjson.decode(body) assert.equals(2, #json.data) res = client:get("/services/bar/plugins") body = assert.res_status(200, res) json = cjson.decode(body) assert.equals(2, #json.data) res = client:get("/plugins") body = assert.res_status(200, res) json = cjson.decode(body) assert.equals(4, #json.data) assert(helpers.stop_kong()) end) it("#db config db_import is not idempotent when endpoint_key is not used", function() assert(db.plugins:truncate()) assert(db.routes:truncate()) assert(db.services:truncate()) -- note that routes have no name local filename = helpers.make_yaml_file([[ _format_version: "1.1" services: - name: foo url: https://example.com routes: - hosts: ['foo.test'] - name: bar url: https://example.com routes: - hosts: ['bar.test'] ]]) assert(helpers.start_kong({ nginx_conf = "spec/fixtures/custom_nginx.template", })) assert(helpers.kong_exec("config db_import " .. filename, { prefix = helpers.test_conf.prefix, })) local client = helpers.admin_client() local res = client:get("/routes") local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equals(2, #json.data) assert(helpers.kong_exec("config db_import " .. filename, { prefix = helpers.test_conf.prefix, })) client = helpers.admin_client() res = client:get("/routes") body = assert.res_status(200, res) json = cjson.decode(body) assert.equals(4, #json.data) assert(helpers.stop_kong()) end) it("#db config db_export exports a yaml file", function() assert(db.plugins:truncate()) assert(db.routes:truncate()) assert(db.services:truncate()) assert(db.consumers:truncate()) assert(db.acls:truncate()) assert(db.certificates:truncate()) assert(db.ca_certificates:truncate()) assert(db.targets:truncate()) assert(db.upstreams:truncate()) assert(db.keys:truncate()) assert(db.key_sets:truncate()) local filename = os.tmpname() os.remove(filename) filename = filename .. ".yml" -- starting kong just so the prefix is properly initialized assert(helpers.start_kong()) local service1 = bp.services:insert({ name = "service1" }, { nulls = true }) local route1 = bp.routes:insert({ service = service1, methods = { "POST" }, name = "a" }, { nulls = true }) local plugin1 = bp.hmac_auth_plugins:insert({ service = service1, }, { nulls = true }) local plugin2 = bp.key_auth_plugins:insert({ service = service1, }, { nulls = true }) local service2 = bp.services:insert({ name = "service2" }, { nulls = true }) local route2 = bp.routes:insert({ service = service2, methods = { "GET" }, name = "b" }, { nulls = true }) local plugin3 = bp.rate_limiting_plugins:insert({ service = service2, config = { minute = 100, policy = "redis", redis = { host = "localhost" } } }, { nulls = true }) local plugin4 = bp.tcp_log_plugins:insert({ service = service2, }, { nulls = true }) local consumer = bp.consumers:insert(nil, { nulls = true }) local acls = bp.acls:insert({ consumer = consumer }, { nulls = true }) local keyauth = bp.keyauth_credentials:insert({ consumer = consumer, key = "hello" }, { nulls = true }) local keyset = db.key_sets:insert { name = "testing keyset" } local pem_pub, pem_priv = helpers.generate_keys("PEM") local pem_key = db.keys:insert { name = "vault references", set = keyset, kid = "1", pem = { private_key = pem_priv, public_key = pem_pub} } assert(helpers.kong_exec("config db_export " .. filename, { prefix = helpers.test_conf.prefix, })) finally(function() os.remove(filename) end) local f = assert(io.open(filename, "rb")) local content = f:read("*all") f:close() local yaml = assert(lyaml.load(content)) local toplevel_keys = {} for k in pairs(yaml) do toplevel_keys[#toplevel_keys + 1] = k end table.sort(toplevel_keys) assert.same({ "_format_version", "_transform", "acls", "consumers", "key_sets", "keyauth_credentials", "keys", "parameters", "plugins", "routes", "services", }, toplevel_keys) convert_yaml_nulls(yaml) assert.equals("3.0", yaml._format_version) assert.equals(false, yaml._transform) assert.equals(2, #yaml.services) table.sort(yaml.services, sort_by_name) assert.same(service1, yaml.services[1]) assert.same(service2, yaml.services[2]) assert.equals(2, #yaml.routes) table.sort(yaml.routes, sort_by_name) assert.equals(route1.id, yaml.routes[1].id) assert.equals(route1.name, yaml.routes[1].name) assert.equals(service1.id, yaml.routes[1].service) assert.equals(route2.id, yaml.routes[2].id) assert.equals(route2.name, yaml.routes[2].name) assert.equals(service2.id, yaml.routes[2].service) assert.equals(4, #yaml.plugins) table.sort(yaml.plugins, sort_by_name) assert.equals(plugin1.id, yaml.plugins[1].id) assert.equals(plugin1.name, yaml.plugins[1].name) assert.equals(service1.id, yaml.plugins[1].service) assert.equals(plugin2.id, yaml.plugins[2].id) assert.equals(plugin2.name, yaml.plugins[2].name) assert.equals(service1.id, yaml.plugins[2].service) assert.equals(plugin3.id, yaml.plugins[3].id) assert.equals(plugin3.name, yaml.plugins[3].name) assert.equals(plugin4.id, yaml.plugins[4].id) assert.equals(plugin4.name, yaml.plugins[4].name) assert.equals(service2.id, yaml.plugins[3].service) assert.equals(1, #yaml.consumers) assert.same(consumer, yaml.consumers[1]) assert.equals(1, #yaml.acls) assert.equals(acls.group, yaml.acls[1].group) assert.equals(consumer.id, yaml.acls[1].consumer) assert.equals(1, #yaml.keyauth_credentials) assert.equals(keyauth.key, yaml.keyauth_credentials[1].key) assert.equals(consumer.id, yaml.keyauth_credentials[1].consumer) assert.equals(1, #yaml.key_sets) assert.equals(keyset.name, yaml.key_sets[1].name) assert.equals(pem_key.pem.public_key, yaml.keys[1].pem.public_key) end) it("#db config db_import works when foreign keys need to be resolved", function() assert(db.consumers:truncate()) assert(db.basicauth_credentials:truncate()) -- note that routes have no name local filename = helpers.make_yaml_file([[ _format_version: "1.1" consumers: - username: consumer basicauth_credentials: - username: username password: password ]]) assert(helpers.start_kong({ nginx_conf = "spec/fixtures/custom_nginx.template", })) assert(helpers.kong_exec("config db_import " .. filename, { prefix = helpers.test_conf.prefix, })) local client = helpers.admin_client() local res = client:get("/consumers") local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equals(1, #json.data) local res = client:get("/basic-auths") local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equals(1, #json.data) assert(helpers.stop_kong()) assert(db.consumers:truncate()) assert(db.basicauth_credentials:truncate()) end) it("#db config parse works when foreign keys need to be resolved", function() -- note that routes have no name local filename = helpers.make_yaml_file([[ _format_version: "1.1" consumers: - username: consumer basicauth_credentials: - username: username password: password ]]) assert(helpers.kong_exec("config parse " .. filename, { prefix = helpers.test_conf.prefix, })) end) it("config init creates kong.yml by default", function() local kong_yml_exists = false if lfs.attributes("kong.yml") then kong_yml_exists = true shell.run("mv kong.yml kong.yml~", nil, 0) end finally(function() if kong_yml_exists then shell.run("mv kong.yml~ kong.yml", nil, 0) else os.remove("kong.yml") end end) os.remove("kong.yml") assert.is_nil(lfs.attributes("kong.yml")) assert(helpers.kong_exec("config init", { prefix = helpers.test_conf.prefix, })) assert.not_nil(lfs.attributes("kong.yml")) assert(helpers.kong_exec("config parse kong.yml", { prefix = helpers.test_conf.prefix, })) end) it("config init can take an argument", function() local tmpname = os.tmpname() .. ".yml" finally(function() os.remove(tmpname) end) os.remove(tmpname) assert.is_nil(lfs.attributes(tmpname)) assert(helpers.kong_exec("config init " .. tmpname, { prefix = helpers.test_conf.prefix, })) assert.not_nil(lfs.attributes(tmpname)) assert(helpers.kong_exec("config parse " .. tmpname, { prefix = helpers.test_conf.prefix, })) end) end)