From 746f8310a78b8d9c0f9f085512190dc26f1ccbfa Mon Sep 17 00:00:00 2001 From: Rajesh Date: Wed, 30 Sep 2026 11:05:54 +0530 Subject: [PATCH 1/5] HDDS-13066 Return a clear bucket-link error when the linked source does not exist --- .../dist/src/main/smoketest/basic/links.robot | 8 +++ .../apache/hadoop/ozone/om/OzoneManager.java | 9 +++ .../ozone/om/TestBucketManagerImpl.java | 56 +++++++++++++++++++ 3 files changed, 73 insertions(+) diff --git a/hadoop-ozone/dist/src/main/smoketest/basic/links.robot b/hadoop-ozone/dist/src/main/smoketest/basic/links.robot index 41da993bae14..3103f6b9a1f3 100644 --- a/hadoop-ozone/dist/src/main/smoketest/basic/links.robot +++ b/hadoop-ozone/dist/src/main/smoketest/basic/links.robot @@ -94,6 +94,14 @@ Link to non-existent bucket ${result} = Execute And Ignore Error ozone sh key list ${target}/dangling-link Should Contain ${result} BUCKET_NOT_FOUND +Link to non-existent source volume + ${missing} = Generate Random String 5 [NUMBERS] + ${missingVol} = Set Variable ${missing}-missing-source + Execute ozone sh bucket link ${missingVol}/any-bucket ${target}/dangling-missing-vol + ${result} = Execute And Ignore Error ozone sh key list ${target}/dangling-missing-vol + Should Contain ${result} BUCKET_NOT_FOUND + Should Contain ${result} Cannot follow bucket link + Key create passthrough Ozone Shell Batch bucket link ${source}/bucket1 ${target}/link1 ... bucket create ${source}/bucket1 diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java index fdc6f95b48db..f483f3304202 100644 --- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java +++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java @@ -93,6 +93,7 @@ import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_TYPE_KEY; import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_SNAPSHOT_SST_FILTERING_SERVICE_INTERVAL; import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_THREAD_NUMBER_DIR_DELETION; +import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.BUCKET_NOT_FOUND; import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.DETECTED_LOOP_IN_BUCKET_LINKS; import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.FEATURE_NOT_ENABLED; import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INTERNAL_ERROR; @@ -102,6 +103,7 @@ import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.NOT_SUPPORTED_OPERATION; import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.PERMISSION_DENIED; import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.TOKEN_ERROR_OTHER; +import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.VOLUME_NOT_FOUND; import static org.apache.hadoop.ozone.om.lock.OzoneManagerLock.LeveledResource.BUCKET_LOCK; import static org.apache.hadoop.ozone.om.lock.OzoneManagerLock.LeveledResource.VOLUME_LOCK; import static org.apache.hadoop.ozone.om.ratis.OzoneManagerRatisServer.RaftServerStatus.LEADER_AND_NOT_READY; @@ -5416,6 +5418,13 @@ private OmBucketInfo resolveBucketLink( if (allowDanglingBuckets) { return null; } + + if (!visited.isEmpty() + && (e.getResult() == VOLUME_NOT_FOUND || e.getResult() == BUCKET_NOT_FOUND)) { + throw new OMException( + "Cannot follow bucket link: linked source bucket does not exist", + BUCKET_NOT_FOUND); + } throw e; } if (!info.isLink()) { diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestBucketManagerImpl.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestBucketManagerImpl.java index 6252836b57de..5501b46640ad 100644 --- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestBucketManagerImpl.java +++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestBucketManagerImpl.java @@ -30,6 +30,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; @@ -68,6 +69,7 @@ import org.apache.hadoop.ozone.om.protocol.OzoneManagerProtocol; import org.apache.hadoop.ozone.om.request.OMRequestTestUtils; import org.apache.hadoop.security.UserGroupInformation; +import org.apache.commons.lang3.tuple.Pair; import org.apache.ozone.test.OzoneTestBase; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -660,6 +662,60 @@ private static void denySourceRead(OmMetadataReader metadataReader, String sourc }).when(metadataReader).checkAcls(any(), any(), any(), any(), any(), any()); } + @Test + void testResolveBucketLinkMissingSourceVolume() throws Exception { + String targetVolume = volumeName(); + String missingSourceVolume = volumeName(); + OmBucketInfo danglingLink = OmBucketInfo.newBuilder() + .setVolumeName(targetVolume) + .setBucketName("dangling-link") + .setSourceVolume(missingSourceVolume) + .setSourceBucket("any-bucket") + .build(); + BucketManager bucketManager = mock(BucketManager.class); + when(bucketManager.getBucketInfo(targetVolume, "dangling-link")).thenReturn(danglingLink); + when(bucketManager.getBucketInfo(missingSourceVolume, "any-bucket")) + .thenThrow(new OMException("Volume doesn't exist", ResultCodes.VOLUME_NOT_FOUND)); + OzoneManager omSpy = spy(omTestManagers.getOzoneManager()); + HddsWhiteboxTestUtils.setInternalState(omSpy, "bucketManager", bucketManager); + when(omSpy.getAclsEnabled()).thenReturn(false); + + OMException omEx = assertThrows(OMException.class, + () -> omSpy.resolveBucketLink(Pair.of(targetVolume, "dangling-link"))); + assertEquals(ResultCodes.BUCKET_NOT_FOUND, omEx.getResult()); + assertTrue(omEx.getMessage().contains("Cannot follow bucket link")); + } + + @Test + void testListKeysOnLinkWithMissingSourceVolume() throws Exception { + String targetVolume = volumeName(); + String missingSourceVolume = volumeName(); + OmBucketInfo danglingLink = OmBucketInfo.newBuilder() + .setVolumeName(targetVolume) + .setBucketName("dangling-link-list") + .setSourceVolume(missingSourceVolume) + .setSourceBucket("any-bucket") + .build(); + BucketManager bucketManager = mock(BucketManager.class); + when(bucketManager.getBucketInfo(targetVolume, "dangling-link-list")).thenReturn(danglingLink); + when(bucketManager.getBucketInfo(missingSourceVolume, "any-bucket")) + .thenThrow(new OMException("Volume doesn't exist", ResultCodes.VOLUME_NOT_FOUND)); + OzoneManager om = omTestManagers.getOzoneManager(); + OzoneManager omSpy = spy(om); + HddsWhiteboxTestUtils.setInternalState(omSpy, "bucketManager", bucketManager); + when(omSpy.getAclsEnabled()).thenReturn(false); + OmMetadataReader metadataReader = (OmMetadataReader) HddsWhiteboxTestUtils.getInternalState(om, + "omMetadataReader"); + HddsWhiteboxTestUtils.setInternalState(metadataReader, "ozoneManager", omSpy); + + OMException omEx = assertThrows(OMException.class, + () -> omSpy.listKeys(targetVolume, "dangling-link-list", null, null, 100)); + assertEquals(ResultCodes.BUCKET_NOT_FOUND, omEx.getResult()); + assertTrue(omEx.getMessage().contains("Cannot follow bucket link")); + verify(bucketManager).getBucketInfo(eq(targetVolume), eq("dangling-link-list")); + verify(bucketManager).getBucketInfo(eq(missingSourceVolume), eq("any-bucket")); + } + private OzoneManager createAclEnabledOmSpy(BucketManager bucketManager, OmMetadataReader metadataReader) { OzoneManager omSpy = spy(omTestManagers.getOzoneManager()); HddsWhiteboxTestUtils.setInternalState(omSpy, "bucketManager", bucketManager); From 56cc74fc0fd76da7815939d5d7f348463e1387ba Mon Sep 17 00:00:00 2001 From: Rajesh Date: Wed, 30 Sep 2026 12:03:32 +0530 Subject: [PATCH 2/5] checkstyle fix --- .../java/org/apache/hadoop/ozone/om/TestBucketManagerImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestBucketManagerImpl.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestBucketManagerImpl.java index 5501b46640ad..8443a2e3cf00 100644 --- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestBucketManagerImpl.java +++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestBucketManagerImpl.java @@ -45,6 +45,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import org.apache.commons.lang3.tuple.Pair; import org.apache.hadoop.crypto.key.KeyProvider; import org.apache.hadoop.crypto.key.KeyProviderCryptoExtension; import org.apache.hadoop.hdds.client.DefaultReplicationConfig; @@ -69,7 +70,6 @@ import org.apache.hadoop.ozone.om.protocol.OzoneManagerProtocol; import org.apache.hadoop.ozone.om.request.OMRequestTestUtils; import org.apache.hadoop.security.UserGroupInformation; -import org.apache.commons.lang3.tuple.Pair; import org.apache.ozone.test.OzoneTestBase; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; From 97efc090ffdb354d7fcd59c00bab828ffd89dea6 Mon Sep 17 00:00:00 2001 From: Rajesh Date: Tue, 6 Oct 2026 17:56:46 +0530 Subject: [PATCH 3/5] addressing the review comments --- .../org/apache/hadoop/ozone/om/TestBucketManagerImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestBucketManagerImpl.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestBucketManagerImpl.java index 8443a2e3cf00..fb32057c8e00 100644 --- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestBucketManagerImpl.java +++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestBucketManagerImpl.java @@ -683,7 +683,7 @@ void testResolveBucketLinkMissingSourceVolume() throws Exception { OMException omEx = assertThrows(OMException.class, () -> omSpy.resolveBucketLink(Pair.of(targetVolume, "dangling-link"))); assertEquals(ResultCodes.BUCKET_NOT_FOUND, omEx.getResult()); - assertTrue(omEx.getMessage().contains("Cannot follow bucket link")); + assertThat(omEx.getMessage()).contains("Cannot follow bucket link"); } @Test @@ -711,7 +711,7 @@ void testListKeysOnLinkWithMissingSourceVolume() throws Exception { OMException omEx = assertThrows(OMException.class, () -> omSpy.listKeys(targetVolume, "dangling-link-list", null, null, 100)); assertEquals(ResultCodes.BUCKET_NOT_FOUND, omEx.getResult()); - assertTrue(omEx.getMessage().contains("Cannot follow bucket link")); + assertThat(omEx.getMessage()).contains("Cannot follow bucket link"); verify(bucketManager).getBucketInfo(eq(targetVolume), eq("dangling-link-list")); verify(bucketManager).getBucketInfo(eq(missingSourceVolume), eq("any-bucket")); } From 4f433990b921dc2ee6bd3ff76c74bb31a56475c4 Mon Sep 17 00:00:00 2001 From: Rajesh Date: Wed, 7 Oct 2026 13:17:09 +0530 Subject: [PATCH 4/5] review comments fix --- .../dist/src/main/smoketest/basic/links.robot | 1 + .../ozone/om/TestBucketManagerImpl.java | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/hadoop-ozone/dist/src/main/smoketest/basic/links.robot b/hadoop-ozone/dist/src/main/smoketest/basic/links.robot index 3103f6b9a1f3..bc8a8289faf2 100644 --- a/hadoop-ozone/dist/src/main/smoketest/basic/links.robot +++ b/hadoop-ozone/dist/src/main/smoketest/basic/links.robot @@ -93,6 +93,7 @@ Link to non-existent bucket Execute ozone sh bucket link ${source}/no-such-bucket ${target}/dangling-link ${result} = Execute And Ignore Error ozone sh key list ${target}/dangling-link Should Contain ${result} BUCKET_NOT_FOUND + Should Contain ${result} Cannot follow bucket link Link to non-existent source volume ${missing} = Generate Random String 5 [NUMBERS] diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestBucketManagerImpl.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestBucketManagerImpl.java index fb32057c8e00..08dcc3135603 100644 --- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestBucketManagerImpl.java +++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestBucketManagerImpl.java @@ -686,6 +686,30 @@ void testResolveBucketLinkMissingSourceVolume() throws Exception { assertThat(omEx.getMessage()).contains("Cannot follow bucket link"); } + @Test + void testResolveBucketLinkMissingSourceBucket() throws Exception { + String targetVolume = volumeName(); + String sourceVolume = volumeName(); + OmBucketInfo danglingLink = OmBucketInfo.newBuilder() + .setVolumeName(targetVolume) + .setBucketName("dangling-link-bucket") + .setSourceVolume(sourceVolume) + .setSourceBucket("no-such-bucket") + .build(); + BucketManager bucketManager = mock(BucketManager.class); + when(bucketManager.getBucketInfo(targetVolume, "dangling-link-bucket")).thenReturn(danglingLink); + when(bucketManager.getBucketInfo(sourceVolume, "no-such-bucket")) + .thenThrow(new OMException("Bucket not found", ResultCodes.BUCKET_NOT_FOUND)); + OzoneManager omSpy = spy(omTestManagers.getOzoneManager()); + HddsWhiteboxTestUtils.setInternalState(omSpy, "bucketManager", bucketManager); + when(omSpy.getAclsEnabled()).thenReturn(false); + + OMException omEx = assertThrows(OMException.class, + () -> omSpy.resolveBucketLink(Pair.of(targetVolume, "dangling-link-bucket"))); + assertEquals(ResultCodes.BUCKET_NOT_FOUND, omEx.getResult()); + assertThat(omEx.getMessage()).contains("Cannot follow bucket link"); + } + @Test void testListKeysOnLinkWithMissingSourceVolume() throws Exception { String targetVolume = volumeName(); From 9c9d3448292c151fa51afd047742de59dcbbd35c Mon Sep 17 00:00:00 2001 From: Rajesh Date: Thu, 8 Oct 2026 16:02:09 +0530 Subject: [PATCH 5/5] addressing some more review comments --- .../dist/src/main/smoketest/basic/links.robot | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/hadoop-ozone/dist/src/main/smoketest/basic/links.robot b/hadoop-ozone/dist/src/main/smoketest/basic/links.robot index bc8a8289faf2..fcd395042098 100644 --- a/hadoop-ozone/dist/src/main/smoketest/basic/links.robot +++ b/hadoop-ozone/dist/src/main/smoketest/basic/links.robot @@ -96,13 +96,18 @@ Link to non-existent bucket Should Contain ${result} Cannot follow bucket link Link to non-existent source volume - ${missing} = Generate Random String 5 [NUMBERS] - ${missingVol} = Set Variable ${missing}-missing-source - Execute ozone sh bucket link ${missingVol}/any-bucket ${target}/dangling-missing-vol + Execute ozone sh bucket link no-such-volume/no-such-bucket ${target}/dangling-missing-vol ${result} = Execute And Ignore Error ozone sh key list ${target}/dangling-missing-vol Should Contain ${result} BUCKET_NOT_FOUND Should Contain ${result} Cannot follow bucket link +Link chain to broken source + Execute ozone sh bucket link no-such-volume/no-such-bucket ${target}/link-bucket2 + Execute ozone sh bucket link ${target}/link-bucket2 ${target}/link-bucket1 + ${result} = Execute And Ignore Error ozone sh key list ${target}/link-bucket1 + Should Contain ${result} BUCKET_NOT_FOUND + Should Contain ${result} Cannot follow bucket link + Key create passthrough Ozone Shell Batch bucket link ${source}/bucket1 ${target}/link1 ... bucket create ${source}/bucket1