From fa21117dfe06a3d0446cfb4b441bd4a957cd79cf Mon Sep 17 00:00:00 2001
From: Gleb Kashkin <g.kashkin@tarantool.org>
Date: Mon, 1 Apr 2024 12:47:53 +0000
Subject: [PATCH] config: verify replicaset to contain an instance

It is very easy to misplace a config option to a different level, for
example create an empty replicaset `sharding` with storage role, instead
of configuring sharding option to `storage`:
```
groups:
  g-001:
    replicasets:
      sharding:
        roles:
        - storage
      r-001:
        instances:
          i-001: {}
```

This patch adds validation that forbids creating an empty group or
replicaset. Note that a group or a replicaset could still be defined
in multiple config sources and may appear empty in one of them, the
check is performed on the merged cluster config.

Closes #9895

NO_DOC=bugfix
---
 ...h-9895-forbid-empty-group-or-replicaset.md |  5 ++++
 src/box/lua/config/configdata.lua             | 20 +++++++++++++
 test/config-luatest/cluster_config_test.lua   | 30 +++++++++++++++++++
 3 files changed, 55 insertions(+)
 create mode 100644 changelogs/unreleased/gh-9895-forbid-empty-group-or-replicaset.md

diff --git a/changelogs/unreleased/gh-9895-forbid-empty-group-or-replicaset.md b/changelogs/unreleased/gh-9895-forbid-empty-group-or-replicaset.md
new file mode 100644
index 0000000000..5b47bf8e32
--- /dev/null
+++ b/changelogs/unreleased/gh-9895-forbid-empty-group-or-replicaset.md
@@ -0,0 +1,5 @@
+## bugfix/config
+
+* Added additional validation to a cluster's configuration.
+  Now it is forbidden to create an empty group or
+  replicaset (gh-9895).
diff --git a/src/box/lua/config/configdata.lua b/src/box/lua/config/configdata.lua
index 81321b9d4e..d83fb18d2b 100644
--- a/src/box/lua/config/configdata.lua
+++ b/src/box/lua/config/configdata.lua
@@ -731,12 +731,32 @@ local function validate_anon(found, peers, failover, leader)
     end
 end
 
+local function validate_misplacing(cconfig)
+    for group_name, group_cfg in pairs(cconfig.groups) do
+        if group_cfg.replicasets == nil or
+                next(group_cfg.replicasets) == nil then
+            error(('group %q should include at ' ..
+                   'least one replicaset.'):format(group_name), 0)
+        end
+
+        for replicaset_name, replicaset_cfg in pairs(group_cfg.replicasets) do
+            if replicaset_cfg.instances == nil or
+                    next(replicaset_cfg.instances) == nil then
+                error(('replicaset %q should include at ' ..
+                       'least one instance.'):format(replicaset_name), 0)
+            end
+        end
+    end
+end
+
 local function new(iconfig, cconfig, instance_name)
     -- Find myself in a cluster config, determine peers in the same
     -- replicaset.
     local found = cluster_config:find_instance(cconfig, instance_name)
     assert(found ~= nil)
 
+    validate_misplacing(cconfig)
+
     -- Precalculate configuration with applied defaults.
     local iconfig_def = instance_config:apply_default(iconfig)
 
diff --git a/test/config-luatest/cluster_config_test.lua b/test/config-luatest/cluster_config_test.lua
index 075263962a..26d41bb922 100644
--- a/test/config-luatest/cluster_config_test.lua
+++ b/test/config-luatest/cluster_config_test.lua
@@ -281,3 +281,33 @@ g.test_instance_uri_errors = function(g)
         end)
     end)
 end
+
+-- Attempt to pass an empty group and an empty replicaset.
+g.test_misplace_option = function(g)
+    local config = cbuilder.new()
+        :use_group('g-001')
+
+        :use_replicaset('r-001')
+        :add_instance('i-001', {})
+
+        :use_group('sharding')
+        :set_group_option('roles', {'storage'})
+        :config()
+
+    cluster.startup_error(g, config, "group \"sharding\" should " ..
+                                     "include at least one replicaset.")
+
+    local config = cbuilder.new()
+        :use_group('g-001')
+
+        :use_replicaset('r-001')
+        :add_instance('i-001', {})
+
+        :use_replicaset('sharding')
+        :set_replicaset_option('roles', {'storage'})
+
+        :config()
+
+    cluster.startup_error(g, config, "replicaset \"sharding\" should " ..
+                                     "include at least one instance.")
+end
-- 
GitLab