From dde7342c52078b3777859b0abfacdcb3970e2c1b Mon Sep 17 00:00:00 2001 From: Alexander Turenko <alexander.turenko@tarantool.org> Date: Sat, 23 Dec 2023 03:58:40 +0300 Subject: [PATCH] box: support TT_* uri env vars with query params `TT_LISTEN` and `TT_REPLICATION` environment variables were interpreted by `box.cfg()` in a confusing way if query parameters with values are present. For example, `localhost:3301?transport=plain` was interpreted as the following map: `{['localhost:3301?transport'] = 'plain'}`. Later, `box.cfg()` looks into this map for known URI fields like `login`, `password`, `uri`, `host`, `service` and so on. It found nothing and doesn't start a listening socket. The reason of such a behaviour is that the environment value is interpreted as a mapping in the `key=value,key=value` format, because there is `=` in it. The patch changes this behavior for an `key=value,key=value` environment variable that contains `?` in a key: now such a value is not interpreted as a mapping. Note: Everything said above is also applicable to the so called multilisten case: when several URIs are defined in the environment variable. The following URI list is interpreted correctly now. NOWRAP ```sh export TT_LISTEN=localhost:3301?transport=plain,localhost:3302?transport=plain ``` NOWRAP Note 2: Examples are given with the `plain` transport, which is default, but the query parameters are the way to define TLS options. They're supported in Tarantool Enterprise Edition, see [1]. Fixes #9539 NO_DOC=bugfix [1]: https://www.tarantool.io/en/doc/latest/enterprise/security/#traffic-encryption --- ...9539-box-cfg-env-vars-with-query-params.md | 4 + src/box/lua/load_cfg.lua | 45 +++- test/box-luatest/box_cfg_env_test.lua | 255 ++++++++++++++++++ 3 files changed, 295 insertions(+), 9 deletions(-) create mode 100644 changelogs/unreleased/gh-9539-box-cfg-env-vars-with-query-params.md create mode 100644 test/box-luatest/box_cfg_env_test.lua diff --git a/changelogs/unreleased/gh-9539-box-cfg-env-vars-with-query-params.md b/changelogs/unreleased/gh-9539-box-cfg-env-vars-with-query-params.md new file mode 100644 index 0000000000..59fe7ceb4d --- /dev/null +++ b/changelogs/unreleased/gh-9539-box-cfg-env-vars-with-query-params.md @@ -0,0 +1,4 @@ +## bugfix/box + +* Now `box.cfg()` correctly interprets the `TT_LISTEN` and `TT_REPLICATION` + environment variables with query parameters (gh-9539). diff --git a/src/box/lua/load_cfg.lua b/src/box/lua/load_cfg.lua index fd678e918a..1a5fe6e2e4 100644 --- a/src/box/lua/load_cfg.lua +++ b/src/box/lua/load_cfg.lua @@ -1209,9 +1209,12 @@ local function get_option_from_env(option) if param_type:find('table') and (raw_value:startswith('{') or raw_value:startswith('[')) then return json.decode(raw_value) - elseif param_type:find('table') and raw_value:find('=') then + end + + if param_type:find('table') and raw_value:find('=') then assert(not param_type:find('boolean')) local res = {} + local contains_uri = false for _, v in ipairs(raw_value:split(',')) do local eq = v:find('=') if eq == nil then @@ -1226,17 +1229,37 @@ local function get_option_from_env(option) 'in `key=value` or `value` format, ' .. '`key` must not be empty')) end + -- Don't interpret `=` as a key-value separator if + -- there is `?` in a key. + -- + -- Otherwise, for example, + -- `localhost:3301?transport=plain` would be + -- interpreted as the following map. + -- + -- { + -- ['localhost:3301?transport'] = 'plain', + -- } + if lhs:find('?') then + contains_uri = true + end res[lhs] = tonumber(rhs) or rhs end - return res - elseif param_type:find('table') and raw_value:find(',') then + if not contains_uri then + return res + end + -- Fall through otherwise. + end + + if param_type:find('table') and raw_value:find(',') then assert(not param_type:find('boolean')) local res = {} for i, v in ipairs(raw_value:split(',')) do res[i] = tonumber(v) or v end return res - elseif param_type:find('boolean') then + end + + if param_type:find('boolean') then assert(param_type == 'boolean') if raw_value:lower() == 'false' then return false @@ -1244,20 +1267,24 @@ local function get_option_from_env(option) return true end error(err_msg_fmt:format(env_var_name, option, '"true" or "false"')) - elseif param_type == 'number' then + end + + if param_type == 'number' then local res = tonumber(raw_value) if res == nil then error(err_msg_fmt:format(env_var_name, option, 'convertible to a number')) end return res - elseif param_type:find('number') then + end + + if param_type:find('number') then assert(not param_type:find('boolean')) return tonumber(raw_value) or raw_value - else - assert(param_type == 'string') - return raw_value end + + assert(param_type == 'string') + return raw_value end -- Get options from env vars for given set. diff --git a/test/box-luatest/box_cfg_env_test.lua b/test/box-luatest/box_cfg_env_test.lua new file mode 100644 index 0000000000..57f7af9375 --- /dev/null +++ b/test/box-luatest/box_cfg_env_test.lua @@ -0,0 +1,255 @@ +local fun = require('fun') +local json = require('json') +local t = require('luatest') +local treegen = require('test.treegen') +local justrun = require('test.justrun') + +local g = t.group() + +g.before_all(treegen.init) +g.after_all(treegen.clean) + +-- TT_LISTEN and TT_REPLICATION have many allowed forms. +-- +-- We can't listen on hardcoded port numbers in the test, because +-- we don't know whether it will be free on running of the test +-- suite. Attempt to do so would ruin the stability of the test. +-- +-- So, these test cases don't call `box.cfg()`, but rather use an +-- internal API to parse environment variables into box.cfg option +-- values. +local uri_list_cases = { + -- Plain URI. + { + env = '3301', + cfg = 3301, + }, + { + env = 'localhost:3301', + cfg = 'localhost:3301', + }, + { + env = 'localhost:3301?transport=plain', + cfg = 'localhost:3301?transport=plain', + }, + -- Array of plain URIs. + { + env = '3301,3302', + cfg = {3301, 3302}, + }, + { + env = 'localhost:3301,localhost:3302', + cfg = {'localhost:3301', 'localhost:3302'}, + }, + { + env = 'localhost:3301?transport=plain,' .. + 'localhost:3302?transport=plain', + cfg = {'localhost:3301?transport=plain', + 'localhost:3302?transport=plain'}, + }, + -- Mapping. + { + env = 'uri=3301', + cfg = {uri = 3301}, + }, + { + env = 'uri=localhost:3301', + cfg = {uri = 'localhost:3301'}, + }, + { + env = 'login=foo,password=bar,uri=localhost:3301', + cfg = {login = 'foo', password = 'bar', uri = 'localhost:3301'}, + }, + -- JSON object. + { + env = '{"uri": "3301"}', + cfg = {uri = '3301'}, + }, + { + env = '{"uri": "localhost:3301"}', + cfg = {uri = 'localhost:3301'}, + }, + { + env = '{"uri": "localhost:3301", "params": {"transport": "plain"}}', + cfg = {uri = 'localhost:3301', params = {transport = 'plain'}}, + }, + -- JSON array of numbers. + { + env = '[3301, 3302]', + cfg = {3301, 3302}, + }, + -- JSON array of strings. + { + env = '["3301", "3302"]', + cfg = {'3301', '3302'}, + }, + { + env = '["localhost:3301", "localhost:3302"]', + cfg = {'localhost:3301', 'localhost:3302'}, + }, + { + env = '["localhost:3301?transport=plain", ' .. + '"localhost:3302?transport=plain"]', + cfg = {'localhost:3301?transport=plain', + 'localhost:3302?transport=plain'}, + }, + -- JSON array of objects. + { + env = '[{"uri": "3301"}, {"uri": "3302"}]', + cfg = { + {uri = '3301'}, + {uri = '3302'}, + }, + }, + { + env = '[{"uri": "localhost:3301"}, {"uri": "localhost:3302"}]', + cfg = { + {uri = 'localhost:3301'}, + {uri = 'localhost:3302'}, + }, + }, + { + env = '[{"uri": "localhost:3301", "params": {"transport": "plain"}},' .. + '{"uri": "localhost:3302", "params": {"transport": "plain"}}]', + cfg = { + {uri = 'localhost:3301', params = {transport = 'plain'}}, + {uri = 'localhost:3302', params = {transport = 'plain'}}, + }, + }, +} + +-- Verify TT_LISTEN and TT_REPLICATION using box.internal.cfg.env. +-- +-- It is a kind of a unit test. +-- +-- All the test cases are run using one popen call +-- (justrun.tarantool()) that speeds up the execution. +g.test_uri_list = function(g) + -- Write cases.lua and main.lua. + local dir = treegen.prepare_directory(g, {}, {}) + treegen.write_script(dir, 'cases.lua', json.encode(uri_list_cases)) + treegen.write_script(dir, 'main.lua', string.dump(function() + local json = require('json') + local fio = require('fio') + local t = require('luatest') + + -- Read cases.lua. + local fh, err = fio.open('cases.lua', {'O_RDONLY'}) + if err ~= nil then + error(err) + end + local cases_str, err = fh:read() + if err ~= nil then + error(err) + end + fh:close() + local cases = json.decode(cases_str) + + local function verify(case, env_var_name, box_cfg_option_name) + os.setenv(env_var_name, case.env) + local res = box.internal.cfg.env[box_cfg_option_name] + local ok, err = pcall(t.assert_equals, res, case.cfg) + if not ok then + error(err.message, 0) + end + os.setenv(env_var_name, nil) + end + + -- Set the environment variable, get the box.cfg option + -- from box.internal.cfg.env, check it against a reference + -- value, unset the environment variable. + for _, case in ipairs(cases) do + verify(case, 'TT_LISTEN', 'listen') + verify(case, 'TT_REPLICATION', 'replication') + end + end)) + + local opts = {nojson = true, stderr = true} + local res = justrun.tarantool(dir, {}, {'main.lua'}, opts) + t.assert_equals(res, { + exit_code = 0, + stdout = '', + stderr = '', + }) +end + +-- These test cases use a real box.cfg() call inside a child +-- process. A kind of a functional test. +local cases = { + -- String options. + { + env = {['TT_LOG_FORMAT'] = 'json'}, + cfg = {log_format = 'json'}, + }, + { + env = {['TT_LOG_LEVEL'] = 'debug'}, + cfg = {log_level = 'debug'}, + }, + { + env = {['TT_CUSTOM_PROC_TITLE'] = 'mybox'}, + cfg = {custom_proc_title = 'mybox'}, + }, + -- Numeric options. + { + env = {['TT_READAHEAD'] = '32640'}, + cfg = {readahead = 32640}, + }, + { + env = {['TT_LOG_LEVEL'] = '7'}, + cfg = {log_level = 7}, + }, + { + env = {['TT_REPLICATION_TIMEOUT'] = '0.3'}, + cfg = {replication_timeout = 0.3}, + }, + { + env = {['TT_IPROTO_THREADS'] = '16'}, + cfg = {iproto_threads = 16}, + }, + -- Boolean options. + { + env = {['TT_MEMTX_USE_MVCC_ENGINE'] = 'true'}, + cfg = {memtx_use_mvcc_engine = true}, + }, + { + env = {['TT_MEMTX_USE_MVCC_ENGINE'] = 'false'}, + cfg = {memtx_use_mvcc_engine = false}, + }, + { + env = {['TT_STRIP_CORE'] = 'false'}, + cfg = {strip_core = false}, + }, +} + +-- Write a script to be used in the test cases below. +g.before_all(function(g) + g.dir = treegen.prepare_directory(g, {}, {}) + treegen.write_script(g.dir, 'main.lua', [[ + local json = require('json') + box.cfg() + print(json.encode(box.cfg)) + os.exit() + ]]) +end) + +-- Verify TT_* variables of different types. +for i, case in ipairs(cases) do + g[('test_box_cfg_%03d'):format(i)] = function(g) + local res = justrun.tarantool(g.dir, case.env, {'main.lua'}) + + local res_cfg = fun.iter(case.cfg):map(function(box_cfg_option_name) + if res.stdout == nil or #res.stdout ~= 1 then + return box_cfg_option_name, '???' + end + return box_cfg_option_name, res.stdout[1][box_cfg_option_name] + end):tomap() + + t.assert_equals({ + exit_code = res.exit_code, + cfg = res_cfg, + }, { + exit_code = 0, + cfg = case.cfg, + }) + end +end -- GitLab