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");

Reply via email to