diff --git a/apisix/admin/standalone.lua b/apisix/admin/standalone.lua index 46f9a8001bbf..c2ae52a4dd5f 100644 --- a/apisix/admin/standalone.lua +++ b/apisix/admin/standalone.lua @@ -28,6 +28,7 @@ local events = require("apisix.events") local core = require("apisix.core") local config_yaml = require("apisix.core.config_yaml") local config_validate = require("apisix.admin.config_validate") +local file = require("apisix.cli.file") local ALL_RESOURCE_KEYS = config_validate.get_all_resource_keys() @@ -128,6 +129,18 @@ local function update(ctx) return core.response.exit(204) end + -- resolve ${{VAR}} / $ENV:// references before validation, so that the + -- schema check and the stored config both see the resolved values. The + -- file-based standalone path resolves these in config_yaml.parse(); the + -- Admin API path decodes JSON straight into the config and would otherwise + -- validate and store the literal "${{...}}" string. + local resolved, resolve_err = file.resolve_conf_var(req_body) + if not resolved then + return core.response.exit(400, { + error_msg = "failed to resolve variables in config: " .. resolve_err + }) + end + local valid, error_msg = validate_configuration(req_body, false) if not valid then return core.response.exit(400, { error_msg = error_msg }) diff --git a/t/admin/standalone.spec.ts b/t/admin/standalone.spec.ts index 5a71c4122a91..54af5d240d05 100644 --- a/t/admin/standalone.spec.ts +++ b/t/admin/standalone.spec.ts @@ -944,4 +944,96 @@ describe('Validate API - Standalone', () => { }); }); }); + + describe('Variable resolution', () => { + it('resolves ${{VAR}} references in config pushed via the Admin API', async () => { + mockDigest += 1; + const config = { + routes: [ + { + id: 'r_var', + uri: '/r_var', + upstream: { + nodes: { '127.0.0.1:1980': 1 }, + type: 'roundrobin', + }, + // The proxy-rewrite uri uses a ${{VAR:=default}} reference. The + // gateway must resolve it to the default ("/hello"); if it were + // left literal, the upstream would receive "${{...}}" instead of + // "/hello" and would not return the hello body. + plugins: { + 'proxy-rewrite': { uri: '${{STANDALONE_ENV_TEST:=/hello}}' }, + }, + }, + ], + }; + const putResp = await client.put(ENDPOINT, config, { + headers: { [HEADER_DIGEST]: mockDigest }, + }); + expect(putResp.status).toEqual(202); + + const resp = await client.get('/r_var'); + expect(resp.status).toEqual(200); + expect(resp.data).toEqual('hello world\n'); + }); + + it('coerces a resolved ${{VAR}} to its native type before validation', async () => { + mockDigest += 1; + const config = { + routes: [ + { + id: 'r_var_typed', + uri: '/r_var_typed', + upstream: { + nodes: { '127.0.0.1:1980': 1 }, + type: 'roundrobin', + // retries is an integer field. The reference resolves to the + // default "2", which resolve_conf_var coerces to the number 2; + // a literal string "2" would fail integer schema validation + // with 400, so a 202 proves the value was coerced. + retries: '${{STANDALONE_ENV_RETRIES:=2}}', + }, + plugins: { + 'proxy-rewrite': { uri: '/hello' }, + }, + }, + ], + }; + const putResp = await client.put(ENDPOINT, config, { + headers: { [HEADER_DIGEST]: mockDigest }, + }); + expect(putResp.status).toEqual(202); + + const resp = await client.get('/r_var_typed'); + expect(resp.status).toEqual(200); + expect(resp.data).toEqual('hello world\n'); + }); + + it('rejects config with an unresolvable ${{VAR}} reference', async () => { + mockDigest += 1; + const config = { + routes: [ + { + id: 'r_var_bad', + uri: '/r_var_bad', + upstream: { + nodes: { '127.0.0.1:1980': 1 }, + type: 'roundrobin', + }, + // No matching environment variable and no ":=default", so + // resolution fails and the push is rejected with 400 rather than + // storing a literal "${{...}}". + plugins: { + 'proxy-rewrite': { uri: '${{STANDALONE_ENV_UNDEFINED}}' }, + }, + }, + ], + }; + const resp = await client + .put(ENDPOINT, config, { headers: { [HEADER_DIGEST]: mockDigest } }) + .catch((err) => err.response); + expect(resp.status).toEqual(400); + expect(resp.data.error_msg).toContain('failed to resolve variables in config'); + }); + }); });