From a1544d3bbc029c6fb2a148e580afe2b20e269b8d Mon Sep 17 00:00:00 2001
From: Alexander Turenko <alexander.turenko@tarantool.org>
Date: Fri, 24 May 2024 13:28:16 +0300
Subject: [PATCH] config: expose configuration status from box.info

Fixes #10044

@TarantoolBot document
Title: Configuration status is shown in `box.info.config` now

There is the `config:info([version])` method, but in order to access it
over iproto an application developer should add something like the
following into the application code:

```lua
_G.config = require('config')
```

It is not convenient, at least because it requires an attention from the
application developer and it can't be solved solely by an administrator.

Now, the `config:info('v2')` result is reported in the `config` field of
the `box.info` table. It is accessible over iproto if appropriate
privileges are granted for a calling user.
---
 changelogs/unreleased/box-info-config.md  |   3 +
 src/box/lua/info.c                        |  37 +++++++
 test/box-luatest/box_info_config_test.lua | 117 ++++++++++++++++++++++
 test/box/info.result                      |   1 +
 4 files changed, 158 insertions(+)
 create mode 100644 changelogs/unreleased/box-info-config.md
 create mode 100644 test/box-luatest/box_info_config_test.lua

diff --git a/changelogs/unreleased/box-info-config.md b/changelogs/unreleased/box-info-config.md
new file mode 100644
index 0000000000..ed078dc570
--- /dev/null
+++ b/changelogs/unreleased/box-info-config.md
@@ -0,0 +1,3 @@
+## feature/config
+
+* Expose configuration status from `box.info.config` (gh-10044).
diff --git a/src/box/lua/info.c b/src/box/lua/info.c
index 97b637b3b3..44d28a1725 100644
--- a/src/box/lua/info.c
+++ b/src/box/lua/info.c
@@ -759,6 +759,42 @@ lbox_info_hostname(struct lua_State *L)
 	return 1;
 }
 
+static int
+lbox_info_config(struct lua_State *L)
+{
+	/* require('config'):info('v2') */
+	lua_getglobal(L, "require");
+	lua_pushliteral(L, "config");
+	if (lua_pcall(L, 1, 1, 0) != 0)
+		goto error;
+	/* Stack: config. */
+	lua_getfield(L, -1, "info");
+	/* Stack: config, config.info. */
+	lua_insert(L, -2);
+	/* Stack: config.info, config. */
+	lua_pushliteral(L, "v2");
+	/* Stack: config.info, config, 'v2'. */
+	if (lua_pcall(L, 2, 1, 0) != 0)
+		goto error;
+	return 1;
+
+error:
+	/*
+	 * An error shouldn't occur by construction.
+	 *
+	 * However, box.info() is an important call and we
+	 * shouldn't fail it in any circumstances, including a
+	 * problem in the config:info() implementation.
+	 *
+	 * So, we don't raise an error here and place it to the
+	 * result instead.
+	 */
+	lua_newtable(L);
+	lua_insert(L, -2);
+	lua_setfield(L, -2, "error");
+	return 1;
+}
+
 static const struct luaL_Reg lbox_info_dynamic_meta[] = {
 	{"id", lbox_info_id},
 	{"uuid", lbox_info_uuid},
@@ -784,6 +820,7 @@ static const struct luaL_Reg lbox_info_dynamic_meta[] = {
 	{"synchro", lbox_info_synchro},
 	{"schema_version", lbox_schema_version},
 	{"hostname", lbox_info_hostname},
+	{"config", lbox_info_config},
 	{NULL, NULL}
 };
 
diff --git a/test/box-luatest/box_info_config_test.lua b/test/box-luatest/box_info_config_test.lua
new file mode 100644
index 0000000000..d429d7acaa
--- /dev/null
+++ b/test/box-luatest/box_info_config_test.lua
@@ -0,0 +1,117 @@
+local t = require('luatest')
+local server = require('luatest.server')
+local cbuilder = require('test.config-luatest.cbuilder')
+local cluster = require('test.config-luatest.cluster')
+
+local g = t.group()
+
+g.before_all(cluster.init)
+g.after_each(cluster.drop)
+g.after_all(cluster.clean)
+
+g.after_each(function(g)
+    if g.server ~= nil then
+        g.server:drop()
+        g.server = nil
+    end
+end)
+
+-- Verify box.info.config when tarantool is started from a script.
+g.test_with_script = function(g)
+    g.server = server:new()
+    g.server:start()
+
+    g.server:exec(function()
+        local function verify_config(res)
+            t.assert_equals(res, {
+                status = 'uninitialized',
+                alerts = {},
+                meta = {
+                    -- No 'active' field, because there is no
+                    -- active configuration.
+                    last = {},
+                },
+            })
+        end
+
+        verify_config(box.info.config)
+        verify_config(box.info().config)
+    end)
+end
+
+-- Verify box.info.config when tarantool is started from a config.
+g.test_with_config = function(g)
+    local config = cbuilder.new()
+        :add_instance('i-001', {})
+        :config()
+    local cluster = cluster.new(g, config)
+    cluster:start()
+
+    cluster['i-001']:exec(function()
+        local function verify_config(res)
+            t.assert_equals(res, {
+                status = 'ready',
+                alerts = {},
+                meta = {
+                    active = {},
+                    last = {},
+                },
+            })
+        end
+
+        verify_config(box.info.config)
+        verify_config(box.info().config)
+    end)
+end
+
+-- box.info() should work always, even if config:info() is broken.
+g.test_broken_config_info = function(g)
+    local config = cbuilder.new()
+        :add_instance('i-001', {})
+        :config()
+    local cluster = cluster.new(g, config)
+    cluster:start()
+
+    cluster['i-001']:exec(function()
+        local config = require('config')
+
+        local function verify_config(res)
+            t.assert_equals(res, {
+                error = 'config:info() is broken',
+            })
+        end
+
+        -- Break the method.
+        config.info = function(_self, _version)
+            error('config:info() is broken', 0)
+        end
+
+        verify_config(box.info.config)
+        verify_config(box.info().config)
+    end)
+end
+
+-- box.info() should work always, even if config module is broken.
+g.test_broken_config_module = function(g)
+    local config = cbuilder.new()
+        :add_instance('i-001', {})
+        :config()
+    local cluster = cluster.new(g, config)
+    cluster:start()
+
+    cluster['i-001']:exec(function()
+        local loaders = require('internal.loaders')
+
+        local function verify_config(res)
+            t.assert_type(res, 'table')
+            t.assert_str_contains(res.error, "module 'config' not found")
+        end
+
+        -- Unload the module and break the next require('config').
+        package.loaded.config = nil
+        loaders.builtin.config = nil
+
+        verify_config(box.info.config)
+        verify_config(box.info().config)
+    end)
+end
diff --git a/test/box/info.result b/test/box/info.result
index 19bb50a328..c1e00d825a 100644
--- a/test/box/info.result
+++ b/test/box/info.result
@@ -79,6 +79,7 @@ table.sort(t)
 t
 ---
 - - cluster
+  - config
   - election
   - gc
   - hostname
-- 
GitLab