This is an automated email from the ASF dual-hosted git repository.
adutra pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git
The following commit(s) were added to refs/heads/main by this push:
new 5f86f98d Add production readiness checks (#956)
5f86f98d is described below
commit 5f86f98d0b6e713a27290f3cfbc9c0e30e4ab967
Author: Alexandre Dutra <[email protected]>
AuthorDate: Fri Feb 7 20:14:36 2025 +0100
Add production readiness checks (#956)
---
.../EclipseLinkProductionReadinessChecks.java | 59 +++++++
.../PolarisEclipseLinkPersistenceUnit.java | 68 +++++---
polaris-core/build.gradle.kts | 3 +
.../core/config/ProductionReadinessCheck.java | 55 +++++++
.../quarkus/config/ProductionReadinessChecks.java | 182 +++++++++++++++++++++
.../polaris/service/auth/JWTRSAKeyPairFactory.java | 7 -
6 files changed, 344 insertions(+), 30 deletions(-)
diff --git
a/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/EclipseLinkProductionReadinessChecks.java
b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/EclipseLinkProductionReadinessChecks.java
new file mode 100644
index 00000000..be0ae5b7
--- /dev/null
+++
b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/EclipseLinkProductionReadinessChecks.java
@@ -0,0 +1,59 @@
+/*
+ * 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.extension.persistence.impl.eclipselink;
+
+import static
org.eclipse.persistence.config.PersistenceUnitProperties.JDBC_URL;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Produces;
+import java.io.IOException;
+import java.nio.file.Path;
+import org.apache.polaris.core.config.ProductionReadinessCheck;
+import org.apache.polaris.core.config.ProductionReadinessCheck.Error;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ApplicationScoped
+public class EclipseLinkProductionReadinessChecks {
+
+ private static final Logger LOGGER =
+ LoggerFactory.getLogger(EclipseLinkProductionReadinessChecks.class);
+
+ @Produces
+ public ProductionReadinessCheck checkJdbcUrl(EclipseLinkConfiguration
eclipseLinkConfiguration) {
+ try {
+ var confFile =
eclipseLinkConfiguration.configurationFile().map(Path::toString).orElse(null);
+ var persistenceUnitName =
+ confFile != null ? eclipseLinkConfiguration.persistenceUnit() : null;
+ var unit =
+ PolarisEclipseLinkPersistenceUnit.locatePersistenceUnit(confFile,
persistenceUnitName);
+ var properties = unit.loadProperties();
+ var jdbcUrl = properties.get(JDBC_URL);
+ if (jdbcUrl != null && jdbcUrl.startsWith("jdbc:h2")) {
+ return ProductionReadinessCheck.of(
+ Error.of(
+ "The current persistence unit (jdbc:h2) is intended for tests
only.",
+ "polaris.persistence.eclipselink.configuration-file"));
+ }
+ } catch (IOException e) {
+ LOGGER.error("Failed to check JDBC URL", e);
+ }
+ return ProductionReadinessCheck.OK;
+ }
+}
diff --git
a/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkPersistenceUnit.java
b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkPersistenceUnit.java
index 7ea8749f..89d9d0eb 100644
---
a/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkPersistenceUnit.java
+++
b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkPersistenceUnit.java
@@ -57,17 +57,26 @@ sealed interface PolarisEclipseLinkPersistenceUnit
FileSystemPolarisEclipseLinkPersistenceUnit,
JarFilePolarisEclipseLinkPersistenceUnit {
- EntityManagerFactory createEntityManagerFactory(RealmContext realmContext)
throws IOException;
+ Map<String, String> loadProperties() throws IOException;
+
+ EntityManagerFactory createEntityManagerFactory(@Nonnull RealmContext
realmContext)
+ throws IOException;
record ClasspathResourcePolarisEclipseLinkPersistenceUnit(
URL resource, String resourceName, String persistenceUnitName)
implements PolarisEclipseLinkPersistenceUnit {
@Override
- public EntityManagerFactory createEntityManagerFactory(RealmContext
realmContext)
- throws IOException {
- Map<String, String> properties = loadProperties(resource,
persistenceUnitName, realmContext);
+ public Map<String, String> loadProperties() throws IOException {
+ var properties = internalLoadProperties(resource, persistenceUnitName);
properties.put(ECLIPSELINK_PERSISTENCE_XML, resourceName);
+ return properties;
+ }
+
+ @Override
+ public EntityManagerFactory createEntityManagerFactory(@Nonnull
RealmContext realmContext)
+ throws IOException {
+ var properties = transformJdbcUrl(loadProperties(), realmContext);
return Persistence.createEntityManagerFactory(persistenceUnitName,
properties);
}
}
@@ -76,13 +85,19 @@ sealed interface PolarisEclipseLinkPersistenceUnit
implements PolarisEclipseLinkPersistenceUnit {
@Override
- public EntityManagerFactory createEntityManagerFactory(RealmContext
realmContext)
- throws IOException {
- Map<String, String> properties =
- loadProperties(path.toUri().toURL(), persistenceUnitName,
realmContext);
+ public Map<String, String> loadProperties() throws IOException {
+ var properties = internalLoadProperties(path.toUri().toURL(),
persistenceUnitName);
Path archiveDirectory = path.getParent();
String descriptorPath =
archiveDirectory.getParent().relativize(path).toString();
properties.put(ECLIPSELINK_PERSISTENCE_XML, descriptorPath);
+ return properties;
+ }
+
+ @Override
+ public EntityManagerFactory createEntityManagerFactory(@Nonnull
RealmContext realmContext)
+ throws IOException {
+ var properties = transformJdbcUrl(loadProperties(), realmContext);
+ Path archiveDirectory = path.getParent();
ClassLoader prevClassLoader =
Thread.currentThread().getContextClassLoader();
try (URLClassLoader currentClassLoader =
new URLClassLoader(
@@ -101,10 +116,16 @@ sealed interface PolarisEclipseLinkPersistenceUnit
implements PolarisEclipseLinkPersistenceUnit {
@Override
- public EntityManagerFactory createEntityManagerFactory(RealmContext
realmContext)
- throws IOException {
- Map<String, String> properties = loadProperties(confUrl,
persistenceUnitName, realmContext);
+ public Map<String, String> loadProperties() throws IOException {
+ var properties = internalLoadProperties(confUrl, persistenceUnitName);
properties.put(ECLIPSELINK_PERSISTENCE_XML, descriptorPath);
+ return properties;
+ }
+
+ @Override
+ public EntityManagerFactory createEntityManagerFactory(@Nonnull
RealmContext realmContext)
+ throws IOException {
+ var properties = transformJdbcUrl(loadProperties(), realmContext);
ClassLoader prevClassLoader =
Thread.currentThread().getContextClassLoader();
try (URLClassLoader currentClassLoader =
new URLClassLoader(new URL[] {jarUrl},
this.getClass().getClassLoader())) {
@@ -117,7 +138,7 @@ sealed interface PolarisEclipseLinkPersistenceUnit
}
static PolarisEclipseLinkPersistenceUnit locatePersistenceUnit(
- String confFile, String persistenceUnitName) throws IOException {
+ @Nullable String confFile, @Nullable String persistenceUnitName) throws
IOException {
if (persistenceUnitName == null) {
persistenceUnitName = "polaris";
}
@@ -182,11 +203,8 @@ sealed interface PolarisEclipseLinkPersistenceUnit
}
/** Load the persistence unit properties from a given configuration file */
- private static Map<String, String> loadProperties(
- @Nonnull URL confFile,
- @Nonnull String persistenceUnitName,
- @Nonnull RealmContext realmContext)
- throws IOException {
+ private static Map<String, String> internalLoadProperties(
+ @Nonnull URL confFile, @Nonnull String persistenceUnitName) throws
IOException {
try (InputStream input = confFile.openStream()) {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
@@ -203,12 +221,7 @@ sealed interface PolarisEclipseLinkPersistenceUnit
nodeMap.getNamedItem("name").getNodeValue(),
nodeMap.getNamedItem("value").getNodeValue());
}
- // Replace database name in JDBC URL with realm
- if (properties.containsKey(JDBC_URL)) {
- properties.put(
- JDBC_URL,
- properties.get(JDBC_URL).replace("{realm}",
realmContext.getRealmIdentifier()));
- }
+
return properties;
} catch (XPathExpressionException
| ParserConfigurationException
@@ -221,4 +234,13 @@ sealed interface PolarisEclipseLinkPersistenceUnit
throw new IOException(str, e);
}
}
+
+ private static Map<String, String> transformJdbcUrl(
+ Map<String, String> properties, RealmContext realmContext) {
+ if (properties.containsKey(JDBC_URL)) {
+ properties.put(
+ JDBC_URL, properties.get(JDBC_URL).replace("{realm}",
realmContext.getRealmIdentifier()));
+ }
+ return properties;
+ }
}
diff --git a/polaris-core/build.gradle.kts b/polaris-core/build.gradle.kts
index a346a74e..b611509f 100644
--- a/polaris-core/build.gradle.kts
+++ b/polaris-core/build.gradle.kts
@@ -44,6 +44,9 @@ dependencies {
compileOnly(libs.jetbrains.annotations)
compileOnly(libs.spotbugs.annotations)
+ compileOnly(project(":polaris-immutables"))
+ annotationProcessor(project(":polaris-immutables", configuration =
"processor"))
+
constraints {
implementation("org.xerial.snappy:snappy-java:1.1.10.7") {
because("Vulnerability detected in 1.1.8.2")
diff --git
a/polaris-core/src/main/java/org/apache/polaris/core/config/ProductionReadinessCheck.java
b/polaris-core/src/main/java/org/apache/polaris/core/config/ProductionReadinessCheck.java
new file mode 100644
index 00000000..d899c9f7
--- /dev/null
+++
b/polaris-core/src/main/java/org/apache/polaris/core/config/ProductionReadinessCheck.java
@@ -0,0 +1,55 @@
+/*
+ * 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.List;
+import org.apache.polaris.immutables.PolarisImmutable;
+import org.immutables.value.Value;
+
+/** Represents the result of a production readiness check. */
+@PolarisImmutable
+public interface ProductionReadinessCheck {
+
+ ProductionReadinessCheck OK =
ImmutableProductionReadinessCheck.builder().build();
+
+ static ProductionReadinessCheck of(Error... errors) {
+ return
ImmutableProductionReadinessCheck.builder().addErrors(errors).build();
+ }
+
+ default boolean ready() {
+ return getErrors().isEmpty();
+ }
+
+ @Value.Parameter(order = 1)
+ List<Error> getErrors();
+
+ @PolarisImmutable
+ interface Error {
+
+ static Error of(String message, String offendingProperty) {
+ return ImmutableError.of(message, offendingProperty);
+ }
+
+ @Value.Parameter(order = 1)
+ String message();
+
+ @Value.Parameter(order = 2)
+ String offendingProperty();
+ }
+}
diff --git
a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/ProductionReadinessChecks.java
b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/ProductionReadinessChecks.java
new file mode 100644
index 00000000..b69e5318
--- /dev/null
+++
b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/ProductionReadinessChecks.java
@@ -0,0 +1,182 @@
+/*
+ * 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 jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.event.Observes;
+import jakarta.enterprise.event.Startup;
+import jakarta.enterprise.inject.Instance;
+import jakarta.enterprise.inject.Produces;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
+import org.apache.polaris.core.config.ProductionReadinessCheck;
+import org.apache.polaris.core.config.ProductionReadinessCheck.Error;
+import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
+import org.apache.polaris.service.auth.AuthenticationConfiguration;
+import
org.apache.polaris.service.auth.AuthenticationConfiguration.TokenBrokerConfiguration.RSAKeyPairConfiguration;
+import
org.apache.polaris.service.auth.AuthenticationConfiguration.TokenBrokerConfiguration.SymmetricKeyConfiguration;
+import org.apache.polaris.service.auth.Authenticator;
+import org.apache.polaris.service.auth.JWTRSAKeyPairFactory;
+import org.apache.polaris.service.auth.JWTSymmetricKeyFactory;
+import
org.apache.polaris.service.auth.TestInlineBearerTokenPolarisAuthenticator;
+import org.apache.polaris.service.auth.TestOAuth2ApiService;
+import org.apache.polaris.service.auth.TokenBrokerFactory;
+import org.apache.polaris.service.catalog.api.IcebergRestOAuth2ApiService;
+import org.apache.polaris.service.context.DefaultRealmContextResolver;
+import org.apache.polaris.service.context.RealmContextResolver;
+import org.apache.polaris.service.context.TestRealmContextResolver;
+import
org.apache.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory;
+import org.eclipse.microprofile.config.Config;
+import org.eclipse.microprofile.config.ConfigValue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ApplicationScoped
+public class ProductionReadinessChecks {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(ProductionReadinessChecks.class);
+
+ /**
+ * A warning sign ⚠ {@code 26A0} with variant selector {@code FE0F}. The
sign is preceded by a
+ * null character {@code 0000} to ensure that the warning sign is displayed
correctly regardless
+ * of the log pattern (some log patterns seem to interfere with non-ASCII
characters).
+ */
+ private static final String WARNING_SIGN_UTF_8 = "\u0000\u26A0\uFE0F";
+
+ /** A simple warning sign displayed when the character set is not UTF-8. */
+ private static final String WARNING_SIGN_PLAIN = "!!!";
+
+ public void warnOnFailedChecks(
+ @Observes Startup event, Instance<ProductionReadinessCheck> checks) {
+ List<Error> errors = checks.stream().flatMap(check ->
check.getErrors().stream()).toList();
+ if (!errors.isEmpty()) {
+ String warning =
+ Charset.defaultCharset().equals(StandardCharsets.UTF_8)
+ ? WARNING_SIGN_UTF_8
+ : WARNING_SIGN_PLAIN;
+ LOGGER.warn("{} Production readiness checks failed! Check the warnings
below.", warning);
+ errors.forEach(
+ error ->
+ LOGGER.warn(
+ "- {} Offending configuration option: '{}'.",
+ error.message(),
+ error.offendingProperty()));
+ LOGGER.warn(
+ "Refer to
https://polaris.apache.org/in-dev/unreleased/configuring-polaris-for-production
for more information.");
+ }
+ }
+
+ @Produces
+ public ProductionReadinessCheck checkAuthenticator(
+ Authenticator<String, AuthenticatedPolarisPrincipal> authenticator) {
+ if (authenticator instanceof TestInlineBearerTokenPolarisAuthenticator) {
+ return ProductionReadinessCheck.of(
+ Error.of(
+ "The current authenticator is intended for tests only.",
+ "polaris.authentication.authenticator.type"));
+ }
+
+ return ProductionReadinessCheck.OK;
+ }
+
+ @Produces
+ public ProductionReadinessCheck
checkTokenService(IcebergRestOAuth2ApiService service) {
+ if (service instanceof TestOAuth2ApiService) {
+ return ProductionReadinessCheck.of(
+ Error.of(
+ "The current token service is intended for tests only.",
+ "polaris.authentication.token-service.type"));
+ }
+ return ProductionReadinessCheck.OK;
+ }
+
+ @Produces
+ public ProductionReadinessCheck checkTokenBroker(
+ AuthenticationConfiguration configuration, TokenBrokerFactory factory) {
+ if (factory instanceof JWTRSAKeyPairFactory) {
+ if (configuration
+ .tokenBroker()
+ .rsaKeyPair()
+ .map(RSAKeyPairConfiguration::publicKeyFile)
+ .isEmpty()) {
+ return ProductionReadinessCheck.of(
+ Error.of(
+ "A public key file wasn't provided and will be generated.",
+
"polaris.authentication.token-broker.rsa-key-pair.public-key-file"));
+ }
+ if (configuration
+ .tokenBroker()
+ .rsaKeyPair()
+ .map(RSAKeyPairConfiguration::privateKeyFile)
+ .isEmpty()) {
+ return ProductionReadinessCheck.of(
+ Error.of(
+ "A private key file wasn't provided and will be generated.",
+
"polaris.authentication.token-broker.rsa-key-pair.private-key-file"));
+ }
+ }
+ if (factory instanceof JWTSymmetricKeyFactory) {
+ if (configuration
+ .tokenBroker()
+ .symmetricKey()
+ .map(SymmetricKeyConfiguration::secret)
+ .isPresent()) {
+ return ProductionReadinessCheck.of(
+ Error.of(
+ "A symmetric key secret was provided through configuration
rather than through a secret file.",
+ "polaris.authentication.token-broker.symmetric-key.secret"));
+ }
+ }
+ return ProductionReadinessCheck.OK;
+ }
+
+ @Produces
+ public ProductionReadinessCheck checkMetastore(MetaStoreManagerFactory
factory) {
+ if (factory instanceof InMemoryPolarisMetaStoreManagerFactory) {
+ return ProductionReadinessCheck.of(
+ Error.of(
+ "The current metastore is intended for tests only.",
"polaris.persistence.type"));
+ }
+ return ProductionReadinessCheck.OK;
+ }
+
+ @Produces
+ public ProductionReadinessCheck checkRealmResolver(Config config,
RealmContextResolver resolver) {
+ if (resolver instanceof TestRealmContextResolver) {
+ return ProductionReadinessCheck.of(
+ Error.of(
+ "The current realm context resolver is intended for tests only.",
+ "polaris.realm-context.type"));
+ }
+ if (resolver instanceof DefaultRealmContextResolver) {
+ ConfigValue configValue =
config.getConfigValue("polaris.realm-context.require-header");
+ boolean userProvided =
+ configValue.getSourceOrdinal() > 250; // ordinal for
application.properties in classpath
+ if ("false".equals(configValue.getValue()) && !userProvided) {
+ return ProductionReadinessCheck.of(
+ Error.of(
+ "The realm context resolver is configured to map requests
without a realm header to the default realm.",
+ "polaris.realm-context.require-header"));
+ }
+ }
+ return ProductionReadinessCheck.OK;
+ }
+}
diff --git
a/service/common/src/main/java/org/apache/polaris/service/auth/JWTRSAKeyPairFactory.java
b/service/common/src/main/java/org/apache/polaris/service/auth/JWTRSAKeyPairFactory.java
index 89e43cab..47dca5df 100644
---
a/service/common/src/main/java/org/apache/polaris/service/auth/JWTRSAKeyPairFactory.java
+++
b/service/common/src/main/java/org/apache/polaris/service/auth/JWTRSAKeyPairFactory.java
@@ -29,15 +29,11 @@ import org.apache.polaris.core.context.RealmContext;
import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
import
org.apache.polaris.service.auth.AuthenticationConfiguration.TokenBrokerConfiguration;
import
org.apache.polaris.service.auth.AuthenticationConfiguration.TokenBrokerConfiguration.RSAKeyPairConfiguration;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
@ApplicationScoped
@Identifier("rsa-key-pair")
public class JWTRSAKeyPairFactory implements TokenBrokerFactory {
- private static final Logger LOGGER =
LoggerFactory.getLogger(JWTRSAKeyPairFactory.class);
-
private final MetaStoreManagerFactory metaStoreManagerFactory;
private final TokenBrokerConfiguration tokenBrokerConfiguration;
private final RSAKeyPairConfiguration keyPairConfiguration;
@@ -63,9 +59,6 @@ public class JWTRSAKeyPairFactory implements
TokenBrokerFactory {
}
private RSAKeyPairConfiguration generateKeyPair() {
- LOGGER.warn(
- "No public and private key files were provided; these will be
generated. "
- + "This should not be done in production!");
try {
Path privateFileLocation = Files.createTempFile("polaris-private",
".pem");
Path publicFileLocation = Files.createTempFile("polaris-public", ".pem");