@@ -874,6 +874,72 @@ const TString DATABASE_2_YAML_CONFIG_2_UPDATED = R"(
874874 some_removed_feature_flag_example: true
875875)" ;
876876
877+ const TString VERSIONED_MAIN_CONFIG_V0_STALE = R"(
878+ ---
879+ metadata:
880+ kind: MainConfig
881+ cluster: ""
882+ version: 0
883+ config:
884+ log_config:
885+ cluster_name: clusterA
886+ )" ;
887+
888+ const TString VERSIONED_MAIN_CONFIG_V1 = R"(
889+ ---
890+ metadata:
891+ kind: MainConfig
892+ cluster: ""
893+ version: 1
894+ config:
895+ log_config:
896+ cluster_name: clusterA
897+ )" ;
898+
899+ const TString VERSIONED_MAIN_CONFIG_V2 = R"(
900+ ---
901+ metadata:
902+ kind: MainConfig
903+ cluster: ""
904+ version: 2
905+ config:
906+ log_config:
907+ cluster_name: clusterB
908+ )" ;
909+
910+ const TString VERSIONED_DATABASE_CONFIG_V0_STALE = R"(
911+ ---
912+ metadata:
913+ kind: DatabaseConfig
914+ database: "/dc-1/users/tenant-1"
915+ version: 0
916+ config:
917+ feature_flags:
918+ some_removed_feature_flag_example: true
919+ )" ;
920+
921+ const TString VERSIONED_DATABASE_CONFIG_V1 = R"(
922+ ---
923+ metadata:
924+ kind: DatabaseConfig
925+ database: "/dc-1/users/tenant-1"
926+ version: 1
927+ config:
928+ feature_flags:
929+ some_removed_feature_flag_example: true
930+ )" ;
931+
932+ const TString VERSIONED_DATABASE_CONFIG_V2 = R"(
933+ ---
934+ metadata:
935+ kind: DatabaseConfig
936+ database: "/dc-1/users/tenant-1"
937+ version: 2
938+ config:
939+ feature_flags:
940+ some_removed_feature_flag_example: false
941+ )" ;
942+
877943
878944void InitializeTestConfigItems ()
879945{
@@ -1116,7 +1182,101 @@ TVector<ui64> CheckConfigureLogAffected(TTenantTestRuntime &runtime,
11161182 return {reply->Record .GetAddedItemIds ().begin (), reply->Record .GetAddedItemIds ().end ()};
11171183}
11181184
1185+ void DoFetchMainConfigFromConsole (TTenantTestRuntime &runtime, TString &yamlConfig)
1186+ {
1187+ auto *event = new TEvConsole::TEvGetAllConfigsRequest;
1188+ runtime.SendToConsole (event);
1189+
1190+ TAutoPtr<IEventHandle> handle;
1191+ auto response = runtime.GrabEdgeEventRethrow <TEvConsole::TEvGetAllConfigsResponse>(handle);
1192+
1193+ UNIT_ASSERT_C (response->Record .GetResponse ().config_size () == 1 ,
1194+ " expected exactly one config in response" );
1195+ UNIT_ASSERT_C (response->Record .GetResponse ().identity_size () == 1 ,
1196+ " expected exactly one identity in response" );
1197+ UNIT_ASSERT_C (response->Record .GetResponse ().identity (0 ).type_case () == Ydb::DynamicConfig::ConfigIdentity::kCluster ,
1198+ " expected kCluster identity in response" );
1199+
1200+ yamlConfig = response->Record .GetResponse ().config (0 );
1201+ }
1202+
1203+ void DoFetchDatabaseConfigFromConsole (TTenantTestRuntime &runtime, const TString &databasePath, TString &yamlConfig)
1204+ {
1205+ auto *event = new TEvConsole::TEvGetAllConfigsRequest;
1206+ event->Record .SetIngressDatabase (databasePath);
1207+ runtime.SendToConsole (event);
1208+
1209+ TAutoPtr<IEventHandle> handle;
1210+ auto response = runtime.GrabEdgeEventRethrow <TEvConsole::TEvGetAllConfigsResponse>(handle);
1211+
1212+ UNIT_ASSERT_C (response->Record .GetResponse ().config_size () == 1 ,
1213+ " expected at least one config in response for database: " << databasePath);
1214+ UNIT_ASSERT_C (response->Record .GetResponse ().identity_size () == 1 ,
1215+ " expected exactly one identity in response" );
1216+ UNIT_ASSERT_C (response->Record .GetResponse ().identity (0 ).type_case () == Ydb::DynamicConfig::ConfigIdentity::kDatabase ,
1217+ " expected kDatabase identity in response" );
1218+ UNIT_ASSERT_C (response->Record .GetResponse ().identity (0 ).has_database (),
1219+ " kDatabase identity in response with no database set" );
1220+ UNIT_ASSERT_EQUAL_C (response->Record .GetResponse ().identity (0 ).database (), databasePath,
1221+ " database in response not match requested database" );
1222+
1223+ yamlConfig = response->Record .GetResponse ().config (0 );
1224+ }
1225+
1226+ void DoReplaceYamlConfig (TTenantTestRuntime &runtime,
1227+ const TString &yaml,
1228+ Ydb::StatusIds::StatusCode expectedCode,
1229+ const TString &errorSubstring = {},
1230+ bool allowAbsentDatabase = false )
1231+ {
1232+ auto *event = new TEvConsole::TEvReplaceYamlConfigRequest;
1233+ event->Record .MutableRequest ()->set_config (yaml);
1234+ if (allowAbsentDatabase) {
1235+ event->Record .MutableRequest ()->set_allow_absent_database (true );
1236+ }
1237+ runtime.SendToConsole (event);
1238+
1239+ TAutoPtr<IEventHandle> handle;
1240+ auto [success, error] = runtime.GrabEdgeEvents <
1241+ TEvConsole::TEvReplaceYamlConfigResponse,
1242+ TEvConsole::TEvGenericError>(handle);
1243+
1244+ if (expectedCode == Ydb::StatusIds::SUCCESS ) {
1245+ UNIT_ASSERT_C (success != nullptr ,
1246+ " expected success but got error: " <<
1247+ (error ? error->Record .ShortDebugString () : TString (" no event" )));
1248+ } else {
1249+ UNIT_ASSERT_C (error != nullptr ,
1250+ " expected error " << static_cast <int >(expectedCode) << " but got success" );
1251+ UNIT_ASSERT_VALUES_EQUAL (error->Record .GetYdbStatus (), expectedCode);
1252+ if (!errorSubstring.empty ()) {
1253+ TString allMessages;
1254+ for (const auto & issue : error->Record .GetIssues ()) {
1255+ allMessages += issue.message ();
1256+ allMessages += " ;" ;
1257+ }
1258+ UNIT_ASSERT_STRING_CONTAINS (allMessages, errorSubstring);
1259+ }
1260+ }
1261+ }
1262+
1263+ void DoEnsureMainConfigReplacedWith (TTenantTestRuntime &runtime, TString replaceConfig)
1264+ {
1265+ TString consoleConfig;
1266+ TString expectedConfig = NYamlConfig::UpgradeMainConfigVersion (replaceConfig);
1267+
1268+ DoFetchMainConfigFromConsole (runtime, consoleConfig);
1269+ UNIT_ASSERT_VALUES_EQUAL_C (consoleConfig, expectedConfig, " CONSOLE config version not match replace config version" );
1270+ }
1271+
1272+ void DoEnsureDatabaseConfigReplacedWith (TTenantTestRuntime &runtime, const TString& database, TString replaceConfig)
1273+ {
1274+ TString consoleConfig;
1275+ TString expectedConfig = NYamlConfig::UpgradeDatabaseConfigVersion (replaceConfig);
11191276
1277+ DoFetchDatabaseConfigFromConsole (runtime, database, consoleConfig);
1278+ UNIT_ASSERT_VALUES_EQUAL_C (consoleConfig, expectedConfig, " CONSOLE config version not match replace config version" );
1279+ }
11201280
11211281} // anonymous namespace
11221282
@@ -4520,6 +4680,93 @@ Y_UNIT_TEST_SUITE(TConsoleInMemoryConfigSubscriptionTests) {
45204680 UNIT_ASSERT (notification->Get ()->Record .GetVolatileConfigs ()[0 ].GetNotChanged ());
45214681 UNIT_ASSERT (notification->Get ()->Record .GetVolatileConfigs ()[1 ].GetNotChanged ());
45224682 }
4683+
4684+ Y_UNIT_TEST (TestReplaceMainYamlConfigVersionCheck) {
4685+ TTenantTestRuntime runtime (DefaultConsoleTestConfig ());
4686+
4687+ // Fresh runtime: YamlVersion = 0. Per documentation, the client must send
4688+ // the *current* stored version. A version ahead of stored is rejected.
4689+ DoReplaceYamlConfig (runtime, VERSIONED_MAIN_CONFIG_V1 ,
4690+ Ydb::StatusIds::BAD_REQUEST , " Version mismatch" );
4691+
4692+ // version: 0 matches stored YamlVersion = 0 — accepted; YamlVersion becomes 1.
4693+ DoReplaceYamlConfig (runtime, VERSIONED_MAIN_CONFIG_V0_STALE ,
4694+ Ydb::StatusIds::SUCCESS );
4695+ DoEnsureMainConfigReplacedWith (runtime, VERSIONED_MAIN_CONFIG_V0_STALE );
4696+
4697+ // version: 2 with stored = 1 — rejected.
4698+ DoReplaceYamlConfig (runtime, VERSIONED_MAIN_CONFIG_V2 ,
4699+ Ydb::StatusIds::BAD_REQUEST , " Version mismatch" );
4700+
4701+ // Re-applying the same body with the stale version: 0 is silently
4702+ // accepted (documented idempotent behaviour: wire == stored - 1 with
4703+ // identical content). YamlVersion stays at 1.
4704+ DoReplaceYamlConfig (runtime, VERSIONED_MAIN_CONFIG_V0_STALE ,
4705+ Ydb::StatusIds::SUCCESS );
4706+ DoEnsureMainConfigReplacedWith (runtime, VERSIONED_MAIN_CONFIG_V0_STALE );
4707+
4708+ // version: 1 matches stored YamlVersion = 1 — accepted; YamlVersion becomes 2.
4709+ DoReplaceYamlConfig (runtime, VERSIONED_MAIN_CONFIG_V1 ,
4710+ Ydb::StatusIds::SUCCESS );
4711+ DoEnsureMainConfigReplacedWith (runtime, VERSIONED_MAIN_CONFIG_V1 );
4712+
4713+ // version: 2 with a differing body now matches stored — accepted; YamlVersion = 3.
4714+ DoReplaceYamlConfig (runtime, VERSIONED_MAIN_CONFIG_V2 ,
4715+ Ydb::StatusIds::SUCCESS );
4716+ DoEnsureMainConfigReplacedWith (runtime, VERSIONED_MAIN_CONFIG_V2 );
4717+ }
4718+
4719+ Y_UNIT_TEST (TestReplaceDatabaseYamlConfigVersionCheck) {
4720+ NKikimrConfig::TAppConfig appcfg;
4721+ appcfg.MutableFeatureFlags ()->SetDatabaseYamlConfigAllowed (true );
4722+ TTenantTestRuntime runtime (MultipleTenantsConsoleTestConfig (), appcfg);
4723+
4724+ const TString DATABASE = " /dc-1/users/tenant-1" ;
4725+
4726+ // ValidateDatabaseConfig appends the database config into the main config
4727+ // and re-validates the result, which requires a non-empty MainYamlConfig.
4728+ // Seed a minimal main config first (fresh YamlVersion = 0, so wire = 0).
4729+ DoReplaceYamlConfig (runtime, VERSIONED_MAIN_CONFIG_V0_STALE ,
4730+ Ydb::StatusIds::SUCCESS );
4731+
4732+ // Fresh runtime: no per-database config stored, so currentVersion = 0.
4733+ // Per documentation, the wire version must equal the stored version.
4734+ // Sending version: 1 must be rejected.
4735+ DoReplaceYamlConfig (runtime, VERSIONED_DATABASE_CONFIG_V1 ,
4736+ Ydb::StatusIds::BAD_REQUEST , " Version mismatch" ,
4737+ /* allowAbsentDatabase = */ true );
4738+
4739+ // version: 0 matches stored currentVersion = 0 — accepted; stored version becomes 1.
4740+ DoReplaceYamlConfig (runtime, VERSIONED_DATABASE_CONFIG_V0_STALE ,
4741+ Ydb::StatusIds::SUCCESS , {},
4742+ /* allowAbsentDatabase = */ true );
4743+ DoEnsureDatabaseConfigReplacedWith (runtime, DATABASE , VERSIONED_DATABASE_CONFIG_V0_STALE );
4744+
4745+ // version: 2 with stored = 1 — rejected.
4746+ DoReplaceYamlConfig (runtime, VERSIONED_DATABASE_CONFIG_V2 ,
4747+ Ydb::StatusIds::BAD_REQUEST , " Version mismatch" ,
4748+ /* allowAbsentDatabase = */ true );
4749+
4750+ // Re-applying the same body with the stale version: 0 is silently
4751+ // accepted (documented idempotent behaviour: wire == stored - 1 with
4752+ // identical content). Stored version stays at 1.
4753+ DoReplaceYamlConfig (runtime, VERSIONED_DATABASE_CONFIG_V0_STALE ,
4754+ Ydb::StatusIds::SUCCESS , {},
4755+ /* allowAbsentDatabase = */ true );
4756+ DoEnsureDatabaseConfigReplacedWith (runtime, DATABASE , VERSIONED_DATABASE_CONFIG_V0_STALE );
4757+
4758+ // version: 1 matches stored = 1 — accepted; stored version becomes 2.
4759+ DoReplaceYamlConfig (runtime, VERSIONED_DATABASE_CONFIG_V1 ,
4760+ Ydb::StatusIds::SUCCESS , {},
4761+ /* allowAbsentDatabase = */ true );
4762+ DoEnsureDatabaseConfigReplacedWith (runtime, DATABASE , VERSIONED_DATABASE_CONFIG_V1 );
4763+
4764+ // version: 2 with a differing body now matches stored = 2 — accepted; version = 3.
4765+ DoReplaceYamlConfig (runtime, VERSIONED_DATABASE_CONFIG_V2 ,
4766+ Ydb::StatusIds::SUCCESS , {},
4767+ /* allowAbsentDatabase = */ true );
4768+ DoEnsureDatabaseConfigReplacedWith (runtime, DATABASE , VERSIONED_DATABASE_CONFIG_V2 );
4769+ }
45234770}
45244771
45254772Y_UNIT_TEST_SUITE (TConsoleConfigHelpersTests) {
0 commit comments