diff --git a/changelog/unreleased/kong/standardize-redis-conifguration-acme.yml b/changelog/unreleased/kong/standardize-redis-conifguration-acme.yml new file mode 100644 index 000000000000..c1c4ca5fc318 --- /dev/null +++ b/changelog/unreleased/kong/standardize-redis-conifguration-acme.yml @@ -0,0 +1,3 @@ +message: "**ACME**: Standardize redis configuration across plugins. The redis configuration right now follows common schema that is shared across other plugins." +type: deprecation +scope: Plugin diff --git a/kong-3.6.0-0.rockspec b/kong-3.6.0-0.rockspec index 5e9ec6846656..04dabfb1d75d 100644 --- a/kong-3.6.0-0.rockspec +++ b/kong-3.6.0-0.rockspec @@ -181,6 +181,7 @@ build = { ["kong.tools.ip"] = "kong/tools/ip.lua", ["kong.tools.http"] = "kong/tools/http.lua", ["kong.tools.cjson"] = "kong/tools/cjson.lua", + ["kong.tools.redis.schema"] = "kong/tools/redis/schema.lua", ["kong.runloop.handler"] = "kong/runloop/handler.lua", ["kong.runloop.events"] = "kong/runloop/events.lua", @@ -478,14 +479,18 @@ build = { ["kong.plugins.acme.api"] = "kong/plugins/acme/api.lua", ["kong.plugins.acme.client"] = "kong/plugins/acme/client.lua", + ["kong.plugins.acme.clustering.compat.redis_translation"] = "kong/plugins/acme/clustering/compat/redis_translation.lua", ["kong.plugins.acme.daos"] = "kong/plugins/acme/daos.lua", ["kong.plugins.acme.handler"] = "kong/plugins/acme/handler.lua", ["kong.plugins.acme.migrations.000_base_acme"] = "kong/plugins/acme/migrations/000_base_acme.lua", ["kong.plugins.acme.migrations.001_280_to_300"] = "kong/plugins/acme/migrations/001_280_to_300.lua", ["kong.plugins.acme.migrations.002_320_to_330"] = "kong/plugins/acme/migrations/002_320_to_330.lua", + ["kong.plugins.acme.migrations.003_350_to_360"] = "kong/plugins/acme/migrations/003_350_to_360.lua", ["kong.plugins.acme.migrations"] = "kong/plugins/acme/migrations/init.lua", ["kong.plugins.acme.schema"] = "kong/plugins/acme/schema.lua", ["kong.plugins.acme.storage.kong"] = "kong/plugins/acme/storage/kong.lua", + ["kong.plugins.acme.storage.config_adapters"] = "kong/plugins/acme/storage/config_adapters/init.lua", + ["kong.plugins.acme.storage.config_adapters.redis"] = "kong/plugins/acme/storage/config_adapters/redis.lua", ["kong.plugins.acme.reserved_words"] = "kong/plugins/acme/reserved_words.lua", ["kong.plugins.prometheus.api"] = "kong/plugins/prometheus/api.lua", diff --git a/kong/clustering/compat/checkers.lua b/kong/clustering/compat/checkers.lua index 78498222e726..4866d5fbda49 100644 --- a/kong/clustering/compat/checkers.lua +++ b/kong/clustering/compat/checkers.lua @@ -23,6 +23,28 @@ end local compatible_checkers = { + { 3006000000, --[[ 3.6.0.0 ]] + function(config_table, dp_version, log_suffix) + local has_update + local redis_plugins_update = { + acme = require("kong.plugins.acme.clustering.compat.redis_translation").adapter + } + + for _, plugin in ipairs(config_table.plugins or {}) do + local adapt_fn = redis_plugins_update[plugin.name] + if adapt_fn and type(adapt_fn) == "function" then + has_update = adapt_fn(plugin.config) + if has_update then + log_warn_message('adapts ' .. plugin.name .. ' plugin redis configuration to older version', + 'revert to older schema', + dp_version, log_suffix) + end + end + end + + return has_update + end, + }, { 3005000000, --[[ 3.5.0.0 ]] function(config_table, dp_version, log_suffix) local has_update diff --git a/kong/plugins/acme/client.lua b/kong/plugins/acme/client.lua index 8f3378377d5b..8254d92fefeb 100644 --- a/kong/plugins/acme/client.lua +++ b/kong/plugins/acme/client.lua @@ -2,6 +2,7 @@ local acme = require "resty.acme.client" local util = require "resty.acme.util" local x509 = require "resty.openssl.x509" local reserved_words = require "kong.plugins.acme.reserved_words" +local config_adapters = require "kong.plugins.acme.storage.config_adapters" local cjson = require "cjson" local ngx_ssl = require "ngx.ssl" @@ -82,7 +83,7 @@ local function new_storage_adapter(conf) if not storage then return nil, nil, "storage is nil" end - local storage_config = conf.storage_config[storage] + local storage_config = config_adapters.adapt_config(conf.storage, conf.storage_config) if not storage_config then return nil, nil, storage .. " is not defined in plugin storage config" end @@ -101,6 +102,7 @@ local function new(conf) if err then return nil, err end + local storage_config = config_adapters.adapt_config(conf.storage, conf.storage_config) local account_name = account_name(conf) local account, err = cached_get(st, account_name, deserialize_account) if err then @@ -125,7 +127,7 @@ local function new(conf) account_key = account.key, api_uri = url, storage_adapter = storage_full_path, - storage_config = conf.storage_config[conf.storage], + storage_config = storage_config, eab_kid = conf.eab_kid, eab_hmac_key = conf.eab_hmac_key, challenge_start_callback = hybrid_mode and function() diff --git a/kong/plugins/acme/clustering/compat/redis_translation.lua b/kong/plugins/acme/clustering/compat/redis_translation.lua new file mode 100644 index 000000000000..9c1e43690248 --- /dev/null +++ b/kong/plugins/acme/clustering/compat/redis_translation.lua @@ -0,0 +1,23 @@ +local function adapter(config_to_update) + if config_to_update.storage == "redis" then + config_to_update.storage_config.redis = { + host = config_to_update.storage_config.redis.host, + port = config_to_update.storage_config.redis.port, + auth = config_to_update.storage_config.redis.password, + database = config_to_update.storage_config.redis.database, + ssl = config_to_update.storage_config.redis.ssl, + ssl_verify = config_to_update.storage_config.redis.ssl_verify, + ssl_server_name = config_to_update.storage_config.redis.server_name, + namespace = config_to_update.storage_config.redis.extra_options.namespace, + scan_count = config_to_update.storage_config.redis.extra_options.scan_count + } + + return true + end + + return false +end + +return { + adapter = adapter +} diff --git a/kong/plugins/acme/migrations/003_350_to_360.lua b/kong/plugins/acme/migrations/003_350_to_360.lua new file mode 100644 index 000000000000..084f772170c5 --- /dev/null +++ b/kong/plugins/acme/migrations/003_350_to_360.lua @@ -0,0 +1,41 @@ +return { + postgres = { + up = [[ + DO $$ + BEGIN + UPDATE plugins + SET config = + config + #- '{storage_config,redis}' + + || jsonb_build_object( + 'storage_config', + (config -> 'storage_config') - 'redis' + || jsonb_build_object( + 'redis', + jsonb_build_object( + 'host', config #> '{storage_config, redis, host}', + 'port', config #> '{storage_config, redis, port}', + 'password', config #> '{storage_config, redis, auth}', + 'username', config #> '{storage_config, redis, username}', + 'ssl', config #> '{storage_config, redis, ssl}', + 'ssl_verify', config #> '{storage_config, redis, ssl_verify}', + 'server_name', config #> '{storage_config, redis, ssl_server_name}', + 'timeout', config #> '{storage_config, redis, timeout}', + 'database', config #> '{storage_config, redis, database}' + ) || jsonb_build_object( + 'extra_options', + jsonb_build_object( + 'scan_count', config #> '{storage_config, redis, scan_count}', + 'namespace', config #> '{storage_config, redis, namespace}' + ) + ) + ) + ) + WHERE name = 'acme'; + EXCEPTION WHEN UNDEFINED_COLUMN OR UNDEFINED_TABLE THEN + -- Do nothing, accept existing state + END$$; + ]], + }, +} diff --git a/kong/plugins/acme/migrations/init.lua b/kong/plugins/acme/migrations/init.lua index bb8bb45beb45..6ecb4346c352 100644 --- a/kong/plugins/acme/migrations/init.lua +++ b/kong/plugins/acme/migrations/init.lua @@ -2,4 +2,5 @@ return { "000_base_acme", "001_280_to_300", "002_320_to_330", + "003_350_to_360", } diff --git a/kong/plugins/acme/schema.lua b/kong/plugins/acme/schema.lua index df50fc743d1c..ee2e4ebcb8dc 100644 --- a/kong/plugins/acme/schema.lua +++ b/kong/plugins/acme/schema.lua @@ -1,5 +1,9 @@ local typedefs = require "kong.db.schema.typedefs" local reserved_words = require "kong.plugins.acme.reserved_words" +local redis_schema = require "kong.tools.redis.schema" +local deprecation = require("kong.deprecation") + +local tablex = require "pl.tablex" local CERT_TYPES = { "rsa", "ecc" } @@ -34,13 +38,9 @@ local SHM_STORAGE_SCHEMA = { local KONG_STORAGE_SCHEMA = { } -local REDIS_STORAGE_SCHEMA = { - { host = typedefs.host, }, - { port = typedefs.port, }, - { database = { type = "number", description = "The index of the Redis database to use.", } }, +-- deprecated old schema +local REDIS_LEGACY_SCHEMA_FIELDS = { { auth = { type = "string", referenceable = true, description = "The Redis password to use for authentication. " } }, - { ssl = { type = "boolean", required = true, default = false, description = "Whether to use SSL/TLS encryption when connecting to the Redis server."} }, - { ssl_verify = { type = "boolean", required = true, default = false, description = "Whether to verify the SSL/TLS certificate presented by the Redis server. This should be a boolean value." } }, { ssl_server_name = typedefs.sni { required = false, description = "The expected server name for the SSL/TLS certificate presented by the Redis server." }}, { namespace = { @@ -55,6 +55,29 @@ local REDIS_STORAGE_SCHEMA = { { scan_count = { type = "number", required = false, default = 10, description = "The number of keys to return in Redis SCAN calls." } }, } +local REDIS_STORAGE_SCHEMA = tablex.copy(redis_schema.config_schema.fields) +for _,v in ipairs(REDIS_LEGACY_SCHEMA_FIELDS) do + table.insert(REDIS_STORAGE_SCHEMA, v) +end + +table.insert(REDIS_STORAGE_SCHEMA, { extra_options = { + description = "Custom ACME Redis options", + type = "record", + fields = { + { + namespace = { + type = "string", + description = "A namespace to prepend to all keys stored in Redis.", + required = true, + default = "", + len_min = 0, + custom_validator = validate_namespace + } + }, + { scan_count = { type = "number", required = false, default = 10, description = "The number of keys to return in Redis SCAN calls." } }, + } +} }) + local CONSUL_STORAGE_SCHEMA = { { https = { type = "boolean", default = false, description = "Boolean representation of https."}, }, { host = typedefs.host}, @@ -248,6 +271,28 @@ local schema = { end } }, + { custom_entity_check = { + field_sources = { "config.storage_config.redis.namespace", "config.storage_config.redis.scan_count", "config.storage_config.redis.auth", "config.storage_config.redis.ssl_server_name" }, + fn = function(entity) + if (entity.config.storage_config.redis.namespace or ngx.null) ~= ngx.null and entity.config.storage_config.redis.namespace ~= "" then + deprecation("acme: config.storage_config.redis.namespace is deprecated, please use config.storage_config.redis.extra_options.namespace instead", + { after = "4.0", }) + end + if (entity.config.storage_config.redis.scan_count or ngx.null) ~= ngx.null and entity.config.storage_config.redis.scan_count ~= 10 then + deprecation("acme: config.storage_config.redis.scan_count is deprecated, please use config.storage_config.redis.extra_options.scan_count instead", + { after = "4.0", }) + end + if (entity.config.storage_config.redis.auth or ngx.null) ~= ngx.null then + deprecation("acme: config.storage_config.redis.auth is deprecated, please use config.storage_config.redis.password instead", + { after = "4.0", }) + end + if (entity.config.storage_config.redis.ssl_server_name or ngx.null) ~= ngx.null then + deprecation("acme: config.storage_config.redis.ssl_server_name is deprecated, please use config.storage_config.redis.server_name instead", + { after = "4.0", }) + end + return true + end + } } }, } diff --git a/kong/plugins/acme/storage/config_adapters/init.lua b/kong/plugins/acme/storage/config_adapters/init.lua new file mode 100644 index 000000000000..2340d8ac9bf3 --- /dev/null +++ b/kong/plugins/acme/storage/config_adapters/init.lua @@ -0,0 +1,28 @@ +local redis_config_adapter = require "kong.plugins.acme.storage.config_adapters.redis" + +local function load_adapters() + local adapters_mapping = { + redis = redis_config_adapter + } + + local function identity(config) + return config + end + + local default_value_mt = { __index = function() return identity end } + + setmetatable(adapters_mapping, default_value_mt) + + return adapters_mapping +end + +local adapters = load_adapters() + +local function adapt_config(storage_type, storage_config) + local adapter_fn = adapters[storage_type] + return adapter_fn(storage_config[storage_type]) +end + +return { + adapt_config = adapt_config +} diff --git a/kong/plugins/acme/storage/config_adapters/redis.lua b/kong/plugins/acme/storage/config_adapters/redis.lua new file mode 100644 index 000000000000..0797d2eacb2b --- /dev/null +++ b/kong/plugins/acme/storage/config_adapters/redis.lua @@ -0,0 +1,16 @@ +local function redis_config_adapter(conf) + return { + host = conf.host, + port = conf.port, + database = conf.database, + auth = conf.password or conf.auth, -- allow conf.auth until 4.0 version + ssl = conf.ssl, + ssl_verify = conf.ssl_verify, + ssl_server_name = conf.server_name or conf.ssl_server_name, -- allow conf.ssl_server_name until 4.0 version + + namespace = conf.extra_options.namespace or conf.namespace, -- allow conf.namespace until 4.0 version + scan_count = conf.extra_options.scan_count or conf.scan_count, -- allow conf.scan_count until 4.0 version + } +end + +return redis_config_adapter diff --git a/kong/tools/redis/schema.lua b/kong/tools/redis/schema.lua new file mode 100644 index 000000000000..e40a72532e7a --- /dev/null +++ b/kong/tools/redis/schema.lua @@ -0,0 +1,38 @@ +local typedefs = require "kong.db.schema.typedefs" +local DEFAULT_TIMEOUT = 2000 + +return { + config_schema = { + type = "record", + fields = { + { host = typedefs.host }, + { port = typedefs.port }, + { timeout = typedefs.timeout { default = DEFAULT_TIMEOUT } }, + { username = { description = "Username to use for Redis connections. If undefined, ACL authentication won't be performed. This requires Redis v6.0.0+. To be compatible with Redis v5.x.y, you can set it to `default`.", type = "string", + referenceable = true + } }, + { password = { description = "Password to use for Redis connections. If undefined, no AUTH commands are sent to Redis.", type = "string", + encrypted = true, + referenceable = true, + len_min = 0 + } }, + { database = { description = "Database to use for the Redis connection when using the `redis` strategy", type = "integer", + default = 0 + } }, + { ssl = { description = "If set to true, uses SSL to connect to Redis.", + type = "boolean", + required = false, + default = false + } }, + { ssl_verify = { description = "If set to true, verifies the validity of the server SSL certificate. If setting this parameter, also configure `lua_ssl_trusted_certificate` in `kong.conf` to specify the CA (or server) certificate used by your Redis server. You may also need to configure `lua_ssl_verify_depth` accordingly.", + type = "boolean", + required = false, + default = false + } }, + { server_name = typedefs.sni { required = false } } + }, + entity_checks = { + { mutually_required = { "host", "port" }, }, + }, + } +} diff --git a/spec/02-integration/01-helpers/01-helpers_spec.lua b/spec/02-integration/01-helpers/01-helpers_spec.lua index c4e383ffd236..ccc0232afce2 100644 --- a/spec/02-integration/01-helpers/01-helpers_spec.lua +++ b/spec/02-integration/01-helpers/01-helpers_spec.lua @@ -1443,4 +1443,91 @@ describe("helpers: utilities", function() end, "Is a directory") end) end) + + describe("partial_match()", function() + describe("positive mod", function() + it("allows to match to tables paritally", function() + local partial_table = { + x = 100, + y = { + z = 200 + } + } + local full_table = { + x = 100, + a = "test1", + y = { + b = "test2", + z = 200 + } + } + + assert.partial_match(partial_table, full_table) + end) + + it("fails if tables do not match paritally", function() + local partial_table = { + x = 100, + y = { + z = 77 + } + } + local full_table = { + x = 100, + a = "test1", + y = { + b = "test2", + z = 200 + } + } + + local ok, err_actual = pcall(function() assert.partial_match(partial_table, full_table) end) + assert.falsy(ok) + assert.matches(".*Values at key %(string%) 'y%.z' should be equal but are not.\nExpected: %(number%) 77, given: %(number%) 200\n", err_actual.message) + end) + end) + + describe("negative mod", function() + it("allows to verify if tables do not match", function() + local partial_table = { + x = 77, + y = { + z = 88 + } + } + + local full_table = { + x = 100, + a = "test1", + y = { + b = "test2", + z = 200 + } + } + + assert.does_not.partial_match(partial_table, full_table) + end) + + it("fails if tables do match paritally", function() + local partial_table = { + x = 100, + y = { + z = 77 + } + } + local full_table = { + x = 100, + a = "test1", + y = { + b = "test2", + z = 200 + } + } + + local ok, err_actual = pcall(function() assert.does_not.partial_match(partial_table, full_table) end) + assert.falsy(ok) + assert.matches(".*Values at key %(string%) 'x' should not be equal", err_actual.message) + end) + end) + end) end) diff --git a/spec/02-integration/09-hybrid_mode/09-config-compat_spec.lua b/spec/02-integration/09-hybrid_mode/09-config-compat_spec.lua index e3fe12f9bb54..af3a0aaf404b 100644 --- a/spec/02-integration/09-hybrid_mode/09-config-compat_spec.lua +++ b/spec/02-integration/09-hybrid_mode/09-config-compat_spec.lua @@ -108,10 +108,14 @@ describe("CP/DP config compat transformations #" .. strategy, function() end) describe("plugin config fields", function() - local rate_limit, cors, opentelemetry, zipkin + local function do_assert(node_id, node_version, expected_entity) + local plugin = get_plugin(node_id, node_version, expected_entity.name) + assert.same(expected_entity.config, plugin.config) + assert.equals(CLUSTERING_SYNC_STATUS.NORMAL, get_sync_status(node_id)) + end - lazy_setup(function() - rate_limit = admin.plugins:insert { + it("removes new fields before sending them to older DP nodes", function() + local rate_limit = admin.plugins:insert { name = "rate-limiting", enabled = true, config = { @@ -125,29 +129,6 @@ describe("CP/DP config compat transformations #" .. strategy, function() -- ]] }, } - - cors = admin.plugins:insert { - name = "cors", - enabled = true, - config = { - -- [[ new fields 3.5.0 - private_network = false - -- ]] - } - } - end) - - lazy_teardown(function() - admin.plugins:remove({ id = rate_limit.id }) - end) - - local function do_assert(node_id, node_version, expected_entity) - local plugin = get_plugin(node_id, node_version, expected_entity.name) - assert.same(expected_entity.config, plugin.config) - assert.equals(CLUSTERING_SYNC_STATUS.NORMAL, get_sync_status(node_id)) - end - - it("removes new fields before sending them to older DP nodes", function() --[[ For 3.0.x should not have: error_code, error_message, sync_rate @@ -177,29 +158,75 @@ describe("CP/DP config compat transformations #" .. strategy, function() expected = utils.cycle_aware_deep_copy(rate_limit) expected.config.sync_rate = nil do_assert(utils.uuid(), "3.3.0", expected) + + -- cleanup + admin.plugins:remove({ id = rate_limit.id }) end) it("does not remove fields from DP nodes that are already compatible", function() + local rate_limit = admin.plugins:insert { + name = "rate-limiting", + enabled = true, + config = { + second = 1, + policy = "local", + + -- [[ new fields + error_code = 403, + error_message = "go away!", + sync_rate = -1, + -- ]] + }, + } + do_assert(utils.uuid(), "3.4.0", rate_limit) + + -- cleanup + admin.plugins:remove({ id = rate_limit.id }) end) describe("compatibility test for cors plugin", function() it("removes `config.private_network` before sending them to older(less than 3.5.0.0) DP nodes", function() + local cors = admin.plugins:insert { + name = "cors", + enabled = true, + config = { + -- [[ new fields 3.5.0 + private_network = false + -- ]] + } + } + assert.not_nil(cors.config.private_network) local expected_cors = utils.cycle_aware_deep_copy(cors) expected_cors.config.private_network = nil do_assert(utils.uuid(), "3.4.0", expected_cors) + + -- cleanup + admin.plugins:remove({ id = cors.id }) end) it("does not remove `config.private_network` from DP nodes that are already compatible", function() + local cors = admin.plugins:insert { + name = "cors", + enabled = true, + config = { + -- [[ new fields 3.5.0 + private_network = false + -- ]] + } + } do_assert(utils.uuid(), "3.5.0", cors) + + -- cleanup + admin.plugins:remove({ id = cors.id }) end) end) describe("compatibility tests for opentelemetry plugin", function() it("replaces `aws` values of `header_type` property with default `preserve`", function() -- [[ 3.5.x ]] -- - opentelemetry = admin.plugins:insert { + local opentelemetry = admin.plugins:insert { name = "opentelemetry", enabled = true, config = { @@ -244,7 +271,7 @@ describe("CP/DP config compat transformations #" .. strategy, function() describe("compatibility tests for zipkin plugin", function() it("replaces `aws` and `gcp` values of `header_type` property with default `preserve`", function() -- [[ 3.5.x ]] -- - zipkin = admin.plugins:insert { + local zipkin = admin.plugins:insert { name = "zipkin", enabled = true, config = { @@ -284,6 +311,58 @@ describe("CP/DP config compat transformations #" .. strategy, function() admin.plugins:remove({ id = zipkin.id }) end) end) + + describe("compatibility tests for redis standarization", function() + describe("acme plugin", function() + it("translates standardized redis config to older acme structure", function() + -- [[ 3.6.x ]] -- + local acme = admin.plugins:insert { + name = "acme", + enabled = true, + config = { + account_email = "test@example.com", + storage = "redis", + storage_config = { + -- [[ new structure redis + redis = { + host = "localhost", + port = 57198, + username = "test", + password = "secret", + database = 2, + timeout = 1100, + ssl = true, + ssl_verify = true, + server_name = "example.test", + extra_options = { + namespace = "test_namespace", + scan_count = 13 + } + } + -- ]] + } + } + } + + local expected_acme_prior_36 = utils.cycle_aware_deep_copy(acme) + expected_acme_prior_36.config.storage_config.redis = { + host = "localhost", + port = 57198, + auth = "secret", + database = 2, + ssl = true, + ssl_verify = true, + ssl_server_name = "example.test", + namespace = "test_namespace", + scan_count = 13 + } + do_assert(utils.uuid(), "3.5.0", expected_acme_prior_36) + + -- cleanup + admin.plugins:remove({ id = acme.id }) + end) + end) + end) end) end) diff --git a/spec/03-plugins/29-acme/05-redis_storage_spec.lua b/spec/03-plugins/29-acme/05-redis_storage_spec.lua index 99e0b46e64f7..970d736bab01 100644 --- a/spec/03-plugins/29-acme/05-redis_storage_spec.lua +++ b/spec/03-plugins/29-acme/05-redis_storage_spec.lua @@ -1,7 +1,9 @@ local redis_storage = require("resty.acme.storage.redis") local reserved_words = require "kong.plugins.acme.reserved_words" +local cjson = require "cjson" local helpers = require "spec.helpers" +local config_adapters = require "kong.plugins.acme.storage.config_adapters" describe("Plugin: acme (storage.redis)", function() it("should successfully connect to the Redis SSL port", function() @@ -24,6 +26,37 @@ describe("Plugin: acme (storage.redis)", function() assert.equal("bar", value) end) + describe("when using config adapter", function() + it("should successfully connect to the Redis SSL port", function() + local storage_type = "redis" + local new_config = { + redis = { + host = helpers.redis_host, + port = helpers.redis_port, + database = 0, + password = nil, + ssl = false, + ssl_verify = false, + server_name = nil, + extra_options = { + namespace = "test", + scan_count = 13 + } + } + } + local storage_config = config_adapters.adapt_config(storage_type, new_config) + + local storage, err = redis_storage.new(storage_config) + assert.is_nil(err) + assert.not_nil(storage) + local err = storage:set("foo", "bar", 10) + assert.is_nil(err) + local value, err = storage:get("foo") + assert.is_nil(err) + assert.equal("bar", value) + end) + end) + describe("redis namespace", function() local config = { host = helpers.redis_host, @@ -224,6 +257,7 @@ describe("Plugin: acme (storage.redis)", function() before_each(function() client = helpers.admin_client() + helpers.clean_logfile() end) after_each(function() @@ -232,6 +266,15 @@ describe("Plugin: acme (storage.redis)", function() end end) + local function delete_plugin(admin_client, plugin) + local res = assert(admin_client:send({ + method = "DELETE", + path = "/plugins/" .. plugin.id, + })) + + assert.res_status(204, res) + end + it("successfully create acme plugin with valid namespace", function() local res = assert(client:send { method = "POST", @@ -248,13 +291,66 @@ describe("Plugin: acme (storage.redis)", function() redis = { host = helpers.redis_host, port = helpers.redis_port, - namespace = "namespace1:", + password = "test", + server_name = "example.test", + extra_options = { + namespace = "namespace1:", + scan_count = 13 + } + }, + }, + }, + }, + }) + local json = cjson.decode(assert.res_status(201, res)) + delete_plugin(client, json) + assert.logfile().has.no.line("acme: config.storage_config.redis.namespace is deprecated, " .. + "please use config.storage_config.redis.extra_options.namespace instead (deprecated after 4.0)", true) + assert.logfile().has.no.line("acme: config.storage_config.redis.scan_count is deprecated, " .. + "please use config.storage_config.redis.extra_options.scan_count instead (deprecated after 4.0)", true) + assert.logfile().has.no.line("acme: config.storage_config.redis.auth is deprecated, " .. + "please use config.storage_config.redis.password instead (deprecated after 4.0)", true) + assert.logfile().has.no.line("acme: config.storage_config.redis.ssl_server_name is deprecated, " .. + "please use config.storage_config.redis.server_name instead (deprecated after 4.0)", true) + end) + + it("successfully create acme plugin with legacy fields", function() + local res = assert(client:send { + method = "POST", + path = "/plugins", + headers = { ["Content-Type"] = "application/json" }, + body = { + name = "acme", + config = { + account_email = "test@test.com", + api_uri = "https://api.acme.org", + storage = "redis", + preferred_chain = "test", + storage_config = { + redis = { + host = helpers.redis_host, + port = helpers.redis_port, + + auth = "test", + ssl_server_name = "example.test", + scan_count = 13, + namespace = "namespace2:", }, }, }, }, }) - assert.res_status(201, res) + + local json = cjson.decode(assert.res_status(201, res)) + delete_plugin(client, json) + assert.logfile().has.line("acme: config.storage_config.redis.namespace is deprecated, " .. + "please use config.storage_config.redis.extra_options.namespace instead (deprecated after 4.0)", true) + assert.logfile().has.line("acme: config.storage_config.redis.scan_count is deprecated, " .. + "please use config.storage_config.redis.extra_options.scan_count instead (deprecated after 4.0)", true) + assert.logfile().has.line("acme: config.storage_config.redis.auth is deprecated, " .. + "please use config.storage_config.redis.password instead (deprecated after 4.0)", true) + assert.logfile().has.line("acme: config.storage_config.redis.ssl_server_name is deprecated, " .. + "please use config.storage_config.redis.server_name instead (deprecated after 4.0)", true) end) it("fail to create acme plugin with invalid namespace", function() @@ -274,7 +370,9 @@ describe("Plugin: acme (storage.redis)", function() redis = { host = helpers.redis_host, port = helpers.redis_port, - namespace = v, + extra_options = { + namespace = v, + } }, }, }, @@ -329,7 +427,9 @@ describe("Plugin: acme (storage.redis)", function() redis = { host = helpers.redis_host, port = helpers.redis_port, - -- namespace: "", default to empty + -- extra_options = { + -- namespace: "", default to empty + -- } }, }, }, @@ -379,7 +479,9 @@ describe("Plugin: acme (storage.redis)", function() redis = { host = helpers.redis_host, port = helpers.redis_port, - namespace = namespace, -- change namespace + extra_options = { + namespace = namespace, -- change namespace + } }, }, }, diff --git a/spec/05-migration/plugins/acme/migrations/003_350_to_360_spec.lua b/spec/05-migration/plugins/acme/migrations/003_350_to_360_spec.lua new file mode 100644 index 000000000000..77dae348495c --- /dev/null +++ b/spec/05-migration/plugins/acme/migrations/003_350_to_360_spec.lua @@ -0,0 +1,70 @@ + +local cjson = require "cjson" +local uh = require "spec.upgrade_helpers" + +if uh.database_type() == 'postgres' then + describe("acme plugin migration", function() + lazy_setup(function() + assert(uh.start_kong()) + end) + + lazy_teardown(function () + assert(uh.stop_kong(nil, true)) + end) + + uh.setup(function () + local admin_client = assert(uh.admin_client()) + + local res = assert(admin_client:send { + method = "POST", + path = "/plugins/", + body = { + name = "acme", + config = { + account_email = "test@example.com", + storage = "redis", + storage_config = { + redis = { + host = "localhost", + port = 57198, + auth = "secret", + database = 2 + } + } + } + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + assert.res_status(201, res) + admin_client:close() + end) + + uh.new_after_up("has updated acme redis configuration", function () + local admin_client = assert(uh.admin_client()) + local res = assert(admin_client:send { + method = "GET", + path = "/plugins/" + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.equal(1, #body.data) + assert.equal("acme", body.data[1].name) + local expected_config = { + account_email = "test@example.com", + storage = "redis", + storage_config = { + redis = { + host = "localhost", + port = 57198, + password = "secret", + database = 2 + } + } + } + + assert.partial_match(expected_config, body.data[1].config) + admin_client:close() + end) + end) +end diff --git a/spec/helpers.lua b/spec/helpers.lua index 256e1139648b..102b2ce45e1d 100644 --- a/spec/helpers.lua +++ b/spec/helpers.lua @@ -3133,6 +3133,59 @@ do end end +--- +-- Assertion to partially compare two lua tables. +-- @function partial_match +-- @param partial_table the table with subset of fields expect to match +-- @param full_table the full table that should contain partial_table and potentially other fields +local function partial_match(state, arguments) + + local function deep_matches(t1, t2, parent_keys) + for key, v in pairs(t1) do + local compound_key = (parent_keys and parent_keys .. "." .. key) or key + if type(v) == "table" then + local ok, compound_key, v1, v2 = deep_matches(t1[key], t2[key], compound_key) + if not ok then + return ok, compound_key, v1, v2 + end + else + if (state.mod == true and t1[key] ~= t2[key]) or (state.mod == false and t1[key] == t2[key]) then + return false, compound_key, t1[key], t2[key] + end + end + end + + return true + end + + local partial_table = arguments[1] + local full_table = arguments[2] + + local ok, compound_key, v1, v2 = deep_matches(partial_table, full_table) + + if not ok then + arguments[1] = compound_key + arguments[2] = v1 + arguments[3] = v2 + arguments.n = 3 + + return not state.mod + end + + return state.mod +end + +say:set("assertion.partial_match.negative", [[ +Values at key %s should not be equal +]]) +say:set("assertion.partial_match.positive", [[ +Values at key %s should be equal but are not. +Expected: %s, given: %s +]]) +luassert:register("assertion", "partial_match", partial_match, + "assertion.partial_match.positive", + "assertion.partial_match.negative") + ---------------- -- Shell helpers