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 4c1433d9 Ability to read root credentials from file when bootstrapping
(#940)
4c1433d9 is described below
commit 4c1433d9ce10737d17ab9b78eee6c9a4cfcda614
Author: Alexandre Dutra <[email protected]>
AuthorDate: Tue Feb 11 13:49:14 2025 +0100
Ability to read root credentials from file when bootstrapping (#940)
---
.../EclipseLinkPolarisMetaStoreManagerFactory.java | 6 +-
.../LocalPolarisMetaStoreManagerFactory.java | 18 +--
.../core/persistence/MetaStoreManagerFactory.java | 3 +-
.../persistence/PolarisCredentialsBootstrap.java | 115 ----------------
.../persistence/PrincipalSecretsGenerator.java | 37 ++++-
.../persistence/bootstrap/RootCredentials.java | 50 +++++++
.../persistence/bootstrap/RootCredentialsSet.java | 153 +++++++++++++++++++++
.../PolarisCredentialsBootstrapTest.java | 113 ---------------
.../persistence/PrincipalSecretsGeneratorTest.java | 3 +-
.../bootstrap/RootCredentialsSetTest.java | 137 ++++++++++++++++++
.../persistence/bootstrap/credentials-invalid.json | 3 +
.../persistence/bootstrap/credentials-invalid.yaml | 20 +++
.../core/persistence/bootstrap/credentials.json | 11 ++
.../core/persistence/bootstrap/credentials.yaml | 27 ++++
.../org/apache/polaris/admintool/BaseCommand.java | 1 +
.../apache/polaris/admintool/BootstrapCommand.java | 116 +++++++++++-----
.../polaris/admintool/BootstrapCommandTest.java | 92 ++++++++++++-
.../apache/polaris/admintool/PurgeCommandTest.java | 26 +++-
.../org/apache/polaris/admintool/credentials.json | 10 ++
.../org/apache/polaris/admintool/credentials.yaml | 25 ++++
.../quarkus/catalog/BasePolarisCatalogTest.java | 4 +-
.../test/PolarisIntegrationTestFixture.java | 4 +-
.../InMemoryPolarisMetaStoreManagerFactory.java | 15 +-
23 files changed, 685 insertions(+), 304 deletions(-)
diff --git
a/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/EclipseLinkPolarisMetaStoreManagerFactory.java
b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/EclipseLinkPolarisMetaStoreManagerFactory.java
index 5e27dddc..a68fa783 100644
---
a/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/EclipseLinkPolarisMetaStoreManagerFactory.java
+++
b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/EclipseLinkPolarisMetaStoreManagerFactory.java
@@ -29,9 +29,9 @@ import org.apache.polaris.core.PolarisConfigurationStore;
import org.apache.polaris.core.PolarisDiagnostics;
import org.apache.polaris.core.context.RealmContext;
import org.apache.polaris.core.persistence.LocalPolarisMetaStoreManagerFactory;
-import org.apache.polaris.core.persistence.PolarisCredentialsBootstrap;
import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
import org.apache.polaris.core.persistence.PolarisMetaStoreSession;
+import org.apache.polaris.core.persistence.bootstrap.RootCredentialsSet;
import org.apache.polaris.core.storage.PolarisStorageIntegrationProvider;
/**
@@ -72,7 +72,7 @@ public class EclipseLinkPolarisMetaStoreManagerFactory
protected PolarisMetaStoreSession createMetaStoreSession(
@Nonnull PolarisEclipseLinkStore store,
@Nonnull RealmContext realmContext,
- @Nullable PolarisCredentialsBootstrap credentialsBootstrap,
+ @Nullable RootCredentialsSet rootCredentialsSet,
@Nonnull PolarisDiagnostics diagnostics) {
return new PolarisEclipseLinkMetaStoreSessionImpl(
store,
@@ -80,7 +80,7 @@ public class EclipseLinkPolarisMetaStoreManagerFactory
realmContext,
configurationFile(),
persistenceUnitName(),
- secretsGenerator(realmContext, credentialsBootstrap),
+ secretsGenerator(realmContext, rootCredentialsSet),
diagnostics);
}
diff --git
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java
index 59d326dd..fe809949 100644
---
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java
+++
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java
@@ -34,6 +34,7 @@ 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.entity.PolarisPrincipalSecrets;
+import org.apache.polaris.core.persistence.bootstrap.RootCredentialsSet;
import org.apache.polaris.core.persistence.cache.EntityCache;
import org.apache.polaris.core.storage.cache.StorageCredentialCache;
import org.slf4j.Logger;
@@ -71,26 +72,25 @@ public abstract class
LocalPolarisMetaStoreManagerFactory<StoreType>
protected abstract PolarisMetaStoreSession createMetaStoreSession(
@Nonnull StoreType store,
@Nonnull RealmContext realmContext,
- @Nullable PolarisCredentialsBootstrap credentialsBootstrap,
+ @Nullable RootCredentialsSet rootCredentialsSet,
@Nonnull PolarisDiagnostics diagnostics);
protected PrincipalSecretsGenerator secretsGenerator(
- RealmContext realmContext, @Nullable PolarisCredentialsBootstrap
credentialsBootstrap) {
- if (credentialsBootstrap != null) {
+ RealmContext realmContext, @Nullable RootCredentialsSet
rootCredentialsSet) {
+ if (rootCredentialsSet != null) {
return PrincipalSecretsGenerator.bootstrap(
- realmContext.getRealmIdentifier(), credentialsBootstrap);
+ realmContext.getRealmIdentifier(), rootCredentialsSet);
} else {
return PrincipalSecretsGenerator.RANDOM_SECRETS;
}
}
private void initializeForRealm(
- RealmContext realmContext, PolarisCredentialsBootstrap
credentialsBootstrap) {
+ RealmContext realmContext, RootCredentialsSet rootCredentialsSet) {
final StoreType backingStore = createBackingStore(diagnostics);
sessionSupplierMap.put(
realmContext.getRealmIdentifier(),
- () ->
- createMetaStoreSession(backingStore, realmContext,
credentialsBootstrap, diagnostics));
+ () -> createMetaStoreSession(backingStore, realmContext,
rootCredentialsSet, diagnostics));
PolarisMetaStoreManager metaStoreManager =
new PolarisMetaStoreManagerImpl(realmContext, diagnostics,
configurationStore, clock);
@@ -99,13 +99,13 @@ public abstract class
LocalPolarisMetaStoreManagerFactory<StoreType>
@Override
public synchronized Map<String, PrincipalSecretsResult> bootstrapRealms(
- List<String> realms, PolarisCredentialsBootstrap credentialsBootstrap) {
+ List<String> realms, RootCredentialsSet rootCredentialsSet) {
Map<String, PrincipalSecretsResult> results = new HashMap<>();
for (String realm : realms) {
RealmContext realmContext = () -> realm;
if (!metaStoreManagerMap.containsKey(realmContext.getRealmIdentifier()))
{
- initializeForRealm(realmContext, credentialsBootstrap);
+ initializeForRealm(realmContext, rootCredentialsSet);
PrincipalSecretsResult secretsResult =
bootstrapServiceAndCreatePolarisPrincipalForRealm(
realmContext,
metaStoreManagerMap.get(realmContext.getRealmIdentifier()));
diff --git
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/MetaStoreManagerFactory.java
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/MetaStoreManagerFactory.java
index 4e37f034..13557157 100644
---
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/MetaStoreManagerFactory.java
+++
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/MetaStoreManagerFactory.java
@@ -23,6 +23,7 @@ import java.util.Map;
import java.util.function.Supplier;
import
org.apache.polaris.core.auth.PolarisSecretsManager.PrincipalSecretsResult;
import org.apache.polaris.core.context.RealmContext;
+import org.apache.polaris.core.persistence.bootstrap.RootCredentialsSet;
import org.apache.polaris.core.persistence.cache.EntityCache;
import org.apache.polaris.core.storage.cache.StorageCredentialCache;
@@ -38,7 +39,7 @@ public interface MetaStoreManagerFactory {
EntityCache getOrCreateEntityCache(RealmContext realmContext);
Map<String, PrincipalSecretsResult> bootstrapRealms(
- List<String> realms, PolarisCredentialsBootstrap credentialsBootstrap);
+ List<String> realms, RootCredentialsSet rootCredentialsSet);
/** Purge all metadata for the realms provided */
void purgeRealms(List<String> realms);
diff --git
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisCredentialsBootstrap.java
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisCredentialsBootstrap.java
deleted file mode 100644
index f12f58db..00000000
---
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisCredentialsBootstrap.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * 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.persistence;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Splitter;
-import jakarta.annotation.Nullable;
-import java.util.AbstractMap.SimpleEntry;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import org.apache.polaris.core.entity.PolarisEntityConstants;
-import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
-
-/**
- * A utility to parse and provide credentials for Polaris realms and
principals during a bootstrap
- * phase.
- */
-public class PolarisCredentialsBootstrap {
-
- public static final PolarisCredentialsBootstrap EMPTY =
- new PolarisCredentialsBootstrap(new HashMap<>());
-
- /**
- * Parse credentials from the system property {@code
polaris.bootstrap.credentials} or the
- * environment variable {@code POLARIS_BOOTSTRAP_CREDENTIALS}, whichever is
set.
- *
- * <p>See {@link #fromString(String)} for the expected format.
- */
- public static PolarisCredentialsBootstrap fromEnvironment() {
- return fromString(
- System.getProperty(
- "polaris.bootstrap.credentials",
System.getenv().get("POLARIS_BOOTSTRAP_CREDENTIALS")));
- }
-
- /**
- * Parse a string of credentials in the format:
- *
- * <pre>
- * realm1,client1,secret1;realm2,client2,secret2;...
- * </pre>
- */
- public static PolarisCredentialsBootstrap fromString(@Nullable String
credentialsString) {
- return credentialsString != null && !credentialsString.isBlank()
- ?
fromList(Splitter.on(';').trimResults().splitToList(credentialsString))
- : EMPTY;
- }
-
- /**
- * Parse a list of credentials; each element should be in the format: {@code
- * realm,clientId,clientSecret}.
- */
- public static PolarisCredentialsBootstrap fromList(List<String>
credentialsList) {
- Map<String, Map.Entry<String, String>> credentials = new HashMap<>();
- for (String triplet : credentialsList) {
- if (!triplet.isBlank()) {
- List<String> parts =
Splitter.on(',').trimResults().splitToList(triplet);
- if (parts.size() != 3) {
- throw new IllegalArgumentException("Invalid credentials format: " +
triplet);
- }
- String realmName = parts.get(0);
- String clientId = parts.get(1);
- String clientSecret = parts.get(2);
-
- if (credentials.containsKey(realmName)) {
- throw new IllegalArgumentException("Duplicate realm: " + realmName);
- }
- credentials.put(realmName, new SimpleEntry<>(clientId, clientSecret));
- }
- }
- return credentials.isEmpty() ? EMPTY : new
PolarisCredentialsBootstrap(credentials);
- }
-
- @VisibleForTesting final Map<String, Map.Entry<String, String>> credentials;
-
- private PolarisCredentialsBootstrap(Map<String, Map.Entry<String, String>>
credentials) {
- this.credentials = credentials;
- }
-
- /**
- * Get the secrets for the specified principal in the specified realm, if
available among the
- * credentials that were supplied for bootstrap. Only credentials for the
root principal are
- * supported.
- */
- public Optional<PolarisPrincipalSecrets> getSecrets(
- String realmName, long principalId, String principalName) {
- if (principalName.equals(PolarisEntityConstants.getRootPrincipalName())) {
- return Optional.ofNullable(credentials.get(realmName))
- .map(
- credentials -> {
- String clientId = credentials.getKey();
- String secret = credentials.getValue();
- return new PolarisPrincipalSecrets(principalId, clientId,
secret, secret);
- });
- }
- return Optional.empty();
- }
-}
diff --git
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PrincipalSecretsGenerator.java
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/PrincipalSecretsGenerator.java
index 270d8569..1befc8b1 100644
---
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PrincipalSecretsGenerator.java
+++
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/PrincipalSecretsGenerator.java
@@ -21,7 +21,9 @@ package org.apache.polaris.core.persistence;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import java.util.Optional;
+import org.apache.polaris.core.entity.PolarisEntityConstants;
import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
+import org.apache.polaris.core.persistence.bootstrap.RootCredentialsSet;
/**
* An interface for generating principal secrets. It enables detaching the
secret generation logic
@@ -31,8 +33,8 @@ import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
* ID and secret overrides via system properties or environment variables,
which can be useful for
* bootstrapping new realms.
*
- * <p>See {@link PolarisCredentialsBootstrap} for more information on the
expected environment
- * variable name, and the format of the bootstrap credentials.
+ * <p>See {@link RootCredentialsSet} for more information on the expected
environment variable name,
+ * and the format of the bootstrap credentials.
*/
@FunctionalInterface
public interface PrincipalSecretsGenerator {
@@ -56,14 +58,37 @@ public interface PrincipalSecretsGenerator {
PolarisPrincipalSecrets produceSecrets(@Nonnull String principalName, long
principalId);
static PrincipalSecretsGenerator bootstrap(String realmName) {
- return bootstrap(realmName, PolarisCredentialsBootstrap.fromEnvironment());
+ return bootstrap(realmName, RootCredentialsSet.fromEnvironment());
}
static PrincipalSecretsGenerator bootstrap(
- String realmName, @Nullable PolarisCredentialsBootstrap
credentialsSupplier) {
+ String realmName, @Nullable RootCredentialsSet rootCredentialsSet) {
return (principalName, principalId) ->
- Optional.ofNullable(credentialsSupplier)
- .flatMap(credentials -> credentials.getSecrets(realmName,
principalId, principalName))
+ Optional.ofNullable(rootCredentialsSet)
+ .flatMap(
+ credentialsSet -> getSecrets(realmName, principalName,
principalId, credentialsSet))
.orElseGet(() -> RANDOM_SECRETS.produceSecrets(principalName,
principalId));
}
+
+ /**
+ * Get the secrets for the specified principal in the specified realm, if
available among the
+ * credentials that were supplied for bootstrap. Only credentials for the
root principal are
+ * supported.
+ */
+ private static Optional<PolarisPrincipalSecrets> getSecrets(
+ String realmName,
+ String principalName,
+ long principalId,
+ RootCredentialsSet rootCredentialsSet) {
+ if (principalName.equals(PolarisEntityConstants.getRootPrincipalName())) {
+ return
Optional.ofNullable(rootCredentialsSet.credentials().get(realmName))
+ .map(
+ credentials -> {
+ String clientId = credentials.clientId();
+ String secret = credentials.clientSecret();
+ return new PolarisPrincipalSecrets(principalId, clientId,
secret, secret);
+ });
+ }
+ return Optional.empty();
+ }
}
diff --git
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/bootstrap/RootCredentials.java
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/bootstrap/RootCredentials.java
new file mode 100644
index 00000000..26f14357
--- /dev/null
+++
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/bootstrap/RootCredentials.java
@@ -0,0 +1,50 @@
+/*
+ * 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.persistence.bootstrap;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import org.apache.polaris.immutables.PolarisImmutable;
+import org.immutables.value.Value;
+
+@PolarisImmutable
+@JsonSerialize(as = ImmutableRootCredentials.class)
+@JsonDeserialize(as = ImmutableRootCredentials.class)
+public interface RootCredentials {
+
+ @Value.Parameter(order = 1)
+ @JsonProperty("client-id")
+ String clientId();
+
+ @Value.Parameter(order = 2)
+ @Value.Redacted
+ @JsonProperty("client-secret")
+ String clientSecret();
+
+ @Value.Check
+ default void check() {
+ if (clientId().isEmpty()) {
+ throw new IllegalArgumentException("clientId cannot be empty");
+ }
+ if (clientSecret().isEmpty()) {
+ throw new IllegalArgumentException("clientSecret cannot be empty");
+ }
+ }
+}
diff --git
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/bootstrap/RootCredentialsSet.java
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/bootstrap/RootCredentialsSet.java
new file mode 100644
index 00000000..0300612c
--- /dev/null
+++
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/bootstrap/RootCredentialsSet.java
@@ -0,0 +1,153 @@
+/*
+ * 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.persistence.bootstrap;
+
+import static
com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.google.common.base.Splitter;
+import jakarta.annotation.Nullable;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.polaris.immutables.PolarisImmutable;
+import org.immutables.value.Value;
+
+/**
+ * A utility to parse and provide credentials for Polaris realms and
principals during a bootstrap
+ * phase.
+ */
+@PolarisImmutable
+@JsonSerialize(as = ImmutableRootCredentialsSet.class)
+@JsonDeserialize(as = ImmutableRootCredentialsSet.class)
[email protected](jdkOnly = true)
+public interface RootCredentialsSet {
+
+ RootCredentialsSet EMPTY = ImmutableRootCredentialsSet.builder().build();
+
+ String SYSTEM_PROPERTY = "polaris.bootstrap.credentials";
+ String ENVIRONMENT_VARIABLE = "POLARIS_BOOTSTRAP_CREDENTIALS";
+
+ /**
+ * Parse credentials from the system property {@value #SYSTEM_PROPERTY} or
the environment
+ * variable {@value #ENVIRONMENT_VARIABLE}, whichever is set.
+ *
+ * <p>See {@link #fromString(String)} for the expected format.
+ */
+ static RootCredentialsSet fromEnvironment() {
+ return fromString(
+ System.getProperty(SYSTEM_PROPERTY,
System.getenv().get(ENVIRONMENT_VARIABLE)));
+ }
+
+ /**
+ * Parse a string of credentials in the format:
+ *
+ * <pre>
+ * realm1,client1,secret1;realm2,client2,secret2;...
+ * </pre>
+ */
+ static RootCredentialsSet fromString(@Nullable String credentialsString) {
+ return credentialsString != null && !credentialsString.isBlank()
+ ?
fromList(Splitter.on(';').trimResults().splitToList(credentialsString))
+ : EMPTY;
+ }
+
+ /**
+ * Parse a list of credentials; each element should be in the format: {@code
+ * realm,clientId,clientSecret}.
+ */
+ static RootCredentialsSet fromList(List<String> credentialsList) {
+ Map<String, RootCredentials> credentials = new HashMap<>();
+ for (String triplet : credentialsList) {
+ if (!triplet.isBlank()) {
+ List<String> parts =
Splitter.on(',').trimResults().splitToList(triplet);
+ if (parts.size() != 3) {
+ throw new IllegalArgumentException("Invalid credentials format: " +
triplet);
+ }
+ String realm = parts.get(0);
+ RootCredentials creds = ImmutableRootCredentials.of(parts.get(1),
parts.get(2));
+ if (credentials.containsKey(realm)) {
+ throw new IllegalArgumentException("Duplicate realm: " + realm);
+ }
+ credentials.put(realm, creds);
+ }
+ }
+ return credentials.isEmpty() ? EMPTY :
ImmutableRootCredentialsSet.of(credentials);
+ }
+
+ /**
+ * Parse credentials set from any URL containing a valid YAML or JSON
credentials file.
+ *
+ * <p>The expected YAML format is:
+ *
+ * <pre>
+ * realm1:
+ * client-id: client1
+ * client-secret: secret1
+ * realm2:
+ * client-id: client2
+ * client-secret: secret2
+ * # etc.
+ * </pre>
+ *
+ * Multiple YAMl documents are also supported; all documents will be merged
into a single set of
+ * credentials.
+ *
+ * <p>The expected JSON format is:
+ *
+ * <pre>
+ * {
+ * "realm1": {
+ * "client-id": "client1",
+ * "client-secret": "secret1"
+ * },
+ * "realm2": {
+ * "client-id": "client2",
+ * "client-secret": "secret2"
+ * }
+ * }
+ * </pre>
+ */
+ static RootCredentialsSet fromUrl(URL url) {
+ YAMLFactory factory = new YAMLFactory();
+ ObjectMapper mapper = new
ObjectMapper(factory).configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
+ try (var parser = factory.createParser(url)) {
+ var values = mapper.readValues(parser, RootCredentialsSet.class);
+ var builder = ImmutableRootCredentialsSet.builder();
+ while (values.hasNext()) {
+ builder.putAllCredentials(values.next().credentials());
+ }
+ return builder.build();
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Failed to read credentials file: " +
url, e);
+ }
+ }
+
+ /** Get all the credentials contained in this set, keyed by realm
identifier. */
+ @JsonAnyGetter
+ @JsonAnySetter
+ @Value.Parameter(order = 0)
+ Map<String, RootCredentials> credentials();
+}
diff --git
a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisCredentialsBootstrapTest.java
b/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisCredentialsBootstrapTest.java
deleted file mode 100644
index bd4ecec6..00000000
---
a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisCredentialsBootstrapTest.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * 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.persistence;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-import java.util.List;
-import org.junit.jupiter.api.Test;
-
-class PolarisCredentialsBootstrapTest {
-
- @Test
- void nullString() {
- PolarisCredentialsBootstrap credentials =
PolarisCredentialsBootstrap.fromString(null);
- assertThat(credentials.credentials).isEmpty();
- }
-
- @Test
- void emptyString() {
- PolarisCredentialsBootstrap credentials =
PolarisCredentialsBootstrap.fromString("");
- assertThat(credentials.credentials).isEmpty();
- }
-
- @Test
- void blankString() {
- PolarisCredentialsBootstrap credentials =
PolarisCredentialsBootstrap.fromString(" ");
- assertThat(credentials.credentials).isEmpty();
- }
-
- @Test
- void invalidString() {
- assertThatThrownBy(() -> PolarisCredentialsBootstrap.fromString("test"))
- .hasMessage("Invalid credentials format: test");
- }
-
- @Test
- void duplicateRealm() {
- assertThatThrownBy(
- () ->
- PolarisCredentialsBootstrap.fromString(
- "realm1,client1a,secret1a;realm1,client1b,secret1b"))
- .hasMessage("Duplicate realm: realm1");
- }
-
- @Test
- void getSecretsValidString() {
- PolarisCredentialsBootstrap credentials =
- PolarisCredentialsBootstrap.fromString(
- " ; realm1 , client1 , secret1 ; realm2 , client2 , secret2 ; ");
- assertCredentials(credentials);
- }
-
- @Test
- void getSecretsValidList() {
- PolarisCredentialsBootstrap credentials =
- PolarisCredentialsBootstrap.fromList(
- List.of("realm1,client1,secret1", "realm2,client2,secret2"));
- assertCredentials(credentials);
- }
-
- @Test
- void getSecretsValidSystemProperty() {
- PolarisCredentialsBootstrap credentials =
PolarisCredentialsBootstrap.fromEnvironment();
- assertThat(credentials.credentials).isEmpty();
- try {
- System.setProperty(
- "polaris.bootstrap.credentials",
"realm1,client1,secret1;realm2,client2,secret2");
- credentials = PolarisCredentialsBootstrap.fromEnvironment();
- assertCredentials(credentials);
- } finally {
- System.clearProperty("polaris.bootstrap.credentials");
- }
- }
-
- private void assertCredentials(PolarisCredentialsBootstrap credentials) {
- assertThat(credentials.getSecrets("realm3", 123, "root")).isEmpty();
- assertThat(credentials.getSecrets("nonexistent", 123, "root")).isEmpty();
- assertThat(credentials.getSecrets("realm1", 123, "non-root")).isEmpty();
- assertThat(credentials.getSecrets("realm1", 123, "root"))
- .hasValueSatisfying(
- secrets -> {
- assertThat(secrets.getPrincipalId()).isEqualTo(123);
- assertThat(secrets.getPrincipalClientId()).isEqualTo("client1");
- assertThat(secrets.getMainSecret()).isEqualTo("secret1");
- assertThat(secrets.getSecondarySecret()).isEqualTo("secret1");
- });
- assertThat(credentials.getSecrets("realm2", 123, "root"))
- .hasValueSatisfying(
- secrets -> {
- assertThat(secrets.getPrincipalId()).isEqualTo(123);
- assertThat(secrets.getPrincipalClientId()).isEqualTo("client2");
- assertThat(secrets.getMainSecret()).isEqualTo("secret2");
- assertThat(secrets.getSecondarySecret()).isEqualTo("secret2");
- });
- }
-}
diff --git
a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PrincipalSecretsGeneratorTest.java
b/polaris-core/src/test/java/org/apache/polaris/core/persistence/PrincipalSecretsGeneratorTest.java
index d220d7ef..6ffb2a41 100644
---
a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PrincipalSecretsGeneratorTest.java
+++
b/polaris-core/src/test/java/org/apache/polaris/core/persistence/PrincipalSecretsGeneratorTest.java
@@ -22,6 +22,7 @@ import static
org.apache.polaris.core.persistence.PrincipalSecretsGenerator.boot
import static org.assertj.core.api.Assertions.assertThat;
import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
+import org.apache.polaris.core.persistence.bootstrap.RootCredentialsSet;
import org.junit.jupiter.api.Test;
class PrincipalSecretsGeneratorTest {
@@ -39,7 +40,7 @@ class PrincipalSecretsGeneratorTest {
@Test
void testSecretOverride() {
PrincipalSecretsGenerator gen =
- bootstrap("test-Realm",
PolarisCredentialsBootstrap.fromString("test-Realm,client1,sec2"));
+ bootstrap("test-Realm",
RootCredentialsSet.fromString("test-Realm,client1,sec2"));
PolarisPrincipalSecrets s = gen.produceSecrets("root", 123);
assertThat(s).isNotNull();
assertThat(s.getPrincipalId()).isEqualTo(123);
diff --git
a/polaris-core/src/test/java/org/apache/polaris/core/persistence/bootstrap/RootCredentialsSetTest.java
b/polaris-core/src/test/java/org/apache/polaris/core/persistence/bootstrap/RootCredentialsSetTest.java
new file mode 100644
index 00000000..c6c5ecd7
--- /dev/null
+++
b/polaris-core/src/test/java/org/apache/polaris/core/persistence/bootstrap/RootCredentialsSetTest.java
@@ -0,0 +1,137 @@
+/*
+ * 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.persistence.bootstrap;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.net.URL;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class RootCredentialsSetTest {
+
+ @Test
+ void nullString() {
+ RootCredentialsSet credentials = RootCredentialsSet.fromString(null);
+ assertThat(credentials.credentials()).isEmpty();
+ }
+
+ @Test
+ void emptyString() {
+ RootCredentialsSet credentials = RootCredentialsSet.fromString("");
+ assertThat(credentials.credentials()).isEmpty();
+ }
+
+ @Test
+ void blankString() {
+ RootCredentialsSet credentials = RootCredentialsSet.fromString(" ");
+ assertThat(credentials.credentials()).isEmpty();
+ }
+
+ @Test
+ void invalidString() {
+ assertThatThrownBy(() -> RootCredentialsSet.fromString("test"))
+ .hasMessage("Invalid credentials format: test");
+ }
+
+ @Test
+ void duplicateRealm() {
+ assertThatThrownBy(
+ () ->
+
RootCredentialsSet.fromString("realm1,client1a,secret1a;realm1,client1b,secret1b"))
+ .hasMessage("Duplicate realm: realm1");
+ }
+
+ @Test
+ void getSecretsValidString() {
+ RootCredentialsSet credentials =
+ RootCredentialsSet.fromString(
+ " ; realm1 , client1 , secret1 ; realm2 , client2 , secret2 ; ");
+ assertCredentials(credentials);
+ }
+
+ @Test
+ void getSecretsValidList() {
+ RootCredentialsSet credentials =
+ RootCredentialsSet.fromList(List.of("realm1,client1,secret1",
"realm2,client2,secret2"));
+ assertCredentials(credentials);
+ }
+
+ @Test
+ void getSecretsValidSystemProperty() {
+ RootCredentialsSet credentials = RootCredentialsSet.fromEnvironment();
+ assertThat(credentials.credentials()).isEmpty();
+ try {
+ System.setProperty(
+ RootCredentialsSet.SYSTEM_PROPERTY,
"realm1,client1,secret1;realm2,client2,secret2");
+ credentials = RootCredentialsSet.fromEnvironment();
+ assertCredentials(credentials);
+ } finally {
+ System.clearProperty(RootCredentialsSet.SYSTEM_PROPERTY);
+ }
+ }
+
+ @Test
+ void getSecretsValidJson() {
+ URL resource = getClass().getResource("credentials.json");
+ RootCredentialsSet set = RootCredentialsSet.fromUrl(resource);
+ assertCredentials(set);
+ }
+
+ @Test
+ void getSecretsValidYaml() {
+ URL resource = getClass().getResource("credentials.yaml");
+ RootCredentialsSet set = RootCredentialsSet.fromUrl(resource);
+ assertCredentials(set);
+ }
+
+ @Test
+ void getSecretsInvalidJson() {
+ URL resource = getClass().getResource("credentials-invalid.json");
+ assertThatThrownBy(() -> RootCredentialsSet.fromUrl(resource))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Failed to read credentials file")
+ .rootCause()
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining(
+ "Cannot build RootCredentials, some of required attributes are not
set [clientId, clientSecret]");
+ }
+
+ @Test
+ void getSecretsInvalidYaml() {
+ URL resource = getClass().getResource("credentials-invalid.yaml");
+ assertThatThrownBy(() -> RootCredentialsSet.fromUrl(resource))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Failed to read credentials file")
+ .rootCause()
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining(
+ "Cannot build RootCredentials, some of required attributes are not
set [clientId, clientSecret]");
+ }
+
+ private static void assertCredentials(RootCredentialsSet set) {
+ assertThat(set.credentials()).hasSize(2);
+ assertThat(set.credentials().keySet()).containsExactlyInAnyOrder("realm1",
"realm2");
+ assertThat(set.credentials().get("realm1"))
+ .isEqualTo(ImmutableRootCredentials.of("client1", "secret1"));
+ assertThat(set.credentials().get("realm2"))
+ .isEqualTo(ImmutableRootCredentials.of("client2", "secret2"));
+ }
+}
diff --git
a/polaris-core/src/test/resources/org/apache/polaris/core/persistence/bootstrap/credentials-invalid.json
b/polaris-core/src/test/resources/org/apache/polaris/core/persistence/bootstrap/credentials-invalid.json
new file mode 100644
index 00000000..5bab6c70
--- /dev/null
+++
b/polaris-core/src/test/resources/org/apache/polaris/core/persistence/bootstrap/credentials-invalid.json
@@ -0,0 +1,3 @@
+{
+ "realm1": {}
+}
\ No newline at end of file
diff --git
a/polaris-core/src/test/resources/org/apache/polaris/core/persistence/bootstrap/credentials-invalid.yaml
b/polaris-core/src/test/resources/org/apache/polaris/core/persistence/bootstrap/credentials-invalid.yaml
new file mode 100644
index 00000000..0e943b66
--- /dev/null
+++
b/polaris-core/src/test/resources/org/apache/polaris/core/persistence/bootstrap/credentials-invalid.yaml
@@ -0,0 +1,20 @@
+#
+# 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.
+#
+
+realm1: {}
diff --git
a/polaris-core/src/test/resources/org/apache/polaris/core/persistence/bootstrap/credentials.json
b/polaris-core/src/test/resources/org/apache/polaris/core/persistence/bootstrap/credentials.json
new file mode 100644
index 00000000..46646140
--- /dev/null
+++
b/polaris-core/src/test/resources/org/apache/polaris/core/persistence/bootstrap/credentials.json
@@ -0,0 +1,11 @@
+{
+ "realm1": {
+ "client-id": "client1",
+ "client-secret": "secret1"
+ },
+ "realm2": {
+ "client-id": "client2",
+ "client-secret": "secret2",
+ "extra-field": "extra-value"
+ }
+}
\ No newline at end of file
diff --git
a/polaris-core/src/test/resources/org/apache/polaris/core/persistence/bootstrap/credentials.yaml
b/polaris-core/src/test/resources/org/apache/polaris/core/persistence/bootstrap/credentials.yaml
new file mode 100644
index 00000000..7978fc5d
--- /dev/null
+++
b/polaris-core/src/test/resources/org/apache/polaris/core/persistence/bootstrap/credentials.yaml
@@ -0,0 +1,27 @@
+#
+# 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.
+#
+
+realm1:
+ client-id: client1
+ client-secret: secret1
+---
+realm2:
+ client-id: client2
+ client-secret: secret2
+ extra-field: extra-value
diff --git
a/quarkus/admin/src/main/java/org/apache/polaris/admintool/BaseCommand.java
b/quarkus/admin/src/main/java/org/apache/polaris/admintool/BaseCommand.java
index 173c990e..c56fe971 100644
--- a/quarkus/admin/src/main/java/org/apache/polaris/admintool/BaseCommand.java
+++ b/quarkus/admin/src/main/java/org/apache/polaris/admintool/BaseCommand.java
@@ -26,6 +26,7 @@ import picocli.CommandLine.Spec;
public abstract class BaseCommand implements Callable<Integer> {
+ public static final int EXIT_CODE_USAGE = 2;
public static final int EXIT_CODE_BOOTSTRAP_ERROR = 3;
public static final int EXIT_CODE_PURGE_ERROR = 4;
diff --git
a/quarkus/admin/src/main/java/org/apache/polaris/admintool/BootstrapCommand.java
b/quarkus/admin/src/main/java/org/apache/polaris/admintool/BootstrapCommand.java
index e1b61e5a..e77ee9b5 100644
---
a/quarkus/admin/src/main/java/org/apache/polaris/admintool/BootstrapCommand.java
+++
b/quarkus/admin/src/main/java/org/apache/polaris/admintool/BootstrapCommand.java
@@ -18,10 +18,11 @@
*/
package org.apache.polaris.admintool;
+import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import
org.apache.polaris.core.auth.PolarisSecretsManager.PrincipalSecretsResult;
-import org.apache.polaris.core.persistence.PolarisCredentialsBootstrap;
+import org.apache.polaris.core.persistence.bootstrap.RootCredentialsSet;
import picocli.CommandLine;
@CommandLine.Command(
@@ -30,49 +31,92 @@ import picocli.CommandLine;
description = "Bootstraps realms and root principal credentials.")
public class BootstrapCommand extends BaseCommand {
- @CommandLine.Option(
- names = {"-r", "--realm"},
- paramLabel = "<realm>",
- required = true,
- description = "The name of a realm to bootstrap.")
- List<String> realms;
+ @CommandLine.ArgGroup(multiplicity = "1")
+ InputOptions inputOptions;
- @CommandLine.Option(
- names = {"-c", "--credential"},
- paramLabel = "<realm,clientId,clientSecret>",
- description =
- "Root principal credentials to bootstrap. Must be of the form
'realm,clientId,clientSecret'.")
- List<String> credentials;
+ static class InputOptions {
+
+ @CommandLine.ArgGroup(multiplicity = "1", exclusive = false)
+ StandardInputOptions stdinOptions;
+
+ @CommandLine.ArgGroup(multiplicity = "1")
+ FileInputOptions fileOptions;
+
+ static class StandardInputOptions {
+
+ @CommandLine.Option(
+ names = {"-r", "--realm"},
+ paramLabel = "<realm>",
+ required = true,
+ description = "The name of a realm to bootstrap.")
+ List<String> realms;
+
+ @CommandLine.Option(
+ names = {"-c", "--credential"},
+ paramLabel = "<realm,clientId,clientSecret>",
+ description =
+ "Root principal credentials to bootstrap. Must be of the form
'realm,clientId,clientSecret'.")
+ List<String> credentials;
+ }
+
+ static class FileInputOptions {
+ @CommandLine.Option(
+ names = {"-f", "--credentials-file"},
+ paramLabel = "<file>",
+ description = "A file containing root principal credentials to
bootstrap.")
+ Path file;
+ }
+ }
@Override
public Integer call() {
- PolarisCredentialsBootstrap credentialsBootstrap =
- credentials == null || credentials.isEmpty()
- ? PolarisCredentialsBootstrap.EMPTY
- : PolarisCredentialsBootstrap.fromList(credentials);
+ try {
+ RootCredentialsSet rootCredentialsSet;
+ List<String> realms; // TODO Iterable
+
+ if (inputOptions.fileOptions != null) {
+ rootCredentialsSet =
+
RootCredentialsSet.fromUrl(inputOptions.fileOptions.file.toUri().toURL());
+ realms = rootCredentialsSet.credentials().keySet().stream().toList();
+ } else {
+ realms = inputOptions.stdinOptions.realms;
+ rootCredentialsSet =
+ inputOptions.stdinOptions.credentials == null
+ || inputOptions.stdinOptions.credentials.isEmpty()
+ ? RootCredentialsSet.EMPTY
+ :
RootCredentialsSet.fromList(inputOptions.stdinOptions.credentials);
+ }
- // Execute the bootstrap
- Map<String, PrincipalSecretsResult> results =
- metaStoreManagerFactory.bootstrapRealms(realms, credentialsBootstrap);
+ // Execute the bootstrap
+ Map<String, PrincipalSecretsResult> results =
+ metaStoreManagerFactory.bootstrapRealms(realms, rootCredentialsSet);
- // Log any errors:
- boolean success = true;
- for (Map.Entry<String, PrincipalSecretsResult> result :
results.entrySet()) {
- if (!result.getValue().isSuccess()) {
- String realm = result.getKey();
- spec.commandLine()
- .getErr()
- .printf(
- "Bootstrapping '%s' failed: %s%n",
- realm, result.getValue().getReturnStatus().toString());
- success = false;
+ // Log any errors:
+ boolean success = true;
+ for (Map.Entry<String, PrincipalSecretsResult> result :
results.entrySet()) {
+ if (result.getValue().isSuccess()) {
+ String realm = result.getKey();
+ spec.commandLine().getOut().printf("Realm '%s' successfully
bootstrapped.%n", realm);
+ } else {
+ String realm = result.getKey();
+ spec.commandLine()
+ .getErr()
+ .printf(
+ "Bootstrapping '%s' failed: %s%n",
+ realm, result.getValue().getReturnStatus().toString());
+ success = false;
+ }
}
- }
- if (success) {
- spec.commandLine().getOut().println("Bootstrap completed successfully.");
- return 0;
- } else {
+ if (success) {
+ spec.commandLine().getOut().println("Bootstrap completed
successfully.");
+ return 0;
+ } else {
+ spec.commandLine().getErr().println("Bootstrap encountered errors
during operation.");
+ return EXIT_CODE_BOOTSTRAP_ERROR;
+ }
+ } catch (Exception e) {
+ e.printStackTrace(spec.commandLine().getErr());
spec.commandLine().getErr().println("Bootstrap encountered errors during
operation.");
return EXIT_CODE_BOOTSTRAP_ERROR;
}
diff --git
a/quarkus/admin/src/test/java/org/apache/polaris/admintool/BootstrapCommandTest.java
b/quarkus/admin/src/test/java/org/apache/polaris/admintool/BootstrapCommandTest.java
index a3a3f5d3..b00bef2d 100644
---
a/quarkus/admin/src/test/java/org/apache/polaris/admintool/BootstrapCommandTest.java
+++
b/quarkus/admin/src/test/java/org/apache/polaris/admintool/BootstrapCommandTest.java
@@ -18,6 +18,8 @@
*/
package org.apache.polaris.admintool;
+import static
org.apache.polaris.admintool.BaseCommand.EXIT_CODE_BOOTSTRAP_ERROR;
+import static org.apache.polaris.admintool.BaseCommand.EXIT_CODE_USAGE;
import static
org.apache.polaris.admintool.PostgresTestResourceLifecycleManager.INIT_SCRIPT;
import static org.assertj.core.api.Assertions.assertThat;
@@ -26,8 +28,17 @@ import io.quarkus.test.common.TestResourceScope;
import io.quarkus.test.common.WithTestResource;
import io.quarkus.test.junit.main.Launch;
import io.quarkus.test.junit.main.LaunchResult;
+import io.quarkus.test.junit.main.QuarkusMainLauncher;
import io.quarkus.test.junit.main.QuarkusMainTest;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Objects;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
@QuarkusMainTest
@WithTestResource(
@@ -36,6 +47,15 @@ import org.junit.jupiter.api.Test;
initArgs = @ResourceArg(name = INIT_SCRIPT, value =
"org/apache/polaris/admintool/init.sql"))
class BootstrapCommandTest {
+ private static Path json;
+ private static Path yaml;
+
+ @BeforeAll
+ static void prepareFiles(@TempDir Path temp) throws IOException {
+ json = copyResource(temp, "credentials.json");
+ yaml = copyResource(temp, "credentials.yaml");
+ }
+
@Test
@Launch(
value = {
@@ -49,7 +69,75 @@ class BootstrapCommandTest {
"-c",
"realm2,root,s3cr3t"
})
- public void testBootstrap(LaunchResult result) {
- assertThat(result.getOutput()).contains("Bootstrap completed
successfully.");
+ public void testBootstrapFromCommandLineArguments(LaunchResult result) {
+ assertThat(result.getOutput())
+ .contains("Realm 'realm1' successfully bootstrapped.")
+ .contains("Realm 'realm2' successfully bootstrapped.")
+ .contains("Bootstrap completed successfully.");
+ }
+
+ @Test
+ @Launch(
+ value = {
+ "bootstrap",
+ "-r",
+ "realm1",
+ "-c",
+ "invalid syntax",
+ },
+ exitCode = EXIT_CODE_BOOTSTRAP_ERROR)
+ public void testBootstrapInvalidCredentials(LaunchResult result) {
+ assertThat(result.getErrorOutput())
+ .contains("Invalid credentials format: invalid syntax")
+ .contains("Bootstrap encountered errors during operation.");
+ }
+
+ @Test
+ @Launch(
+ value = {"bootstrap", "-r", "realm1", "-f", "/irrelevant"},
+ exitCode = EXIT_CODE_USAGE)
+ public void testBootstrapInvalidArguments(LaunchResult result) {
+ assertThat(result.getErrorOutput())
+ .contains(
+ "Error: (-r=<realm> [-r=<realm>]...
[-c=<realm,clientId,clientSecret>]...) "
+ + "and -f=<file> are mutually exclusive (specify only one)");
+ }
+
+ @Test
+ public void testBootstrapFromValidJsonFile(QuarkusMainLauncher launcher) {
+ LaunchResult result = launcher.launch("bootstrap", "-f", json.toString());
+ assertThat(result.exitCode()).isEqualTo(0);
+ assertThat(result.getOutput())
+ .contains("Realm 'realm1' successfully bootstrapped.")
+ .contains("Realm 'realm2' successfully bootstrapped.")
+ .contains("Bootstrap completed successfully.");
+ }
+
+ @Test
+ public void testBootstrapFromValidYamlFile(QuarkusMainLauncher launcher) {
+ LaunchResult result = launcher.launch("bootstrap", "-f", yaml.toString());
+ assertThat(result.exitCode()).isEqualTo(0);
+ assertThat(result.getOutput())
+ .contains("Realm 'realm1' successfully bootstrapped.")
+ .contains("Realm 'realm2' successfully bootstrapped.")
+ .contains("Bootstrap completed successfully.");
+ }
+
+ @Test
+ public void testBootstrapFromInvalidFile(QuarkusMainLauncher launcher) {
+ LaunchResult result = launcher.launch("bootstrap", "-f",
"/non/existing/file");
+ assertThat(result.exitCode()).isEqualTo(EXIT_CODE_BOOTSTRAP_ERROR);
+ assertThat(result.getErrorOutput())
+ .contains("Failed to read credentials file: file:/non/existing/file")
+ .contains("Bootstrap encountered errors during operation.");
+ }
+
+ private static Path copyResource(Path temp, String resource) throws
IOException {
+ URL source =
Objects.requireNonNull(BootstrapCommandTest.class.getResource(resource));
+ Path dest = temp.resolve(resource);
+ try (InputStream in = source.openStream()) {
+ Files.copy(in, dest);
+ }
+ return dest;
}
}
diff --git
a/quarkus/admin/src/test/java/org/apache/polaris/admintool/PurgeCommandTest.java
b/quarkus/admin/src/test/java/org/apache/polaris/admintool/PurgeCommandTest.java
index c0fc4b4c..ba60d9e2 100644
---
a/quarkus/admin/src/test/java/org/apache/polaris/admintool/PurgeCommandTest.java
+++
b/quarkus/admin/src/test/java/org/apache/polaris/admintool/PurgeCommandTest.java
@@ -22,22 +22,40 @@ import static org.assertj.core.api.Assertions.assertThat;
import io.quarkus.runtime.StartupEvent;
import io.quarkus.test.common.WithTestResource;
+import io.quarkus.test.junit.QuarkusTestProfile;
+import io.quarkus.test.junit.TestProfile;
import io.quarkus.test.junit.main.Launch;
import io.quarkus.test.junit.main.LaunchResult;
import io.quarkus.test.junit.main.QuarkusMainTest;
import jakarta.enterprise.event.Observes;
import java.util.List;
+import java.util.Map;
import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
-import org.apache.polaris.core.persistence.PolarisCredentialsBootstrap;
+import org.apache.polaris.core.persistence.bootstrap.RootCredentialsSet;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.junit.jupiter.api.Test;
@QuarkusMainTest
@WithTestResource(PostgresTestResourceLifecycleManager.class)
+@TestProfile(PurgeCommandTest.Profile.class)
class PurgeCommandTest {
- void preBootstrap(@Observes StartupEvent event, MetaStoreManagerFactory
metaStoreManagerFactory) {
- metaStoreManagerFactory.bootstrapRealms(
- List.of("realm1", "realm2"), PolarisCredentialsBootstrap.EMPTY);
+ public static class Profile implements QuarkusTestProfile {
+
+ @Override
+ public Map<String, String> getConfigOverrides() {
+ return Map.of("pre-bootstrap", "true");
+ }
+ }
+
+ void preBootstrap(
+ @Observes StartupEvent event,
+ @ConfigProperty(name = "pre-bootstrap", defaultValue = "false") boolean
preBootstrap,
+ MetaStoreManagerFactory metaStoreManagerFactory) {
+ if (preBootstrap) {
+ metaStoreManagerFactory.bootstrapRealms(
+ List.of("realm1", "realm2"), RootCredentialsSet.EMPTY);
+ }
}
@Test
diff --git
a/quarkus/admin/src/test/resources/org/apache/polaris/admintool/credentials.json
b/quarkus/admin/src/test/resources/org/apache/polaris/admintool/credentials.json
new file mode 100644
index 00000000..de858205
--- /dev/null
+++
b/quarkus/admin/src/test/resources/org/apache/polaris/admintool/credentials.json
@@ -0,0 +1,10 @@
+{
+ "realm1" : {
+ "client-id" : "client1",
+ "client-secret" : "secret1"
+ },
+ "realm2" : {
+ "client-id": "client2",
+ "client-secret": "secret2"
+ }
+}
\ No newline at end of file
diff --git
a/quarkus/admin/src/test/resources/org/apache/polaris/admintool/credentials.yaml
b/quarkus/admin/src/test/resources/org/apache/polaris/admintool/credentials.yaml
new file mode 100644
index 00000000..34268d7c
--- /dev/null
+++
b/quarkus/admin/src/test/resources/org/apache/polaris/admintool/credentials.yaml
@@ -0,0 +1,25 @@
+#
+# 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.
+#
+
+realm1:
+ client-id: client1
+ client-secret: secret1
+realm2:
+ client-id: client2
+ client-secret: secret2
\ No newline at end of file
diff --git
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/BasePolarisCatalogTest.java
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/BasePolarisCatalogTest.java
index 373df251..97741176 100644
---
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/BasePolarisCatalogTest.java
+++
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/BasePolarisCatalogTest.java
@@ -82,10 +82,10 @@ import org.apache.polaris.core.entity.PolarisEntityType;
import org.apache.polaris.core.entity.PrincipalEntity;
import org.apache.polaris.core.entity.TaskEntity;
import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
-import org.apache.polaris.core.persistence.PolarisCredentialsBootstrap;
import org.apache.polaris.core.persistence.PolarisEntityManager;
import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
import org.apache.polaris.core.persistence.PolarisMetaStoreSession;
+import org.apache.polaris.core.persistence.bootstrap.RootCredentialsSet;
import org.apache.polaris.core.persistence.cache.EntityCache;
import org.apache.polaris.core.storage.PolarisCredentialProperty;
import org.apache.polaris.core.storage.PolarisStorageIntegration;
@@ -335,7 +335,7 @@ public class BasePolarisCatalogTest extends
CatalogTests<BasePolarisCatalog> {
@Override
public Map<String, PrincipalSecretsResult> bootstrapRealms(
- List<String> realms, PolarisCredentialsBootstrap
credentialsBootstrap) {
+ List<String> realms, RootCredentialsSet rootCredentialsSet) {
throw new NotImplementedException("Bootstrapping realms is not
supported");
}
diff --git
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/test/PolarisIntegrationTestFixture.java
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/test/PolarisIntegrationTestFixture.java
index b7e38dd4..45a8ff67 100644
---
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/test/PolarisIntegrationTestFixture.java
+++
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/test/PolarisIntegrationTestFixture.java
@@ -40,9 +40,9 @@ 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.entity.PolarisPrincipalSecrets;
-import org.apache.polaris.core.persistence.PolarisCredentialsBootstrap;
import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
import org.apache.polaris.core.persistence.PolarisMetaStoreSession;
+import org.apache.polaris.core.persistence.bootstrap.RootCredentialsSet;
import
org.apache.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory;
import org.apache.polaris.service.quarkus.auth.TokenUtils;
import org.junit.jupiter.api.TestInfo;
@@ -97,7 +97,7 @@ public class PolarisIntegrationTestFixture {
private PolarisPrincipalSecrets fetchAdminSecrets() {
if (!(helper.metaStoreManagerFactory instanceof
InMemoryPolarisMetaStoreManagerFactory)) {
helper.metaStoreManagerFactory.bootstrapRealms(
- List.of(realm), PolarisCredentialsBootstrap.fromEnvironment());
+ List.of(realm), RootCredentialsSet.fromEnvironment());
}
RealmContext realmContext = () -> realm;
diff --git
a/service/common/src/main/java/org/apache/polaris/service/persistence/InMemoryPolarisMetaStoreManagerFactory.java
b/service/common/src/main/java/org/apache/polaris/service/persistence/InMemoryPolarisMetaStoreManagerFactory.java
index c9bd21f1..7088fe7b 100644
---
a/service/common/src/main/java/org/apache/polaris/service/persistence/InMemoryPolarisMetaStoreManagerFactory.java
+++
b/service/common/src/main/java/org/apache/polaris/service/persistence/InMemoryPolarisMetaStoreManagerFactory.java
@@ -34,11 +34,11 @@ import org.apache.polaris.core.PolarisDiagnostics;
import
org.apache.polaris.core.auth.PolarisSecretsManager.PrincipalSecretsResult;
import org.apache.polaris.core.context.RealmContext;
import org.apache.polaris.core.persistence.LocalPolarisMetaStoreManagerFactory;
-import org.apache.polaris.core.persistence.PolarisCredentialsBootstrap;
import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
import org.apache.polaris.core.persistence.PolarisMetaStoreSession;
import org.apache.polaris.core.persistence.PolarisTreeMapMetaStoreSessionImpl;
import org.apache.polaris.core.persistence.PolarisTreeMapStore;
+import org.apache.polaris.core.persistence.bootstrap.RootCredentialsSet;
import org.apache.polaris.core.storage.PolarisStorageIntegrationProvider;
import org.apache.polaris.service.context.RealmContextConfiguration;
@@ -77,13 +77,10 @@ public class InMemoryPolarisMetaStoreManagerFactory
protected PolarisMetaStoreSession createMetaStoreSession(
@Nonnull PolarisTreeMapStore store,
@Nonnull RealmContext realmContext,
- @Nullable PolarisCredentialsBootstrap credentialsBootstrap,
+ @Nullable RootCredentialsSet rootCredentialsSet,
@Nonnull PolarisDiagnostics diagnostics) {
return new PolarisTreeMapMetaStoreSessionImpl(
- store,
- storageIntegration,
- secretsGenerator(realmContext, credentialsBootstrap),
- diagnostics);
+ store, storageIntegration, secretsGenerator(realmContext,
rootCredentialsSet), diagnostics);
}
@Override
@@ -107,10 +104,8 @@ public class InMemoryPolarisMetaStoreManagerFactory
}
private void bootstrapRealmsAndPrintCredentials(List<String> realms) {
- PolarisCredentialsBootstrap credentialsBootstrap =
- PolarisCredentialsBootstrap.fromEnvironment();
- Map<String, PrincipalSecretsResult> results =
- this.bootstrapRealms(realms, credentialsBootstrap);
+ RootCredentialsSet rootCredentialsSet =
RootCredentialsSet.fromEnvironment();
+ Map<String, PrincipalSecretsResult> results = this.bootstrapRealms(realms,
rootCredentialsSet);
bootstrappedRealms.addAll(realms);
for (String realmId : realms) {