adnanhemani commented on code in PR #4023:
URL: https://github.com/apache/polaris/pull/4023#discussion_r2979657287


##########
runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java:
##########
@@ -755,8 +806,19 @@ public boolean removeProperties(Namespace namespace, 
Set<String> properties)
     Map<String, String> updatedProperties = new 
HashMap<>(entity.getPropertiesAsMap());
     properties.forEach(updatedProperties::remove);
 
-    PolarisEntity updatedEntity =
-        new 
PolarisEntity.Builder(entity).setProperties(updatedProperties).build();
+    PolarisEntity.Builder updatedEntityBuilder =
+        new PolarisEntity.Builder(entity).setProperties(updatedProperties);
+
+    // If polaris.storage.name is being removed, clear the storage config 
override
+    if (properties.contains(POLARIS_STORAGE_NAME_PROPERTY)) {
+      LOGGER.debug("Clearing storage name override on namespace {}", 
namespace);
+      Map<String, String> updatedInternalProperties =
+          new HashMap<>(entity.getInternalPropertiesAsMap());
+      
updatedInternalProperties.remove(PolarisEntityConstants.getStorageConfigInfoPropertyName());

Review Comment:
   could it be potentially easier to just transform 
`POLARIS_STORAGE_NAME_PROPERTY` into 
`PolarisEntityConstants.getStorageConfigInfoPropertyName()` prior to removing 
all the properties the first time?



##########
polaris-core/src/test/java/org/apache/polaris/core/storage/WithStorageNameTest.java:
##########
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.core.storage;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+import org.apache.polaris.core.storage.aws.AwsStorageConfigurationInfo;
+import org.apache.polaris.core.storage.azure.AzureStorageConfigurationInfo;
+import org.apache.polaris.core.storage.gcp.GcpStorageConfigurationInfo;
+import org.junit.jupiter.api.Test;
+
+class WithStorageNameTest {
+
+  @Test
+  void awsFieldsPreserved() {
+    AwsStorageConfigurationInfo base =
+        AwsStorageConfigurationInfo.builder()
+            .allowedLocations(List.of("s3://bucket/path"))
+            .storageName("original")
+            .roleARN("arn:aws:iam::123456789012:role/test-role")
+            .externalId("ext-id-123")
+            .region("us-west-2")
+            .endpoint("https://s3.example.com";)
+            .endpointInternal("https://s3-internal.example.com";)
+            .stsEndpoint("https://sts.example.com";)
+            .pathStyleAccess(true)
+            .stsUnavailable(true)
+            .kmsUnavailable(false)
+            .currentKmsKey("arn:aws:kms:us-west-2:123456789012:key/test")
+            
.allowedKmsKeys(List.of("arn:aws:kms:us-west-2:123456789012:key/test"))
+            .build();
+
+    PolarisStorageConfigurationInfo result =
+        PolarisStorageConfigurationInfo.withStorageName(base, "new-storage");
+
+    assertThat(result).isInstanceOf(AwsStorageConfigurationInfo.class);
+    AwsStorageConfigurationInfo awsResult = (AwsStorageConfigurationInfo) 
result;
+    assertThat(awsResult.getStorageName()).isEqualTo("new-storage");
+    
assertThat(awsResult.getAllowedLocations()).containsExactly("s3://bucket/path");
+    
assertThat(awsResult.getRoleARN()).isEqualTo("arn:aws:iam::123456789012:role/test-role");
+    assertThat(awsResult.getExternalId()).isEqualTo("ext-id-123");
+    assertThat(awsResult.getRegion()).isEqualTo("us-west-2");
+    assertThat(awsResult.getEndpoint()).isEqualTo("https://s3.example.com";);
+    
assertThat(awsResult.getEndpointInternal()).isEqualTo("https://s3-internal.example.com";);
+    
assertThat(awsResult.getStsEndpoint()).isEqualTo("https://sts.example.com";);
+    assertThat(awsResult.getPathStyleAccess()).isTrue();
+    assertThat(awsResult.getStsUnavailable()).isTrue();
+    assertThat(awsResult.getKmsUnavailable()).isFalse();
+    assertThat(awsResult.getCurrentKmsKey())
+        .isEqualTo("arn:aws:kms:us-west-2:123456789012:key/test");
+    assertThat(awsResult.getAllowedKmsKeys())
+        .containsExactly("arn:aws:kms:us-west-2:123456789012:key/test");
+  }
+
+  @Test
+  void awsNullStorageNameClearsOverride() {
+    AwsStorageConfigurationInfo base =
+        AwsStorageConfigurationInfo.builder()
+            .allowedLocations(List.of("s3://bucket/path"))
+            .storageName("original")
+            .roleARN("arn:aws:iam::123456789012:role/test-role")
+            .build();
+
+    PolarisStorageConfigurationInfo result =
+        PolarisStorageConfigurationInfo.withStorageName(base, null);
+
+    assertThat(result.getStorageName()).isNull();
+    
assertThat(result.getAllowedLocations()).containsExactly("s3://bucket/path");
+  }
+
+  @Test
+  void azureFieldsPreserved() {
+    AzureStorageConfigurationInfo base =
+        AzureStorageConfigurationInfo.builder()
+            
.allowedLocations(List.of("abfss://[email protected]/path"))
+            .storageName("original")
+            .tenantId("tenant-123")
+            .multiTenantAppName("my-app")
+            .consentUrl("https://consent.example.com";)
+            .hierarchical(true)
+            .build();
+
+    PolarisStorageConfigurationInfo result =
+        PolarisStorageConfigurationInfo.withStorageName(base, "azure-storage");
+
+    assertThat(result).isInstanceOf(AzureStorageConfigurationInfo.class);
+    AzureStorageConfigurationInfo azureResult = 
(AzureStorageConfigurationInfo) result;
+    assertThat(azureResult.getStorageName()).isEqualTo("azure-storage");
+    assertThat(azureResult.getAllowedLocations())
+        
.containsExactly("abfss://[email protected]/path");
+    assertThat(azureResult.getTenantId()).isEqualTo("tenant-123");
+    assertThat(azureResult.getMultiTenantAppName()).isEqualTo("my-app");
+    
assertThat(azureResult.getConsentUrl()).isEqualTo("https://consent.example.com";);
+    assertThat(azureResult.isHierarchical()).isTrue();
+  }
+
+  @Test
+  void gcpFieldsPreserved() {
+    GcpStorageConfigurationInfo base =
+        GcpStorageConfigurationInfo.builder()
+            .allowedLocations(List.of("gs://bucket/path"))
+            .storageName("original")
+            .gcpServiceAccount("[email protected]")
+            .build();
+
+    PolarisStorageConfigurationInfo result =
+        PolarisStorageConfigurationInfo.withStorageName(base, "gcp-storage");
+
+    assertThat(result).isInstanceOf(GcpStorageConfigurationInfo.class);
+    GcpStorageConfigurationInfo gcpResult = (GcpStorageConfigurationInfo) 
result;
+    assertThat(gcpResult.getStorageName()).isEqualTo("gcp-storage");
+    
assertThat(gcpResult.getAllowedLocations()).containsExactly("gs://bucket/path");
+    
assertThat(gcpResult.getGcpServiceAccount()).isEqualTo("[email protected]");
+  }
+
+  @Test
+  void fileFieldsPreserved() {
+    FileStorageConfigurationInfo base =
+        FileStorageConfigurationInfo.builder()
+            .allowedLocations(List.of("file:///tmp/warehouse"))
+            .storageName("original")
+            .build();
+
+    PolarisStorageConfigurationInfo result =
+        PolarisStorageConfigurationInfo.withStorageName(base, "file-storage");
+
+    assertThat(result).isInstanceOf(FileStorageConfigurationInfo.class);
+    assertThat(result.getStorageName()).isEqualTo("file-storage");
+    
assertThat(result.getAllowedLocations()).containsExactly("file:///tmp/warehouse");
+  }
+
+  @Test
+  void serializationRoundTrip() {

Review Comment:
   what is this test trying to test? That an overriden 
`PolarisStorageConfigurationInfo` can be serialized/deserialized? Is there any 
custom logic that puts this into doubt?



##########
polaris-core/src/main/java/org/apache/polaris/core/storage/StorageNameValidator.java:
##########
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.core.storage;
+
+import jakarta.annotation.Nullable;
+import java.util.regex.Pattern;
+
+/**
+ * Validates storage name references used in {@code polaris.storage.name} 
property overrides.
+ *
+ * <p>Storage names are symbolic references to server-side credential 
configurations (e.g. {@code
+ * polaris.storage.aws.<storageName>.access-key}). They must be valid Quarkus 
config key segments.
+ */
+public final class StorageNameValidator {
+
+  /** Maximum length for a storage name. */
+  public static final int MAX_LENGTH = 128;
+
+  /** Alphanumeric characters, hyphens, and underscores only. */
+  private static final Pattern VALID_PATTERN = 
Pattern.compile("^[a-zA-Z0-9_-]+$");
+
+  private StorageNameValidator() {}
+
+  /**
+   * Validates a storage name format.
+   *
+   * @param storageName the name to validate
+   * @throws IllegalArgumentException if the name is invalid
+   */
+  public static void validate(String storageName) {
+    if (storageName == null || storageName.isEmpty()) {
+      throw new IllegalArgumentException("Storage name must not be null or 
empty");
+    }
+    if (storageName.length() > MAX_LENGTH) {
+      throw new IllegalArgumentException(
+          String.format(
+              "Storage name must not exceed %d characters, got %d",
+              MAX_LENGTH, storageName.length()));
+    }
+    if (!VALID_PATTERN.matcher(storageName).matches()) {
+      throw new IllegalArgumentException(
+          String.format(
+              "Storage name '%s' contains invalid characters. "
+                  + "Only alphanumeric characters, hyphens, and underscores 
are allowed.",
+              storageName));
+    }
+  }
+
+  /**
+   * Normalizes a blank or empty string to {@code null}. Non-blank values are 
trimmed.
+   *
+   * @param value the input value
+   * @return {@code null} if the input is null, empty, or blank; otherwise the 
trimmed value
+   */
+  public static @Nullable String normalizeBlankToNull(@Nullable String value) {

Review Comment:
   Is there a reason this shouldn't be called from within `validate`?



##########
runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/StorageNameOverrideTest.java:
##########
@@ -0,0 +1,309 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.service.catalog.iceberg;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.iceberg.exceptions.BadRequestException;
+import org.apache.polaris.core.PolarisDiagnostics;
+import org.apache.polaris.core.auth.PolarisPrincipal;
+import org.apache.polaris.core.config.FeatureConfiguration;
+import org.apache.polaris.core.config.RealmConfig;
+import org.apache.polaris.core.context.CallContext;
+import org.apache.polaris.core.entity.CatalogEntity;
+import org.apache.polaris.core.entity.PolarisEntity;
+import org.apache.polaris.core.entity.PolarisEntityConstants;
+import org.apache.polaris.core.entity.PolarisEntitySubType;
+import org.apache.polaris.core.entity.PolarisEntityType;
+import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
+import 
org.apache.polaris.core.persistence.resolver.PolarisResolutionManifestCatalogView;
+import org.apache.polaris.core.persistence.resolver.ResolverFactory;
+import org.apache.polaris.core.storage.FileStorageConfigurationInfo;
+import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
+import org.apache.polaris.core.storage.aws.AwsStorageConfigurationInfo;
+import org.apache.polaris.service.catalog.io.FileIOFactory;
+import org.apache.polaris.service.catalog.io.FileIOUtil;
+import org.apache.polaris.service.catalog.io.StorageAccessConfigProvider;
+import org.apache.polaris.service.events.PolarisEventMetadataFactory;
+import org.apache.polaris.service.events.listeners.PolarisEventListener;
+import org.apache.polaris.service.task.TaskExecutor;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for the static helper methods in {@link IcebergCatalog} that 
handle {@code
+ * polaris.storage.name} property overrides. These test the pure logic without 
requiring a full
+ * Quarkus test context.
+ */
+class StorageNameOverrideTest {
+
+  private static final String STORAGE_CONFIG_KEY =
+      PolarisEntityConstants.getStorageConfigInfoPropertyName();
+
+  @Test
+  void storageConfigFromPropertyOverride_noProperty_returnsNull() throws 
Exception {
+    // Use reflection since the method is package-private/static
+    var method =

Review Comment:
   really not a fan of reflection...
   
   would much rather prefer doing `@VisibleForTest` on the method.



##########
polaris-core/src/test/java/org/apache/polaris/core/storage/WithStorageNameTest.java:
##########
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.core.storage;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+import org.apache.polaris.core.storage.aws.AwsStorageConfigurationInfo;
+import org.apache.polaris.core.storage.azure.AzureStorageConfigurationInfo;
+import org.apache.polaris.core.storage.gcp.GcpStorageConfigurationInfo;
+import org.junit.jupiter.api.Test;
+
+class WithStorageNameTest {
+
+  @Test
+  void awsFieldsPreserved() {
+    AwsStorageConfigurationInfo base =
+        AwsStorageConfigurationInfo.builder()
+            .allowedLocations(List.of("s3://bucket/path"))

Review Comment:
   nit: would prefer if you pulled all the variables out and referred to them 
twice. It would make the test more readable and maintainable :)



##########
runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/StorageNameOverrideTest.java:
##########
@@ -0,0 +1,309 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.service.catalog.iceberg;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.iceberg.exceptions.BadRequestException;
+import org.apache.polaris.core.PolarisDiagnostics;
+import org.apache.polaris.core.auth.PolarisPrincipal;
+import org.apache.polaris.core.config.FeatureConfiguration;
+import org.apache.polaris.core.config.RealmConfig;
+import org.apache.polaris.core.context.CallContext;
+import org.apache.polaris.core.entity.CatalogEntity;
+import org.apache.polaris.core.entity.PolarisEntity;
+import org.apache.polaris.core.entity.PolarisEntityConstants;
+import org.apache.polaris.core.entity.PolarisEntitySubType;
+import org.apache.polaris.core.entity.PolarisEntityType;
+import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
+import 
org.apache.polaris.core.persistence.resolver.PolarisResolutionManifestCatalogView;
+import org.apache.polaris.core.persistence.resolver.ResolverFactory;
+import org.apache.polaris.core.storage.FileStorageConfigurationInfo;
+import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
+import org.apache.polaris.core.storage.aws.AwsStorageConfigurationInfo;
+import org.apache.polaris.service.catalog.io.FileIOFactory;
+import org.apache.polaris.service.catalog.io.FileIOUtil;
+import org.apache.polaris.service.catalog.io.StorageAccessConfigProvider;
+import org.apache.polaris.service.events.PolarisEventMetadataFactory;
+import org.apache.polaris.service.events.listeners.PolarisEventListener;
+import org.apache.polaris.service.task.TaskExecutor;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for the static helper methods in {@link IcebergCatalog} that 
handle {@code
+ * polaris.storage.name} property overrides. These test the pure logic without 
requiring a full
+ * Quarkus test context.
+ */
+class StorageNameOverrideTest {
+
+  private static final String STORAGE_CONFIG_KEY =
+      PolarisEntityConstants.getStorageConfigInfoPropertyName();
+
+  @Test
+  void storageConfigFromPropertyOverride_noProperty_returnsNull() throws 
Exception {
+    // Use reflection since the method is package-private/static
+    var method =
+        IcebergCatalog.class.getDeclaredMethod(
+            "storageConfigFromPropertyOverride", Map.class, List.class);
+    method.setAccessible(true);
+
+    PolarisStorageConfigurationInfo result =
+        (PolarisStorageConfigurationInfo) method.invoke(null, Map.of("other", 
"value"), List.of());
+    assertThat(result).isNull();
+  }
+
+  @Test
+  void storageConfigFromPropertyOverride_withValidStorageName() throws 
Exception {
+    var method =
+        IcebergCatalog.class.getDeclaredMethod(
+            "storageConfigFromPropertyOverride", Map.class, List.class);
+    method.setAccessible(true);
+
+    AwsStorageConfigurationInfo catalogConfig =
+        AwsStorageConfigurationInfo.builder()
+            .allowedLocations(List.of("s3://bucket/path"))
+            .roleARN("arn:aws:iam::123456789012:role/test-role")
+            .storageName("catalog-storage")
+            .build();
+
+    PolarisEntity catalogEntity = createEntityWithStorageConfig(catalogConfig);
+
+    Map<String, String> properties = new HashMap<>();
+    properties.put(IcebergCatalog.POLARIS_STORAGE_NAME_PROPERTY, "ns-storage");
+
+    PolarisStorageConfigurationInfo result =
+        (PolarisStorageConfigurationInfo) method.invoke(null, properties, 
List.of(catalogEntity));
+
+    assertThat(result).isNotNull();
+    assertThat(result.getStorageName()).isEqualTo("ns-storage");
+    assertThat(result).isInstanceOf(AwsStorageConfigurationInfo.class);
+    assertThat(((AwsStorageConfigurationInfo) result).getRoleARN())
+        .isEqualTo("arn:aws:iam::123456789012:role/test-role");
+  }
+
+  @Test
+  void storageConfigFromPropertyOverride_blankValue_clearsStorageName() throws 
Exception {
+    var method =
+        IcebergCatalog.class.getDeclaredMethod(
+            "storageConfigFromPropertyOverride", Map.class, List.class);
+    method.setAccessible(true);
+
+    AwsStorageConfigurationInfo catalogConfig =
+        AwsStorageConfigurationInfo.builder()
+            .allowedLocations(List.of("s3://bucket/path"))
+            .roleARN("arn:aws:iam::123456789012:role/test-role")
+            .storageName("catalog-storage")
+            .build();
+
+    PolarisEntity catalogEntity = createEntityWithStorageConfig(catalogConfig);
+
+    Map<String, String> properties = new HashMap<>();
+    properties.put(IcebergCatalog.POLARIS_STORAGE_NAME_PROPERTY, "  ");
+
+    PolarisStorageConfigurationInfo result =
+        (PolarisStorageConfigurationInfo) method.invoke(null, properties, 
List.of(catalogEntity));
+
+    assertThat(result).isNotNull();
+    assertThat(result.getStorageName()).isNull();
+  }
+
+  @Test
+  void storageConfigFromPropertyOverride_noParentConfig_throws() throws 
Exception {
+    var method =
+        IcebergCatalog.class.getDeclaredMethod(
+            "storageConfigFromPropertyOverride", Map.class, List.class);
+    method.setAccessible(true);
+
+    Map<String, String> properties = new HashMap<>();
+    properties.put(IcebergCatalog.POLARIS_STORAGE_NAME_PROPERTY, "ns-storage");
+
+    assertThatThrownBy(() -> method.invoke(null, properties, List.of()))
+        
.hasCauseInstanceOf(org.apache.iceberg.exceptions.BadRequestException.class)

Review Comment:
   no need for the full class path, import instead.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to