* The server must also be configured to reject request body sizes larger than 1MB (1000000
diff --git a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisManagementServiceIntegrationTest.java b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisManagementServiceIntegrationTest.java
index e830a18ab4..d743351d9f 100644
--- a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisManagementServiceIntegrationTest.java
+++ b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisManagementServiceIntegrationTest.java
@@ -90,10 +90,11 @@
* @implSpec @implSpec This test expects the server to be configured with the following features
* configured:
*
*/
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/PolarisCallContext.java b/polaris-core/src/main/java/org/apache/polaris/core/PolarisCallContext.java
index 3b5e962514..510cd0f6cd 100644
--- a/polaris-core/src/main/java/org/apache/polaris/core/PolarisCallContext.java
+++ b/polaris-core/src/main/java/org/apache/polaris/core/PolarisCallContext.java
@@ -21,6 +21,7 @@
import jakarta.annotation.Nonnull;
import java.time.Clock;
import java.time.ZoneId;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
import org.apache.polaris.core.persistence.BasePersistence;
/**
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java
index 96f449acbd..564be49dac 100644
--- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java
+++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java
@@ -98,8 +98,8 @@
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.iceberg.exceptions.ForbiddenException;
-import org.apache.polaris.core.PolarisConfiguration;
-import org.apache.polaris.core.PolarisConfigurationStore;
+import org.apache.polaris.core.config.FeatureConfiguration;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
import org.apache.polaris.core.context.CallContext;
import org.apache.polaris.core.entity.PolarisBaseEntity;
import org.apache.polaris.core.entity.PolarisEntityConstants;
@@ -510,7 +510,7 @@ public void authorizeOrThrow(
boolean enforceCredentialRotationRequiredState =
featureConfig.getConfiguration(
CallContext.getCurrentContext().getPolarisCallContext(),
- PolarisConfiguration.ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING);
+ FeatureConfiguration.ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING);
if (enforceCredentialRotationRequiredState
&& authenticatedPrincipal
.getPrincipalEntity()
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/config/BehaviorChangeConfiguration.java b/polaris-core/src/main/java/org/apache/polaris/core/config/BehaviorChangeConfiguration.java
new file mode 100644
index 0000000000..a64c594028
--- /dev/null
+++ b/polaris-core/src/main/java/org/apache/polaris/core/config/BehaviorChangeConfiguration.java
@@ -0,0 +1,46 @@
+/*
+ * 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.config;
+
+import java.util.Optional;
+
+/**
+ * Internal configuration flags for non-feature behavior changes in Polaris. These flags control
+ * subtle behavior adjustments and bug fixes, not user-facing catalog settings. They are intended
+ * for internal use only, are inherently unstable, and may be removed at any time. When introducing
+ * a new flag, consider the trade-off between maintenance burden and the risk of an unguarded
+ * behavior change. Flags here are generally short-lived and should either be removed or promoted to
+ * stable feature flags before the next release.
+ *
+ * @param The type of the configuration
+ */
+public class BehaviorChangeConfiguration extends PolarisConfiguration {
+
+ protected BehaviorChangeConfiguration(
+ String key, String description, T defaultValue, Optional catalogConfig) {
+ super(key, description, defaultValue, catalogConfig);
+ }
+
+ public static final BehaviorChangeConfiguration VALIDATE_VIEW_LOCATION_OVERLAP =
+ PolarisConfiguration.builder()
+ .key("STORAGE_CREDENTIAL_CACHE_DURATION_SECONDS")
+ .description("If true, validate that view locations don't overlap when views are created")
+ .defaultValue(true)
+ .buildBehaviorChangeConfiguration();
+}
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/PolarisConfiguration.java b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java
similarity index 61%
rename from polaris-core/src/main/java/org/apache/polaris/core/PolarisConfiguration.java
rename to polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java
index ca1962e3c3..087d0525ee 100644
--- a/polaris-core/src/main/java/org/apache/polaris/core/PolarisConfiguration.java
+++ b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java
@@ -16,115 +16,26 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.polaris.core;
+package org.apache.polaris.core.config;
-import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.apache.polaris.core.admin.model.StorageConfigInfo;
-import org.apache.polaris.core.context.CallContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-public class PolarisConfiguration {
-
- private static final Logger LOGGER = LoggerFactory.getLogger(PolarisConfiguration.class);
-
- public final String key;
- public final String description;
- public final T defaultValue;
- private final Optional catalogConfigImpl;
- private final Class typ;
-
- @SuppressWarnings("unchecked")
- public PolarisConfiguration(
+/**
+ * Configurations for features within Polaris. These configurations are intended to be customized
+ * and many expose user-facing catalog-level configurations. These configurations are stable over
+ * time.
+ *
+ * @param
+ */
+public class FeatureConfiguration extends PolarisConfiguration {
+ protected FeatureConfiguration(
String key, String description, T defaultValue, Optional catalogConfig) {
- this.key = key;
- this.description = description;
- this.defaultValue = defaultValue;
- this.catalogConfigImpl = catalogConfig;
- this.typ = (Class) defaultValue.getClass();
- }
-
- public boolean hasCatalogConfig() {
- return catalogConfigImpl.isPresent();
- }
-
- public String catalogConfig() {
- return catalogConfigImpl.orElseThrow(
- () ->
- new IllegalStateException(
- "Attempted to read a catalog config key from a configuration that doesn't have one."));
- }
-
- T cast(Object value) {
- return this.typ.cast(value);
- }
-
- public static class Builder {
- private String key;
- private String description;
- private T defaultValue;
- private Optional catalogConfig = Optional.empty();
-
- public Builder key(String key) {
- this.key = key;
- return this;
- }
-
- public Builder description(String description) {
- this.description = description;
- return this;
- }
-
- @SuppressWarnings("unchecked")
- public Builder defaultValue(T defaultValue) {
- if (defaultValue instanceof List>) {
- // Type-safe handling of List
- this.defaultValue = (T) new ArrayList<>((List>) defaultValue);
- } else {
- this.defaultValue = defaultValue;
- }
- return this;
- }
-
- public Builder catalogConfig(String catalogConfig) {
- this.catalogConfig = Optional.of(catalogConfig);
- return this;
- }
-
- public PolarisConfiguration build() {
- if (key == null || description == null || defaultValue == null) {
- throw new IllegalArgumentException("key, description, and defaultValue are required");
- }
- return new PolarisConfiguration<>(key, description, defaultValue, catalogConfig);
- }
- }
-
- /**
- * Returns the value of a `PolarisConfiguration`, or the default if it cannot be loaded. This
- * method does not need to be used when a `CallContext` is already available
- */
- public static T loadConfig(PolarisConfiguration configuration) {
- var callContext = CallContext.getCurrentContext();
- if (callContext == null) {
- LOGGER.warn(
- String.format(
- "Unable to load current call context; using %s = %s",
- configuration.key, configuration.defaultValue));
- return configuration.defaultValue;
- }
- return callContext
- .getPolarisCallContext()
- .getConfigurationStore()
- .getConfiguration(callContext.getPolarisCallContext(), configuration);
- }
-
- public static Builder builder() {
- return new Builder<>();
+ super(key, description, defaultValue, catalogConfig);
}
- public static final PolarisConfiguration
+ public static final FeatureConfiguration
ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING =
PolarisConfiguration.builder()
.key("ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING")
@@ -132,9 +43,9 @@ public static Builder builder() {
"If set to true, require that principals must rotate their credentials before being used "
+ "for anything else.")
.defaultValue(false)
- .build();
+ .buildFeatureConfiguration();
- public static final PolarisConfiguration SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION =
+ public static final FeatureConfiguration SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION =
PolarisConfiguration.builder()
.key("SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION")
.description(
@@ -147,9 +58,9 @@ public static Builder builder() {
+ " \"credential-vending\" and can use server-default environment variables or credential config\n"
+ " files for all storage access, or in test/dev scenarios.")
.defaultValue(false)
- .build();
+ .buildFeatureConfiguration();
- public static final PolarisConfiguration ALLOW_TABLE_LOCATION_OVERLAP =
+ public static final FeatureConfiguration ALLOW_TABLE_LOCATION_OVERLAP =
PolarisConfiguration.builder()
.key("ALLOW_TABLE_LOCATION_OVERLAP")
.catalogConfig("allow.overlapping.table.location")
@@ -157,58 +68,58 @@ public static Builder builder() {
"If set to true, allow one table's location to reside within another table's location. "
+ "This is only enforced within a given namespace.")
.defaultValue(false)
- .build();
+ .buildFeatureConfiguration();
- public static final PolarisConfiguration ALLOW_NAMESPACE_LOCATION_OVERLAP =
+ public static final FeatureConfiguration ALLOW_NAMESPACE_LOCATION_OVERLAP =
PolarisConfiguration.builder()
.key("ALLOW_NAMESPACE_LOCATION_OVERLAP")
.description(
"If set to true, allow one namespace's location to reside within another namespace's location. "
+ "This is only enforced within a parent catalog or namespace.")
.defaultValue(false)
- .build();
+ .buildFeatureConfiguration();
- public static final PolarisConfiguration ALLOW_EXTERNAL_METADATA_FILE_LOCATION =
+ public static final FeatureConfiguration ALLOW_EXTERNAL_METADATA_FILE_LOCATION =
PolarisConfiguration.builder()
.key("ALLOW_EXTERNAL_METADATA_FILE_LOCATION")
.description(
"If set to true, allows metadata files to be located outside the default metadata directory.")
.defaultValue(false)
- .build();
+ .buildFeatureConfiguration();
- public static final PolarisConfiguration ALLOW_OVERLAPPING_CATALOG_URLS =
+ public static final FeatureConfiguration ALLOW_OVERLAPPING_CATALOG_URLS =
PolarisConfiguration.builder()
.key("ALLOW_OVERLAPPING_CATALOG_URLS")
.description("If set to true, allows catalog URLs to overlap.")
.defaultValue(false)
- .build();
+ .buildFeatureConfiguration();
- public static final PolarisConfiguration ALLOW_UNSTRUCTURED_TABLE_LOCATION =
+ public static final FeatureConfiguration ALLOW_UNSTRUCTURED_TABLE_LOCATION =
PolarisConfiguration.builder()
.key("ALLOW_UNSTRUCTURED_TABLE_LOCATION")
.catalogConfig("allow.unstructured.table.location")
.description("If set to true, allows unstructured table locations.")
.defaultValue(false)
- .build();
+ .buildFeatureConfiguration();
- public static final PolarisConfiguration ALLOW_EXTERNAL_TABLE_LOCATION =
+ public static final FeatureConfiguration ALLOW_EXTERNAL_TABLE_LOCATION =
PolarisConfiguration.builder()
.key("ALLOW_EXTERNAL_TABLE_LOCATION")
.catalogConfig("allow.external.table.location")
.description(
"If set to true, allows tables to have external locations outside the default structure.")
.defaultValue(false)
- .build();
+ .buildFeatureConfiguration();
- public static final PolarisConfiguration ALLOW_EXTERNAL_CATALOG_CREDENTIAL_VENDING =
+ public static final FeatureConfiguration ALLOW_EXTERNAL_CATALOG_CREDENTIAL_VENDING =
PolarisConfiguration.builder()
.key("ALLOW_EXTERNAL_CATALOG_CREDENTIAL_VENDING")
.catalogConfig("enable.credential.vending")
.description("If set to true, allow credential vending for external catalogs.")
.defaultValue(true)
- .build();
+ .buildFeatureConfiguration();
- public static final PolarisConfiguration> SUPPORTED_CATALOG_STORAGE_TYPES =
+ public static final FeatureConfiguration> SUPPORTED_CATALOG_STORAGE_TYPES =
PolarisConfiguration.>builder()
.key("SUPPORTED_CATALOG_STORAGE_TYPES")
.catalogConfig("supported.storage.types")
@@ -219,34 +130,34 @@ public static Builder builder() {
StorageConfigInfo.StorageTypeEnum.AZURE.name(),
StorageConfigInfo.StorageTypeEnum.GCS.name(),
StorageConfigInfo.StorageTypeEnum.FILE.name()))
- .build();
+ .buildFeatureConfiguration();
- public static final PolarisConfiguration CLEANUP_ON_NAMESPACE_DROP =
+ public static final FeatureConfiguration CLEANUP_ON_NAMESPACE_DROP =
PolarisConfiguration.builder()
.key("CLEANUP_ON_NAMESPACE_DROP")
.catalogConfig("cleanup.on.namespace.drop")
.description("If set to true, clean up data when a namespace is dropped")
.defaultValue(false)
- .build();
+ .buildFeatureConfiguration();
- public static final PolarisConfiguration CLEANUP_ON_CATALOG_DROP =
+ public static final FeatureConfiguration CLEANUP_ON_CATALOG_DROP =
PolarisConfiguration.builder()
.key("CLEANUP_ON_CATALOG_DROP")
.catalogConfig("cleanup.on.catalog.drop")
.description("If set to true, clean up data when a catalog is dropped")
.defaultValue(false)
- .build();
+ .buildFeatureConfiguration();
- public static final PolarisConfiguration DROP_WITH_PURGE_ENABLED =
+ public static final FeatureConfiguration DROP_WITH_PURGE_ENABLED =
PolarisConfiguration.builder()
.key("DROP_WITH_PURGE_ENABLED")
.catalogConfig("drop-with-purge.enabled")
.description(
"If set to true, allows tables to be dropped with the purge parameter set to true.")
.defaultValue(true)
- .build();
+ .buildFeatureConfiguration();
- public static final PolarisConfiguration STORAGE_CREDENTIAL_DURATION_SECONDS =
+ public static final FeatureConfiguration STORAGE_CREDENTIAL_DURATION_SECONDS =
PolarisConfiguration.builder()
.key("STORAGE_CREDENTIAL_DURATION_SECONDS")
.description(
@@ -254,22 +165,22 @@ public static Builder builder() {
+ " longer (or shorter) durations is dependent on the storage provider. GCS"
+ " current does not respect this value.")
.defaultValue(60 * 60) // 1 hour
- .build();
+ .buildFeatureConfiguration();
- public static final PolarisConfiguration STORAGE_CREDENTIAL_CACHE_DURATION_SECONDS =
+ public static final FeatureConfiguration STORAGE_CREDENTIAL_CACHE_DURATION_SECONDS =
PolarisConfiguration.builder()
.key("STORAGE_CREDENTIAL_CACHE_DURATION_SECONDS")
.description(
"How long to store storage credentials in the local cache. This should be less than "
+ STORAGE_CREDENTIAL_DURATION_SECONDS.key)
.defaultValue(30 * 60) // 30 minutes
- .build();
+ .buildFeatureConfiguration();
- public static final PolarisConfiguration MAX_METADATA_REFRESH_RETRIES =
+ public static final FeatureConfiguration MAX_METADATA_REFRESH_RETRIES =
PolarisConfiguration.builder()
.key("MAX_METADATA_REFRESH_RETRIES")
.description(
"How many times to retry refreshing metadata when the previous error was retryable")
.defaultValue(2)
- .build();
+ .buildFeatureConfiguration();
}
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/config/PolarisConfiguration.java b/polaris-core/src/main/java/org/apache/polaris/core/config/PolarisConfiguration.java
new file mode 100644
index 0000000000..bcb3809881
--- /dev/null
+++ b/polaris-core/src/main/java/org/apache/polaris/core/config/PolarisConfiguration.java
@@ -0,0 +1,141 @@
+/*
+ * 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.config;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import org.apache.polaris.core.context.CallContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An ABC for Polaris configurations that alter the service's behavior
+ *
+ * @param The type of the configuration
+ */
+public abstract class PolarisConfiguration {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(PolarisConfiguration.class);
+
+ public final String key;
+ public final String description;
+ public final T defaultValue;
+ private final Optional catalogConfigImpl;
+ private final Class typ;
+
+ @SuppressWarnings("unchecked")
+ protected PolarisConfiguration(
+ String key, String description, T defaultValue, Optional catalogConfig) {
+ this.key = key;
+ this.description = description;
+ this.defaultValue = defaultValue;
+ this.catalogConfigImpl = catalogConfig;
+ this.typ = (Class) defaultValue.getClass();
+ }
+
+ public boolean hasCatalogConfig() {
+ return catalogConfigImpl.isPresent();
+ }
+
+ public String catalogConfig() {
+ return catalogConfigImpl.orElseThrow(
+ () ->
+ new IllegalStateException(
+ "Attempted to read a catalog config key from a configuration that doesn't have one."));
+ }
+
+ T cast(Object value) {
+ return this.typ.cast(value);
+ }
+
+ public static class Builder {
+ private String key;
+ private String description;
+ private T defaultValue;
+ private Optional catalogConfig = Optional.empty();
+
+ public Builder key(String key) {
+ this.key = key;
+ return this;
+ }
+
+ public Builder description(String description) {
+ this.description = description;
+ return this;
+ }
+
+ @SuppressWarnings("unchecked")
+ public Builder defaultValue(T defaultValue) {
+ if (defaultValue instanceof List>) {
+ // Type-safe handling of List
+ this.defaultValue = (T) new ArrayList<>((List>) defaultValue);
+ } else {
+ this.defaultValue = defaultValue;
+ }
+ return this;
+ }
+
+ public Builder catalogConfig(String catalogConfig) {
+ this.catalogConfig = Optional.of(catalogConfig);
+ return this;
+ }
+
+ public FeatureConfiguration buildFeatureConfiguration() {
+ if (key == null || description == null || defaultValue == null) {
+ throw new IllegalArgumentException("key, description, and defaultValue are required");
+ }
+ return new FeatureConfiguration<>(key, description, defaultValue, catalogConfig);
+ }
+
+ public BehaviorChangeConfiguration buildBehaviorChangeConfiguration() {
+ if (key == null || description == null || defaultValue == null) {
+ throw new IllegalArgumentException("key, description, and defaultValue are required");
+ }
+ if (catalogConfig.isPresent()) {
+ throw new IllegalArgumentException(
+ "catalogConfig is not valid for behavior change configs");
+ }
+ return new BehaviorChangeConfiguration<>(key, description, defaultValue, catalogConfig);
+ }
+ }
+
+ /**
+ * Returns the value of a `PolarisConfiguration`, or the default if it cannot be loaded. This
+ * method does not need to be used when a `CallContext` is already available
+ */
+ public static T loadConfig(PolarisConfiguration configuration) {
+ var callContext = CallContext.getCurrentContext();
+ if (callContext == null) {
+ LOGGER.warn(
+ String.format(
+ "Unable to load current call context; using %s = %s",
+ configuration.key, configuration.defaultValue));
+ return configuration.defaultValue;
+ }
+ return callContext
+ .getPolarisCallContext()
+ .getConfigurationStore()
+ .getConfiguration(callContext.getPolarisCallContext(), configuration);
+ }
+
+ public static Builder builder() {
+ return new Builder<>();
+ }
+}
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/PolarisConfigurationStore.java b/polaris-core/src/main/java/org/apache/polaris/core/config/PolarisConfigurationStore.java
similarity index 98%
rename from polaris-core/src/main/java/org/apache/polaris/core/PolarisConfigurationStore.java
rename to polaris-core/src/main/java/org/apache/polaris/core/config/PolarisConfigurationStore.java
index a3c9022060..65e6133515 100644
--- a/polaris-core/src/main/java/org/apache/polaris/core/PolarisConfigurationStore.java
+++ b/polaris-core/src/main/java/org/apache/polaris/core/config/PolarisConfigurationStore.java
@@ -16,13 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.polaris.core;
+package org.apache.polaris.core.config;
import com.google.common.base.Preconditions;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
+import org.apache.polaris.core.PolarisCallContext;
import org.apache.polaris.core.entity.CatalogEntity;
/**
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisStorageConfigurationInfo.java b/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisStorageConfigurationInfo.java
index 6b0638e837..9631d95b42 100644
--- a/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisStorageConfigurationInfo.java
+++ b/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisStorageConfigurationInfo.java
@@ -36,9 +36,9 @@
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import org.apache.polaris.core.PolarisConfiguration;
import org.apache.polaris.core.PolarisDiagnostics;
import org.apache.polaris.core.admin.model.Catalog;
+import org.apache.polaris.core.config.FeatureConfiguration;
import org.apache.polaris.core.context.CallContext;
import org.apache.polaris.core.entity.CatalogEntity;
import org.apache.polaris.core.entity.PolarisEntity;
@@ -168,7 +168,7 @@ public static Optional forEntityPath(
.getConfiguration(
CallContext.getCurrentContext().getPolarisCallContext(),
catalog,
- PolarisConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION);
+ FeatureConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION);
if (!allowEscape
&& catalog.getCatalogType() != Catalog.TypeEnum.EXTERNAL
&& baseLocation != null) {
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java b/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java
index 591a67f15d..9b1c64900b 100644
--- a/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java
+++ b/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java
@@ -18,8 +18,8 @@
*/
package org.apache.polaris.core.storage.aws;
-import static org.apache.polaris.core.PolarisConfiguration.STORAGE_CREDENTIAL_DURATION_SECONDS;
-import static org.apache.polaris.core.PolarisConfiguration.loadConfig;
+import static org.apache.polaris.core.config.FeatureConfiguration.STORAGE_CREDENTIAL_DURATION_SECONDS;
+import static org.apache.polaris.core.config.PolarisConfiguration.loadConfig;
import jakarta.annotation.Nonnull;
import java.net.URI;
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureCredentialsStorageIntegration.java b/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureCredentialsStorageIntegration.java
index 4013cbd81a..62e4fc4dc1 100644
--- a/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureCredentialsStorageIntegration.java
+++ b/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureCredentialsStorageIntegration.java
@@ -45,8 +45,8 @@
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
-import org.apache.polaris.core.PolarisConfiguration;
import org.apache.polaris.core.PolarisDiagnostics;
+import org.apache.polaris.core.config.FeatureConfiguration;
import org.apache.polaris.core.storage.InMemoryStorageIntegration;
import org.apache.polaris.core.storage.PolarisCredentialProperty;
import org.slf4j.Logger;
@@ -126,7 +126,7 @@ public EnumMap getSubscopedCreds(
// clock skew between the client and server,
OffsetDateTime startTime = start.truncatedTo(ChronoUnit.SECONDS).atOffset(ZoneOffset.UTC);
int intendedDurationSeconds =
- PolarisConfiguration.loadConfig(PolarisConfiguration.STORAGE_CREDENTIAL_DURATION_SECONDS);
+ FeatureConfiguration.loadConfig(FeatureConfiguration.STORAGE_CREDENTIAL_DURATION_SECONDS);
OffsetDateTime intendedEndTime =
start.plusSeconds(intendedDurationSeconds).atOffset(ZoneOffset.UTC);
OffsetDateTime maxAllowedEndTime =
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/storage/cache/StorageCredentialCache.java b/polaris-core/src/main/java/org/apache/polaris/core/storage/cache/StorageCredentialCache.java
index bfc75214c6..a80b38aa45 100644
--- a/polaris-core/src/main/java/org/apache/polaris/core/storage/cache/StorageCredentialCache.java
+++ b/polaris-core/src/main/java/org/apache/polaris/core/storage/cache/StorageCredentialCache.java
@@ -29,7 +29,8 @@
import java.util.function.Function;
import org.apache.iceberg.exceptions.UnprocessableEntityException;
import org.apache.polaris.core.PolarisCallContext;
-import org.apache.polaris.core.PolarisConfiguration;
+import org.apache.polaris.core.config.FeatureConfiguration;
+import org.apache.polaris.core.config.PolarisConfiguration;
import org.apache.polaris.core.entity.PolarisEntity;
import org.apache.polaris.core.entity.PolarisEntityType;
import org.apache.polaris.core.persistence.dao.entity.ScopedCredentialsResult;
@@ -96,15 +97,15 @@ public long expireAfterRead(
private static long maxCacheDurationMs() {
var cacheDurationSeconds =
PolarisConfiguration.loadConfig(
- PolarisConfiguration.STORAGE_CREDENTIAL_CACHE_DURATION_SECONDS);
+ FeatureConfiguration.STORAGE_CREDENTIAL_CACHE_DURATION_SECONDS);
var credentialDurationSeconds =
- PolarisConfiguration.loadConfig(PolarisConfiguration.STORAGE_CREDENTIAL_DURATION_SECONDS);
+ PolarisConfiguration.loadConfig(FeatureConfiguration.STORAGE_CREDENTIAL_DURATION_SECONDS);
if (cacheDurationSeconds >= credentialDurationSeconds) {
throw new IllegalArgumentException(
String.format(
"%s should be less than %s",
- PolarisConfiguration.STORAGE_CREDENTIAL_CACHE_DURATION_SECONDS.key,
- PolarisConfiguration.STORAGE_CREDENTIAL_DURATION_SECONDS.key));
+ FeatureConfiguration.STORAGE_CREDENTIAL_CACHE_DURATION_SECONDS.key,
+ FeatureConfiguration.STORAGE_CREDENTIAL_DURATION_SECONDS.key));
} else {
return cacheDurationSeconds * 1000L;
}
diff --git a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapAtomicOperationMetaStoreManagerTest.java b/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapAtomicOperationMetaStoreManagerTest.java
index 42e61f7d48..274841c52a 100644
--- a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapAtomicOperationMetaStoreManagerTest.java
+++ b/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapAtomicOperationMetaStoreManagerTest.java
@@ -22,9 +22,9 @@
import java.time.ZoneId;
import org.apache.polaris.core.PolarisCallContext;
-import org.apache.polaris.core.PolarisConfigurationStore;
import org.apache.polaris.core.PolarisDefaultDiagServiceImpl;
import org.apache.polaris.core.PolarisDiagnostics;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
import org.apache.polaris.core.persistence.transactional.PolarisTreeMapMetaStoreSessionImpl;
import org.apache.polaris.core.persistence.transactional.PolarisTreeMapStore;
import org.mockito.Mockito;
diff --git a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java b/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java
index 65f8080d91..d7491e0384 100644
--- a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java
+++ b/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java
@@ -22,9 +22,9 @@
import java.time.ZoneId;
import org.apache.polaris.core.PolarisCallContext;
-import org.apache.polaris.core.PolarisConfigurationStore;
import org.apache.polaris.core.PolarisDefaultDiagServiceImpl;
import org.apache.polaris.core.PolarisDiagnostics;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
import org.apache.polaris.core.persistence.transactional.PolarisMetaStoreManagerImpl;
import org.apache.polaris.core.persistence.transactional.PolarisTreeMapMetaStoreSessionImpl;
import org.apache.polaris.core.persistence.transactional.PolarisTreeMapStore;
diff --git a/polaris-core/src/test/java/org/apache/polaris/core/storage/InMemoryStorageIntegrationTest.java b/polaris-core/src/test/java/org/apache/polaris/core/storage/InMemoryStorageIntegrationTest.java
index aa5317e998..0f1f4f151b 100644
--- a/polaris-core/src/test/java/org/apache/polaris/core/storage/InMemoryStorageIntegrationTest.java
+++ b/polaris-core/src/test/java/org/apache/polaris/core/storage/InMemoryStorageIntegrationTest.java
@@ -26,9 +26,9 @@
import java.util.Map;
import java.util.Set;
import org.apache.polaris.core.PolarisCallContext;
-import org.apache.polaris.core.PolarisConfigurationStore;
import org.apache.polaris.core.PolarisDefaultDiagServiceImpl;
import org.apache.polaris.core.PolarisDiagnostics;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
import org.apache.polaris.core.context.CallContext;
import org.apache.polaris.core.storage.aws.AwsStorageConfigurationInfo;
import org.assertj.core.api.Assertions;
diff --git a/polaris-core/src/test/java/org/apache/polaris/service/storage/PolarisConfigurationStoreTest.java b/polaris-core/src/test/java/org/apache/polaris/service/storage/PolarisConfigurationStoreTest.java
index a0da2c5d8f..6abb76b8ad 100644
--- a/polaris-core/src/test/java/org/apache/polaris/service/storage/PolarisConfigurationStoreTest.java
+++ b/polaris-core/src/test/java/org/apache/polaris/service/storage/PolarisConfigurationStoreTest.java
@@ -20,9 +20,12 @@
import jakarta.annotation.Nullable;
import java.util.List;
+import java.util.function.Supplier;
import org.apache.polaris.core.PolarisCallContext;
-import org.apache.polaris.core.PolarisConfiguration;
-import org.apache.polaris.core.PolarisConfigurationStore;
+import org.apache.polaris.core.config.BehaviorChangeConfiguration;
+import org.apache.polaris.core.config.FeatureConfiguration;
+import org.apache.polaris.core.config.PolarisConfiguration;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -91,6 +94,50 @@ private static PolarisConfiguration buildConfig(String key, T defaultValu
.key(key)
.description("")
.defaultValue(defaultValue)
- .build();
+ .buildFeatureConfiguration();
+ }
+
+ private static class PolarisConfigurationConsumer {
+
+ private final PolarisCallContext polarisCallContext;
+ private final PolarisConfigurationStore configurationStore;
+
+ public PolarisConfigurationConsumer(
+ PolarisCallContext polarisCallContext, PolarisConfigurationStore configurationStore) {
+ this.polarisCallContext = polarisCallContext;
+ this.configurationStore = configurationStore;
+ }
+
+ public T consumeConfiguration(
+ PolarisConfiguration config, Supplier code, T defaultVal) {
+ if (configurationStore.getConfiguration(polarisCallContext, config)) {
+ return code.get();
+ }
+ return defaultVal;
+ }
+ }
+
+ @Test
+ public void testBehaviorAndFeatureConfigs() {
+ PolarisConfigurationConsumer consumer =
+ new PolarisConfigurationConsumer(null, new PolarisConfigurationStore() {});
+
+ FeatureConfiguration featureConfig =
+ PolarisConfiguration.builder()
+ .key("example")
+ .description("example")
+ .defaultValue(true)
+ .buildFeatureConfiguration();
+
+ BehaviorChangeConfiguration behaviorChangeConfig =
+ PolarisConfiguration.builder()
+ .key("example")
+ .description("example")
+ .defaultValue(true)
+ .buildBehaviorChangeConfiguration();
+
+ consumer.consumeConfiguration(behaviorChangeConfig, () -> 21, 22);
+
+ consumer.consumeConfiguration(featureConfig, () -> 42, 43);
}
}
diff --git a/quarkus/admin/src/main/java/org/apache/polaris/admintool/config/QuarkusProducers.java b/quarkus/admin/src/main/java/org/apache/polaris/admintool/config/QuarkusProducers.java
index 6788742ba8..07ad023629 100644
--- a/quarkus/admin/src/main/java/org/apache/polaris/admintool/config/QuarkusProducers.java
+++ b/quarkus/admin/src/main/java/org/apache/polaris/admintool/config/QuarkusProducers.java
@@ -25,9 +25,9 @@
import jakarta.enterprise.inject.Instance;
import jakarta.enterprise.inject.Produces;
import java.time.Clock;
-import org.apache.polaris.core.PolarisConfigurationStore;
import org.apache.polaris.core.PolarisDefaultDiagServiceImpl;
import org.apache.polaris.core.PolarisDiagnostics;
+import org.apache.polaris.core.config.PolarisConfigurationStore;
import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
import org.apache.polaris.core.storage.PolarisStorageIntegration;
diff --git a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusBehaviorChangesConfiguration.java b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusBehaviorChangesConfiguration.java
new file mode 100644
index 0000000000..852529a0cc
--- /dev/null
+++ b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusBehaviorChangesConfiguration.java
@@ -0,0 +1,91 @@
+/*
+ * 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.quarkus.config;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.quarkus.runtime.annotations.StaticInitSafe;
+import io.smallrye.config.ConfigMapping;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.polaris.service.config.FeaturesConfiguration;
+
+@StaticInitSafe
+@ConfigMapping(prefix = "polaris.behavior-changes")
+// FIXME: this should extend FeatureConfiguration, but that causes conflicts with
+// QuarkusFeaturesConfiguration
+public interface QuarkusBehaviorChangesConfiguration {
+
+ Map defaults();
+
+ Map realmOverrides();
+
+ default Map parseDefaults(ObjectMapper objectMapper) {
+ return convertMap(objectMapper, defaults());
+ }
+
+ default Map> parseRealmOverrides(ObjectMapper objectMapper) {
+ Map> m = new HashMap<>();
+ for (String realm : realmOverrides().keySet()) {
+ m.put(realm, convertMap(objectMapper, realmOverrides().get(realm).overrides()));
+ }
+ return m;
+ }
+
+ private static Map convertMap(
+ ObjectMapper objectMapper, Map properties) {
+ Map m = new HashMap<>();
+ for (String configName : properties.keySet()) {
+ String json = properties.get(configName);
+ try {
+ JsonNode node = objectMapper.readTree(json);
+ m.put(configName, configValue(node));
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(
+ "Invalid JSON value for feature configuration: " + configName, e);
+ }
+ }
+ return m;
+ }
+
+ private static Object configValue(JsonNode node) {
+ return switch (node.getNodeType()) {
+ case BOOLEAN -> node.asBoolean();
+ case STRING -> node.asText();
+ case NUMBER ->
+ switch (node.numberType()) {
+ case INT, LONG -> node.asLong();
+ case FLOAT, DOUBLE -> node.asDouble();
+ default ->
+ throw new IllegalArgumentException("Unsupported number type: " + node.numberType());
+ };
+ case ARRAY -> {
+ List