From f7f7a2349ecc96b505a6f3dac7f4e8fae84d004f Mon Sep 17 00:00:00 2001 From: Michael Martin Date: Wed, 18 Dec 2024 06:45:38 -0800 Subject: [PATCH] fix(db): ensure that flattened_errors is encoded as a JSON array (#14019) --- .../kong/fix-error-flattening-json.yml | 3 + kong/db/errors.lua | 6 +- kong/tools/cjson.lua | 19 +- .../05-error-flattening_spec.lua | 2653 +++++++++++++++++ 4 files changed, 2677 insertions(+), 4 deletions(-) create mode 100644 changelog/unreleased/kong/fix-error-flattening-json.yml create mode 100644 spec/01-unit/01-db/01-schema/11-declarative_config/05-error-flattening_spec.lua diff --git a/changelog/unreleased/kong/fix-error-flattening-json.yml b/changelog/unreleased/kong/fix-error-flattening-json.yml new file mode 100644 index 000000000000..dbd50176123b --- /dev/null +++ b/changelog/unreleased/kong/fix-error-flattening-json.yml @@ -0,0 +1,3 @@ +message: "Fixed an issue where `POST /config?flatten_errors=1` could return a JSON object instead of an empty array." +type: bugfix +scope: Core diff --git a/kong/db/errors.lua b/kong/db/errors.lua index a1f96cccd1bf..0ac400f824a4 100644 --- a/kong/db/errors.lua +++ b/kong/db/errors.lua @@ -3,6 +3,7 @@ local pl_keys = require("pl.tablex").keys local nkeys = require("table.nkeys") local table_isarray = require("table.isarray") local uuid = require("kong.tools.uuid") +local json = require("kong.tools.cjson") local type = type @@ -21,6 +22,7 @@ local concat = table.concat local sort = table.sort local insert = table.insert local remove = table.remove +local new_array = json.new_array local sorted_keys = function(tbl) @@ -720,7 +722,7 @@ do ---@param ns? string ---@param flattened? table local function categorize_errors(errs, ns, flattened) - flattened = flattened or {} + flattened = flattened or new_array() for field, err in drain(errs) do local errtype = type(err) @@ -1020,7 +1022,7 @@ do ---@param input table ---@return table function flatten_errors(input, err_t) - local flattened = {} + local flattened = new_array() for entity_type, section_errors in drain(err_t) do if type(section_errors) ~= "table" then diff --git a/kong/tools/cjson.lua b/kong/tools/cjson.lua index 5ce04e1003e0..dff99fb6d8a2 100644 --- a/kong/tools/cjson.lua +++ b/kong/tools/cjson.lua @@ -1,6 +1,9 @@ local cjson = require "cjson.safe".new() local CJSON_MAX_PRECISION = require "kong.constants".CJSON_MAX_PRECISION +local new_tab = require("table.new") +local setmetatable = setmetatable +local array_mt = cjson.array_mt cjson.decode_array_with_array_mt(true) cjson.encode_sparse_array(nil, nil, 2^15) @@ -14,7 +17,19 @@ _M.encode = cjson.encode _M.decode_with_array_mt = cjson.decode -_M.array_mt = cjson.array_mt - +_M.array_mt = array_mt + +--- Creates a new table with the cjson array metatable. +--- +--- This ensures that the table will be encoded as a JSON array, even if it +--- is empty. +--- +---@param size? integer +---@return table +function _M.new_array(size) + local t = size and new_tab(size, 0) or {} + setmetatable(t, array_mt) + return t +end return _M diff --git a/spec/01-unit/01-db/01-schema/11-declarative_config/05-error-flattening_spec.lua b/spec/01-unit/01-db/01-schema/11-declarative_config/05-error-flattening_spec.lua new file mode 100644 index 000000000000..afac10a79644 --- /dev/null +++ b/spec/01-unit/01-db/01-schema/11-declarative_config/05-error-flattening_spec.lua @@ -0,0 +1,2653 @@ +local cjson = require("cjson") +local tablex = require("pl.tablex") + +local TESTS = { + { + input = { + config = { + _format_version = "3.0", + _transform = true, + certificates = { + { + cert = "-----BEGIN CERTIFICATE-----\ +MIICIzCCAYSgAwIBAgIUUMiD8e3GDZ+vs7XBmdXzMxARUrgwCgYIKoZIzj0EAwIw\ +IzENMAsGA1UECgwES29uZzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIyMTIzMDA0\ +MDcwOFoXDTQyMTIyNTA0MDcwOFowIzENMAsGA1UECgwES29uZzESMBAGA1UEAwwJ\ +bG9jYWxob3N0MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBxSldGzzRAtjt825q\ +Uwl+BNgxecswnvbQFLiUDqJjVjCfs/B53xQfV97ddxsRymES2viC2kjAm1Ete4TH\ +CQmVltUBItHzI77HB+UsfqHoUdjl3lC/HC1yDSPBp5wd9eRRSagdl0eiJwnB9lof\ +MEnmOQLg177trb/YPz1vcCCZj7ikhzCjUzBRMB0GA1UdDgQWBBSUI6+CKqKFz/Te\ +ZJppMNl/Dh6d9DAfBgNVHSMEGDAWgBSUI6+CKqKFz/TeZJppMNl/Dh6d9DAPBgNV\ +HRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA4GMADCBiAJCAZL3qX21MnGtQcl9yOMr\ +hNR54VrDKgqLR+ChU7/358n/sK/sVOjmrwVyQ52oUyqaQlfBQS2EufQVO/01+2sx\ +86gzAkIB/4Ilf4RluN2/gqHYlVEDRZzsqbwVJBHLeNKsZBSJkhNNpJBwa2Ndl9/i\ +u2tDk0KZFSAvRnqRAo9iDBUkIUI1ahA=\ +-----END CERTIFICATE-----", + key = "-----BEGIN EC PRIVATE KEY-----\ +MIHcAgEBBEIARPKnAYLB54bxBvkDfqV4NfZ+Mxl79rlaYRB6vbWVwFpy+E2pSZBR\ +doCy1tHAB/uPo+QJyjIK82Zwa3Kq0i1D2QigBwYFK4EEACOhgYkDgYYABAHFKV0b\ +PNEC2O3zbmpTCX4E2DF5yzCe9tAUuJQOomNWMJ+z8HnfFB9X3t13GxHKYRLa+ILa\ +SMCbUS17hMcJCZWW1QEi0fMjvscH5Sx+oehR2OXeUL8cLXINI8GnnB315FFJqB2X\ +R6InCcH2Wh8wSeY5AuDXvu2tv9g/PW9wIJmPuKSHMA==\ +-----END EC PRIVATE KEY-----", + tags = { + "certificate-01", + }, + }, + { + cert = "-----BEGIN CERTIFICATE-----\ +MIICIzCCAYSgAwIBAgIUUMiD8e3GDZ+vs7XBmdXzMxARUrgwCgYIKoZIzj0EAwIw\ +IzENMAsGA1UECgwES29uZzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIyMTIzMDA0\ +MDcwOFoXDTQyohnoooooooooooooooooooooooooooooooooooooooooooasdfa\ +Uwl+BNgxecswnvbQFLiUDqJjVjCfs/B53xQfV97ddxsRymES2viC2kjAm1Ete4TH\ +CQmVltUBItHzI77AAAAAAAAAAAAAAAC/HC1yDSBBBBBBBBBBBBBdl0eiJwnB9lof\ +MEnmOQLg177trb/AAAAAAAAAAAAAAACjUzBRMBBBBBBBBBBBBBBUI6+CKqKFz/Te\ +ZJppMNl/Dh6d9DAAAAAAAAAAAAAAAASUI6+CKqBBBBBBBBBBBBB/Dh6d9DAPBgNV\ +HRMBAf8EBTADAQHAAAAAAAAAAAAAAAMCA4GMADBBBBBBBBBBBBB1MnGtQcl9yOMr\ +hNR54VrDKgqLR+CAAAAAAAAAAAAAAAjmrwVyQ5BBBBBBBBBBBBBEufQVO/01+2sx\ +86gzAkIB/4Ilf4RluN2/gqHYlVEDRZzsqbwVJBHLeNKsZBSJkhNNpJBwa2Ndl9/i\ +u2tDk0KZFSAvRnqRAo9iDBUkIUI1ahA=\ +-----END CERTIFICATE-----", + key = "-----BEGIN EC PRIVATE KEY-----\ +MIHcAgEBBEIARPKnAYLB54bxBvkDfqV4NfZ+Mxl79rlaYRB6vbWVwFpy+E2pSZBR\ +doCy1tHAB/uPo+QJyjIK82Zwa3Kq0i1D2QigBwYFK4EEACOhgYkDgYYABAHFKV0b\ +PNEC2O3zbmpTCX4E2DF5yzCe9tAUuJQOomNWMJ+z8HnfFB9X3t13GxHKYRLa+ILa\ +SMCbUS17hMcJCZWW1QEi0fMjvscH5Sx+oehR2OXeUL8cLXINI8GnnB315FFJqB2X\ +R6InCcH2Wh8wSeY5AuDXvu2tv9g/PW9wIJmPuKSHMA==\ +-----END EC PRIVATE KEY-----", + tags = { + "certificate-02", + }, + }, + }, + consumers = { + { + tags = { + "consumer-01", + }, + username = "valid_user", + }, + { + not_allowed = true, + tags = { + "consumer-02", + }, + username = "bobby_in_json_body", + }, + { + tags = { + "consumer-03", + }, + username = "super_valid_user", + }, + { + basicauth_credentials = { + { + password = "hard2guess", + tags = { + "basicauth_credentials-01", + "consumer-04", + }, + username = "superduper", + }, + { + extra_field = "NO!", + password = "12354", + tags = { + "basicauth_credentials-02", + "consumer-04", + }, + username = "dont-add-extra-fields-yo", + }, + }, + tags = { + "consumer-04", + }, + username = "credentials", + }, + }, + plugins = { + { + config = { + http_endpoint = "invalid::#//url", + }, + name = "http-log", + tags = { + "global_plugin-01", + }, + }, + }, + services = { + { + host = "localhost", + name = "nope", + port = 1234, + protocol = "nope", + routes = { + { + hosts = { + "test", + }, + methods = { + "GET", + }, + name = "valid.route", + protocols = { + "http", + "https", + }, + tags = { + "route_service-01", + "service-01", + }, + }, + { + name = "nope.route", + protocols = { + "tcp", + }, + tags = { + "route_service-02", + "service-01", + }, + }, + }, + tags = { + "service-01", + }, + }, + { + host = "localhost", + name = "mis-matched", + path = "/path", + protocol = "tcp", + routes = { + { + hosts = { + "test", + }, + methods = { + "GET", + }, + name = "invalid", + protocols = { + "http", + "https", + }, + tags = { + "route_service-03", + "service-02", + }, + }, + }, + tags = { + "service-02", + }, + }, + { + name = "okay", + routes = { + { + hosts = { + "test", + }, + methods = { + "GET", + }, + name = "probably-valid", + plugins = { + { + config = { + not_endpoint = "anything", + }, + name = "http-log", + tags = { + "route_service_plugin-01", + "route_service-04", + "service-03", + }, + }, + }, + protocols = { + "http", + "https", + }, + tags = { + "route_service-04", + "service-03", + }, + }, + }, + tags = { + "service-03", + }, + url = "http://localhost:1234", + }, + { + name = "bad-service-plugins", + plugins = { + { + config = {}, + name = "i-dont-exist", + tags = { + "service_plugin-01", + "service-04", + }, + }, + { + config = { + deeply = { + nested = { + undefined = true, + }, + }, + port = 1234, + }, + name = "tcp-log", + tags = { + "service_plugin-02", + "service-04", + }, + }, + }, + tags = { + "service-04", + }, + url = "http://localhost:1234", + }, + { + client_certificate = { + cert = "", + key = "", + tags = { + "service_client_certificate-01", + "service-05", + }, + }, + name = "bad-client-cert", + tags = { + "service-05", + }, + url = "https://localhost:1234", + }, + { + id = 123456, + name = "invalid-id", + tags = { + "service-06", + "invalid-id", + }, + url = "https://localhost:1234", + }, + { + name = "invalid-tags", + tags = { + "service-07", + "invalid-tags", + { + 1, + 2, + 3, + }, + true, + }, + url = "https://localhost:1234", + }, + { + name = "", + tags = { + "service-08", + "invalid_service_name-01", + }, + url = "https://localhost:1234", + }, + { + name = 1234, + tags = { + "service-09", + "invalid_service_name-02", + }, + url = "https://localhost:1234", + }, + }, + upstreams = { + { + hash_on = "ip", + name = "ok", + tags = { + "upstream-01", + }, + }, + { + hash_on = "ip", + healthchecks = { + active = { + concurrency = -1, + healthy = { + interval = 0, + successes = 0, + }, + http_path = "/", + https_sni = "example.com", + https_verify_certificate = true, + timeout = 1, + type = "http", + unhealthy = { + http_failures = 0, + interval = 0, + }, + }, + }, + host_header = 123, + name = "bad", + tags = { + "upstream-02", + }, + }, + { + name = "ok-bad-targets", + tags = { + "upstream-03", + }, + targets = { + { + tags = { + "upstream_target-01", + "upstream-03", + }, + target = "127.0.0.1:99", + }, + { + tags = { + "upstream_target-02", + "upstream-03", + }, + target = "hostname:1.0", + }, + }, + }, + }, + vaults = { + { + config = { + prefix = "SSL_", + }, + name = "env", + prefix = "test", + tags = { + "vault-01", + }, + }, + { + config = { + prefix = "SSL_", + }, + name = "vault-not-installed", + prefix = "env", + tags = { + "vault-02", + "vault-not-installed", + }, + }, + }, + }, + err_t = { + certificates = { + nil, + { + cert = "invalid certificate: x509.new: error:688010A:asn1 encoding routines:asn1_item_embed_d2i:nested asn1 error:asn1/tasn_dec.c:349:", + }, + }, + consumers = { + nil, + { + not_allowed = "unknown field", + }, + nil, + { + basicauth_credentials = { + nil, + { + extra_field = "unknown field", + }, + }, + }, + }, + plugins = { + { + config = { + http_endpoint = "missing host in url", + }, + }, + }, + services = { + { + protocol = "expected one of: grpc, grpcs, http, https, tcp, tls, tls_passthrough, udp", + routes = { + nil, + { + ["@entity"] = { + "must set one of 'sources', 'destinations', 'snis' when 'protocols' is 'tcp', 'tls' or 'udp'", + }, + }, + }, + }, + { + ["@entity"] = { + "failed conditional validation given value of field 'protocol'", + }, + path = "value must be null", + }, + { + routes = { + { + plugins = { + { + config = { + http_endpoint = "required field missing", + not_endpoint = "unknown field", + }, + }, + }, + }, + }, + }, + { + plugins = { + { + name = "plugin 'i-dont-exist' not enabled; add it to the 'plugins' configuration property", + }, + { + config = { + deeply = "unknown field", + host = "required field missing", + }, + }, + }, + }, + { + client_certificate = { + cert = "length must be at least 1", + key = "length must be at least 1", + }, + }, + { + id = "expected a string", + }, + { + tags = { + nil, + nil, + "expected a string", + "expected a string", + }, + }, + { + name = "length must be at least 1", + }, + { + name = "expected a string", + }, + }, + upstreams = { + nil, + { + healthchecks = { + active = { + concurrency = "value should be between 1 and 2147483648", + }, + }, + host_header = "expected a string", + }, + { + targets = { + nil, + { + target = "Invalid target ('hostname:1.0'); not a valid hostname or ip address", + }, + }, + }, + }, + vaults = { + nil, + { + name = "vault 'vault-not-installed' is not installed", + }, + }, + }, + }, + output = { + err_t = { + code = 14, + fields = {}, + flattened_errors = { + { + entity = { + config = { + prefix = "SSL_", + }, + name = "vault-not-installed", + prefix = "env", + tags = { + "vault-02", + "vault-not-installed", + }, + }, + entity_name = "vault-not-installed", + entity_tags = { + "vault-02", + "vault-not-installed", + }, + entity_type = "vault", + errors = { + { + field = "name", + message = "vault 'vault-not-installed' is not installed", + type = "field", + }, + }, + }, + { + entity = { + tags = { + "upstream_target-02", + "upstream-03", + }, + target = "hostname:1.0", + }, + entity_tags = { + "upstream_target-02", + "upstream-03", + }, + entity_type = "target", + errors = { + { + field = "target", + message = "Invalid target ('hostname:1.0'); not a valid hostname or ip address", + type = "field", + }, + }, + }, + { + entity = { + hash_on = "ip", + healthchecks = { + active = { + concurrency = -1, + healthy = { + interval = 0, + successes = 0, + }, + http_path = "/", + https_sni = "example.com", + https_verify_certificate = true, + timeout = 1, + type = "http", + unhealthy = { + http_failures = 0, + interval = 0, + }, + }, + }, + host_header = 123, + name = "bad", + tags = { + "upstream-02", + }, + }, + entity_name = "bad", + entity_tags = { + "upstream-02", + }, + entity_type = "upstream", + errors = { + { + field = "host_header", + message = "expected a string", + type = "field", + }, + { + field = "healthchecks.active.concurrency", + message = "value should be between 1 and 2147483648", + type = "field", + }, + }, + }, + { + entity = { + name = 1234, + tags = { + "service-09", + "invalid_service_name-02", + }, + url = "https://localhost:1234", + }, + entity_tags = { + "service-09", + "invalid_service_name-02", + }, + entity_type = "service", + errors = { + { + field = "name", + message = "expected a string", + type = "field", + }, + }, + }, + { + entity = { + name = "", + tags = { + "service-08", + "invalid_service_name-01", + }, + url = "https://localhost:1234", + }, + entity_tags = { + "service-08", + "invalid_service_name-01", + }, + entity_type = "service", + errors = { + { + field = "name", + message = "length must be at least 1", + type = "field", + }, + }, + }, + { + entity = { + name = "invalid-tags", + tags = { + "service-07", + "invalid-tags", + { + 1, + 2, + 3, + }, + true, + }, + url = "https://localhost:1234", + }, + entity_name = "invalid-tags", + entity_type = "service", + errors = { + { + field = "tags.4", + message = "expected a string", + type = "field", + }, + { + field = "tags.3", + message = "expected a string", + type = "field", + }, + }, + }, + { + entity = { + id = 123456, + name = "invalid-id", + tags = { + "service-06", + "invalid-id", + }, + url = "https://localhost:1234", + }, + entity_name = "invalid-id", + entity_tags = { + "service-06", + "invalid-id", + }, + entity_type = "service", + errors = { + { + field = "id", + message = "expected a string", + type = "field", + }, + }, + }, + { + entity = { + cert = "", + key = "", + tags = { + "service_client_certificate-01", + "service-05", + }, + }, + entity_tags = { + "service_client_certificate-01", + "service-05", + }, + entity_type = "certificate", + errors = { + { + field = "key", + message = "length must be at least 1", + type = "field", + }, + { + field = "cert", + message = "length must be at least 1", + type = "field", + }, + }, + }, + { + entity = { + config = {}, + name = "i-dont-exist", + tags = { + "service_plugin-01", + "service-04", + }, + }, + entity_name = "i-dont-exist", + entity_tags = { + "service_plugin-01", + "service-04", + }, + entity_type = "plugin", + errors = { + { + field = "name", + message = "plugin 'i-dont-exist' not enabled; add it to the 'plugins' configuration property", + type = "field", + }, + }, + }, + { + entity = { + config = { + deeply = { + nested = { + undefined = true, + }, + }, + port = 1234, + }, + name = "tcp-log", + tags = { + "service_plugin-02", + "service-04", + }, + }, + entity_name = "tcp-log", + entity_tags = { + "service_plugin-02", + "service-04", + }, + entity_type = "plugin", + errors = { + { + field = "config.host", + message = "required field missing", + type = "field", + }, + { + field = "config.deeply", + message = "unknown field", + type = "field", + }, + }, + }, + { + entity = { + config = { + not_endpoint = "anything", + }, + name = "http-log", + tags = { + "route_service_plugin-01", + "route_service-04", + "service-03", + }, + }, + entity_name = "http-log", + entity_tags = { + "route_service_plugin-01", + "route_service-04", + "service-03", + }, + entity_type = "plugin", + errors = { + { + field = "config.not_endpoint", + message = "unknown field", + type = "field", + }, + { + field = "config.http_endpoint", + message = "required field missing", + type = "field", + }, + }, + }, + { + entity = { + host = "localhost", + name = "mis-matched", + path = "/path", + protocol = "tcp", + tags = { + "service-02", + }, + }, + entity_name = "mis-matched", + entity_tags = { + "service-02", + }, + entity_type = "service", + errors = { + { + field = "path", + message = "value must be null", + type = "field", + }, + { + message = "failed conditional validation given value of field 'protocol'", + type = "entity", + }, + }, + }, + { + entity = { + name = "nope.route", + protocols = { + "tcp", + }, + tags = { + "route_service-02", + "service-01", + }, + }, + entity_name = "nope.route", + entity_tags = { + "route_service-02", + "service-01", + }, + entity_type = "route", + errors = { + { + message = "must set one of 'sources', 'destinations', 'snis' when 'protocols' is 'tcp', 'tls' or 'udp'", + type = "entity", + }, + }, + }, + { + entity = { + host = "localhost", + name = "nope", + port = 1234, + protocol = "nope", + tags = { + "service-01", + }, + }, + entity_name = "nope", + entity_tags = { + "service-01", + }, + entity_type = "service", + errors = { + { + field = "protocol", + message = "expected one of: grpc, grpcs, http, https, tcp, tls, tls_passthrough, udp", + type = "field", + }, + }, + }, + { + entity = { + config = { + http_endpoint = "invalid::#//url", + }, + name = "http-log", + tags = { + "global_plugin-01", + }, + }, + entity_name = "http-log", + entity_tags = { + "global_plugin-01", + }, + entity_type = "plugin", + errors = { + { + field = "config.http_endpoint", + message = "missing host in url", + type = "field", + }, + }, + }, + { + entity = { + extra_field = "NO!", + password = "12354", + tags = { + "basicauth_credentials-02", + "consumer-04", + }, + username = "dont-add-extra-fields-yo", + }, + entity_tags = { + "basicauth_credentials-02", + "consumer-04", + }, + entity_type = "basicauth_credential", + errors = { + { + field = "extra_field", + message = "unknown field", + type = "field", + }, + }, + }, + { + entity = { + not_allowed = true, + tags = { + "consumer-02", + }, + username = "bobby_in_json_body", + }, + entity_tags = { + "consumer-02", + }, + entity_type = "consumer", + errors = { + { + field = "not_allowed", + message = "unknown field", + type = "field", + }, + }, + }, + { + entity = { + cert = "-----BEGIN CERTIFICATE-----\ +MIICIzCCAYSgAwIBAgIUUMiD8e3GDZ+vs7XBmdXzMxARUrgwCgYIKoZIzj0EAwIw\ +IzENMAsGA1UECgwES29uZzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIyMTIzMDA0\ +MDcwOFoXDTQyohnoooooooooooooooooooooooooooooooooooooooooooasdfa\ +Uwl+BNgxecswnvbQFLiUDqJjVjCfs/B53xQfV97ddxsRymES2viC2kjAm1Ete4TH\ +CQmVltUBItHzI77AAAAAAAAAAAAAAAC/HC1yDSBBBBBBBBBBBBBdl0eiJwnB9lof\ +MEnmOQLg177trb/AAAAAAAAAAAAAAACjUzBRMBBBBBBBBBBBBBBUI6+CKqKFz/Te\ +ZJppMNl/Dh6d9DAAAAAAAAAAAAAAAASUI6+CKqBBBBBBBBBBBBB/Dh6d9DAPBgNV\ +HRMBAf8EBTADAQHAAAAAAAAAAAAAAAMCA4GMADBBBBBBBBBBBBB1MnGtQcl9yOMr\ +hNR54VrDKgqLR+CAAAAAAAAAAAAAAAjmrwVyQ5BBBBBBBBBBBBBEufQVO/01+2sx\ +86gzAkIB/4Ilf4RluN2/gqHYlVEDRZzsqbwVJBHLeNKsZBSJkhNNpJBwa2Ndl9/i\ +u2tDk0KZFSAvRnqRAo9iDBUkIUI1ahA=\ +-----END CERTIFICATE-----", + key = "-----BEGIN EC PRIVATE KEY-----\ +MIHcAgEBBEIARPKnAYLB54bxBvkDfqV4NfZ+Mxl79rlaYRB6vbWVwFpy+E2pSZBR\ +doCy1tHAB/uPo+QJyjIK82Zwa3Kq0i1D2QigBwYFK4EEACOhgYkDgYYABAHFKV0b\ +PNEC2O3zbmpTCX4E2DF5yzCe9tAUuJQOomNWMJ+z8HnfFB9X3t13GxHKYRLa+ILa\ +SMCbUS17hMcJCZWW1QEi0fMjvscH5Sx+oehR2OXeUL8cLXINI8GnnB315FFJqB2X\ +R6InCcH2Wh8wSeY5AuDXvu2tv9g/PW9wIJmPuKSHMA==\ +-----END EC PRIVATE KEY-----", + tags = { + "certificate-02", + }, + }, + entity_tags = { + "certificate-02", + }, + entity_type = "certificate", + errors = { + { + field = "cert", + message = "invalid certificate: x509.new: error:688010A:asn1 encoding routines:asn1_item_embed_d2i:nested asn1 error:asn1/tasn_dec.c:349:", + type = "field", + }, + }, + }, + }, + message = "declarative config is invalid: {}", + name = "invalid declarative configuration", + }, + }, + }, + + { + input = { + config = { + _format_version = "3.0", + _transform = true, + upstreams = { + { + hash_on = "ip", + healthchecks = { + active = { + concurrency = -1, + healthy = { + interval = 0, + successes = 0, + }, + http_path = "/", + https_sni = "example.com", + https_verify_certificate = true, + timeout = 1, + type = "http", + unhealthy = { + http_failures = 0, + interval = 0, + }, + }, + }, + host_header = 123, + name = "bad", + tags = { + "upstream-01", + }, + }, + }, + }, + err_t = { + upstreams = { + { + healthchecks = { + active = { + concurrency = "value should be between 1 and 2147483648", + }, + }, + host_header = "expected a string", + }, + }, + }, + }, + output = { + err_t = { + code = 14, + fields = {}, + flattened_errors = { + { + entity = { + hash_on = "ip", + healthchecks = { + active = { + concurrency = -1, + healthy = { + interval = 0, + successes = 0, + }, + http_path = "/", + https_sni = "example.com", + https_verify_certificate = true, + timeout = 1, + type = "http", + unhealthy = { + http_failures = 0, + interval = 0, + }, + }, + }, + host_header = 123, + name = "bad", + tags = { + "upstream-01", + }, + }, + entity_name = "bad", + entity_tags = { + "upstream-01", + }, + entity_type = "upstream", + errors = { + { + field = "host_header", + message = "expected a string", + type = "field", + }, + { + field = "healthchecks.active.concurrency", + message = "value should be between 1 and 2147483648", + type = "field", + }, + }, + }, + }, + message = "declarative config is invalid: {}", + name = "invalid declarative configuration", + }, + }, + }, + + { + input = { + config = { + _format_version = "3.0", + _transform = true, + services = { + { + client_certificate = { + cert = "", + key = "", + tags = { + "service_client_certificate-01", + "service-01", + }, + }, + name = "bad-client-cert", + plugins = { + { + config = {}, + name = "i-do-not-exist", + tags = { + "service_plugin-01", + }, + }, + }, + routes = { + { + hosts = { + "test", + }, + paths = { + "/", + }, + plugins = { + { + config = { + a = { + b = { + c = "def", + }, + }, + }, + name = "http-log", + tags = { + "route_service_plugin-01", + }, + }, + }, + protocols = { + "http", + }, + tags = { + "service_route-01", + }, + }, + { + hosts = { + "invalid", + }, + paths = { + "/", + }, + protocols = { + "nope", + }, + tags = { + "service_route-02", + }, + }, + }, + tags = { + "service-01", + }, + url = "https://localhost:1234", + }, + }, + }, + err_t = { + services = { + { + client_certificate = { + cert = "length must be at least 1", + key = "length must be at least 1", + }, + plugins = { + { + name = "plugin 'i-do-not-exist' not enabled; add it to the 'plugins' configuration property", + }, + }, + routes = { + { + plugins = { + { + config = { + a = "unknown field", + http_endpoint = "required field missing", + }, + }, + }, + }, + { + protocols = "unknown type: nope", + }, + }, + }, + }, + }, + }, + output = { + err_t = { + code = 14, + fields = {}, + flattened_errors = { + { + entity = { + config = {}, + name = "i-do-not-exist", + tags = { + "service_plugin-01", + }, + }, + entity_name = "i-do-not-exist", + entity_tags = { + "service_plugin-01", + }, + entity_type = "plugin", + errors = { + { + field = "name", + message = "plugin 'i-do-not-exist' not enabled; add it to the 'plugins' configuration property", + type = "field", + }, + }, + }, + { + entity = { + cert = "", + key = "", + tags = { + "service_client_certificate-01", + "service-01", + }, + }, + entity_tags = { + "service_client_certificate-01", + "service-01", + }, + entity_type = "certificate", + errors = { + { + field = "key", + message = "length must be at least 1", + type = "field", + }, + { + field = "cert", + message = "length must be at least 1", + type = "field", + }, + }, + }, + { + entity = { + config = { + a = { + b = { + c = "def", + }, + }, + }, + name = "http-log", + tags = { + "route_service_plugin-01", + }, + }, + entity_name = "http-log", + entity_tags = { + "route_service_plugin-01", + }, + entity_type = "plugin", + errors = { + { + field = "config.http_endpoint", + message = "required field missing", + type = "field", + }, + { + field = "config.a", + message = "unknown field", + type = "field", + }, + }, + }, + { + entity = { + hosts = { + "invalid", + }, + paths = { + "/", + }, + protocols = { + "nope", + }, + tags = { + "service_route-02", + }, + }, + entity_tags = { + "service_route-02", + }, + entity_type = "route", + errors = { + { + field = "protocols", + message = "unknown type: nope", + type = "field", + }, + }, + }, + }, + message = "declarative config is invalid: {}", + name = "invalid declarative configuration", + }, + }, + }, + + { + input = { + config = { + _format_version = "3.0", + _transform = true, + consumers = { + { + basicauth_credentials = { + { + id = "089091f4-cb8b-48f5-8463-8319097be716", + password = "pwd", + tags = { + "consumer-01-credential-01", + }, + username = "user-01", + }, + { + id = "b1443d61-ccd9-4359-b82a-f37515442295", + password = "pwd", + tags = { + "consumer-01-credential-02", + }, + username = "user-11", + }, + { + id = "2603d010-edbe-4713-94ef-145e281cbf4c", + password = "pwd", + tags = { + "consumer-01-credential-03", + }, + username = "user-02", + }, + { + id = "760cf441-613c-48a2-b377-36aebc9f9ed0", + password = "pwd", + tags = { + "consumer-01-credential-04", + }, + username = "user-11", + }, + }, + id = "cdac30ee-cd7e-465c-99b6-84f3e4e17015", + tags = { + "consumer-01", + }, + username = "consumer-01", + }, + { + basicauth_credentials = { + { + id = "d0cd1919-bb07-4c85-b407-f33feb74f6e2", + password = "pwd", + tags = { + "consumer-02-credential-01", + }, + username = "user-99", + }, + }, + id = "c0c021f5-dae1-4031-bcf6-42e3c4d9ced9", + tags = { + "consumer-02", + }, + username = "consumer-02", + }, + { + basicauth_credentials = { + { + id = "7e8bcd10-cdcd-41f1-8c4d-9790d34aa67d", + password = "pwd", + tags = { + "consumer-03-credential-01", + }, + username = "user-01", + }, + { + id = "7fe186bd-44e5-4b97-854d-85a24929889d", + password = "pwd", + tags = { + "consumer-03-credential-02", + }, + username = "user-33", + }, + { + id = "6547c325-5332-41fc-a954-d4972926cdb5", + password = "pwd", + tags = { + "consumer-03-credential-03", + }, + username = "user-02", + }, + { + id = "e091a955-9ee1-4403-8d0a-a7f1f8bafaca", + password = "pwd", + tags = { + "consumer-03-credential-04", + }, + username = "user-33", + }, + }, + id = "9acb0270-73aa-4968-b9e4-a4924e4aced5", + tags = { + "consumer-03", + }, + username = "consumer-03", + }, + }, + }, + err_t = { + consumers = { + { + basicauth_credentials = { + nil, + nil, + nil, + "uniqueness violation: 'basicauth_credentials' entity with username set to 'user-11' already declared", + }, + }, + nil, + { + basicauth_credentials = { + "uniqueness violation: 'basicauth_credentials' entity with username set to 'user-01' already declared", + nil, + "uniqueness violation: 'basicauth_credentials' entity with username set to 'user-02' already declared", + "uniqueness violation: 'basicauth_credentials' entity with username set to 'user-33' already declared", + }, + }, + }, + }, + }, + output = { + err_t = { + code = 14, + fields = {}, + flattened_errors = { + { + entity = { + consumer = { + id = "9acb0270-73aa-4968-b9e4-a4924e4aced5", + }, + id = "7e8bcd10-cdcd-41f1-8c4d-9790d34aa67d", + password = "pwd", + tags = { + "consumer-03-credential-01", + }, + username = "user-01", + }, + entity_id = "7e8bcd10-cdcd-41f1-8c4d-9790d34aa67d", + entity_tags = { + "consumer-03-credential-01", + }, + entity_type = "basicauth_credential", + errors = { + { + message = "uniqueness violation: 'basicauth_credentials' entity with username set to 'user-01' already declared", + type = "entity", + }, + }, + }, + { + entity = { + consumer = { + id = "9acb0270-73aa-4968-b9e4-a4924e4aced5", + }, + id = "6547c325-5332-41fc-a954-d4972926cdb5", + password = "pwd", + tags = { + "consumer-03-credential-03", + }, + username = "user-02", + }, + entity_id = "6547c325-5332-41fc-a954-d4972926cdb5", + entity_tags = { + "consumer-03-credential-03", + }, + entity_type = "basicauth_credential", + errors = { + { + message = "uniqueness violation: 'basicauth_credentials' entity with username set to 'user-02' already declared", + type = "entity", + }, + }, + }, + { + entity = { + consumer = { + id = "9acb0270-73aa-4968-b9e4-a4924e4aced5", + }, + id = "e091a955-9ee1-4403-8d0a-a7f1f8bafaca", + password = "pwd", + tags = { + "consumer-03-credential-04", + }, + username = "user-33", + }, + entity_id = "e091a955-9ee1-4403-8d0a-a7f1f8bafaca", + entity_tags = { + "consumer-03-credential-04", + }, + entity_type = "basicauth_credential", + errors = { + { + message = "uniqueness violation: 'basicauth_credentials' entity with username set to 'user-33' already declared", + type = "entity", + }, + }, + }, + { + entity = { + consumer = { + id = "cdac30ee-cd7e-465c-99b6-84f3e4e17015", + }, + id = "760cf441-613c-48a2-b377-36aebc9f9ed0", + password = "pwd", + tags = { + "consumer-01-credential-04", + }, + username = "user-11", + }, + entity_id = "760cf441-613c-48a2-b377-36aebc9f9ed0", + entity_tags = { + "consumer-01-credential-04", + }, + entity_type = "basicauth_credential", + errors = { + { + message = "uniqueness violation: 'basicauth_credentials' entity with username set to 'user-11' already declared", + type = "entity", + }, + }, + }, + }, + message = "declarative config is invalid: {}", + name = "invalid declarative configuration", + }, + }, + }, + + { + input = { + config = { + _format_version = "3.0", + _transform = true, + services = { + { + host = "localhost", + id = "0175e0e8-3de9-56b4-96f1-b12dcb4b6691", + name = "nope", + port = 1234, + protocol = "nope", + tags = { + "service-01", + }, + }, + }, + }, + err_t = { + services = { + { + protocol = "expected one of: grpc, grpcs, http, https, tcp, tls, tls_passthrough, udp", + }, + }, + }, + }, + output = { + err_t = { + code = 14, + fields = {}, + flattened_errors = { + { + entity = { + host = "localhost", + id = "0175e0e8-3de9-56b4-96f1-b12dcb4b6691", + name = "nope", + port = 1234, + protocol = "nope", + tags = { + "service-01", + }, + }, + entity_id = "0175e0e8-3de9-56b4-96f1-b12dcb4b6691", + entity_name = "nope", + entity_tags = { + "service-01", + }, + entity_type = "service", + errors = { + { + field = "protocol", + message = "expected one of: grpc, grpcs, http, https, tcp, tls, tls_passthrough, udp", + type = "field", + }, + }, + }, + }, + message = "declarative config is invalid: {}", + name = "invalid declarative configuration", + }, + }, + }, + + { + input = { + config = { + _format_version = "3.0", + _transform = true, + services = { + { + host = "localhost", + id = "cb019421-62c2-47a8-b714-d7567b114037", + name = "test", + port = 1234, + protocol = "nope", + routes = { + { + super_duper_invalid = true, + tags = { + "route-01", + }, + }, + }, + tags = { + "service-01", + }, + }, + }, + }, + err_t = { + services = { + { + protocol = "expected one of: grpc, grpcs, http, https, tcp, tls, tls_passthrough, udp", + routes = { + { + ["@entity"] = { + "must set one of 'methods', 'hosts', 'headers', 'paths', 'snis' when 'protocols' is 'https'", + }, + super_duper_invalid = "unknown field", + }, + }, + }, + }, + }, + }, + output = { + err_t = { + code = 14, + fields = {}, + flattened_errors = { + { + entity = { + service = { + id = "cb019421-62c2-47a8-b714-d7567b114037", + }, + super_duper_invalid = true, + tags = { + "route-01", + }, + }, + entity_tags = { + "route-01", + }, + entity_type = "route", + errors = { + { + field = "super_duper_invalid", + message = "unknown field", + type = "field", + }, + { + message = "must set one of 'methods', 'hosts', 'headers', 'paths', 'snis' when 'protocols' is 'https'", + type = "entity", + }, + }, + }, + { + entity = { + host = "localhost", + id = "cb019421-62c2-47a8-b714-d7567b114037", + name = "test", + port = 1234, + protocol = "nope", + tags = { + "service-01", + }, + }, + entity_id = "cb019421-62c2-47a8-b714-d7567b114037", + entity_name = "test", + entity_tags = { + "service-01", + }, + entity_type = "service", + errors = { + { + field = "protocol", + message = "expected one of: grpc, grpcs, http, https, tcp, tls, tls_passthrough, udp", + type = "field", + }, + }, + }, + }, + message = "declarative config is invalid: {}", + name = "invalid declarative configuration", + }, + }, + }, + + { + input = { + config = { + _format_version = "3.0", + _transform = true, + services = { + { + id = 1234, + name = false, + tags = { + "service-01", + { + 1.5, + }, + }, + url = "http://localhost:1234", + }, + }, + }, + err_t = { + services = { + { + id = "expected a string", + name = "expected a string", + tags = { + nil, + "expected a string", + }, + }, + }, + }, + }, + output = { + err_t = { + code = 14, + fields = {}, + flattened_errors = { + { + entity = { + id = 1234, + name = false, + tags = { + "service-01", + { + 1.5, + }, + }, + url = "http://localhost:1234", + }, + entity_type = "service", + errors = { + { + field = "tags.2", + message = "expected a string", + type = "field", + }, + { + field = "name", + message = "expected a string", + type = "field", + }, + { + field = "id", + message = "expected a string", + type = "field", + }, + }, + }, + }, + message = "declarative config is invalid: {}", + name = "invalid declarative configuration", + }, + }, + }, + + { + input = { + config = { + _format_version = "3.0", + _transform = true, + abnormal_extra_field = 123, + services = { + { + host = "localhost", + name = "nope", + port = 1234, + protocol = "nope", + routes = { + { + hosts = { + "test", + }, + methods = { + "GET", + }, + name = "valid.route", + protocols = { + "http", + "https", + }, + tags = { + "route_service-01", + "service-01", + }, + }, + { + name = "nope.route", + protocols = { + "tcp", + }, + tags = { + "route_service-02", + "service-01", + }, + }, + }, + tags = { + "service-01", + }, + }, + { + host = "localhost", + name = "mis-matched", + path = "/path", + protocol = "tcp", + routes = { + { + hosts = { + "test", + }, + methods = { + "GET", + }, + name = "invalid", + protocols = { + "http", + "https", + }, + tags = { + "route_service-03", + "service-02", + }, + }, + }, + tags = { + "service-02", + }, + }, + { + name = "okay", + routes = { + { + hosts = { + "test", + }, + methods = { + "GET", + }, + name = "probably-valid", + plugins = { + { + config = { + not_endpoint = "anything", + }, + name = "http-log", + tags = { + "route_service_plugin-01", + "route_service-04", + "service-03", + }, + }, + }, + protocols = { + "http", + "https", + }, + tags = { + "route_service-04", + "service-03", + }, + }, + }, + tags = { + "service-03", + }, + url = "http://localhost:1234", + }, + }, + }, + err_t = { + abnormal_extra_field = "unknown field", + services = { + { + protocol = "expected one of: grpc, grpcs, http, https, tcp, tls, tls_passthrough, udp", + routes = { + nil, + { + ["@entity"] = { + "must set one of 'sources', 'destinations', 'snis' when 'protocols' is 'tcp', 'tls' or 'udp'", + }, + }, + }, + }, + { + ["@entity"] = { + "failed conditional validation given value of field 'protocol'", + }, + path = "value must be null", + }, + { + routes = { + { + plugins = { + { + config = { + http_endpoint = "required field missing", + not_endpoint = "unknown field", + }, + }, + }, + }, + }, + }, + }, + }, + }, + output = { + err_t = { + code = 14, + fields = { + abnormal_extra_field = "unknown field", + }, + flattened_errors = { + { + entity = { + config = { + not_endpoint = "anything", + }, + name = "http-log", + tags = { + "route_service_plugin-01", + "route_service-04", + "service-03", + }, + }, + entity_name = "http-log", + entity_tags = { + "route_service_plugin-01", + "route_service-04", + "service-03", + }, + entity_type = "plugin", + errors = { + { + field = "config.not_endpoint", + message = "unknown field", + type = "field", + }, + { + field = "config.http_endpoint", + message = "required field missing", + type = "field", + }, + }, + }, + { + entity = { + host = "localhost", + name = "mis-matched", + path = "/path", + protocol = "tcp", + tags = { + "service-02", + }, + }, + entity_name = "mis-matched", + entity_tags = { + "service-02", + }, + entity_type = "service", + errors = { + { + field = "path", + message = "value must be null", + type = "field", + }, + { + message = "failed conditional validation given value of field 'protocol'", + type = "entity", + }, + }, + }, + { + entity = { + name = "nope.route", + protocols = { + "tcp", + }, + tags = { + "route_service-02", + "service-01", + }, + }, + entity_name = "nope.route", + entity_tags = { + "route_service-02", + "service-01", + }, + entity_type = "route", + errors = { + { + message = "must set one of 'sources', 'destinations', 'snis' when 'protocols' is 'tcp', 'tls' or 'udp'", + type = "entity", + }, + }, + }, + { + entity = { + host = "localhost", + name = "nope", + port = 1234, + protocol = "nope", + tags = { + "service-01", + }, + }, + entity_name = "nope", + entity_tags = { + "service-01", + }, + entity_type = "service", + errors = { + { + field = "protocol", + message = "expected one of: grpc, grpcs, http, https, tcp, tls, tls_passthrough, udp", + type = "field", + }, + }, + }, + }, + message = "declarative config is invalid: {abnormal_extra_field=\"unknown field\"}", + name = "invalid declarative configuration", + }, + }, + }, + + { + input = { + config = { + _format_version = "3.0", + _transform = true, + consumers = { + { + acls = { + { + group = "app", + tags = { + "k8s-name:app-acl", + "k8s-namespace:default", + "k8s-kind:Secret", + "k8s-uid:f1c5661c-a087-4c4b-b545-2d8b3870d661", + "k8s-version:v1", + }, + }, + }, + basicauth_credentials = { + { + password = "6ef728de-ba68-4e59-acb9-6e502c28ae0b", + tags = { + "k8s-name:app-cred", + "k8s-namespace:default", + "k8s-kind:Secret", + "k8s-uid:aadd4598-2969-49ea-82ac-6ab5159e2f2e", + "k8s-version:v1", + }, + username = "774f8446-6427-43f9-9962-ce7ab8097fe4", + }, + }, + id = "68d5de9f-2211-5ed8-b827-22f57a492d0f", + tags = { + "k8s-name:app", + "k8s-namespace:default", + "k8s-kind:KongConsumer", + "k8s-uid:7ee19bea-72d5-402b-bf0f-f57bf81032bf", + "k8s-group:configuration.konghq.com", + "k8s-version:v1", + }, + username = "774f8446-6427-43f9-9962-ce7ab8097fe4", + }, + }, + plugins = { + { + config = { + error_code = 429, + error_message = "API rate limit exceeded", + fault_tolerant = true, + hide_client_headers = false, + limit_by = "consumer", + policy = "local", + second = 2000, + }, + consumer = "774f8446-6427-43f9-9962-ce7ab8097fe4", + enabled = true, + name = "rate-limiting", + protocols = { + "grpc", + "grpcs", + "http", + "https", + }, + tags = { + "k8s-name:nginx-sample-1-rate", + "k8s-namespace:default", + "k8s-kind:KongPlugin", + "k8s-uid:5163972c-543d-48ae-b0f6-21701c43c1ff", + "k8s-group:configuration.konghq.com", + "k8s-version:v1", + }, + }, + { + config = { + error_code = 429, + error_message = "API rate limit exceeded", + fault_tolerant = true, + hide_client_headers = false, + limit_by = "consumer", + policy = "local", + second = 2000, + }, + consumer = "774f8446-6427-43f9-9962-ce7ab8097fe4", + enabled = true, + name = "rate-limiting", + protocols = { + "grpc", + "grpcs", + "http", + "https", + }, + tags = { + "k8s-name:nginx-sample-2-rate", + "k8s-namespace:default", + "k8s-kind:KongPlugin", + "k8s-uid:89fa1cd1-78da-4c3e-8c3b-32be1811535a", + "k8s-group:configuration.konghq.com", + "k8s-version:v1", + }, + }, + { + config = { + allow = { + "nginx-sample-1", + "app", + }, + hide_groups_header = false, + }, + enabled = true, + name = "acl", + protocols = { + "grpc", + "grpcs", + "http", + "https", + }, + service = "default.nginx-sample-1.nginx-sample-1.80", + tags = { + "k8s-name:nginx-sample-1", + "k8s-namespace:default", + "k8s-kind:KongPlugin", + "k8s-uid:b9373482-32e1-4ac3-bd2a-8926ab728700", + "k8s-group:configuration.konghq.com", + "k8s-version:v1", + }, + }, + }, + services = { + { + connect_timeout = 60000, + host = "nginx-sample-1.default.80.svc", + id = "8c17ab3e-b6bd-51b2-b5ec-878b4d608b9d", + name = "default.nginx-sample-1.nginx-sample-1.80", + path = "/", + port = 80, + protocol = "http", + read_timeout = 60000, + retries = 5, + routes = { + { + https_redirect_status_code = 426, + id = "84d45463-1faa-55cf-8ef6-4285007b715e", + methods = { + "GET", + }, + name = "default.nginx-sample-1.nginx-sample-1..80", + path_handling = "v0", + paths = { + "/sample/1", + }, + preserve_host = true, + protocols = { + "http", + "https", + }, + regex_priority = 0, + request_buffering = true, + response_buffering = true, + strip_path = false, + tags = { + "k8s-name:nginx-sample-1", + "k8s-namespace:default", + "k8s-kind:Ingress", + "k8s-uid:916a6e5a-eebe-4527-a78d-81963eb3e043", + "k8s-group:networking.k8s.io", + "k8s-version:v1", + }, + }, + }, + tags = { + "k8s-name:nginx-sample-1", + "k8s-namespace:default", + "k8s-kind:Service", + "k8s-uid:f7cc87f4-d5f7-41f8-b4e3-70608017e588", + "k8s-version:v1", + }, + write_timeout = 60000, + }, + }, + upstreams = { + { + algorithm = "round-robin", + name = "nginx-sample-1.default.80.svc", + tags = { + "k8s-name:nginx-sample-1", + "k8s-namespace:default", + "k8s-kind:Service", + "k8s-uid:f7cc87f4-d5f7-41f8-b4e3-70608017e588", + "k8s-version:v1", + }, + targets = { + { + target = "nginx-sample-1.default.svc:80", + }, + }, + }, + }, + }, + err_t = { + plugins = { + { + consumer = { + id = "missing primary key", + }, + }, + }, + }, + }, + output = { + err_t = { + code = 14, + fields = {}, + flattened_errors = { + { + entity = { + config = { + error_code = 429, + error_message = "API rate limit exceeded", + fault_tolerant = true, + hide_client_headers = false, + limit_by = "consumer", + policy = "local", + second = 2000, + }, + consumer = "774f8446-6427-43f9-9962-ce7ab8097fe4", + enabled = true, + name = "rate-limiting", + protocols = { + "grpc", + "grpcs", + "http", + "https", + }, + tags = { + "k8s-name:nginx-sample-1-rate", + "k8s-namespace:default", + "k8s-kind:KongPlugin", + "k8s-uid:5163972c-543d-48ae-b0f6-21701c43c1ff", + "k8s-group:configuration.konghq.com", + "k8s-version:v1", + }, + }, + entity_name = "rate-limiting", + entity_tags = { + "k8s-name:nginx-sample-1-rate", + "k8s-namespace:default", + "k8s-kind:KongPlugin", + "k8s-uid:5163972c-543d-48ae-b0f6-21701c43c1ff", + "k8s-group:configuration.konghq.com", + "k8s-version:v1", + }, + entity_type = "plugin", + errors = { + { + field = "consumer.id", + message = "missing primary key", + type = "field", + }, + }, + }, + }, + message = "declarative config is invalid: {}", + name = "invalid declarative configuration", + }, + }, + }, + + { + input = { + config = { + _format_version = "3.0", + _transform = true, + consumers = { + { + id = "a73dc9a7-93df-584d-97c0-7f41a1bbce3d", + tags = { + "consumer-1", + }, + username = "test-consumer-1", + }, + { + id = "a73dc9a7-93df-584d-97c0-7f41a1bbce32", + tags = { + "consumer-2", + }, + username = "test-consumer-1", + }, + }, + }, + err_t = { + consumers = { + nil, + "uniqueness violation: 'consumers' entity with username set to 'test-consumer-1' already declared", + }, + }, + }, + output = { + err_t = { + code = 14, + fields = {}, + flattened_errors = { + { + entity = { + id = "a73dc9a7-93df-584d-97c0-7f41a1bbce32", + tags = { + "consumer-2", + }, + username = "test-consumer-1", + }, + entity_id = "a73dc9a7-93df-584d-97c0-7f41a1bbce32", + entity_tags = { + "consumer-2", + }, + entity_type = "consumer", + errors = { + { + message = "uniqueness violation: 'consumers' entity with username set to 'test-consumer-1' already declared", + type = "entity", + }, + }, + }, + }, + message = "declarative config is invalid: {}", + name = "invalid declarative configuration", + }, + }, + }, + + { + input = { + config = { + _format_version = "3.0", + _transform = true, + services = { + { + connect_timeout = 60000, + host = "httproute.default.httproute-testing.0", + id = "4e3cb785-a8d0-5866-aa05-117f7c64f24d", + name = "httproute.default.httproute-testing.0", + port = 8080, + protocol = "http", + read_timeout = 60000, + retries = 5, + routes = { + { + https_redirect_status_code = 426, + id = "073fc413-1c03-50b4-8f44-43367c13daba", + name = "httproute.default.httproute-testing.0.0", + path_handling = "v0", + paths = { + "~/httproute-testing$", + "/httproute-testing/", + }, + preserve_host = true, + protocols = { + "http", + "https", + }, + strip_path = true, + tags = {}, + }, + }, + tags = {}, + write_timeout = 60000, + }, + }, + upstreams = { + { + algorithm = "round-robin", + id = "e9792964-6797-482c-bfdf-08220a4f6832", + name = "httproute.default.httproute-testing.0", + tags = { + "k8s-name:httproute-testing", + "k8s-namespace:default", + "k8s-kind:HTTPRoute", + "k8s-uid:f9792964-6797-482c-bfdf-08220a4f6839", + "k8s-group:gateway.networking.k8s.io", + "k8s-version:v1", + }, + targets = { + { + id = "715f9482-4236-5fe5-9ae5-e75c1a498940", + target = "10.244.0.11:80", + weight = 1, + }, + { + id = "89a2966d-773c-580a-b063-6ab4dfd24701", + target = "10.244.0.12:80", + weight = 1, + }, + }, + }, + { + algorithm = "round-robin", + id = "f9792964-6797-482c-bfdf-08220a4f6839", + name = "httproute.default.httproute-testing.1", + tags = { + "k8s-name:httproute-testing", + "k8s-namespace:default", + "k8s-kind:HTTPRoute", + "k8s-uid:f9792964-6797-482c-bfdf-08220a4f6839", + "k8s-group:gateway.networking.k8s.io", + "k8s-version:v1", + }, + targets = { + { + id = "48322e4a-b3b0-591b-8ed6-fd95a6d75019", + tags = { + "target-1", + }, + target = "10.244.0.12:80", + weight = 1, + }, + { + id = "48322e4a-b3b0-591b-8ed6-fd95a6d75019", + tags = { + "target-2", + }, + target = "10.244.0.12:80", + weight = 1, + }, + }, + }, + }, + }, + err_t = { + upstreams = { + nil, + { + targets = { + nil, + "uniqueness violation: 'targets' entity with primary key set to '48322e4a-b3b0-591b-8ed6-fd95a6d75019' already declared", + }, + }, + }, + }, + }, + output = { + err_t = { + code = 14, + fields = {}, + flattened_errors = { + { + entity = { + id = "48322e4a-b3b0-591b-8ed6-fd95a6d75019", + tags = { + "target-2", + }, + target = "10.244.0.12:80", + upstream = { + id = "f9792964-6797-482c-bfdf-08220a4f6839", + }, + weight = 1, + }, + entity_id = "48322e4a-b3b0-591b-8ed6-fd95a6d75019", + entity_tags = { + "target-2", + }, + entity_type = "target", + errors = { + { + message = "uniqueness violation: 'targets' entity with primary key set to '48322e4a-b3b0-591b-8ed6-fd95a6d75019' already declared", + type = "entity", + }, + }, + }, + }, + message = "declarative config is invalid: {}", + name = "invalid declarative configuration", + }, + }, + }, +} + +describe("kong.db.errors.declarative_config_flattened()", function() + local errors + + lazy_setup(function() + -- required to initialize _G.kong for the kong.db.errors module + require("spec.helpers") + errors = require("kong.db.errors") + end) + + it("flattens dbless errors into a single array", function() + local function find_err(needle, haystack) + for i = 1, #haystack do + local err = haystack[i] + + if err.entity_type == needle.entity_type + and err.entity_name == needle.entity_name + and err.entity_id == needle.entity_id + and tablex.deepcompare(err.entity_tags, needle.entity_tags, true) + then + return table.remove(haystack, i) + end + end + end + + for _, elem in ipairs(TESTS) do + local exp = elem.output.err_t + local got = errors:declarative_config_flattened(elem.input.err_t, elem.input.config) + + local missing = {} + for _, err in ipairs(exp.flattened_errors) do + local found = find_err(err, got.flattened_errors) + if found then + assert.same(err, found) + else + table.insert(missing, err) + end + end + + for _, err in ipairs(missing) do + assert.is_nil(err) + end + + assert.equals(0, #got.flattened_errors) + end + + end) + + it("retains errors that it does not understand how to flatten", function() + local input = { foo = { [2] = "some error" } } + local err_t = errors:declarative_config_flattened(input, {}) + assert.equals(0, #err_t.flattened_errors) + assert.same(input, err_t.fields) + end) + + it("ensures that `flattened_errors` encodes to a JSON array when empty", function() + local err_t = errors:declarative_config_flattened({}, {}) + assert.is_table(err_t) + local flattened_errors = assert.is_table(err_t.flattened_errors) + assert.equals(0, #flattened_errors) + assert.same(cjson.array_mt, debug.getmetatable(flattened_errors)) + assert.equals("[]", cjson.encode(flattened_errors)) + end) + + it("throws for invalid inputs", function() + assert.has_error(function() + errors:declarative_config_flattened() + end) + + assert.has_error(function() + errors:declarative_config_flattened(1, 2) + end) + + assert.has_error(function() + errors:declarative_config_flattened({}, 123) + end) + + assert.has_error(function() + errors:declarative_config_flattened(123, {}) + end) + end) +end)