From 45ef4d2a7c160bd2470a9bf1885a19b5054ab0ee Mon Sep 17 00:00:00 2001 From: Laird Nelson Date: Wed, 16 Aug 2023 14:20:58 -0700 Subject: [PATCH 01/15] Adds OciSecretsConfigSourceProvider. Addresses issue #4238. Signed-off-by: Laird Nelson --- bom/pom.xml | 5 + .../etc/spotbugs/exclude.xml | 21 + .../oci/oci-secrets-config-source/pom.xml | 143 ++++++ .../OciSecretsConfigSourceProvider.java | 454 ++++++++++++++++++ .../secrets/configsource/package-info.java | 24 + .../src/main/java/module-info.java | 38 ++ .../oci/secrets/configsource/UsageTest.java | 83 ++++ .../src/test/java/logging.properties | 19 + .../src/test/resources/meta-config.yaml | 21 + integrations/oci/pom.xml | 1 + 10 files changed, 809 insertions(+) create mode 100644 integrations/oci/oci-secrets-config-source/etc/spotbugs/exclude.xml create mode 100644 integrations/oci/oci-secrets-config-source/pom.xml create mode 100644 integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java create mode 100644 integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/package-info.java create mode 100644 integrations/oci/oci-secrets-config-source/src/main/java/module-info.java create mode 100644 integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/UsageTest.java create mode 100644 integrations/oci/oci-secrets-config-source/src/test/java/logging.properties create mode 100644 integrations/oci/oci-secrets-config-source/src/test/resources/meta-config.yaml diff --git a/bom/pom.xml b/bom/pom.xml index c474642f9ca..f2fd0b5bb0b 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -803,6 +803,11 @@ helidon-integrations-oci-sdk-cdi ${helidon.version} + + io.helidon.integrations.oci + helidon-integrations-oci-secrets-configsource + ${helidon.version} + io.helidon.integrations.oci.metrics helidon-integrations-oci-metrics diff --git a/integrations/oci/oci-secrets-config-source/etc/spotbugs/exclude.xml b/integrations/oci/oci-secrets-config-source/etc/spotbugs/exclude.xml new file mode 100644 index 00000000000..c620dfba42f --- /dev/null +++ b/integrations/oci/oci-secrets-config-source/etc/spotbugs/exclude.xml @@ -0,0 +1,21 @@ + + + + diff --git a/integrations/oci/oci-secrets-config-source/pom.xml b/integrations/oci/oci-secrets-config-source/pom.xml new file mode 100644 index 00000000000..a93acc3a0d1 --- /dev/null +++ b/integrations/oci/oci-secrets-config-source/pom.xml @@ -0,0 +1,143 @@ + + + + 4.0.0 + + io.helidon.integrations.oci + helidon-integrations-oci-project + 4.0.0-SNAPSHOT + + helidon-integrations-oci-secrets-config-source + Helidon Config OCI Secrets + + + OCI Secrets Retrieval API Config Source Implementation + + + + + etc/spotbugs/exclude.xml + + + + + + + + org.apache.httpcomponents + httpclient + 4.5.14 + + + org.apache.httpcomponents + httpcore + 4.4.16 + + + + + + + + + + io.helidon.common + helidon-common + + + io.helidon.config + helidon-config + + + io.helidon.integrations.oci.sdk + helidon-integrations-oci-sdk-runtime + + + com.oracle.oci.sdk + oci-java-sdk-common + + + com.oracle.oci.sdk + oci-java-sdk-secrets + + + com.oracle.oci.sdk + oci-java-sdk-vault + + + + + + com.oracle.oci.sdk + oci-java-sdk-common-httpclient-jersey3 + runtime + + + + + + io.helidon.config + helidon-config-yaml-mp + test + + + org.hamcrest + hamcrest-core + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.slf4j + slf4j-jdk14 + test + + + + + + + + src/test/resources + true + + meta-config.yaml + + + + + + maven-surefire-plugin + + + ${compartment-ocid} + src/test/java/logging.properties + ${vault-ocid} + + + + + + diff --git a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java new file mode 100644 index 00000000000..e9ca9b7ba38 --- /dev/null +++ b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java @@ -0,0 +1,454 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.integrations.oci.secrets.configsource; + +import java.lang.System.Logger; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.function.Supplier; + +import io.helidon.common.LazyValue; +import io.helidon.common.Weighted; +import io.helidon.config.AbstractConfigSource; +import io.helidon.config.AbstractConfigSourceBuilder; +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.config.spi.ConfigContent.NodeContent; +import io.helidon.config.spi.ConfigNode.ObjectNode; +import io.helidon.config.spi.ConfigNode.ValueNode; +import io.helidon.config.spi.ConfigSource; +import io.helidon.config.spi.ConfigSourceProvider; +import io.helidon.config.spi.NodeConfigSource; +import io.helidon.config.spi.PollableSource; +import io.helidon.config.spi.PollingStrategy; + +import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; +import com.oracle.bmc.secrets.Secrets; +import com.oracle.bmc.secrets.SecretsClient; +import com.oracle.bmc.secrets.model.Base64SecretBundleContentDetails; +import com.oracle.bmc.secrets.requests.GetSecretBundleRequest; +import com.oracle.bmc.vault.Vaults; +import com.oracle.bmc.vault.VaultsClient; +import com.oracle.bmc.vault.model.SecretSummary; +import com.oracle.bmc.vault.requests.ListSecretsRequest; +import com.oracle.bmc.vault.responses.ListSecretsResponse; + +import static io.helidon.integrations.oci.sdk.runtime.OciExtension.ociAuthenticationProvider; +import static java.lang.System.Logger.Level.WARNING; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.time.Instant.now; +import static java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor; + +/** + * A {@link ConfigSourceProvider} that {@linkplain #create(String, Config) creates} {@link ConfigSource} implementations + * that interact with the Oracle Cloud Infrastructure (OCI) Secrets + * Retrieval and Vault APIs. + * + *

To use, ensure the packaging artifact (e.g. {@code .jar} file or similar) containing this class is present on your + * class or module path as appropriate, and configure a meta-configuration source with a {@code type} of {@code + * oci-secrets}, following the usual Helidon meta-configuration rules.

+ * + *

More specifically:

+ * + *
    + * + *
  1. Ensure you have an authentication mechanism set up to connect to OCI (e.g. a valid OCI configuration file)
  2. + * + *
  3. Ensure there is a classpath resource present named {@code meta-config.yaml}.
  4. + * + *
  5. Ensure the {@code meta-config.yaml} classpath resource contains a {@code source} with a {@code type} of {@code + * oci-secrets} that looks similar to the following, substituting values as appropriate:
    sources:
    + *  - type: 'oci-secrets'
    + *    properties:
    + *      compartment-ocid: 'your vault compartment OCID goes here'
    + *      vault-ocid: 'your vault OCID goes here'
  6. + * + *
+ * + *

Refer to Helidon's documentation concerning meta-configuration for more details.

+ * + * @see ConfigSourceProvider + */ +public final class OciSecretsConfigSourceProvider implements ConfigSourceProvider, Weighted { + + + /* + * Static fields. + */ + + + private static final Logger LOGGER = System.getLogger(OciSecretsConfigSourceProvider.class.getName()); + + private static final Set SUPPORTED_TYPES = Set.of("oci-secrets"); + + private static final double WEIGHT = 300D; + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link OciSecretsConfigSourceProvider}. + * + * @deprecated For use by {@link java.util.ServiceLoader} only. + */ + @Deprecated // For use by java.util.ServiceLoader only. + public OciSecretsConfigSourceProvider() { + super(); + } + + + /* + * Instance methods. + */ + + + /** + * Creates and returns a non-{@code null} {@link ConfigSource} that sources its values from an Oracle Cloud + * Infrastructure (OCI) Vault. + * + * @param type one of the {@linkplain #supported() supported types}, or an {@linkplain ConfigSources#empty() empty + * ConfigSource} will be returned + * + * @param metaConfig a {@link Config} serving as meta-configuration for this provider; must not be {@code null} + * + * @return a non-{@code null} {@link ConfigSource} + * + * @exception NullPointerException if {@code metaConfig} is {@code null} + * + * @see #supported() + * + * @deprecated For use by the Helidon Config subsystem only. + */ + @Deprecated // For use by the Helidon Config subsystem only. + @Override // ConfigSourceProvider + public ConfigSource create(String type, Config metaConfig) { + return this.supports(type) ? SecretBundleConfigSource.builder().config(metaConfig).build() : ConfigSources.empty(); + } + + /** + * Returns a non-{@code null}, immutable {@link Set} of supported types suitable for the Helidon Config subsystem to + * pass to the {@link #create(String, Config)} method. + * + *

This method returns a {@link Set} whose sole element is the string "{@code oci-secrets}".

