From cd58c321bc494d0ccac3350cf373cec6cf0f5fd9 Mon Sep 17 00:00:00 2001
From: Gleb Kashkin <g.kashkin@tarantool.org>
Date: Tue, 11 Apr 2023 07:15:42 +0000
Subject: [PATCH] box: allow to set box.cfg table value via env var

All box.cfg options values used to be plain values or arrays. Now some
options contain key-value or nested tables.

This patch allows all such options to be set through environment
variables too.

Closes #8494
Closes #8051

@TarantoolBot document
Title: Set box.cfg table value via env vars

This patch implements a way to set tables as box.cfg options value in
two different ways:
* as plain key-value table:
```
bash> export TT_LOG_MODULES=aaa=info,bbb=error && ./tarantool
Tarantool 2.10.0-beta1-1977-g2970bd57a
type 'help' for interactive help
tarantool> box.cfg{}
...
2023-04-11 07:22:10.951 [219020] main/103/interactive/box.load_cfg I> set \
'log_modules' configuration option to {"aaa":"info","bbb":"error"}
---
...

tarantool> box.cfg.log_modules.aaa
---
- info
...

tarantool> box.cfg.log_modules.bbb
---
- error
...
```

* as a table in json encoding (important: don't forget to put env var
  value into single quotes):
NO_WRAP
```
bash> export TT_METRICS='{"labels":{"alias":"mystorage"},"include":"all","exclude":["vinyl"]}' && ./tarantool
Tarantool 2.10.0-beta1-1977-g2970bd57a
type 'help' for interactive help
tarantool> box.cfg{}
...
2023-04-11 07:26:03.635 [219288] main/103/interactive/box.load_cfg I> set \
'metrics' configuration option to {"exclude":["vinyl"],"include":"all",\
"labels":{"alias":"mystorage"}}
---
...

tarantool> box.cfg.metrics.include
---
- all
...
```
NO_WRAP
---
 .../gh-8051-set-box-cfg-thru-env.md           |  4 +
 src/box/lua/load_cfg.lua                      | 25 ++++-
 .../gh_8051_set_box_cfg_thru_env_test.lua     | 92 +++++++++++++++++++
 3 files changed, 120 insertions(+), 1 deletion(-)
 create mode 100644 changelogs/unreleased/gh-8051-set-box-cfg-thru-env.md
 create mode 100644 test/box-luatest/gh_8051_set_box_cfg_thru_env_test.lua

diff --git a/changelogs/unreleased/gh-8051-set-box-cfg-thru-env.md b/changelogs/unreleased/gh-8051-set-box-cfg-thru-env.md
new file mode 100644
index 0000000000..58a3659a7a
--- /dev/null
+++ b/changelogs/unreleased/gh-8051-set-box-cfg-thru-env.md
@@ -0,0 +1,4 @@
+## feature/box/cfg
+
+* Implemented a way to set a table as box.cfg{} options value via
+  environment variables (gh-8051).
diff --git a/src/box/lua/load_cfg.lua b/src/box/lua/load_cfg.lua
index 3ccf9bc65e..d4f3fae9e0 100644
--- a/src/box/lua/load_cfg.lua
+++ b/src/box/lua/load_cfg.lua
@@ -1151,7 +1151,30 @@ local function get_option_from_env(option)
 
     -- This code lean on the existing set of template_cfg
     -- types for simplicity.
-    if param_type:find('table') and raw_value:find(',') then
+    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
+        assert(not param_type:find('boolean'))
+        local res = {}
+        for _, v in ipairs(raw_value:split(',')) do
+            local eq = v:find('=')
+            if eq == nil then
+                error(err_msg_fmt:format(env_var_name, option,
+                                         'in `key=value` or `value` format'))
+            end
+            local lhs = string.sub(v, 1, eq - 1)
+            local rhs = string.sub(v, eq + 1)
+
+            if lhs == '' then
+                error(err_msg_fmt:format(env_var_name, option,
+                                         'in `key=value` or `value` format, ' ..
+                                         '`key` must not be empty'))
+            end
+            res[lhs] = tonumber(rhs) or rhs
+        end
+        return res
+    elseif 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
diff --git a/test/box-luatest/gh_8051_set_box_cfg_thru_env_test.lua b/test/box-luatest/gh_8051_set_box_cfg_thru_env_test.lua
new file mode 100644
index 0000000000..fcbec619ec
--- /dev/null
+++ b/test/box-luatest/gh_8051_set_box_cfg_thru_env_test.lua
@@ -0,0 +1,92 @@
+local server = require('luatest.server')
+local popen = require('popen')
+local clock = require('clock')
+
+local t = require('luatest')
+local g = t.group()
+
+g.after_each = function()
+    if g.server ~= nil then
+       g.server:stop()
+   end
+end
+
+local TARANTOOL_PATH = arg[-1]
+
+local function popen_run(command)
+    local cmd = TARANTOOL_PATH .. ' -i 2>&1'
+    local ph = popen.new({cmd}, {
+        shell = true,
+        setsid = true,
+        group_signal = true,
+        stdout = popen.opts.PIPE,
+        stderr = popen.opts.DEVNULL,
+        stdin = popen.opts.PIPE,
+    })
+    t.assert(ph, 'process is not up')
+
+    ph:write(command)
+
+    local output = ''
+    local time_quota = 10.0
+    local start_time = clock.monotonic()
+    while clock.monotonic() - start_time < time_quota do
+        local chunk = ph:read({timeout = 1.0})
+        if chunk == '' or chunk == nil then
+            -- EOF or error
+            break
+        end
+        output = output .. chunk
+    end
+
+    ph:close()
+    return output
+end
+
+g.test_json_table_curly_bracket = function()
+    local env = {["TT_METRICS"] = '{"labels":{"alias":"gh_8051"},' ..
+                                  '"include":"all","exclude":["vinyl"]}'}
+
+    g.server = server:new{alias='json_table_curly_bracket', env=env}
+    g.server:start()
+
+    t.assert_equals(g.server:get_box_cfg().metrics.labels.alias, 'gh_8051')
+end
+
+g.test_json_table_square_bracket = function()
+    local res = popen_run([=[
+        os.setenv('TT_LISTEN', '["localhost:0"]')
+        box.cfg{}
+        box.cfg.listen
+    ]=])
+
+    t.assert_str_contains(res, "- ['localhost:0']")
+end
+
+g.test_plain_table = function()
+    local env = {["TT_LOG_MODULES"] = 'aaa=info,bbb=error'}
+
+    g.server = server:new{alias='plain_table', env=env}
+    g.server:start()
+
+    t.assert_equals(g.server:get_box_cfg().log_modules,
+                    {['aaa'] = 'info', ['bbb'] = 'error'})
+end
+
+g.test_format_error = function()
+    local res = popen_run([[
+        os.setenv('TT_LOG_MODULES', 'aaa=info,bbb')
+        box.cfg{}
+    ]])
+
+    t.assert_str_contains(res, "in `key=value` or `value` format'")
+end
+
+g.test_format_error_empty_key = function()
+    local res = popen_run([[
+        os.setenv('TT_LOG_MODULES', 'aaa=info,=error')
+        box.cfg{}
+    ]])
+
+    t.assert_str_contains(res, "`key` must not be empty'")
+end
-- 
GitLab