From d2c9c0119d513d7a77d6503ee272b3be78fbf9e0 Mon Sep 17 00:00:00 2001
From: Georgy Moshkin <gmoshkin@picodata.io>
Date: Wed, 28 Feb 2024 21:08:53 +0300
Subject: [PATCH] feat: validate configuration against storage

---
 src/config.rs           | 75 +++++++++++++++++++++++++++++++++++++++++
 src/lib.rs              |  3 +-
 test/int/test_basics.py | 74 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 151 insertions(+), 1 deletion(-)

diff --git a/src/config.rs b/src/config.rs
index 8a7aebbfd5..04ddfbab08 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -195,6 +195,81 @@ impl PicodataConfig {
         todo!()
     }
 
+    /// Does validation of configuration parameters which are persisted in the
+    /// storage.
+    pub fn validate_storage(
+        &self,
+        storage: &storage::Clusterwide,
+        raft_storage: &RaftSpaceAccess,
+    ) -> Result<(), Error> {
+        // Cluster id
+        match (raft_storage.cluster_id()?, self.cluster_id()) {
+            (from_storage, from_config) if from_storage != from_config => {
+                return Err(Error::InvalidConfiguration(format!(
+                    "instance restarted with a different `cluster_id`, which is not allowed, was: '{from_storage}' became: '{from_config}'"
+                )));
+            }
+            _ => {}
+        }
+
+        // Instance id
+        let mut instance_id = None;
+        match (raft_storage.instance_id()?, &self.instance.instance_id) {
+            (Some(from_storage), Some(from_config)) if from_storage != from_config => {
+                return Err(Error::InvalidConfiguration(format!(
+                    "instance restarted with a different `instance_id`, which is not allowed, was: '{from_storage}' became: '{from_config}'"
+                )));
+            }
+            (Some(from_storage), _) => {
+                instance_id = Some(from_storage);
+            }
+            _ => {}
+        }
+
+        // Replicaset id
+        if let Some(instance_id) = &instance_id {
+            if let Ok(instance_info) = storage.instances.get(instance_id) {
+                match (&instance_info.replicaset_id, &self.instance.replicaset_id) {
+                    (from_storage, Some(from_config)) if from_storage != from_config => {
+                        return Err(Error::InvalidConfiguration(format!(
+                            "instance restarted with a different `replicaset_id`, which is not allowed, was: '{from_storage}' became: '{from_config}'"
+                        )));
+                    }
+                    _ => {}
+                }
+            }
+        }
+
+        // Tier
+        match (&raft_storage.tier()?, &self.instance.tier) {
+            (Some(from_storage), Some(from_config)) if from_storage != from_config => {
+                return Err(Error::InvalidConfiguration(format!(
+                    "instance restarted with a different `tier`, which is not allowed, was: '{from_storage}' became: '{from_config}'"
+                )));
+            }
+            _ => {}
+        }
+
+        // Advertise address
+        if let Some(raft_id) = raft_storage.raft_id()? {
+            match (
+                storage.peer_addresses.get(raft_id)?,
+                &self.instance.advertise_address,
+            ) {
+                (Some(from_storage), Some(from_config))
+                    if from_storage != from_config.to_host_port() =>
+                {
+                    return Err(Error::InvalidConfiguration(format!(
+                        "instance restarted with a different `advertise_address`, which is not allowed, was: '{from_storage}' became: '{from_config}'"
+                    )));
+                }
+                _ => {}
+            }
+        }
+
+        Ok(())
+    }
+
     #[inline]
     pub fn cluster_id(&self) -> &str {
         match (&self.instance.cluster_id, &self.cluster.cluster_id) {
diff --git a/src/lib.rs b/src/lib.rs
index 1a45d4bb50..2ca5e1c224 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -609,7 +609,6 @@ fn start_discover(
     cfg.listen = Some(config.instance.listen().to_host_port());
     tarantool::set_cfg(&cfg);
 
-    // TODO assert traft::Storage::instance_id == (null || args.instance_id)
     if raft_storage.raft_id().unwrap().is_some() {
         tarantool::set_cfg_field("read_only", true).unwrap();
         return postjoin(config, storage, raft_storage);
@@ -844,6 +843,8 @@ fn postjoin(
 ) -> Result<(), Error> {
     tlog!(Info, "entering post-join phase");
 
+    config.validate_storage(&storage, &raft_storage)?;
+
     if let Some(config) = &config.instance.audit {
         audit::init(config, &raft_storage);
     }
diff --git a/test/int/test_basics.py b/test/int/test_basics.py
index e4ef86d7b0..38cde6353a 100644
--- a/test/int/test_basics.py
+++ b/test/int/test_basics.py
@@ -168,6 +168,80 @@ def test_graceful_stop(instance: Instance):
         assert f.read()[-4:] == b"\xd5\x10\xad\xed"
 
 
+def test_config_storage_conflicts_on_restart(instance: Instance):
+    instance.terminate()
+
+    #
+    # Change cluster_id
+    #
+    was = instance.cluster_id  # type: ignore
+    instance.cluster_id = "new-cluster-id"
+    assert instance.cluster_id != was
+    err = f"""\
+invalid configuration: instance restarted with a different `cluster_id`, which is not allowed, was: '{was}' became: 'new-cluster-id'
+"""  # noqa: E501
+    crawler = log_crawler(instance, err)
+    instance.fail_to_start()
+    assert crawler.matched
+    instance.cluster_id = was
+
+    #
+    # Change instance_id
+    #
+    was = instance.instance_id  # type: ignore
+    instance.instance_id = "new-instance-id"
+    assert instance.instance_id != was
+    err = f"""\
+invalid configuration: instance restarted with a different `instance_id`, which is not allowed, was: '{was}' became: 'new-instance-id'
+"""  # noqa: E501
+    crawler = log_crawler(instance, err)
+    instance.fail_to_start()
+    assert crawler.matched
+    instance.instance_id = was
+
+    #
+    # Change tier
+    #
+    was = instance.tier  # type: ignore
+    instance.tier = "new-tier"
+    assert instance.tier != was
+    err = """\
+invalid configuration: instance restarted with a different `tier`, which is not allowed, was: 'default' became: 'new-tier'
+"""  # noqa: E501
+    crawler = log_crawler(instance, err)
+    instance.fail_to_start()
+    assert crawler.matched
+    instance.tier = was
+
+    #
+    # Change replicaset_id
+    #
+    was = instance.replicaset_id  # type: ignore
+    instance.replicaset_id = "new-replicaset-id"
+    assert instance.replicaset_id != was
+    err = """\
+invalid configuration: instance restarted with a different `replicaset_id`, which is not allowed, was: 'r1' became: 'new-replicaset-id'
+"""  # noqa: E501
+    crawler = log_crawler(instance, err)
+    instance.fail_to_start()
+    assert crawler.matched
+    instance.replicaset_id = was
+
+    #
+    # Change advertise address
+    #
+    was = instance.listen  # type: ignore
+    instance.env["PICODATA_ADVERTISE"] = "example.com:1234"
+    assert instance.env["PICODATA_ADVERTISE"] != was
+    err = f"""\
+invalid configuration: instance restarted with a different `advertise_address`, which is not allowed, was: '{was}' became: 'example.com:1234'
+"""  # noqa: E501
+    crawler = log_crawler(instance, err)
+    instance.fail_to_start()
+    assert crawler.matched
+    del instance.env["PICODATA_ADVERTISE"]
+
+
 def test_whoami(instance: Instance):
     assert instance.call("pico.whoami") == {
         "raft_id": 1,
-- 
GitLab