+ * + * @return a non-{@code null}, immutable {@link Set} + * + * @see #create(String, Config) + * + * @deprecated For use by the Helidon Config subsystem only. + */ + @Deprecated // For use by the Helidon Config subsystem only. + @Override // ConfigSourceProvider + public Set supported() { + return SUPPORTED_TYPES; + } + + /** + * Returns {@code true} if and only if the supplied {@code type} is non-{@code null} and the {@link Set} returned by + * an invocation of the {@link #supported()} method {@linkplain Set#contains(Object) contains} it. + * + * @param type the type to test; may be {@code null} in which case {@code false} will be returned + * + * @return {@code true} if and only if the supplied {@code type} is non-{@code null} and the {@link Set} returned by + * an invocation of the {@link #supported()} method {@linkplain Set#contains(Object) contains} it + * + * @see #supported() + * + * @see #create(String, Config) + * + * @deprecated For use by the Helidon Config subsystem only. + */ + @Deprecated // For use by the Helidon Config subsystem only. + @Override // ConfigSourceProvider + public boolean supports(String type) { + return type != null && this.supported().contains(type); + } + + /** + * Returns a (determinate) weight {{@value #WEIGHT}) for this {@link OciSecretsConfigSourceProvider} when invoked. + * + * @return a determinate weight ({@value #WEIGHT}) when invoked + * + * @deprecated For use by the Helidon service loading subsystem only. + */ + @Deprecated // For use by the Helidon service loading subsystem only. + @Override // Weighted + public double weight() { + return WEIGHT; + } + + + /* + * Inner and nested classes. + */ + + + private static final class SecretBundleConfigSource extends AbstractConfigSource implements NodeConfigSource, PollableSource { + + /** + * The name of a configuration property ({@value #COMPARTMENT_OCID_PROPERTY_NAME}) whose value should be a valid OCI + * Compartment OCID. + */ + private static final String COMPARTMENT_OCID_PROPERTY_NAME = "compartment-ocid"; + + private static final Optional ABSENT_NODE_CONTENT = + Optional.of(NodeContent.builder().node(ObjectNode.empty()).build()); + + private static final Logger LOGGER = System.getLogger(SecretBundleConfigSource.class.getName()); + + /** + * The name of a configuration property ({@value #VAULT_OCID_PROPERTY_NAME}) whose value should be a valid OCI Vault + * OCID. + */ + private static final String VAULT_OCID_PROPERTY_NAME = "vault-ocid"; + + private final Supplier> loader; + + private volatile Instant mostDistantExpirationInstant; + + @SuppressWarnings({"checkstyle:linelength", "try"}) + private SecretBundleConfigSource(Builder b) { + super(b); + String compartmentOcid = b.compartmentOcid; + Supplier secretsSupplier = Objects.requireNonNull(b.secretsSupplier, "b.secretsSupplier"); + String vaultOcid = b.vaultOcid; + Supplier vaultsSupplier = Objects.requireNonNull(b.vaultsSupplier, "b.vaultsSupplier"); + + if (compartmentOcid == null || vaultOcid == null) { + this.loader = () -> ABSENT_NODE_CONTENT; + } else { + ListSecretsRequest listSecretsRequest = ListSecretsRequest.builder() + .compartmentId(compartmentOcid) + .vaultId(vaultOcid) + .build(); + this.loader = () -> { + ListSecretsResponse listSecretsResponse; + try (Vaults v = vaultsSupplier.get()) { + listSecretsResponse = v.listSecrets(listSecretsRequest); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw new IllegalStateException(e.getMessage(), e); + } + Collection secretSummaries = listSecretsResponse.getItems(); + if (secretSummaries == null || secretSummaries.isEmpty()) { + return ABSENT_NODE_CONTENT; + } + Map valueNodes = new ConcurrentHashMap<>(); + List> tasks = new ArrayList<>(secretSummaries.size()); + Instant mostDistantExpirationInstant = SecretBundleConfigSource.this.mostDistantExpirationInstant; + Base64.Decoder decoder = Base64.getDecoder(); + for (SecretSummary ss : secretSummaries) { + tasks.add(() -> { + valueNodes.put(ss.getSecretName(), + ValueNode.create(new String(decoder.decode(((Base64SecretBundleContentDetails) (secretsSupplier.get() + .getSecretBundle(GetSecretBundleRequest.builder() + .secretId(ss.getId()) + .build()) + .getSecretBundle() + .getSecretBundleContent())) + .getContent()), + UTF_8))); + return null; + }); + java.util.Date d = ss.getTimeOfCurrentVersionExpiry(); + Instant i = d == null ? null : d.toInstant(); + if (i != null && (mostDistantExpirationInstant == null || mostDistantExpirationInstant.isBefore(i))) { + mostDistantExpirationInstant = i; + } + } + SecretBundleConfigSource.this.mostDistantExpirationInstant = mostDistantExpirationInstant; + List> futures = null; + try (ExecutorService es = newVirtualThreadPerTaskExecutor()) { + futures = es.invokeAll(tasks); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw new IllegalStateException(e.getMessage(), e); + } + RuntimeException re = null; + for (Future future : futures) { + assert future.isDone(); + try { + future.get(); + } catch (RuntimeException e) { + if (re == null) { + re = e; + } else { + re.addSuppressed(e); + } + } catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + if (re == null) { + re = new IllegalStateException(e.getMessage(), e); + } else { + re.addSuppressed(e); + } + } + } + if (re != null) { + throw re; + } + ObjectNode.Builder onb = ObjectNode.builder(); + for (Entry e : valueNodes.entrySet()) { + onb.addValue(e.getKey(), e.getValue()); + } + return Optional.of(NodeContent.builder() + .node(onb.build()) + .build()); + }; + } + } + + @Deprecated // For use by the Helidon Config subsystem only. + @Override // PollableSource + public boolean isModified(Instant instant) { + Instant i = this.mostDistantExpirationInstant; + return i == null || i.isBefore(instant == null ? now() : instant); + } + + @Deprecated // For use by the Helidon Config subsystem only. + @Override // NodeConfigSource + public Optional load() { + return this.loader.get(); + } + + @Deprecated // For use by the Helidon Config subsystem only. + @Override // PollableSource + public Optional pollingStrategy() { + return super.pollingStrategy(); + } + + + + /* + * Static methods. + */ + + + private static Builder builder() { + return new Builder(); + } + + + /* + * Inner and nested classes. + */ + + + private static final class Builder extends AbstractConfigSourceBuilder { + + private String compartmentOcid; + + private Supplier secretsSupplier; + + private String vaultOcid; + + private Supplier vaultsSupplier; + + private Builder() { + super(); + Supplier adpSupplier = + LazyValue.create(() -> (BasicAuthenticationDetailsProvider) ociAuthenticationProvider().get()); + this.secretsSupplier = LazyValue.create(() -> SecretsClient.builder().build(adpSupplier.get())); + this.vaultsSupplier = LazyValue.create(() -> VaultsClient.builder().build(adpSupplier.get())); + } + + private SecretBundleConfigSource build() { + return new SecretBundleConfigSource(this); + } + + private Builder compartmentOcid(String compartmentOcid) { + this.compartmentOcid = Objects.requireNonNull(compartmentOcid, "compartmentOcid"); + return this; + } + + @Override // AbstractConfigSourceBuilder + protected Builder config(Config metaConfig) { + metaConfig.get("compartment-ocid") + .asString() + .flatMap(s -> s.isBlank() ? Optional.empty() : Optional.of(s)) + .ifPresentOrElse(ocid -> this.compartmentOcid(ocid), + () -> { + if (LOGGER.isLoggable(WARNING)) { + LOGGER.log(WARNING, + "No meta-configuration value supplied for " + + metaConfig.key().toString() + "." + COMPARTMENT_OCID_PROPERTY_NAME + + "); resulting ConfigSource will be empty"); + } + }); + metaConfig.get("vault-ocid") + .asString() + .flatMap(s -> s.isBlank() ? Optional.empty() : Optional.of(s)) + .ifPresentOrElse(ocid -> this.vaultOcid(ocid), + () -> { + if (LOGGER.isLoggable(WARNING)) { + LOGGER.log(WARNING, + "No meta-configuration value supplied for " + + metaConfig.key().toString() + "." + VAULT_OCID_PROPERTY_NAME + + "); resulting ConfigSource will be empty"); + } + }); + return super.config(metaConfig); + } + + private Builder secretsSupplier(Supplier secretsSupplier) { + this.secretsSupplier = Objects.requireNonNull(secretsSupplier, "secretsSupplier"); + return this; + } + + private Builder vaultOcid(String vaultOcid) { + this.vaultOcid = Objects.requireNonNull(vaultOcid, "vaultOcid"); + return this; + } + + private Builder vaultsSupplier(Supplier vaultsSupplier) { + this.vaultsSupplier = Objects.requireNonNull(vaultsSupplier, "vaultsSupplier"); + return this; + } + + } + + } + +} diff --git a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/package-info.java b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/package-info.java new file mode 100644 index 00000000000..12ae6570da8 --- /dev/null +++ b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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. + */ + +/** + * Provides classes and interfaces for using the Oracle Cloud Infrastructure (OCI) Secrets + * Retrieval and Vault APIs + * as part of a {@linkplain io.helidon.config.spi.ConfigSourceProvider} implementation. + */ +package io.helidon.integrations.oci.secrets.configsource; diff --git a/integrations/oci/oci-secrets-config-source/src/main/java/module-info.java b/integrations/oci/oci-secrets-config-source/src/main/java/module-info.java new file mode 100644 index 00000000000..8d0fe6f4c4d --- /dev/null +++ b/integrations/oci/oci-secrets-config-source/src/main/java/module-info.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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. + */ + +/** + * Provides packages containing classes and interfaces for Oracle Cloud Infrastructure (OCI) Secrets + * Retrieval and Vault + * API-using {@linkplain io.helidon.config.spi.ConfigSource configuration sources}. + */ +@SuppressWarnings({ "requires-automatic", "requires-transitive-automatic" }) +module io.helidon.integrations.oci.secrets.configsource { + + exports io.helidon.integrations.oci.secrets.configsource; + + requires io.helidon.common; + requires transitive io.helidon.config; + requires io.helidon.integrations.oci.sdk.runtime; + requires oci.java.sdk.common; + requires oci.java.sdk.secrets; + requires oci.java.sdk.vault; + + provides io.helidon.config.spi.ConfigSourceProvider with io.helidon.integrations.oci.secrets.configsource.OciSecretsConfigSourceProvider; + +} diff --git a/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/UsageTest.java b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/UsageTest.java new file mode 100644 index 00000000000..e06bf8878a3 --- /dev/null +++ b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/UsageTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.integrations.oci.secrets.configsource; + +import java.nio.file.Files; +import java.nio.file.Paths; + +import io.helidon.config.Config; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +final class UsageTest { + + private UsageTest() { + super(); + } + + @Test + final void testUsage() { + // Get a Config object. Because src/test/resources/meta-config.yaml exists, and because it will be processed + // according to the Helidon rules. An + // io.helidon.integrations.oci.secrets.configsource.OciSecretsConfigSourceProvider will be created and any + // ConfigSources it creates will become part of the assembled Config object. + Config c = Config.create(); + + // Make sure non-existent properties don't cause the Vault to get involved. + assertThat(c.get("bogus").asNode().orElse(null), nullValue()); + + // Make sure properties that have nothing to do with the OCI Secrets Retrieval or Vault APIs are handled by some + // other (default) ConfigSource, e.g., System properties, etc. (The OCI Secrets Retrieval API should never be + // consulted for java.home, in other words.) + assertThat(c.get("java.home").asString().orElse(null), is(System.getProperty("java.home"))); + + // Do the rest of this test only if the following assumptions hold. To avoid skipping the rest of this test: + // + // 1. Set up a ${HOME}/.oci/config file following + // https://docs.oracle.com/en-us/iaas/Content/API/Concepts/sdkconfig.htm or similar + // + // 2. Run Maven with all of the following properties: + // + // -Dcompartment-ocid=ocid1.compartment.oci1.iad.123xyz (a valid OCI Compartment OCID) + // -Dvault-ocid=ocid1.vault.oci1.iad.123xyz (a valid OCI Vault OCID) + // -DFrancqueSecret.expectedValue='Some Value' (some value for a secret named FrancqueSecret in that vault) + // + assumeTrue(Files.exists(Paths.get(System.getProperty("user.home"), ".oci", "config"))); // condition 1 + assumeFalse(System.getProperty("compartment-ocid", "").isBlank()); // condition 2 + assumeFalse(System.getProperty("vault-ocid", "").isBlank()); // condition 2 + String expectedValue = System.getProperty("FrancqueSecret.expectedValue", ""); + assumeFalse(expectedValue.isBlank()); // condition 2 + + // + // (Code below this line executes only if the above JUnit assumptions passed. Otherwise control flow stops above.) + // + + // For this test to pass, all of the following must hold: + // + // 1. The vault designated by the vault OCID must hold a secret named FrancqueSecret + // + // 2. The secret named FrancqueSecret must have a value equal to the expected value + assertThat(c.get("FrancqueSecret").asString().orElse(null), is(expectedValue)); + } + +} diff --git a/integrations/oci/oci-secrets-config-source/src/test/java/logging.properties b/integrations/oci/oci-secrets-config-source/src/test/java/logging.properties new file mode 100644 index 00000000000..686736250f3 --- /dev/null +++ b/integrations/oci/oci-secrets-config-source/src/test/java/logging.properties @@ -0,0 +1,19 @@ +# +# Copyright (c) 2023 Oracle and/or its affiliates. +# +# Licensed 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. +# +com.oracle.bmc.level = SEVERE +handlers = java.util.logging.ConsoleHandler +io.helidon.integrations.oci.secrets.configsource.level = FINER +java.util.logging.ConsoleHandler.level = FINER diff --git a/integrations/oci/oci-secrets-config-source/src/test/resources/meta-config.yaml b/integrations/oci/oci-secrets-config-source/src/test/resources/meta-config.yaml new file mode 100644 index 00000000000..a4412b62713 --- /dev/null +++ b/integrations/oci/oci-secrets-config-source/src/test/resources/meta-config.yaml @@ -0,0 +1,21 @@ +# +# Copyright (c) 2023 Oracle and/or its affiliates. +# +# Licensed 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. +# +sources: + - type: 'system-properties' + - type: 'oci-secrets' + properties: # required + compartment-ocid: ${compartment-ocid} + vault-ocid: ${vault-ocid} diff --git a/integrations/oci/pom.xml b/integrations/oci/pom.xml index 6866168a8a0..1a8f9f1b0de 100644 --- a/integrations/oci/pom.xml +++ b/integrations/oci/pom.xml @@ -34,6 +34,7 @@ metrics + oci-secrets-config-source sdk From e12586ecb21d4a662f2f58daeaa3e241cfab7317 Mon Sep 17 00:00:00 2001 From: Laird Nelson Date: Wed, 16 Aug 2023 15:25:42 -0700 Subject: [PATCH 02/15] Squashable commit; corrects documentation typos, removes spurious files Signed-off-by: Laird Nelson --- .../etc/spotbugs/exclude.xml | 21 ------------------- .../oci/oci-secrets-config-source/pom.xml | 8 +++---- .../OciSecretsConfigSourceProvider.java | 21 ++++++------------- .../secrets/configsource/package-info.java | 2 ++ .../src/main/java/module-info.java | 2 ++ .../oci/secrets/configsource/UsageTest.java | 10 ++++----- .../src/test/resources/meta-config.yaml | 2 +- 7 files changed, 20 insertions(+), 46 deletions(-) delete mode 100644 integrations/oci/oci-secrets-config-source/etc/spotbugs/exclude.xml diff --git a/integrations/oci/oci-secrets-config-source/etc/spotbugs/exclude.xml b/integrations/oci/oci-secrets-config-source/etc/spotbugs/exclude.xml deleted file mode 100644 index c620dfba42f..00000000000 --- a/integrations/oci/oci-secrets-config-source/etc/spotbugs/exclude.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - diff --git a/integrations/oci/oci-secrets-config-source/pom.xml b/integrations/oci/oci-secrets-config-source/pom.xml index a93acc3a0d1..72cfce69fb7 100644 --- a/integrations/oci/oci-secrets-config-source/pom.xml +++ b/integrations/oci/oci-secrets-config-source/pom.xml @@ -24,15 +24,15 @@ 4.0.0-SNAPSHOT helidon-integrations-oci-secrets-config-source - Helidon Config OCI Secrets + Helidon Config OCI Secrets ConfigSourceProvider - OCI Secrets Retrieval API Config Source Implementation + OCI Secrets Retrieval API ConfigSourceProvider Implementation - etc/spotbugs/exclude.xml + src/test/java/logging.properties @@ -133,7 +133,7 @@ ${compartment-ocid} - src/test/java/logging.properties + ${java.util.logging.config.file} ${vault-ocid} diff --git a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java index e9ca9b7ba38..f64fef990c6 100644 --- a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java +++ b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java @@ -197,7 +197,7 @@ public boolean supports(String type) { } /** - * Returns a (determinate) weight {{@value #WEIGHT}) for this {@link OciSecretsConfigSourceProvider} when invoked. + * Returns a (determinate) weight ({@value #WEIGHT}) for this {@link OciSecretsConfigSourceProvider} when invoked. * * @return a determinate weight ({@value #WEIGHT}) when invoked * @@ -217,10 +217,6 @@ public double weight() { private static final class SecretBundleConfigSource extends AbstractConfigSource implements NodeConfigSource, PollableSource { - /** - * The name of a configuration property ({@value #COMPARTMENT_OCID_PROPERTY_NAME}) whose value should be a valid OCI - * Compartment OCID. - */ private static final String COMPARTMENT_OCID_PROPERTY_NAME = "compartment-ocid"; private static final Optional ABSENT_NODE_CONTENT = @@ -228,10 +224,6 @@ private static final class SecretBundleConfigSource extends AbstractConfigSource private static final Logger LOGGER = System.getLogger(SecretBundleConfigSource.class.getName()); - /** - * The name of a configuration property ({@value #VAULT_OCID_PROPERTY_NAME}) whose value should be a valid OCI Vault - * OCID. - */ private static final String VAULT_OCID_PROPERTY_NAME = "vault-ocid"; private final Supplier> loader; @@ -245,7 +237,6 @@ private SecretBundleConfigSource(Builder b) { Supplier secretsSupplier = Objects.requireNonNull(b.secretsSupplier, "b.secretsSupplier"); String vaultOcid = b.vaultOcid; Supplier vaultsSupplier = Objects.requireNonNull(b.vaultsSupplier, "b.vaultsSupplier"); - if (compartmentOcid == null || vaultOcid == null) { this.loader = () -> ABSENT_NODE_CONTENT; } else { @@ -277,11 +268,11 @@ private SecretBundleConfigSource(Builder b) { tasks.add(() -> { valueNodes.put(ss.getSecretName(), ValueNode.create(new String(decoder.decode(((Base64SecretBundleContentDetails) (secretsSupplier.get() - .getSecretBundle(GetSecretBundleRequest.builder() - .secretId(ss.getId()) - .build()) - .getSecretBundle() - .getSecretBundleContent())) + .getSecretBundle(GetSecretBundleRequest.builder() + .secretId(ss.getId()) + .build()) + .getSecretBundle() + .getSecretBundleContent())) .getContent()), UTF_8))); return null; diff --git a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/package-info.java b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/package-info.java index 12ae6570da8..a6da275765c 100644 --- a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/package-info.java +++ b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/package-info.java @@ -20,5 +20,7 @@ * Retrieval and Vault APIs * as part of a {@linkplain io.helidon.config.spi.ConfigSourceProvider} implementation. + * + * @see io.helidon.integrations.oci.secrets.configsource.OciSecretsConfigSourceProvider */ package io.helidon.integrations.oci.secrets.configsource; diff --git a/integrations/oci/oci-secrets-config-source/src/main/java/module-info.java b/integrations/oci/oci-secrets-config-source/src/main/java/module-info.java index 8d0fe6f4c4d..cbacd5343b3 100644 --- a/integrations/oci/oci-secrets-config-source/src/main/java/module-info.java +++ b/integrations/oci/oci-secrets-config-source/src/main/java/module-info.java @@ -20,6 +20,8 @@ * Retrieval and Vault * API-using {@linkplain io.helidon.config.spi.ConfigSource configuration sources}. + * + * @see io.helidon.integrations.oci.secrets.configsource.OciSecretsConfigSourceProvider */ @SuppressWarnings({ "requires-automatic", "requires-transitive-automatic" }) module io.helidon.integrations.oci.secrets.configsource { diff --git a/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/UsageTest.java b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/UsageTest.java index e06bf8878a3..1e7a78abb43 100644 --- a/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/UsageTest.java +++ b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/UsageTest.java @@ -36,13 +36,13 @@ private UsageTest() { } @Test - final void testUsage() { + void testUsage() { // Get a Config object. Because src/test/resources/meta-config.yaml exists, and because it will be processed - // according to the Helidon rules. An + // according to the Helidon rules, an // io.helidon.integrations.oci.secrets.configsource.OciSecretsConfigSourceProvider will be created and any // ConfigSources it creates will become part of the assembled Config object. Config c = Config.create(); - + // Make sure non-existent properties don't cause the Vault to get involved. assertThat(c.get("bogus").asNode().orElse(null), nullValue()); @@ -50,7 +50,7 @@ final void testUsage() { // other (default) ConfigSource, e.g., System properties, etc. (The OCI Secrets Retrieval API should never be // consulted for java.home, in other words.) assertThat(c.get("java.home").asString().orElse(null), is(System.getProperty("java.home"))); - + // Do the rest of this test only if the following assumptions hold. To avoid skipping the rest of this test: // // 1. Set up a ${HOME}/.oci/config file following @@ -67,7 +67,7 @@ final void testUsage() { assumeFalse(System.getProperty("vault-ocid", "").isBlank()); // condition 2 String expectedValue = System.getProperty("FrancqueSecret.expectedValue", ""); assumeFalse(expectedValue.isBlank()); // condition 2 - + // // (Code below this line executes only if the above JUnit assumptions passed. Otherwise control flow stops above.) // diff --git a/integrations/oci/oci-secrets-config-source/src/test/resources/meta-config.yaml b/integrations/oci/oci-secrets-config-source/src/test/resources/meta-config.yaml index a4412b62713..748334ff93d 100644 --- a/integrations/oci/oci-secrets-config-source/src/test/resources/meta-config.yaml +++ b/integrations/oci/oci-secrets-config-source/src/test/resources/meta-config.yaml @@ -14,7 +14,7 @@ # limitations under the License. # sources: - - type: 'system-properties' + - type: 'system-properties' # for testing - type: 'oci-secrets' properties: # required compartment-ocid: ${compartment-ocid} From fa7aefa4965bac0753b2c05827077aa470e18954 Mon Sep 17 00:00:00 2001 From: Laird Nelson Date: Wed, 16 Aug 2023 16:22:09 -0700 Subject: [PATCH 03/15] Squashable commit; closes Secrets client properly accounting for exceptions Signed-off-by: Laird Nelson --- .../OciSecretsConfigSourceProvider.java | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java index f64fef990c6..4dd9504d7d0 100644 --- a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java +++ b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java @@ -264,13 +264,13 @@ private SecretBundleConfigSource(Builder b) { List> tasks = new ArrayList<>(secretSummaries.size()); Instant mostDistantExpirationInstant = SecretBundleConfigSource.this.mostDistantExpirationInstant; Base64.Decoder decoder = Base64.getDecoder(); + Secrets secrets = secretsSupplier.get(); for (SecretSummary ss : secretSummaries) { tasks.add(() -> { valueNodes.put(ss.getSecretName(), - ValueNode.create(new String(decoder.decode(((Base64SecretBundleContentDetails) (secretsSupplier.get() - .getSecretBundle(GetSecretBundleRequest.builder() - .secretId(ss.getId()) - .build()) + ValueNode.create(new String(decoder.decode(((Base64SecretBundleContentDetails) (secrets.getSecretBundle(GetSecretBundleRequest.builder() + .secretId(ss.getId()) + .build()) .getSecretBundle() .getSecretBundleContent())) .getContent()), @@ -285,19 +285,40 @@ private SecretBundleConfigSource(Builder b) { } SecretBundleConfigSource.this.mostDistantExpirationInstant = mostDistantExpirationInstant; List> futures = null; + RuntimeException re = null; try (ExecutorService es = newVirtualThreadPerTaskExecutor()) { futures = es.invokeAll(tasks); } catch (RuntimeException e) { - throw e; + re = e; } catch (Exception e) { if (e instanceof InterruptedException) { Thread.currentThread().interrupt(); } - throw new IllegalStateException(e.getMessage(), e); + re = new IllegalStateException(e.getMessage(), e); + } finally { + try { + secrets.close(); + } catch (RuntimeException e) { + if (re == null) { + throw e; + } else { + re.addSuppressed(e); + } + } catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + if (re == null) { + throw new IllegalStateException(e.getMessage(), e); + } else { + re.addSuppressed(e); + } + } + if (re != null) { + throw re; + } } - RuntimeException re = null; for (Future future : futures) { - assert future.isDone(); try { future.get(); } catch (RuntimeException e) { @@ -381,8 +402,10 @@ private Builder() { super(); Supplier adpSupplier = LazyValue.create(() -> (BasicAuthenticationDetailsProvider) ociAuthenticationProvider().get()); - this.secretsSupplier = LazyValue.create(() -> SecretsClient.builder().build(adpSupplier.get())); - this.vaultsSupplier = LazyValue.create(() -> VaultsClient.builder().build(adpSupplier.get())); + SecretsClient.Builder scb = SecretsClient.builder(); + this.secretsSupplier = () -> scb.build(adpSupplier.get()); + VaultsClient.Builder vcb = VaultsClient.builder(); + this.vaultsSupplier = () -> vcb.build(adpSupplier.get()); } private SecretBundleConfigSource build() { From 01c74883bb0830117f836dcb3795df13e2ba7d03 Mon Sep 17 00:00:00 2001 From: Laird Nelson Date: Wed, 16 Aug 2023 16:56:25 -0700 Subject: [PATCH 04/15] Squashable commit; breaks particularly annoying fan-out error handling out into its own method Signed-off-by: Laird Nelson --- .../OciSecretsConfigSourceProvider.java | 153 ++++++++++-------- 1 file changed, 83 insertions(+), 70 deletions(-) diff --git a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java index 4dd9504d7d0..be70c4b2867 100644 --- a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java +++ b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java @@ -230,7 +230,7 @@ private static final class SecretBundleConfigSource extends AbstractConfigSource private volatile Instant mostDistantExpirationInstant; - @SuppressWarnings({"checkstyle:linelength", "try"}) + @SuppressWarnings("try") private SecretBundleConfigSource(Builder b) { super(b); String compartmentOcid = b.compartmentOcid; @@ -238,6 +238,8 @@ private SecretBundleConfigSource(Builder b) { String vaultOcid = b.vaultOcid; Supplier vaultsSupplier = Objects.requireNonNull(b.vaultsSupplier, "b.vaultsSupplier"); if (compartmentOcid == null || vaultOcid == null) { + // (It is not immediately clear why the OCI Java SDK requires a Compartment OCID, since a Vault OCID is + // sufficient to uniquely identify any Vault.) this.loader = () -> ABSENT_NODE_CONTENT; } else { ListSecretsRequest listSecretsRequest = ListSecretsRequest.builder() @@ -261,20 +263,14 @@ private SecretBundleConfigSource(Builder b) { return ABSENT_NODE_CONTENT; } Map valueNodes = new ConcurrentHashMap<>(); - List> tasks = new ArrayList<>(secretSummaries.size()); - Instant mostDistantExpirationInstant = SecretBundleConfigSource.this.mostDistantExpirationInstant; + Collection> tasks = new ArrayList<>(secretSummaries.size()); + Instant mostDistantExpirationInstant = + SecretBundleConfigSource.this.mostDistantExpirationInstant; // volatile read Base64.Decoder decoder = Base64.getDecoder(); Secrets secrets = secretsSupplier.get(); for (SecretSummary ss : secretSummaries) { tasks.add(() -> { - valueNodes.put(ss.getSecretName(), - ValueNode.create(new String(decoder.decode(((Base64SecretBundleContentDetails) (secrets.getSecretBundle(GetSecretBundleRequest.builder() - .secretId(ss.getId()) - .build()) - .getSecretBundle() - .getSecretBundleContent())) - .getContent()), - UTF_8))); + valueNodes.put(ss.getSecretName(), valueNode(secrets, ss, decoder)); return null; }); java.util.Date d = ss.getTimeOfCurrentVersionExpiry(); @@ -283,64 +279,8 @@ private SecretBundleConfigSource(Builder b) { mostDistantExpirationInstant = i; } } - SecretBundleConfigSource.this.mostDistantExpirationInstant = mostDistantExpirationInstant; - List> futures = null; - RuntimeException re = null; - try (ExecutorService es = newVirtualThreadPerTaskExecutor()) { - futures = es.invokeAll(tasks); - } catch (RuntimeException e) { - re = e; - } catch (Exception e) { - if (e instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - re = new IllegalStateException(e.getMessage(), e); - } finally { - try { - secrets.close(); - } catch (RuntimeException e) { - if (re == null) { - throw e; - } else { - re.addSuppressed(e); - } - } catch (Exception e) { - if (e instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - if (re == null) { - throw new IllegalStateException(e.getMessage(), e); - } else { - re.addSuppressed(e); - } - } - if (re != null) { - throw re; - } - } - for (Future future : futures) { - try { - future.get(); - } catch (RuntimeException e) { - if (re == null) { - re = e; - } else { - re.addSuppressed(e); - } - } catch (Exception e) { - if (e instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - if (re == null) { - re = new IllegalStateException(e.getMessage(), e); - } else { - re.addSuppressed(e); - } - } - } - if (re != null) { - throw re; - } + SecretBundleConfigSource.this.mostDistantExpirationInstant = mostDistantExpirationInstant; // volatile write + invokeAll(tasks, secrets); ObjectNode.Builder onb = ObjectNode.builder(); for (Entry e : valueNodes.entrySet()) { onb.addValue(e.getKey(), e.getValue()); @@ -372,7 +312,6 @@ public Optional pollingStrategy() { } - /* * Static methods. */ @@ -382,6 +321,80 @@ private static Builder builder() { return new Builder(); } + private static void invokeAll(Collection> tasks, AutoCloseable secrets) { + RuntimeException re = null; + List> futures = null; + try (ExecutorService es = newVirtualThreadPerTaskExecutor()) { + futures = es.invokeAll(tasks); + } catch (RuntimeException e) { + re = e; + } catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + re = new IllegalStateException(e.getMessage(), e); + } finally { + try { + secrets.close(); + } catch (RuntimeException e) { + if (re == null) { + throw e; + } else { + re.addSuppressed(e); + } + } catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + if (re == null) { + throw new IllegalStateException(e.getMessage(), e); + } else { + re.addSuppressed(e); + } + } + if (re != null) { + throw re; + } + } + assert futures != null; + for (Future future : futures) { + try { + future.get(); + } catch (RuntimeException e) { + if (re == null) { + re = e; + } else { + re.addSuppressed(e); + } + } catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + if (re == null) { + re = new IllegalStateException(e.getMessage(), e); + } else { + re.addSuppressed(e); + } + } + } + if (re != null) { + throw re; + } + } + + @SuppressWarnings("checkstyle:linelength") + private static ValueNode valueNode(Secrets s, SecretSummary ss, Base64.Decoder d) { + return + ValueNode.create(new String(d.decode(((Base64SecretBundleContentDetails) (s.getSecretBundle(GetSecretBundleRequest.builder() + .secretId(ss.getId()) + .build()) + .getSecretBundle() + .getSecretBundleContent())) + .getContent()), + UTF_8) + .intern()); + } + /* * Inner and nested classes. From 4726f65a232a531b046f515f5a8e74aaf66fb93f Mon Sep 17 00:00:00 2001 From: Laird Nelson Date: Wed, 16 Aug 2023 16:59:54 -0700 Subject: [PATCH 05/15] Squashable commit; renames invokeAll method to completeAllSecretsTasks for more clarity Signed-off-by: Laird Nelson --- .../secrets/configsource/OciSecretsConfigSourceProvider.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java index be70c4b2867..75af0ec26f6 100644 --- a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java +++ b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java @@ -280,7 +280,7 @@ private SecretBundleConfigSource(Builder b) { } } SecretBundleConfigSource.this.mostDistantExpirationInstant = mostDistantExpirationInstant; // volatile write - invokeAll(tasks, secrets); + completeAllSecretsTasks(tasks, secrets); ObjectNode.Builder onb = ObjectNode.builder(); for (Entry e : valueNodes.entrySet()) { onb.addValue(e.getKey(), e.getValue()); @@ -321,7 +321,7 @@ private static Builder builder() { return new Builder(); } - private static void invokeAll(Collection> tasks, AutoCloseable secrets) { + private static void completeAllSecretsTasks(Collection> tasks, AutoCloseable secrets) { RuntimeException re = null; List> futures = null; try (ExecutorService es = newVirtualThreadPerTaskExecutor()) { From 4a0c0e27116ef0c1cfd6131282c85b3cb84b0f27 Mon Sep 17 00:00:00 2001 From: Laird Nelson Date: Wed, 16 Aug 2023 17:21:30 -0700 Subject: [PATCH 06/15] Squashable commit; refactors error handling and secret summary acquisition Signed-off-by: Laird Nelson --- .../OciSecretsConfigSourceProvider.java | 80 +++++++++---------- 1 file changed, 38 insertions(+), 42 deletions(-) diff --git a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java index 75af0ec26f6..2ce1527e40b 100644 --- a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java +++ b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java @@ -20,7 +20,6 @@ import java.util.ArrayList; import java.util.Base64; import java.util.Collection; -import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; @@ -56,7 +55,6 @@ import com.oracle.bmc.vault.VaultsClient; import com.oracle.bmc.vault.model.SecretSummary; import com.oracle.bmc.vault.requests.ListSecretsRequest; -import com.oracle.bmc.vault.responses.ListSecretsResponse; import static io.helidon.integrations.oci.sdk.runtime.OciExtension.ociAuthenticationProvider; import static java.lang.System.Logger.Level.WARNING; @@ -217,11 +215,11 @@ public double weight() { private static final class SecretBundleConfigSource extends AbstractConfigSource implements NodeConfigSource, PollableSource { - private static final String COMPARTMENT_OCID_PROPERTY_NAME = "compartment-ocid"; - private static final Optional ABSENT_NODE_CONTENT = Optional.of(NodeContent.builder().node(ObjectNode.empty()).build()); + private static final String COMPARTMENT_OCID_PROPERTY_NAME = "compartment-ocid"; + private static final Logger LOGGER = System.getLogger(SecretBundleConfigSource.class.getName()); private static final String VAULT_OCID_PROPERTY_NAME = "vault-ocid"; @@ -247,27 +245,16 @@ private SecretBundleConfigSource(Builder b) { .vaultId(vaultOcid) .build(); this.loader = () -> { - ListSecretsResponse listSecretsResponse; - try (Vaults v = vaultsSupplier.get()) { - listSecretsResponse = v.listSecrets(listSecretsRequest); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - if (e instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - throw new IllegalStateException(e.getMessage(), e); - } - Collection secretSummaries = listSecretsResponse.getItems(); + Collection secretSummaries = secretSummaries(vaultsSupplier, listSecretsRequest); if (secretSummaries == null || secretSummaries.isEmpty()) { return ABSENT_NODE_CONTENT; } Map valueNodes = new ConcurrentHashMap<>(); Collection> tasks = new ArrayList<>(secretSummaries.size()); - Instant mostDistantExpirationInstant = - SecretBundleConfigSource.this.mostDistantExpirationInstant; // volatile read Base64.Decoder decoder = Base64.getDecoder(); Secrets secrets = secretsSupplier.get(); + Instant mostDistantExpirationInstant = + SecretBundleConfigSource.this.mostDistantExpirationInstant; // volatile read for (SecretSummary ss : secretSummaries) { tasks.add(() -> { valueNodes.put(ss.getSecretName(), valueNode(secrets, ss, decoder)); @@ -323,9 +310,27 @@ private static Builder builder() { private static void completeAllSecretsTasks(Collection> tasks, AutoCloseable secrets) { RuntimeException re = null; - List> futures = null; try (ExecutorService es = newVirtualThreadPerTaskExecutor()) { - futures = es.invokeAll(tasks); + for (Future future : es.invokeAll(tasks)) { + try { + future.get(); + } catch (RuntimeException e) { + if (re == null) { + re = e; + } else { + re.addSuppressed(e); + } + } catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + if (re == null) { + re = new IllegalStateException(e.getMessage(), e); + } else { + re.addSuppressed(e); + } + } + } } catch (RuntimeException e) { re = e; } catch (Exception e) { @@ -356,29 +361,20 @@ private static void completeAllSecretsTasks(Collection> throw re; } } - assert futures != null; - for (Future future : futures) { - try { - future.get(); - } catch (RuntimeException e) { - if (re == null) { - re = e; - } else { - re.addSuppressed(e); - } - } catch (Exception e) { - if (e instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - if (re == null) { - re = new IllegalStateException(e.getMessage(), e); - } else { - re.addSuppressed(e); - } + } + + @SuppressWarnings("try") + private static Collection secretSummaries(Supplier vaultsSupplier, + ListSecretsRequest listSecretsRequest) { + try (Vaults v = vaultsSupplier.get()) { + return v.listSecrets(listSecretsRequest).getItems(); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); } - } - if (re != null) { - throw re; + throw new IllegalStateException(e.getMessage(), e); } } From fad43f521c54a425595f9d1ebd6dcfbf38a5867d Mon Sep 17 00:00:00 2001 From: Laird Nelson Date: Wed, 16 Aug 2023 17:43:42 -0700 Subject: [PATCH 07/15] Squashable commit; cleans up documentation, refactors a few lambdas, etc. Signed-off-by: Laird Nelson --- .../OciSecretsConfigSourceProvider.java | 81 +++++++++++++++---- 1 file changed, 65 insertions(+), 16 deletions(-) diff --git a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java index 2ce1527e40b..e22562490d2 100644 --- a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java +++ b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java @@ -139,11 +139,13 @@ public OciSecretsConfigSourceProvider() { * @param type one of the {@linkplain #supported() supported types}, or an {@linkplain ConfigSources#empty() empty * ConfigSource} will be returned * - * @param metaConfig a {@link Config} serving as meta-configuration for this provider; must not be {@code null} + * @param metaConfig a {@link Config} serving as meta-configuration for this provider; must not be {@code null} when + * {@code type} is {@linkplain #supports(String) supported} * * @return a non-{@code null} {@link ConfigSource} * - * @exception NullPointerException if {@code metaConfig} is {@code null} + * @exception NullPointerException if {@code type} is {@linkplain #supports(String) supported} and {@code + * metaConfig} is {@code null} * * @see #supported() * @@ -215,6 +217,12 @@ public double weight() { private static final class SecretBundleConfigSource extends AbstractConfigSource implements NodeConfigSource, PollableSource { + + /* + * Static fields. + */ + + private static final Optional ABSENT_NODE_CONTENT = Optional.of(NodeContent.builder().node(ObjectNode.empty()).build()); @@ -224,21 +232,33 @@ private static final class SecretBundleConfigSource extends AbstractConfigSource private static final String VAULT_OCID_PROPERTY_NAME = "vault-ocid"; + + /* + * Instance fields. + */ + + private final Supplier> loader; private volatile Instant mostDistantExpirationInstant; + + /* + * Constructors. + */ + + @SuppressWarnings("try") private SecretBundleConfigSource(Builder b) { super(b); - String compartmentOcid = b.compartmentOcid; Supplier secretsSupplier = Objects.requireNonNull(b.secretsSupplier, "b.secretsSupplier"); - String vaultOcid = b.vaultOcid; Supplier vaultsSupplier = Objects.requireNonNull(b.vaultsSupplier, "b.vaultsSupplier"); + String compartmentOcid = b.compartmentOcid; + String vaultOcid = b.vaultOcid; if (compartmentOcid == null || vaultOcid == null) { // (It is not immediately clear why the OCI Java SDK requires a Compartment OCID, since a Vault OCID is // sufficient to uniquely identify any Vault.) - this.loader = () -> ABSENT_NODE_CONTENT; + this.loader = this::absentNodeContent; } else { ListSecretsRequest listSecretsRequest = ListSecretsRequest.builder() .compartmentId(compartmentOcid) @@ -247,7 +267,7 @@ private SecretBundleConfigSource(Builder b) { this.loader = () -> { Collection secretSummaries = secretSummaries(vaultsSupplier, listSecretsRequest); if (secretSummaries == null || secretSummaries.isEmpty()) { - return ABSENT_NODE_CONTENT; + return this.absentNodeContent(); } Map valueNodes = new ConcurrentHashMap<>(); Collection> tasks = new ArrayList<>(secretSummaries.size()); @@ -279,6 +299,12 @@ private SecretBundleConfigSource(Builder b) { } } + + /* + * Instance methods. + */ + + @Deprecated // For use by the Helidon Config subsystem only. @Override // PollableSource public boolean isModified(Instant instant) { @@ -298,6 +324,11 @@ public Optional pollingStrategy() { return super.pollingStrategy(); } + private Optional absentNodeContent() { + this.mostDistantExpirationInstant = null; // volatile write + return ABSENT_NODE_CONTENT; + } + /* * Static methods. @@ -399,6 +430,12 @@ private static ValueNode valueNode(Secrets s, SecretSummary ss, Base64.Decoder d private static final class Builder extends AbstractConfigSourceBuilder { + + /* + * Instance fields. + */ + + private String compartmentOcid; private Supplier secretsSupplier; @@ -407,6 +444,12 @@ private static final class Builder extends AbstractConfigSourceBuilder vaultsSupplier; + + /* + * Constructors. + */ + + private Builder() { super(); Supplier adpSupplier = @@ -417,21 +460,18 @@ private Builder() { this.vaultsSupplier = () -> vcb.build(adpSupplier.get()); } - private SecretBundleConfigSource build() { - return new SecretBundleConfigSource(this); - } - private Builder compartmentOcid(String compartmentOcid) { - this.compartmentOcid = Objects.requireNonNull(compartmentOcid, "compartmentOcid"); - return this; - } + /* + * Instance methods. + */ + @Override // AbstractConfigSourceBuilder protected Builder config(Config metaConfig) { metaConfig.get("compartment-ocid") .asString() .flatMap(s -> s.isBlank() ? Optional.empty() : Optional.of(s)) - .ifPresentOrElse(ocid -> this.compartmentOcid(ocid), + .ifPresentOrElse(this::compartmentOcid, () -> { if (LOGGER.isLoggable(WARNING)) { LOGGER.log(WARNING, @@ -442,8 +482,8 @@ protected Builder config(Config metaConfig) { }); metaConfig.get("vault-ocid") .asString() - .flatMap(s -> s.isBlank() ? Optional.empty() : Optional.of(s)) - .ifPresentOrElse(ocid -> this.vaultOcid(ocid), + .flatMap(s -> s.isBlank() ? Optional.empty() : Optional.of(s)) + .ifPresentOrElse(this::vaultOcid, () -> { if (LOGGER.isLoggable(WARNING)) { LOGGER.log(WARNING, @@ -455,6 +495,15 @@ protected Builder config(Config metaConfig) { return super.config(metaConfig); } + private SecretBundleConfigSource build() { + return new SecretBundleConfigSource(this); + } + + private Builder compartmentOcid(String compartmentOcid) { + this.compartmentOcid = Objects.requireNonNull(compartmentOcid, "compartmentOcid"); + return this; + } + private Builder secretsSupplier(Supplier secretsSupplier) { this.secretsSupplier = Objects.requireNonNull(secretsSupplier, "secretsSupplier"); return this; From 275b6bb0867193dfbdd45e70a3c491291d150ac5 Mon Sep 17 00:00:00 2001 From: Laird Nelson Date: Thu, 17 Aug 2023 09:15:43 -0700 Subject: [PATCH 08/15] Squashable commit; addresses some PR feedback Signed-off-by: Laird Nelson --- .../oci/oci-secrets-config-source/pom.xml | 2 +- .../OciSecretsConfigSourceProvider.java | 189 +++++++++--------- .../oci/secrets/configsource/UsageTest.java | 2 +- 3 files changed, 100 insertions(+), 93 deletions(-) diff --git a/integrations/oci/oci-secrets-config-source/pom.xml b/integrations/oci/oci-secrets-config-source/pom.xml index 72cfce69fb7..63590540185 100644 --- a/integrations/oci/oci-secrets-config-source/pom.xml +++ b/integrations/oci/oci-secrets-config-source/pom.xml @@ -24,7 +24,7 @@ 4.0.0-SNAPSHOT helidon-integrations-oci-secrets-config-source - Helidon Config OCI Secrets ConfigSourceProvider + Helidon Integrations OCI Secrets Config Source OCI Secrets Retrieval API ConfigSourceProvider Implementation diff --git a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java index e22562490d2..83de80447cb 100644 --- a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java +++ b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java @@ -27,8 +27,10 @@ import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; +import java.util.function.Predicate; import java.util.function.Supplier; import io.helidon.common.LazyValue; @@ -36,7 +38,6 @@ import io.helidon.config.AbstractConfigSource; import io.helidon.config.AbstractConfigSourceBuilder; import io.helidon.config.Config; -import io.helidon.config.ConfigSources; import io.helidon.config.spi.ConfigContent.NodeContent; import io.helidon.config.spi.ConfigNode.ObjectNode; import io.helidon.config.spi.ConfigNode.ValueNode; @@ -50,6 +51,7 @@ import com.oracle.bmc.secrets.Secrets; import com.oracle.bmc.secrets.SecretsClient; import com.oracle.bmc.secrets.model.Base64SecretBundleContentDetails; +import com.oracle.bmc.secrets.model.SecretBundleContentDetails; import com.oracle.bmc.secrets.requests.GetSecretBundleRequest; import com.oracle.bmc.vault.Vaults; import com.oracle.bmc.vault.VaultsClient; @@ -78,12 +80,15 @@ *
    * *
  1. Ensure you have an authentication mechanism set up to connect to OCI (e.g. a valid OCI configuration file)
  2. + * href="https://docs.oracle.com/en-us/iaas/Content/API/Concepts/sdkconfig.htm">OCI configuration + * file). Authentication with OCI is accomplished via the {@link + * io.helidon.integrations.oci.sdk.runtime.OciExtension} class; please see its documentation for how and when to set up + * an {@code oci.yaml} classpath resource to further refine the mechanism of authentication. * *
  3. Ensure there is a classpath resource present named {@code meta-config.yaml}.
  4. * - *
  5. Ensure the {@code meta-config.yaml} classpath resource contains a {@code source} with a {@code type} of {@code - * oci-secrets} that looks similar to the following, substituting values as appropriate:
    sources:
    + * 
  6. Ensure the {@code meta-config.yaml} classpath resource contains a {@code sources} element with a {@code type} of + * {@code oci-secrets} that looks similar to the following, substituting values as appropriate:
    sources:
      *  - type: 'oci-secrets'
      *    properties:
      *      compartment-ocid: 'your vault compartment OCID goes here'
    @@ -103,8 +108,6 @@ public final class OciSecretsConfigSourceProvider implements ConfigSourceProvide
          */
     
     
    -    private static final Logger LOGGER = System.getLogger(OciSecretsConfigSourceProvider.class.getName());
    -
         private static final Set SUPPORTED_TYPES = Set.of("oci-secrets");
     
         private static final double WEIGHT = 300D;
    @@ -136,8 +139,7 @@ public OciSecretsConfigSourceProvider() {
          * Infrastructure (OCI) Vault.
          *
    -     * @param type one of the {@linkplain #supported() supported types}, or an {@linkplain ConfigSources#empty() empty
    -     * ConfigSource} will be returned
    +     * @param type one of the {@linkplain #supported() supported types}; not actually used
          *
          * @param metaConfig a {@link Config} serving as meta-configuration for this provider; must not be {@code null} when
          * {@code type} is {@linkplain #supports(String) supported}
    @@ -154,7 +156,7 @@ public OciSecretsConfigSourceProvider() {
         @Deprecated // For use by the Helidon Config subsystem only.
         @Override // ConfigSourceProvider
         public ConfigSource create(String type, Config metaConfig) {
    -        return this.supports(type) ? SecretBundleConfigSource.builder().config(metaConfig).build() : ConfigSources.empty();
    +        return SecretBundleConfigSource.builder().config(metaConfig).build();
         }
     
         /**
    @@ -176,13 +178,13 @@ public Set supported() {
         }
     
         /**
    -     * Returns {@code true} if and only if the supplied {@code type} is non-{@code null} and the {@link Set} returned by
    -     * an invocation of the {@link #supported()} method {@linkplain Set#contains(Object) contains} it.
    +     * Returns {@code true} if and only if the {@link Set} returned by an invocation of the {@link #supported()} method
    +     * {@linkplain Set#contains(Object) contains} it.
          *
    -     * @param type the type to test; may be {@code null} in which case {@code false} will be returned
    +     * @param type the type to test
          *
    -     * @return {@code true} if and only if the supplied {@code type} is non-{@code null} and the {@link Set} returned by
    -     * an invocation of the {@link #supported()} method {@linkplain Set#contains(Object) contains} it
    +     * @return {@code true} if and only if the {@link Set} returned by an invocation of the {@link #supported()} method
    +     * {@linkplain Set#contains(Object) contains} it
          *
          * @see #supported()
          *
    @@ -193,7 +195,7 @@ public Set supported() {
         @Deprecated // For use by the Helidon Config subsystem only.
         @Override // ConfigSourceProvider
         public boolean supports(String type) {
    -        return type != null && this.supported().contains(type);
    +        return this.supported().contains(type);
         }
     
         /**
    @@ -215,7 +217,8 @@ public double weight() {
          */
     
     
    -    private static final class SecretBundleConfigSource extends AbstractConfigSource implements NodeConfigSource, PollableSource {
    +    private static final class SecretBundleConfigSource
    +        extends AbstractConfigSource implements NodeConfigSource, PollableSource {
     
     
             /*
    @@ -264,38 +267,7 @@ private SecretBundleConfigSource(Builder b) {
                         .compartmentId(compartmentOcid)
                         .vaultId(vaultOcid)
                         .build();
    -                this.loader = () -> {
    -                    Collection secretSummaries = secretSummaries(vaultsSupplier, listSecretsRequest);
    -                    if (secretSummaries == null || secretSummaries.isEmpty()) {
    -                        return this.absentNodeContent();
    -                    }
    -                    Map valueNodes = new ConcurrentHashMap<>();
    -                    Collection> tasks = new ArrayList<>(secretSummaries.size());
    -                    Base64.Decoder decoder = Base64.getDecoder();
    -                    Secrets secrets = secretsSupplier.get();
    -                    Instant mostDistantExpirationInstant =
    -                        SecretBundleConfigSource.this.mostDistantExpirationInstant; // volatile read
    -                    for (SecretSummary ss : secretSummaries) {
    -                        tasks.add(() -> {
    -                                valueNodes.put(ss.getSecretName(), valueNode(secrets, ss, decoder));
    -                                return null;
    -                            });
    -                        java.util.Date d = ss.getTimeOfCurrentVersionExpiry();
    -                        Instant i = d == null ? null : d.toInstant();
    -                        if (i != null && (mostDistantExpirationInstant == null || mostDistantExpirationInstant.isBefore(i))) {
    -                            mostDistantExpirationInstant = i;
    -                        }
    -                    }
    -                    SecretBundleConfigSource.this.mostDistantExpirationInstant = mostDistantExpirationInstant; // volatile write
    -                    completeAllSecretsTasks(tasks, secrets);
    -                    ObjectNode.Builder onb = ObjectNode.builder();
    -                    for (Entry e : valueNodes.entrySet()) {
    -                        onb.addValue(e.getKey(), e.getValue());
    -                    }
    -                    return Optional.of(NodeContent.builder()
    -                                       .node(onb.build())
    -                                       .build());
    -                };
    +                this.loader = () -> this.load(vaultsSupplier, secretsSupplier, listSecretsRequest);
                 }
             }
     
    @@ -329,6 +301,41 @@ private Optional absentNodeContent() {
                 return ABSENT_NODE_CONTENT;
             }
     
    +        private Optional load(Supplier vaultsSupplier,
    +                                           Supplier secretsSupplier,
    +                                           ListSecretsRequest listSecretsRequest) {
    +            Collection secretSummaries = secretSummaries(vaultsSupplier, listSecretsRequest);
    +            if (secretSummaries == null || secretSummaries.isEmpty()) {
    +                return this.absentNodeContent();
    +            }
    +            Map valueNodes = new ConcurrentHashMap<>();
    +            Collection> tasks = new ArrayList<>(secretSummaries.size());
    +            Base64.Decoder decoder = Base64.getDecoder();
    +            Secrets secrets = secretsSupplier.get();
    +            Instant mostDistantExpirationInstant =
    +                SecretBundleConfigSource.this.mostDistantExpirationInstant; // volatile read
    +            for (SecretSummary ss : secretSummaries) {
    +                tasks.add(() -> {
    +                        valueNodes.put(ss.getSecretName(), valueNode(secrets, ss, decoder));
    +                        return null;
    +                    });
    +                java.util.Date d = ss.getTimeOfCurrentVersionExpiry();
    +                Instant i = d == null ? null : d.toInstant();
    +                if (i != null && (mostDistantExpirationInstant == null || mostDistantExpirationInstant.isBefore(i))) {
    +                    mostDistantExpirationInstant = i;
    +                }
    +            }
    +            SecretBundleConfigSource.this.mostDistantExpirationInstant = mostDistantExpirationInstant; // volatile write
    +            completeAllSecretsTasks(tasks, secrets);
    +            ObjectNode.Builder onb = ObjectNode.builder();
    +            for (Entry e : valueNodes.entrySet()) {
    +                onb.addValue(e.getKey(), e.getValue());
    +            }
    +            return Optional.of(NodeContent.builder()
    +                               .node(onb.build())
    +                               .build());
    +        }
    +
     
             /*
              * Static methods.
    @@ -339,58 +346,60 @@ private static Builder builder() {
                 return new Builder();
             }
     
    +        private static void closeUnchecked(AutoCloseable c) {
    +            try {
    +                c.close();
    +            } catch (InterruptedException e) {
    +                Thread.currentThread().interrupt();
    +                throw new IllegalStateException(e.getMessage(), e);
    +            } catch (Exception e) {
    +                throw new IllegalStateException(e.getMessage(), e);
    +            }
    +        }
    +
             private static void completeAllSecretsTasks(Collection> tasks, AutoCloseable secrets) {
                 RuntimeException re = null;
                 try (ExecutorService es = newVirtualThreadPerTaskExecutor()) {
                     for (Future future : es.invokeAll(tasks)) {
                         try {
    -                        future.get();
    +                        futureGetUnchecked(future);
                         } catch (RuntimeException e) {
                             if (re == null) {
                                 re = e;
                             } else {
                                 re.addSuppressed(e);
                             }
    -                    } catch (Exception e) {
    -                        if (e instanceof InterruptedException) {
    -                            Thread.currentThread().interrupt();
    -                        }
    -                        if (re == null) {
    -                            re = new IllegalStateException(e.getMessage(), e);
    -                        } else {
    -                            re.addSuppressed(e);
    -                        }
                         }
                     }
                 } catch (RuntimeException e) {
                     re = e;
    -            } catch (Exception e) {
    -                if (e instanceof InterruptedException) {
    -                    Thread.currentThread().interrupt();
    -                }
    +            } catch (InterruptedException e) {
    +                Thread.currentThread().interrupt();
                     re = new IllegalStateException(e.getMessage(), e);
                 } finally {
                     try {
    -                    secrets.close();
    +                    closeUnchecked(secrets);
                     } catch (RuntimeException e) {
                         if (re == null) {
    -                        throw e;
    -                    } else {
    -                        re.addSuppressed(e);
    -                    }
    -                } catch (Exception e) {
    -                    if (e instanceof InterruptedException) {
    -                        Thread.currentThread().interrupt();
    -                    }
    -                    if (re == null) {
    -                        throw new IllegalStateException(e.getMessage(), e);
    +                        re = e;
                         } else {
                             re.addSuppressed(e);
                         }
                     }
    -                if (re != null) {
    -                    throw re;
    -                }
    +            }
    +            if (re != null) {
    +                throw re;
    +            }
    +        }
    +
    +        private static  T futureGetUnchecked(Future future) {
    +            try {
    +                return future.get();
    +            } catch (ExecutionException e) {
    +                throw new IllegalStateException(e.getMessage(), e);
    +            } catch (InterruptedException e) {
    +                Thread.currentThread().interrupt();
    +                throw new IllegalStateException(e.getMessage(), e);
                 }
             }
     
    @@ -401,25 +410,23 @@ private static Collection secretSummaries(Supplier s.isBlank() ? Optional.empty() : Optional.of(s))
    +                    .filter(Predicate.not(String::isBlank))
                         .ifPresentOrElse(this::compartmentOcid,
                                          () -> {
                                              if (LOGGER.isLoggable(WARNING)) {
    @@ -482,7 +489,7 @@ protected Builder config(Config metaConfig) {
                                          });
                     metaConfig.get("vault-ocid")
                         .asString()
    -                    .flatMap(s -> s.isBlank() ? Optional.empty() : Optional.of(s))
    +                    .filter(Predicate.not(String::isBlank))
                         .ifPresentOrElse(this::vaultOcid,
                                          () -> {
                                              if (LOGGER.isLoggable(WARNING)) {
    diff --git a/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/UsageTest.java b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/UsageTest.java
    index 1e7a78abb43..c0023352aee 100644
    --- a/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/UsageTest.java
    +++ b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/UsageTest.java
    @@ -29,7 +29,7 @@
     import static org.junit.jupiter.api.Assumptions.assumeFalse;
     import static org.junit.jupiter.api.Assumptions.assumeTrue;
     
    -final class UsageTest {
    +class UsageTest {
     
         private UsageTest() {
             super();
    
    From a7d8cb8281609b02b5e7b95361b5747d6800ce6f Mon Sep 17 00:00:00 2001
    From: Laird Nelson 
    Date: Thu, 17 Aug 2023 09:28:07 -0700
    Subject: [PATCH 09/15] Squashable commit; documents some warnings suppressions
    
    Signed-off-by: Laird Nelson 
    ---
     .../secrets/configsource/OciSecretsConfigSourceProvider.java    | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java
    index 83de80447cb..a1f8a70b981 100644
    --- a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java
    +++ b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java
    @@ -251,7 +251,6 @@ private static final class SecretBundleConfigSource
              */
     
     
    -        @SuppressWarnings("try")
             private SecretBundleConfigSource(Builder b) {
                 super(b);
                 Supplier secretsSupplier = Objects.requireNonNull(b.secretsSupplier, "b.secretsSupplier");
    @@ -403,6 +402,7 @@ private static  T futureGetUnchecked(Future future) {
                 }
             }
     
    +        // Suppress "[try] auto-closeable resource Vaults has a member method close() that could throw InterruptedException"
             @SuppressWarnings("try")
             private static Collection secretSummaries(Supplier vaultsSupplier,
                                                                                ListSecretsRequest listSecretsRequest) {
    
    From 7360302783496c094f8560f78c910f4e1d4dfd40 Mon Sep 17 00:00:00 2001
    From: Laird Nelson 
    Date: Thu, 17 Aug 2023 09:50:59 -0700
    Subject: [PATCH 10/15] Squashable commit; removes Weighted implementation;
     replaces with Weight annotation per instruction
    
    Signed-off-by: Laird Nelson 
    ---
     .../OciSecretsConfigSourceProvider.java       | 25 ++++---------------
     1 file changed, 5 insertions(+), 20 deletions(-)
    
    diff --git a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java
    index a1f8a70b981..51fc2509135 100644
    --- a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java
    +++ b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java
    @@ -34,7 +34,7 @@
     import java.util.function.Supplier;
     
     import io.helidon.common.LazyValue;
    -import io.helidon.common.Weighted;
    +import io.helidon.common.Weight;
     import io.helidon.config.AbstractConfigSource;
     import io.helidon.config.AbstractConfigSourceBuilder;
     import io.helidon.config.Config;
    @@ -100,7 +100,8 @@
      *
      * @see ConfigSourceProvider
      */
    -public final class OciSecretsConfigSourceProvider implements ConfigSourceProvider, Weighted {
    +@Weight(300D) // a higher weight than default
    +public final class OciSecretsConfigSourceProvider implements ConfigSourceProvider {
     
     
         /*
    @@ -110,8 +111,6 @@ public final class OciSecretsConfigSourceProvider implements ConfigSourceProvide
     
         private static final Set SUPPORTED_TYPES = Set.of("oci-secrets");
     
    -    private static final double WEIGHT = 300D;
    -
     
         /*
          * Constructors.
    @@ -198,19 +197,6 @@ public boolean supports(String type) {
             return this.supported().contains(type);
         }
     
    -    /**
    -     * Returns a (determinate) weight ({@value #WEIGHT}) for this {@link OciSecretsConfigSourceProvider} when invoked.
    -     *
    -     * @return a determinate weight ({@value #WEIGHT}) when invoked
    -     *
    -     * @deprecated For use by the Helidon service loading subsystem only.
    -     */
    -    @Deprecated // For use by the Helidon service loading subsystem only.
    -    @Override // Weighted
    -    public double weight() {
    -        return WEIGHT;
    -    }
    -
     
         /*
          * Inner and nested classes.
    @@ -311,8 +297,7 @@ private Optional load(Supplier vaultsSupplier,
                 Collection> tasks = new ArrayList<>(secretSummaries.size());
                 Base64.Decoder decoder = Base64.getDecoder();
                 Secrets secrets = secretsSupplier.get();
    -            Instant mostDistantExpirationInstant =
    -                SecretBundleConfigSource.this.mostDistantExpirationInstant; // volatile read
    +            Instant mostDistantExpirationInstant = this.mostDistantExpirationInstant; // volatile read
                 for (SecretSummary ss : secretSummaries) {
                     tasks.add(() -> {
                             valueNodes.put(ss.getSecretName(), valueNode(secrets, ss, decoder));
    @@ -324,7 +309,7 @@ private Optional load(Supplier vaultsSupplier,
                         mostDistantExpirationInstant = i;
                     }
                 }
    -            SecretBundleConfigSource.this.mostDistantExpirationInstant = mostDistantExpirationInstant; // volatile write
    +            this.mostDistantExpirationInstant = mostDistantExpirationInstant; // volatile write
                 completeAllSecretsTasks(tasks, secrets);
                 ObjectNode.Builder onb = ObjectNode.builder();
                 for (Entry e : valueNodes.entrySet()) {
    
    From c9b1fad99e6956de55dca31eef40bd4d3436185c Mon Sep 17 00:00:00 2001
    From: Laird Nelson 
    Date: Thu, 17 Aug 2023 10:36:39 -0700
    Subject: [PATCH 11/15] Squashable commit; replicates dependency convergence
     shenanigans in pom.xml from other OCI projects
    
    Signed-off-by: Laird Nelson 
    ---
     .../oci/oci-secrets-config-source/pom.xml     | 36 +++++++++----------
     .../OciSecretsConfigSourceProvider.java       | 14 ++++----
     2 files changed, 24 insertions(+), 26 deletions(-)
    
    diff --git a/integrations/oci/oci-secrets-config-source/pom.xml b/integrations/oci/oci-secrets-config-source/pom.xml
    index 63590540185..f2bc7b919b6 100644
    --- a/integrations/oci/oci-secrets-config-source/pom.xml
    +++ b/integrations/oci/oci-secrets-config-source/pom.xml
    @@ -36,25 +36,6 @@
             
         
     
    -    
    -        
    -            
    -            
    -                org.apache.httpcomponents
    -                httpclient
    -                4.5.14
    -            
    -            
    -                org.apache.httpcomponents
    -                httpcore
    -                4.4.16
    -            
    -        
    -    
    -
         
     
             
    @@ -90,6 +71,23 @@
                 com.oracle.oci.sdk
                 oci-java-sdk-common-httpclient-jersey3
                 runtime
    +            
    +                
    +                
    +                    org.apache.httpcomponents
    +                    httpclient
    +                
    +                
    +                    org.apache.httpcomponents
    +                    httpcore
    +                
    +            
    +        
    +        
    +        
    +            org.glassfish.jersey.connectors
    +            jersey-apache-connector
    +            runtime
             
     
             
    diff --git a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java
    index 51fc2509135..3b3c6c2ae60 100644
    --- a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java
    +++ b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java
    @@ -229,7 +229,7 @@ private static final class SecretBundleConfigSource
     
             private final Supplier> loader;
     
    -        private volatile Instant mostDistantExpirationInstant;
    +        private volatile Instant closestExpirationInstant;
     
     
             /*
    @@ -265,7 +265,7 @@ private SecretBundleConfigSource(Builder b) {
             @Deprecated // For use by the Helidon Config subsystem only.
             @Override // PollableSource
             public boolean isModified(Instant instant) {
    -            Instant i = this.mostDistantExpirationInstant;
    +            Instant i = this.closestExpirationInstant;
                 return i == null || i.isBefore(instant == null ? now() : instant);
             }
     
    @@ -282,7 +282,7 @@ public Optional pollingStrategy() {
             }
     
             private Optional absentNodeContent() {
    -            this.mostDistantExpirationInstant = null; // volatile write
    +            this.closestExpirationInstant = null; // volatile write
                 return ABSENT_NODE_CONTENT;
             }
     
    @@ -297,7 +297,7 @@ private Optional load(Supplier vaultsSupplier,
                 Collection> tasks = new ArrayList<>(secretSummaries.size());
                 Base64.Decoder decoder = Base64.getDecoder();
                 Secrets secrets = secretsSupplier.get();
    -            Instant mostDistantExpirationInstant = this.mostDistantExpirationInstant; // volatile read
    +            Instant closestExpirationInstant = this.closestExpirationInstant; // volatile read
                 for (SecretSummary ss : secretSummaries) {
                     tasks.add(() -> {
                             valueNodes.put(ss.getSecretName(), valueNode(secrets, ss, decoder));
    @@ -305,11 +305,11 @@ private Optional load(Supplier vaultsSupplier,
                         });
                     java.util.Date d = ss.getTimeOfCurrentVersionExpiry();
                     Instant i = d == null ? null : d.toInstant();
    -                if (i != null && (mostDistantExpirationInstant == null || mostDistantExpirationInstant.isBefore(i))) {
    -                    mostDistantExpirationInstant = i;
    +                if (i != null && (closestExpirationInstant == null || i.isBefore(closestExpirationInstant))) {
    +                    closestExpirationInstant = i;
                     }
                 }
    -            this.mostDistantExpirationInstant = mostDistantExpirationInstant; // volatile write
    +            this.closestExpirationInstant = closestExpirationInstant; // volatile write
                 completeAllSecretsTasks(tasks, secrets);
                 ObjectNode.Builder onb = ObjectNode.builder();
                 for (Entry e : valueNodes.entrySet()) {
    
    From bd33f8dea32aedcda3b242905e92c6b5b5a7e651 Mon Sep 17 00:00:00 2001
    From: Laird Nelson 
    Date: Thu, 17 Aug 2023 10:59:50 -0700
    Subject: [PATCH 12/15] Squashable commit; corrects isModified logic
    
    Signed-off-by: Laird Nelson 
    ---
     .../OciSecretsConfigSourceProvider.java       | 25 +++++++++++--------
     1 file changed, 14 insertions(+), 11 deletions(-)
    
    diff --git a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java
    index 3b3c6c2ae60..62942ccb026 100644
    --- a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java
    +++ b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java
    @@ -100,7 +100,7 @@
      *
      * @see ConfigSourceProvider
      */
    -@Weight(300D) // a higher weight than default
    +@Weight(300D) // a higher weight than the default (100D)
     public final class OciSecretsConfigSourceProvider implements ConfigSourceProvider {
     
     
    @@ -229,7 +229,7 @@ private static final class SecretBundleConfigSource
     
             private final Supplier> loader;
     
    -        private volatile Instant closestExpirationInstant;
    +        private volatile Instant closestSecretExpirationInstant;
     
     
             /*
    @@ -241,6 +241,7 @@ private SecretBundleConfigSource(Builder b) {
                 super(b);
                 Supplier secretsSupplier = Objects.requireNonNull(b.secretsSupplier, "b.secretsSupplier");
                 Supplier vaultsSupplier = Objects.requireNonNull(b.vaultsSupplier, "b.vaultsSupplier");
    +            this.closestSecretExpirationInstant = now();
                 String compartmentOcid = b.compartmentOcid;
                 String vaultOcid = b.vaultOcid;
                 if (compartmentOcid == null || vaultOcid == null) {
    @@ -264,9 +265,8 @@ private SecretBundleConfigSource(Builder b) {
     
             @Deprecated // For use by the Helidon Config subsystem only.
             @Override // PollableSource
    -        public boolean isModified(Instant instant) {
    -            Instant i = this.closestExpirationInstant;
    -            return i == null || i.isBefore(instant == null ? now() : instant);
    +        public boolean isModified(Instant pollInstant) {
    +            return isModified(pollInstant, this.closestSecretExpirationInstant); // volatile read
             }
     
             @Deprecated // For use by the Helidon Config subsystem only.
    @@ -282,7 +282,6 @@ public Optional pollingStrategy() {
             }
     
             private Optional absentNodeContent() {
    -            this.closestExpirationInstant = null; // volatile write
                 return ABSENT_NODE_CONTENT;
             }
     
    @@ -297,19 +296,19 @@ private Optional load(Supplier vaultsSupplier,
                 Collection> tasks = new ArrayList<>(secretSummaries.size());
                 Base64.Decoder decoder = Base64.getDecoder();
                 Secrets secrets = secretsSupplier.get();
    -            Instant closestExpirationInstant = this.closestExpirationInstant; // volatile read
    +            Instant closestSecretExpirationInstant = this.closestSecretExpirationInstant; // volatile read
                 for (SecretSummary ss : secretSummaries) {
                     tasks.add(() -> {
                             valueNodes.put(ss.getSecretName(), valueNode(secrets, ss, decoder));
                             return null;
                         });
                     java.util.Date d = ss.getTimeOfCurrentVersionExpiry();
    -                Instant i = d == null ? null : d.toInstant();
    -                if (i != null && (closestExpirationInstant == null || i.isBefore(closestExpirationInstant))) {
    -                    closestExpirationInstant = i;
    +                Instant secretExpirationInstant = d == null ? null : d.toInstant();
    +                if (secretExpirationInstant != null && secretExpirationInstant.isBefore(closestSecretExpirationInstant)) {
    +                    closestSecretExpirationInstant = secretExpirationInstant;
                     }
                 }
    -            this.closestExpirationInstant = closestExpirationInstant; // volatile write
    +            this.closestSecretExpirationInstant = closestSecretExpirationInstant; // volatile write
                 completeAllSecretsTasks(tasks, secrets);
                 ObjectNode.Builder onb = ObjectNode.builder();
                 for (Entry e : valueNodes.entrySet()) {
    @@ -326,6 +325,10 @@ private Optional load(Supplier vaultsSupplier,
              */
     
     
    +        static boolean isModified(Instant pollInstant, Instant closestSecretExpirationInstant) {
    +            return closestSecretExpirationInstant.isBefore(pollInstant);
    +        }
    +
             private static Builder builder() {
                 return new Builder();
             }
    
    From 98c3582ac53596b02c2232bb4af2ed4de7a7bf0c Mon Sep 17 00:00:00 2001
    From: Laird Nelson 
    Date: Mon, 21 Aug 2023 00:31:23 -0700
    Subject: [PATCH 13/15] Squashable commit; addresses PR feedback
    
    Signed-off-by: Laird Nelson 
    ---
     .../OciSecretsConfigSourceProvider.java       | 119 ++++++++++++------
     .../secrets/configsource/IsModifiedTest.java  |  49 ++++++++
     .../secrets/configsource/ValueNodeTest.java   |  48 +++++++
     3 files changed, 180 insertions(+), 36 deletions(-)
     create mode 100644 integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/IsModifiedTest.java
     create mode 100644 integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/ValueNodeTest.java
    
    diff --git a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java
    index 62942ccb026..d2204dff864 100644
    --- a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java
    +++ b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java
    @@ -20,6 +20,7 @@
     import java.util.ArrayList;
     import java.util.Base64;
     import java.util.Collection;
    +import java.util.List;
     import java.util.Map;
     import java.util.Map.Entry;
     import java.util.Objects;
    @@ -30,6 +31,7 @@
     import java.util.concurrent.ExecutionException;
     import java.util.concurrent.ExecutorService;
     import java.util.concurrent.Future;
    +import java.util.function.Function;
     import java.util.function.Predicate;
     import java.util.function.Supplier;
     
    @@ -203,7 +205,7 @@ public boolean supports(String type) {
          */
     
     
    -    private static final class SecretBundleConfigSource
    +    static final class SecretBundleConfigSource
             extends AbstractConfigSource implements NodeConfigSource, PollableSource {
     
     
    @@ -289,7 +291,12 @@ private Optional load(Supplier vaultsSupplier,
                                                Supplier secretsSupplier,
                                                ListSecretsRequest listSecretsRequest) {
                 Collection secretSummaries = secretSummaries(vaultsSupplier, listSecretsRequest);
    -            if (secretSummaries == null || secretSummaries.isEmpty()) {
    +            return this.load(secretSummaries, secretsSupplier);
    +        }
    +
    +        private Optional load(Collection secretSummaries,
    +                                           Supplier secretsSupplier) {
    +            if (secretSummaries.isEmpty()) {
                     return this.absentNodeContent();
                 }
                 Map valueNodes = new ConcurrentHashMap<>();
    @@ -303,13 +310,23 @@ private Optional load(Supplier vaultsSupplier,
                             return null;
                         });
                     java.util.Date d = ss.getTimeOfCurrentVersionExpiry();
    +                // If d is null, which is permitted by the OCI Vaults API, you could interpret it as meaning "this
    +                // secret never ever expires, so never poll it for changes ever again". (This is sort of like if its
    +                // expiration time were set to the end of time.)
    +                //
    +                // Or you could interpret it as the much more common "this secret never had its expiration time set,
    +                // probably by mistake, or because it's a temporary scratch secret, or any of a zillion other possible
    +                // explanations, so we'd better check each poll time to see if the secret is still there". (This is sort
    +                // of like if its expiration time wer set to the beginning of time.)
    +                //
    +                // We opt for the latter interpretation.
                     Instant secretExpirationInstant = d == null ? null : d.toInstant();
                     if (secretExpirationInstant != null && secretExpirationInstant.isBefore(closestSecretExpirationInstant)) {
                         closestSecretExpirationInstant = secretExpirationInstant;
                     }
                 }
                 this.closestSecretExpirationInstant = closestSecretExpirationInstant; // volatile write
    -            completeAllSecretsTasks(tasks, secrets);
    +            completeTasks(tasks, secrets);
                 ObjectNode.Builder onb = ObjectNode.builder();
                 for (Entry e : valueNodes.entrySet()) {
                     onb.addValue(e.getKey(), e.getValue());
    @@ -325,47 +342,46 @@ private Optional load(Supplier vaultsSupplier,
              */
     
     
    -        static boolean isModified(Instant pollInstant, Instant closestSecretExpirationInstant) {
    -            return closestSecretExpirationInstant.isBefore(pollInstant);
    -        }
    -
             private static Builder builder() {
                 return new Builder();
             }
     
    -        private static void closeUnchecked(AutoCloseable c) {
    +        private static void closeUnchecked(AutoCloseable autoCloseable) {
                 try {
    -                c.close();
    +                autoCloseable.close();
    +            } catch (RuntimeException e) {
    +                throw e;
                 } catch (InterruptedException e) {
    +                // (Can legally be thrown by any AutoCloseable. Must preserve interrupt status.)
                     Thread.currentThread().interrupt();
                     throw new IllegalStateException(e.getMessage(), e);
                 } catch (Exception e) {
    +                // (Can legally be thrown by any AutoCloseable.)
                     throw new IllegalStateException(e.getMessage(), e);
                 }
             }
     
    -        private static void completeAllSecretsTasks(Collection> tasks, AutoCloseable secrets) {
    -            RuntimeException re = null;
    -            try (ExecutorService es = newVirtualThreadPerTaskExecutor()) {
    -                for (Future future : es.invokeAll(tasks)) {
    -                    try {
    -                        futureGetUnchecked(future);
    -                    } catch (RuntimeException e) {
    -                        if (re == null) {
    -                            re = e;
    -                        } else {
    -                            re.addSuppressed(e);
    -                        }
    -                    }
    -                }
    +        private static void completeTasks(Collection> tasks, AutoCloseable autoCloseable) {
    +            try (ExecutorService es = newVirtualThreadPerTaskExecutor();
    +                 autoCloseable) {
    +                completeTasks(es, tasks);
                 } catch (RuntimeException e) {
    -                re = e;
    +                throw e;
                 } catch (InterruptedException e) {
    +                // (Can legally be thrown by any AutoCloseable. Must preserve interrupt status.)
                     Thread.currentThread().interrupt();
    -                re = new IllegalStateException(e.getMessage(), e);
    -            } finally {
    +                throw new IllegalStateException(e.getMessage(), e);
    +            } catch (Exception e) {
    +                // (Can legally be thrown by any AutoCloseable.)
    +                throw new IllegalStateException(e.getMessage(), e);
    +            }
    +        }
    +
    +        private static void completeTasks(ExecutorService es, Collection> tasks) {
    +            RuntimeException re = null;
    +            for (Future future : invokeAllUnchecked(es, tasks)) {
                     try {
    -                    closeUnchecked(secrets);
    +                    futureGetUnchecked(future);
                     } catch (RuntimeException e) {
                         if (re == null) {
                             re = e;
    @@ -390,6 +406,23 @@ private static  T futureGetUnchecked(Future future) {
                 }
             }
     
    +        private static  List> invokeAllUnchecked(ExecutorService es, Collection> tasks) {
    +            try {
    +                return es.invokeAll(tasks);
    +            } catch (InterruptedException e) {
    +                Thread.currentThread().interrupt();
    +                throw new IllegalStateException(e.getMessage(), e);
    +            }
    +        }
    +
    +        static boolean isModified(Instant pollInstant, Instant closestSecretExpirationInstant) {
    +            return closestSecretExpirationInstant.isBefore(pollInstant);
    +        }
    +
    +        private static GetSecretBundleRequest request(String secretId) {
    +            return GetSecretBundleRequest.builder().secretId(secretId).build();
    +        }
    +
             // Suppress "[try] auto-closeable resource Vaults has a member method close() that could throw InterruptedException"
             @SuppressWarnings("try")
             private static Collection secretSummaries(Supplier vaultsSupplier,
    @@ -399,22 +432,36 @@ private static Collection secretSummaries(Supplier s.getSecretBundle(r).getSecretBundle().getSecretBundleContent(),
    +                          ss.getId(),
    +                          base64Decoder);
    +        }
    +
    +        private static ValueNode valueNode(Function f,
    +                                           String secretId,
    +                                           Base64.Decoder base64Decoder) {
    +            return valueNode((Base64SecretBundleContentDetails) f.apply(request(secretId)), base64Decoder);
    +        }
    +
    +        private static ValueNode valueNode(Base64SecretBundleContentDetails details,
    +                                           Base64.Decoder base64Decoder) {
    +            return valueNode(details.getContent(), base64Decoder);
    +        }
    +
    +        static ValueNode valueNode(String base64EncodedContent, Base64.Decoder base64Decoder) {
    +            String decodedContent = new String(base64Decoder.decode(base64EncodedContent), UTF_8);
    +            return ValueNode.create(decodedContent.intern());
             }
     
     
    diff --git a/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/IsModifiedTest.java b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/IsModifiedTest.java
    new file mode 100644
    index 00000000000..f4e2fc3ff35
    --- /dev/null
    +++ b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/IsModifiedTest.java
    @@ -0,0 +1,49 @@
    +/*
    + * Copyright (c) 2023 Oracle and/or its affiliates.
    + *
    + * Licensed 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 io.helidon.integrations.oci.secrets.configsource;
    +
    +import java.time.Instant;
    +
    +import org.junit.jupiter.api.Test;
    +
    +import static io.helidon.integrations.oci.secrets.configsource.OciSecretsConfigSourceProvider.SecretBundleConfigSource.isModified;
    +import static org.hamcrest.CoreMatchers.is;
    +import static org.hamcrest.MatcherAssert.assertThat;
    +
    +class IsModifiedTest {
    +
    +    private IsModifiedTest() {
    +        super();
    +    }
    +
    +    @Test
    +    void testIsModified() {
    +        // Test java.time behavior.
    +        Instant now0 = Instant.now();
    +        Instant now1 = Instant.from(now0);
    +        assertThat(now1, is(now0));
    +        Instant later = now0.plusSeconds(500); // arbitrary amount
    +        assertThat(later.isAfter(now0), is(true));
    +        Instant earlier = now0.minusSeconds(500); // arbitrary amount
    +        assertThat(earlier.isBefore(now0), is(true));
    +
    +        // Test that isModified properly encapsulates java.time behavior.
    +        assertThat(isModified(now0, later), is(false));
    +        assertThat(isModified(now0, now1), is(false));
    +        assertThat(isModified(now0, earlier), is(true));
    +    }
    +
    +}
    diff --git a/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/ValueNodeTest.java b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/ValueNodeTest.java
    new file mode 100644
    index 00000000000..c01a1785d78
    --- /dev/null
    +++ b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/ValueNodeTest.java
    @@ -0,0 +1,48 @@
    +/*
    + * Copyright (c) 2023 Oracle and/or its affiliates.
    + *
    + * Licensed 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 io.helidon.integrations.oci.secrets.configsource;
    +
    +import java.util.Base64;
    +
    +import org.junit.jupiter.api.Test;
    +
    +import static io.helidon.integrations.oci.secrets.configsource.OciSecretsConfigSourceProvider.SecretBundleConfigSource.valueNode;
    +import static java.nio.charset.StandardCharsets.UTF_8;
    +import static org.hamcrest.CoreMatchers.is;
    +import static org.hamcrest.MatcherAssert.assertThat;
    +
    +class ValueNodeTest {
    +
    +    private ValueNodeTest() {
    +        super();
    +    }
    +
    +    @Test
    +    void testValueNode() {
    +      // Test the JDK's base64 decoding behavior.
    +      String raw = new String("abc".getBytes(), UTF_8);
    +      byte[] bytes = Base64.getEncoder().encode(raw.getBytes());
    +      String encoded = new String(bytes, UTF_8);
    +      Base64.Decoder decoder = Base64.getDecoder();
    +      bytes = decoder.decode(encoded);
    +      String decoded = new String(bytes, UTF_8);
    +      assertThat(decoded, is(raw));
    +
    +      // Test that valueNode properly encapsulates this platform behavior.
    +      assertThat(valueNode(encoded, decoder).get(), is(decoded));
    +    }
    +
    +}
    
    From 01a1bd57ec518abbace1c16f7b6f4c4ffe35e1a9 Mon Sep 17 00:00:00 2001
    From: Laird Nelson 
    Date: Mon, 21 Aug 2023 01:02:42 -0700
    Subject: [PATCH 14/15] Squashable commit; refactors task creation; adds some
     comments
    
    Signed-off-by: Laird Nelson 
    ---
     .../OciSecretsConfigSourceProvider.java       | 39 +++++++++++--------
     1 file changed, 23 insertions(+), 16 deletions(-)
    
    diff --git a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java
    index d2204dff864..b20882cfe6b 100644
    --- a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java
    +++ b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java
    @@ -21,16 +21,17 @@
     import java.util.Base64;
     import java.util.Collection;
     import java.util.List;
    -import java.util.Map;
     import java.util.Map.Entry;
     import java.util.Objects;
     import java.util.Optional;
     import java.util.Set;
     import java.util.concurrent.Callable;
     import java.util.concurrent.ConcurrentHashMap;
    +import java.util.concurrent.ConcurrentMap;
     import java.util.concurrent.ExecutionException;
     import java.util.concurrent.ExecutorService;
     import java.util.concurrent.Future;
    +import java.util.function.BiConsumer;
     import java.util.function.Function;
     import java.util.function.Predicate;
     import java.util.function.Supplier;
    @@ -299,16 +300,17 @@ private Optional load(Collection secretSum
                 if (secretSummaries.isEmpty()) {
                     return this.absentNodeContent();
                 }
    -            Map valueNodes = new ConcurrentHashMap<>();
    +            ConcurrentMap valueNodes = new ConcurrentHashMap<>();
                 Collection> tasks = new ArrayList<>(secretSummaries.size());
                 Base64.Decoder decoder = Base64.getDecoder();
                 Secrets secrets = secretsSupplier.get();
                 Instant closestSecretExpirationInstant = this.closestSecretExpirationInstant; // volatile read
                 for (SecretSummary ss : secretSummaries) {
    -                tasks.add(() -> {
    -                        valueNodes.put(ss.getSecretName(), valueNode(secrets, ss, decoder));
    -                        return null;
    -                    });
    +                tasks.add(task(valueNodes::put,
    +                               ss.getSecretName(),
    +                               r -> secrets.getSecretBundle(r).getSecretBundle().getSecretBundleContent(),
    +                               ss.getId(),
    +                               decoder));
                     java.util.Date d = ss.getTimeOfCurrentVersionExpiry();
                     // If d is null, which is permitted by the OCI Vaults API, you could interpret it as meaning "this
                     // secret never ever expires, so never poll it for changes ever again". (This is sort of like if its
    @@ -316,8 +318,9 @@ private Optional load(Collection secretSum
                     //
                     // Or you could interpret it as the much more common "this secret never had its expiration time set,
                     // probably by mistake, or because it's a temporary scratch secret, or any of a zillion other possible
    -                // explanations, so we'd better check each poll time to see if the secret is still there". (This is sort
    -                // of like if its expiration time wer set to the beginning of time.)
    +                // common human explanations, so we'd better check each time we poll to see if the secret is still
    +                // there; i.e. we should pretend it is continually expiring". (This is sort of like if its expiration
    +                // time were set to the beginning of time.)
                     //
                     // We opt for the latter interpretation.
                     Instant secretExpirationInstant = d == null ? null : d.toInstant();
    @@ -423,7 +426,8 @@ private static GetSecretBundleRequest request(String secretId) {
                 return GetSecretBundleRequest.builder().secretId(secretId).build();
             }
     
    -        // Suppress "[try] auto-closeable resource Vaults has a member method close() that could throw InterruptedException"
    +        // Suppress "[try] auto-closeable resource Vaults has a member method close() that could throw
    +        // InterruptedException" since we handle it.
             @SuppressWarnings("try")
             private static Collection secretSummaries(Supplier vaultsSupplier,
                                                                                ListSecretsRequest listSecretsRequest) {
    @@ -441,11 +445,15 @@ private static Collection secretSummaries(Supplier s.getSecretBundle(r).getSecretBundle().getSecretBundleContent(),
    -                          ss.getId(),
    -                          base64Decoder);
    +        static Callable task(BiConsumer valueNodes,
    +                                   String secretName,
    +                                   Function f,
    +                                   String secretId,
    +                                   Base64.Decoder base64Decoder) {
    +            return () -> {
    +                valueNodes.accept(secretName, valueNode(f, secretId, base64Decoder));
    +                return null;
    +            };
             }
     
             private static ValueNode valueNode(Function f,
    @@ -454,8 +462,7 @@ private static ValueNode valueNode(Function
    Date: Mon, 21 Aug 2023 09:02:29 -0700
    Subject: [PATCH 15/15] Squashable commit; removes constructors from unit tests
     per request
    
    Signed-off-by: Laird Nelson 
    ---
     .../integrations/oci/secrets/configsource/IsModifiedTest.java | 4 ----
     .../integrations/oci/secrets/configsource/UsageTest.java      | 4 ----
     .../integrations/oci/secrets/configsource/ValueNodeTest.java  | 4 ----
     3 files changed, 12 deletions(-)
    
    diff --git a/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/IsModifiedTest.java b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/IsModifiedTest.java
    index f4e2fc3ff35..b78d2b0164e 100644
    --- a/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/IsModifiedTest.java
    +++ b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/IsModifiedTest.java
    @@ -25,10 +25,6 @@
     
     class IsModifiedTest {
     
    -    private IsModifiedTest() {
    -        super();
    -    }
    -
         @Test
         void testIsModified() {
             // Test java.time behavior.
    diff --git a/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/UsageTest.java b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/UsageTest.java
    index c0023352aee..58b6ca856db 100644
    --- a/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/UsageTest.java
    +++ b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/UsageTest.java
    @@ -31,10 +31,6 @@
     
     class UsageTest {
     
    -    private UsageTest() {
    -        super();
    -    }
    -
         @Test
         void testUsage() {
             // Get a Config object. Because src/test/resources/meta-config.yaml exists, and because it will be processed
    diff --git a/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/ValueNodeTest.java b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/ValueNodeTest.java
    index c01a1785d78..f4d82229c06 100644
    --- a/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/ValueNodeTest.java
    +++ b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/ValueNodeTest.java
    @@ -26,10 +26,6 @@
     
     class ValueNodeTest {
     
    -    private ValueNodeTest() {
    -        super();
    -    }
    -
         @Test
         void testValueNode() {
           // Test the JDK's base64 decoding behavior.