Skip to content

Commit 855fd21

Browse files
committed
[PLAT-18725] Universe ownership validation skip if universe is incompatible with the check
Summary: Diff: https://phorge.dev.yugabyte.com/D44905 introduced a universe owner check related to attach/detach flow. Adding some guardrails around it: - Old universes - non-YSQL universes (since we can't query the yba consistency info on them) - Also will skip this validation if the runtime config `yb.attach_detach.enabled` is not enabled. Test Plan: Manual testing that universe actions are working for old universes and non-YSQL universes Updated Unit tests in UtilTest, UniverseActionsControllerTest, UniverseControllerTest to accomodate new change + more scenarios coverage Reviewers: anijhawan, nsingh Reviewed By: anijhawan Differential Revision: https://phorge.dev.yugabyte.com/D47350
1 parent 5e0588c commit 855fd21

File tree

5 files changed

+241
-15
lines changed

5 files changed

+241
-15
lines changed

managed/src/main/java/com/yugabyte/yw/common/Util.java

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.yugabyte.yw.cloud.PublicCloudConstants.OsType;
2020
import com.yugabyte.yw.commissioner.Common;
2121
import com.yugabyte.yw.commissioner.Common.CloudType;
22+
import com.yugabyte.yw.common.config.GlobalConfKeys;
2223
import com.yugabyte.yw.common.config.RuntimeConfGetter;
2324
import com.yugabyte.yw.common.gflags.GFlagsUtil;
2425
import com.yugabyte.yw.common.logging.LogUtil;
@@ -1200,6 +1201,41 @@ public static void validateUniverseOwnershipAndNotDetached(
12001201
ConfigHelper configHelper,
12011202
YsqlQueryExecutor ysqlQueryExecutor,
12021203
RuntimeConfGetter confGetter) {
1204+
// Skip ownership validation if attach/detach feature is not enabled
1205+
boolean attachDetachEnabled = confGetter.getGlobalConf(GlobalConfKeys.attachDetachEnabled);
1206+
if (!attachDetachEnabled) {
1207+
log.debug(
1208+
"Skipping ownership validation for universe {} (attach/detach feature not enabled)",
1209+
universe.getName());
1210+
return;
1211+
}
1212+
1213+
UniverseDefinitionTaskParams.Cluster primaryCluster =
1214+
universe.getUniverseDetails().getPrimaryCluster();
1215+
if (primaryCluster == null || primaryCluster.userIntent == null) {
1216+
log.warn(
1217+
"Skipping ownership validation for universe {} due to missing primary cluster or user"
1218+
+ " intent",
1219+
universe.getName());
1220+
return;
1221+
}
1222+
1223+
UniverseDefinitionTaskParams.UserIntent primaryIntent = primaryCluster.userIntent;
1224+
1225+
// Skip ownership validation for older DB versions that don't support attach/detach
1226+
String dbVersion = primaryIntent.ybSoftwareVersion;
1227+
if (dbVersion == null
1228+
|| compareYBVersions(
1229+
dbVersion, "2024.2.0.0-b1", "2.23.1.0-b29", true /* suppressFormatError */)
1230+
< 0) {
1231+
log.debug(
1232+
"Skipping ownership validation for universe {} with DB version {} (older than minimum"
1233+
+ " required for attach/detach)",
1234+
universe.getName(),
1235+
dbVersion);
1236+
return;
1237+
}
1238+
12031239
if (universe.getUniverseDetails().universeDetached) {
12041240
throw new PlatformServiceException(
12051241
BAD_REQUEST,
@@ -1232,13 +1268,21 @@ public static boolean isUniverseOwner(
12321268

12331269
UUID storedYwUuid = getStoredYwUuid(universe, ysqlQueryExecutor, confGetter);
12341270

1271+
// If we cannot retrieve ownership info (e.g., YCQL-only universe), skip validation
1272+
if (storedYwUuid == null) {
1273+
log.debug(
1274+
"Cannot determine ownership for universe {} (YSQL not available), assuming owned",
1275+
universe.getName());
1276+
return true;
1277+
}
1278+
12351279
// If stored YW UUID is DETACHED_UNIVERSE_UUID, the universe is orphaned and operations should
12361280
// not be allowed
1237-
if (storedYwUuid == AttachDetachSpec.DETACHED_UNIVERSE_UUID) {
1281+
if (storedYwUuid.equals(AttachDetachSpec.DETACHED_UNIVERSE_UUID)) {
12381282
throw new PlatformServiceException(
12391283
BAD_REQUEST,
12401284
String.format(
1241-
"Universe %s has no owner. Operations are not allowed on orphaned" + " universes.",
1285+
"Universe %s has no owner. Operations are not allowed on orphaned universes.",
12421286
universe.getName()));
12431287
}
12441288
return currentYwUuid.equals(storedYwUuid);
@@ -1249,6 +1293,13 @@ public static UUID getStoredYwUuid(
12491293
try {
12501294
YsqlQueryExecutor.ConsistencyInfoResp consistencyInfo =
12511295
ysqlQueryExecutor.getConsistencyInfo(universe);
1296+
// Return null if consistency info could not be retrieved (e.g., YCQL-only universe)
1297+
if (consistencyInfo == null) {
1298+
log.debug(
1299+
"Could not retrieve consistency info for universe {} (likely YSQL is not enabled)",
1300+
universe.getName());
1301+
return null;
1302+
}
12521303
return consistencyInfo.getYwUUID();
12531304
} catch (Exception e) {
12541305
log.error("Failed to query YW UUID for universe {}: {}", universe.getName(), e);

managed/src/main/java/com/yugabyte/yw/models/AttachDetachSpec.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -783,11 +783,12 @@ private void updateYwUuidInConsistencyTable(
783783
return;
784784
}
785785

786-
if (Util.getStoredYwUuid(universe, ysqlQueryExecutor, confGetter) != DETACHED_UNIVERSE_UUID) {
786+
UUID storedYwUuid = Util.getStoredYwUuid(universe, ysqlQueryExecutor, confGetter);
787+
if (storedYwUuid != null && !storedYwUuid.equals(DETACHED_UNIVERSE_UUID)) {
787788
throw new PlatformServiceException(
788789
BAD_REQUEST,
789790
"Universe already has an owner "
790-
+ Util.getStoredYwUuid(universe, ysqlQueryExecutor, confGetter)
791+
+ storedYwUuid
791792
+ ", please delete universe metadata from this YBA"
792793
+ " instance.");
793794
}

managed/src/test/java/com/yugabyte/yw/common/UtilTest.java

Lines changed: 181 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@
2020
import com.cronutils.utils.StringUtils;
2121
import com.fasterxml.jackson.databind.JsonNode;
2222
import com.google.common.collect.ImmutableMap;
23+
import com.yugabyte.yw.common.config.GlobalConfKeys;
2324
import com.yugabyte.yw.common.config.RuntimeConfGetter;
2425
import com.yugabyte.yw.common.utils.FileUtils;
2526
import com.yugabyte.yw.forms.UniverseDefinitionTaskParams;
2627
import com.yugabyte.yw.forms.UniverseDefinitionTaskParams.Cluster;
2728
import com.yugabyte.yw.forms.UniverseDefinitionTaskParams.ClusterType;
2829
import com.yugabyte.yw.forms.UniverseDefinitionTaskParams.UserIntent;
30+
import com.yugabyte.yw.models.AttachDetachSpec;
2931
import com.yugabyte.yw.models.Customer;
3032
import com.yugabyte.yw.models.Universe;
3133
import com.yugabyte.yw.models.Users;
@@ -607,8 +609,9 @@ public void testIsUniverseOwner_NullStoredYwUuid() {
607609
when(consistencyInfo.getYwUUID()).thenReturn(null);
608610
when(ysqlQueryExecutor.getConsistencyInfo(any(Universe.class))).thenReturn(consistencyInfo);
609611

610-
assertFalse(
611-
"Should return false when stored YW UUID is null (not matching current UUID)",
612+
// When stored UUID is null (YCQL-only universe), assume owned and return true
613+
assertTrue(
614+
"Should return true when stored YW UUID is null (YCQL-only universe, skip validation)",
612615
Util.isUniverseOwner(universe, configHelper, ysqlQueryExecutor, confGetter));
613616
}
614617

@@ -638,15 +641,9 @@ public void testGetStoredYwUuid_NullConsistencyInfo() {
638641
when(universe.getName()).thenReturn("test-universe");
639642
when(ysqlQueryExecutor.getConsistencyInfo(any(Universe.class))).thenReturn(null);
640643

641-
PlatformServiceException exception =
642-
assertThrows(
643-
"Should throw PlatformServiceException when consistency info is null",
644-
PlatformServiceException.class,
645-
() -> Util.getStoredYwUuid(universe, ysqlQueryExecutor, confGetter));
646-
647-
assertTrue(
648-
"Exception should mention error querying YW UUID",
649-
exception.getMessage().contains("Error querying YW UUID"));
644+
// When consistency info is null (e.g., YCQL-only universe), should return null gracefully
645+
UUID result = Util.getStoredYwUuid(universe, ysqlQueryExecutor, confGetter);
646+
assertNull("Should return null when consistency info is null (YCQL-only universe)", result);
650647
}
651648

652649
@Test
@@ -677,8 +674,18 @@ public void testvalidateUniverseOwnershipAndNotDetached_DetachedUniverse() {
677674
YsqlQueryExecutor ysqlQueryExecutor = mock(YsqlQueryExecutor.class);
678675
RuntimeConfGetter confGetter = mock(RuntimeConfGetter.class);
679676

677+
// Mock attach/detach feature enabled
678+
when(confGetter.getGlobalConf(GlobalConfKeys.attachDetachEnabled)).thenReturn(true);
679+
680680
UniverseDefinitionTaskParams taskParams = new UniverseDefinitionTaskParams();
681681
taskParams.universeDetached = true;
682+
683+
// Set up primary cluster with valid version to ensure validation runs
684+
UniverseDefinitionTaskParams.UserIntent userIntent =
685+
new UniverseDefinitionTaskParams.UserIntent();
686+
userIntent.ybSoftwareVersion = "2024.2.0.0-b1"; // Valid version for attach/detach
687+
taskParams.upsertPrimaryCluster(userIntent, null);
688+
682689
when(universe.getUniverseDetails()).thenReturn(taskParams);
683690
when(universe.getName()).thenReturn("test-universe");
684691

@@ -702,9 +709,20 @@ public void testvalidateUniverseOwnershipAndNotDetached_NotOwner() {
702709
YsqlQueryExecutor ysqlQueryExecutor = mock(YsqlQueryExecutor.class);
703710
RuntimeConfGetter confGetter = mock(RuntimeConfGetter.class);
704711

712+
// Mock attach/detach feature enabled
713+
when(confGetter.getGlobalConf(GlobalConfKeys.attachDetachEnabled)).thenReturn(true);
714+
705715
UniverseDefinitionTaskParams taskParams = new UniverseDefinitionTaskParams();
706716
taskParams.universeDetached = false;
717+
718+
// Set up primary cluster with valid version to ensure validation runs
719+
UniverseDefinitionTaskParams.UserIntent userIntent =
720+
new UniverseDefinitionTaskParams.UserIntent();
721+
userIntent.ybSoftwareVersion = "2024.2.0.0-b1"; // Valid version for attach/detach
722+
taskParams.upsertPrimaryCluster(userIntent, null);
723+
707724
when(universe.getUniverseDetails()).thenReturn(taskParams);
725+
when(universe.getName()).thenReturn("test-universe");
708726

709727
UUID currentYwUuid = UUID.randomUUID();
710728
UUID storedYwUuid = UUID.randomUUID(); // Different UUID
@@ -736,9 +754,20 @@ public void testvalidateUniverseOwnershipAndNotDetached_ValidOwner() {
736754
YsqlQueryExecutor ysqlQueryExecutor = mock(YsqlQueryExecutor.class);
737755
RuntimeConfGetter confGetter = mock(RuntimeConfGetter.class);
738756

757+
// Mock attach/detach feature enabled
758+
when(confGetter.getGlobalConf(GlobalConfKeys.attachDetachEnabled)).thenReturn(true);
759+
739760
UniverseDefinitionTaskParams taskParams = new UniverseDefinitionTaskParams();
740761
taskParams.universeDetached = false;
762+
763+
// Set up primary cluster with valid version to ensure validation runs
764+
UniverseDefinitionTaskParams.UserIntent userIntent =
765+
new UniverseDefinitionTaskParams.UserIntent();
766+
userIntent.ybSoftwareVersion = "2024.2.0.0-b1"; // Valid version for attach/detach
767+
taskParams.upsertPrimaryCluster(userIntent, null);
768+
741769
when(universe.getUniverseDetails()).thenReturn(taskParams);
770+
when(universe.getName()).thenReturn("test-universe");
742771

743772
UUID currentYwUuid = UUID.randomUUID();
744773
UUID storedYwUuid = currentYwUuid; // Same UUID
@@ -754,4 +783,145 @@ public void testvalidateUniverseOwnershipAndNotDetached_ValidOwner() {
754783
Util.validateUniverseOwnershipAndNotDetached(
755784
universe, configHelper, ysqlQueryExecutor, confGetter);
756785
}
786+
787+
@Test
788+
public void testIsUniverseOwner_DetachedUniverseUuid() {
789+
Universe universe = mock(Universe.class);
790+
ConfigHelper configHelper = mock(ConfigHelper.class);
791+
YsqlQueryExecutor ysqlQueryExecutor = mock(YsqlQueryExecutor.class);
792+
RuntimeConfGetter confGetter = mock(RuntimeConfGetter.class);
793+
794+
UUID currentYwUuid = UUID.randomUUID();
795+
when(configHelper.getYugawareUUID()).thenReturn(currentYwUuid);
796+
when(universe.getName()).thenReturn("test-universe");
797+
798+
// Stored UUID is DETACHED_UNIVERSE_UUID (orphaned universe)
799+
YsqlQueryExecutor.ConsistencyInfoResp consistencyInfo =
800+
mock(YsqlQueryExecutor.ConsistencyInfoResp.class);
801+
when(consistencyInfo.getYwUUID()).thenReturn(AttachDetachSpec.DETACHED_UNIVERSE_UUID);
802+
when(ysqlQueryExecutor.getConsistencyInfo(any(Universe.class))).thenReturn(consistencyInfo);
803+
804+
PlatformServiceException exception =
805+
assertThrows(
806+
"Should throw exception for orphaned universe (DETACHED_UNIVERSE_UUID)",
807+
PlatformServiceException.class,
808+
() -> Util.isUniverseOwner(universe, configHelper, ysqlQueryExecutor, confGetter));
809+
810+
assertTrue(
811+
"Exception should mention orphaned universe",
812+
exception.getMessage().contains("has no owner")
813+
&& exception.getMessage().contains("orphaned"));
814+
}
815+
816+
@Test
817+
public void testvalidateUniverseOwnershipAndNotDetached_OldDbVersion() {
818+
Universe universe = mock(Universe.class);
819+
ConfigHelper configHelper = mock(ConfigHelper.class);
820+
YsqlQueryExecutor ysqlQueryExecutor = mock(YsqlQueryExecutor.class);
821+
RuntimeConfGetter confGetter = mock(RuntimeConfGetter.class);
822+
823+
// Mock attach/detach feature enabled
824+
when(confGetter.getGlobalConf(GlobalConfKeys.attachDetachEnabled)).thenReturn(true);
825+
826+
UniverseDefinitionTaskParams taskParams = new UniverseDefinitionTaskParams();
827+
taskParams.universeDetached = false;
828+
829+
// Set up universe with old DB version (before attach/detach support)
830+
UniverseDefinitionTaskParams.UserIntent userIntent =
831+
new UniverseDefinitionTaskParams.UserIntent();
832+
userIntent.ybSoftwareVersion = "2.18.0.0-b1"; // Old version
833+
taskParams.upsertPrimaryCluster(userIntent, null);
834+
835+
when(universe.getUniverseDetails()).thenReturn(taskParams);
836+
when(universe.getName()).thenReturn("test-universe");
837+
838+
// Should not throw - validation should be skipped for old versions
839+
Util.validateUniverseOwnershipAndNotDetached(
840+
universe, configHelper, ysqlQueryExecutor, confGetter);
841+
}
842+
843+
@Test
844+
public void testvalidateUniverseOwnershipAndNotDetached_NullDbVersion() {
845+
Universe universe = mock(Universe.class);
846+
ConfigHelper configHelper = mock(ConfigHelper.class);
847+
YsqlQueryExecutor ysqlQueryExecutor = mock(YsqlQueryExecutor.class);
848+
RuntimeConfGetter confGetter = mock(RuntimeConfGetter.class);
849+
850+
// Mock attach/detach feature enabled
851+
when(confGetter.getGlobalConf(GlobalConfKeys.attachDetachEnabled)).thenReturn(true);
852+
853+
UniverseDefinitionTaskParams taskParams = new UniverseDefinitionTaskParams();
854+
taskParams.universeDetached = false;
855+
856+
// Set up universe with null DB version
857+
UniverseDefinitionTaskParams.UserIntent userIntent =
858+
new UniverseDefinitionTaskParams.UserIntent();
859+
userIntent.ybSoftwareVersion = null; // Null version
860+
taskParams.upsertPrimaryCluster(userIntent, null);
861+
862+
when(universe.getUniverseDetails()).thenReturn(taskParams);
863+
when(universe.getName()).thenReturn("test-universe");
864+
865+
// Should not throw - validation should be skipped for null versions
866+
Util.validateUniverseOwnershipAndNotDetached(
867+
universe, configHelper, ysqlQueryExecutor, confGetter);
868+
}
869+
870+
@Test
871+
public void testvalidateUniverseOwnershipAndNotDetached_YcqlOnlyUniverse() {
872+
Universe universe = mock(Universe.class);
873+
ConfigHelper configHelper = mock(ConfigHelper.class);
874+
YsqlQueryExecutor ysqlQueryExecutor = mock(YsqlQueryExecutor.class);
875+
RuntimeConfGetter confGetter = mock(RuntimeConfGetter.class);
876+
877+
// Mock attach/detach feature enabled
878+
when(confGetter.getGlobalConf(GlobalConfKeys.attachDetachEnabled)).thenReturn(true);
879+
880+
UniverseDefinitionTaskParams taskParams = new UniverseDefinitionTaskParams();
881+
taskParams.universeDetached = false;
882+
883+
// Set up YCQL-only universe (YSQL disabled)
884+
UniverseDefinitionTaskParams.UserIntent userIntent =
885+
new UniverseDefinitionTaskParams.UserIntent();
886+
userIntent.ybSoftwareVersion = "2024.2.0.0-b1"; // Valid version
887+
userIntent.enableYSQL = false; // YCQL-only
888+
taskParams.upsertPrimaryCluster(userIntent, null);
889+
890+
when(universe.getUniverseDetails()).thenReturn(taskParams);
891+
when(universe.getName()).thenReturn("test-universe");
892+
893+
// Mock configHelper to return a valid UUID (needed if validation progresses to isUniverseOwner)
894+
when(configHelper.getYugawareUUID()).thenReturn(UUID.randomUUID());
895+
896+
// Should not throw - validation should be skipped for YCQL-only universes
897+
Util.validateUniverseOwnershipAndNotDetached(
898+
universe, configHelper, ysqlQueryExecutor, confGetter);
899+
}
900+
901+
@Test
902+
public void testvalidateUniverseOwnershipAndNotDetached_FeatureDisabled() {
903+
Universe universe = mock(Universe.class);
904+
ConfigHelper configHelper = mock(ConfigHelper.class);
905+
YsqlQueryExecutor ysqlQueryExecutor = mock(YsqlQueryExecutor.class);
906+
RuntimeConfGetter confGetter = mock(RuntimeConfGetter.class);
907+
908+
// Mock attach/detach feature DISABLED
909+
when(confGetter.getGlobalConf(GlobalConfKeys.attachDetachEnabled)).thenReturn(false);
910+
911+
UniverseDefinitionTaskParams taskParams = new UniverseDefinitionTaskParams();
912+
taskParams.universeDetached = true;
913+
914+
// Set up universe with valid version
915+
UniverseDefinitionTaskParams.UserIntent userIntent =
916+
new UniverseDefinitionTaskParams.UserIntent();
917+
userIntent.ybSoftwareVersion = "2024.2.0.0-b1";
918+
taskParams.upsertPrimaryCluster(userIntent, null);
919+
920+
when(universe.getUniverseDetails()).thenReturn(taskParams);
921+
when(universe.getName()).thenReturn("test-universe");
922+
923+
// validation should be skipped entirely when feature is disabled
924+
Util.validateUniverseOwnershipAndNotDetached(
925+
universe, configHelper, ysqlQueryExecutor, confGetter);
926+
}
757927
}

managed/src/test/java/com/yugabyte/yw/controllers/UniverseActionsControllerTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ public void testUniversePauseValidUUID() {
188188
UniverseDefinitionTaskParams.UserIntent userIntent =
189189
new UniverseDefinitionTaskParams.UserIntent();
190190
userIntent.providerType = Common.CloudType.aws;
191+
userIntent.ybSoftwareVersion = "2.20.0.0-b1";
191192
universeDetails.upsertPrimaryCluster(userIntent, null);
192193
universe.setUniverseDetails(universeDetails);
193194
};

managed/src/test/java/com/yugabyte/yw/controllers/UniverseControllerTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ public void testUniverseDestroyValidUUID() {
201201
UniverseDefinitionTaskParams.UserIntent userIntent =
202202
new UniverseDefinitionTaskParams.UserIntent();
203203
userIntent.providerType = Common.CloudType.aws;
204+
userIntent.ybSoftwareVersion = "2.20.0.0-b1";
204205
universeDetails.upsertPrimaryCluster(userIntent, null);
205206
universe.setUniverseDetails(universeDetails);
206207
};
@@ -252,6 +253,7 @@ public void testUniverseDestroyValidUUIDIsForceDelete() {
252253
UniverseDefinitionTaskParams.UserIntent userIntent =
253254
new UniverseDefinitionTaskParams.UserIntent();
254255
userIntent.providerType = Common.CloudType.aws;
256+
userIntent.ybSoftwareVersion = "2.20.0.0-b1";
255257
universeDetails.upsertPrimaryCluster(userIntent, null);
256258
universe.setUniverseDetails(universeDetails);
257259
};
@@ -312,6 +314,7 @@ public void testUniverseDestroyValidUUIDIsForceDeleteAndDeleteBackup(
312314
UniverseDefinitionTaskParams.UserIntent userIntent =
313315
new UniverseDefinitionTaskParams.UserIntent();
314316
userIntent.providerType = Common.CloudType.aws;
317+
userIntent.ybSoftwareVersion = "2.20.0.0-b1";
315318
universeDetails.upsertPrimaryCluster(userIntent, null);
316319
universe.setUniverseDetails(universeDetails);
317320
};

0 commit comments

Comments
 (0)