io.helidon.integrations.oci.metrics
helidon-integrations-oci-metrics
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..f2bc7b919b6
--- /dev/null
+++ b/integrations/oci/oci-secrets-config-source/pom.xml
@@ -0,0 +1,141 @@
+
+
+
+ 4.0.0
+
+ io.helidon.integrations.oci
+ helidon-integrations-oci-project
+ 4.0.0-SNAPSHOT
+
+ helidon-integrations-oci-secrets-config-source
+ Helidon Integrations OCI Secrets Config Source
+
+
+ OCI Secrets Retrieval API ConfigSourceProvider Implementation
+
+
+
+
+ src/test/java/logging.properties
+
+
+
+
+
+
+
+
+ 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
+
+
+
+ org.apache.httpcomponents
+ httpclient
+
+
+ org.apache.httpcomponents
+ httpcore
+
+
+
+
+
+ org.glassfish.jersey.connectors
+ jersey-apache-connector
+ 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}
+ ${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
new file mode 100644
index 00000000000..b20882cfe6b
--- /dev/null
+++ b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java
@@ -0,0 +1,575 @@
+/*
+ * 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.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;
+
+import io.helidon.common.LazyValue;
+import io.helidon.common.Weight;
+import io.helidon.config.AbstractConfigSource;
+import io.helidon.config.AbstractConfigSourceBuilder;
+import io.helidon.config.Config;
+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.model.SecretBundleContentDetails;
+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 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:
+ *
+ *
+ *
+ * - Ensure you have an authentication mechanism set up to connect to OCI (e.g. a valid 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.
+ *
+ * - Ensure there is a classpath resource present named {@code meta-config.yaml}.
+ *
+ * - 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'
+ * vault-ocid: 'your vault OCID goes here'
+ *
+ *
+ *
+ * Refer to Helidon's documentation concerning meta-configuration for more details.
+ *
+ * @see ConfigSourceProvider
+ */
+@Weight(300D) // a higher weight than the default (100D)
+public final class OciSecretsConfigSourceProvider implements ConfigSourceProvider {
+
+
+ /*
+ * Static fields.
+ */
+
+
+ private static final Set SUPPORTED_TYPES = Set.of("oci-secrets");
+
+
+ /*
+ * 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}; 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}
+ *
+ * @return a non-{@code null} {@link ConfigSource}
+ *
+ * @exception NullPointerException if {@code type} is {@linkplain #supports(String) supported} and {@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 SecretBundleConfigSource.builder().config(metaConfig).build();
+ }
+
+ /**
+ * 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 {@link Set} returned by an invocation of the {@link #supported()} method
+ * {@linkplain Set#contains(Object) contains} it.
+ *
+ * @param type the type to test
+ *
+ * @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()
+ *
+ * @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 this.supported().contains(type);
+ }
+
+
+ /*
+ * Inner and nested classes.
+ */
+
+
+ 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());
+
+ 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";
+
+
+ /*
+ * Instance fields.
+ */
+
+
+ private final Supplier extends Optional> loader;
+
+ private volatile Instant closestSecretExpirationInstant;
+
+
+ /*
+ * Constructors.
+ */
+
+
+ private SecretBundleConfigSource(Builder b) {
+ super(b);
+ Supplier extends Secrets> secretsSupplier = Objects.requireNonNull(b.secretsSupplier, "b.secretsSupplier");
+ Supplier extends Vaults> vaultsSupplier = Objects.requireNonNull(b.vaultsSupplier, "b.vaultsSupplier");
+ this.closestSecretExpirationInstant = now();
+ 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 = this::absentNodeContent;
+ } else {
+ ListSecretsRequest listSecretsRequest = ListSecretsRequest.builder()
+ .compartmentId(compartmentOcid)
+ .vaultId(vaultOcid)
+ .build();
+ this.loader = () -> this.load(vaultsSupplier, secretsSupplier, listSecretsRequest);
+ }
+ }
+
+
+ /*
+ * Instance methods.
+ */
+
+
+ @Deprecated // For use by the Helidon Config subsystem only.
+ @Override // PollableSource
+ public boolean isModified(Instant pollInstant) {
+ return isModified(pollInstant, this.closestSecretExpirationInstant); // volatile read
+ }
+
+ @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();
+ }
+
+ private Optional absentNodeContent() {
+ return ABSENT_NODE_CONTENT;
+ }
+
+ private Optional load(Supplier extends Vaults> vaultsSupplier,
+ Supplier extends Secrets> secretsSupplier,
+ ListSecretsRequest listSecretsRequest) {
+ Collection extends SecretSummary> secretSummaries = secretSummaries(vaultsSupplier, listSecretsRequest);
+ return this.load(secretSummaries, secretsSupplier);
+ }
+
+ private Optional load(Collection extends SecretSummary> secretSummaries,
+ Supplier extends Secrets> secretsSupplier) {
+ if (secretSummaries.isEmpty()) {
+ return this.absentNodeContent();
+ }
+ 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(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
+ // 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
+ // 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();
+ if (secretExpirationInstant != null && secretExpirationInstant.isBefore(closestSecretExpirationInstant)) {
+ closestSecretExpirationInstant = secretExpirationInstant;
+ }
+ }
+ this.closestSecretExpirationInstant = closestSecretExpirationInstant; // volatile write
+ completeTasks(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.
+ */
+
+
+ private static Builder builder() {
+ return new Builder();
+ }
+
+ private static void closeUnchecked(AutoCloseable autoCloseable) {
+ try {
+ 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 completeTasks(Collection extends Callable> tasks, AutoCloseable autoCloseable) {
+ try (ExecutorService es = newVirtualThreadPerTaskExecutor();
+ autoCloseable) {
+ completeTasks(es, tasks);
+ } 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 completeTasks(ExecutorService es, Collection extends Callable> tasks) {
+ RuntimeException re = null;
+ for (Future> future : invokeAllUnchecked(es, tasks)) {
+ try {
+ futureGetUnchecked(future);
+ } catch (RuntimeException e) {
+ if (re == null) {
+ re = e;
+ } else {
+ re.addSuppressed(e);
+ }
+ }
+ }
+ 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);
+ }
+ }
+
+ private static List> invokeAllUnchecked(ExecutorService es, Collection extends Callable> 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" since we handle it.
+ @SuppressWarnings("try")
+ private static Collection extends SecretSummary> secretSummaries(Supplier extends Vaults> vaultsSupplier,
+ ListSecretsRequest listSecretsRequest) {
+ try (Vaults v = vaultsSupplier.get()) {
+ return v.listSecrets(listSecretsRequest).getItems();
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (InterruptedException e) {
+ // (Can legally be thrown by any AutoCloseable (such as Vaults). Must preserve interrupt status.)
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(e.getMessage(), e);
+ } catch (Exception e) {
+ // (Can legally be thrown by any AutoCloseable (such as Vaults).)
+ throw new IllegalStateException(e.getMessage(), e);
+ }
+ }
+
+ static Callable task(BiConsumer super String, ? super ValueNode> valueNodes,
+ String secretName,
+ Function super GetSecretBundleRequest, ? extends SecretBundleContentDetails> f,
+ String secretId,
+ Base64.Decoder base64Decoder) {
+ return () -> {
+ valueNodes.accept(secretName, valueNode(f, secretId, base64Decoder));
+ return null;
+ };
+ }
+
+ private static ValueNode valueNode(Function super GetSecretBundleRequest, ? extends SecretBundleContentDetails> 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());
+ }
+
+
+ /*
+ * Inner and nested classes.
+ */
+
+
+ private static final class Builder extends AbstractConfigSourceBuilder {
+
+
+ /*
+ * Instance fields.
+ */
+
+
+ private String compartmentOcid;
+
+ private Supplier extends Secrets> secretsSupplier;
+
+ private String vaultOcid;
+
+ private Supplier extends Vaults> vaultsSupplier;
+
+
+ /*
+ * Constructors.
+ */
+
+
+ private Builder() {
+ super();
+ Supplier extends BasicAuthenticationDetailsProvider> adpSupplier =
+ LazyValue.create(() -> (BasicAuthenticationDetailsProvider) ociAuthenticationProvider().get());
+ SecretsClient.Builder scb = SecretsClient.builder();
+ this.secretsSupplier = () -> scb.build(adpSupplier.get());
+ VaultsClient.Builder vcb = VaultsClient.builder();
+ this.vaultsSupplier = () -> vcb.build(adpSupplier.get());
+ }
+
+
+ /*
+ * Instance methods.
+ */
+
+
+ @Override // AbstractConfigSourceBuilder
+ protected Builder config(Config metaConfig) {
+ metaConfig.get("compartment-ocid")
+ .asString()
+ .filter(Predicate.not(String::isBlank))
+ .ifPresentOrElse(this::compartmentOcid,
+ () -> {
+ 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()
+ .filter(Predicate.not(String::isBlank))
+ .ifPresentOrElse(this::vaultOcid,
+ () -> {
+ 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 SecretBundleConfigSource build() {
+ return new SecretBundleConfigSource(this);
+ }
+
+ private Builder compartmentOcid(String compartmentOcid) {
+ this.compartmentOcid = Objects.requireNonNull(compartmentOcid, "compartmentOcid");
+ return this;
+ }
+
+ private Builder secretsSupplier(Supplier extends Secrets> 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 extends Vaults> 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..a6da275765c
--- /dev/null
+++ b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/package-info.java
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ *
+ * @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
new file mode 100644
index 00000000000..cbacd5343b3
--- /dev/null
+++ b/integrations/oci/oci-secrets-config-source/src/main/java/module-info.java
@@ -0,0 +1,40 @@
+/*
+ * 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}.
+ *
+ * @see io.helidon.integrations.oci.secrets.configsource.OciSecretsConfigSourceProvider
+ */
+@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/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..b78d2b0164e
--- /dev/null
+++ b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/IsModifiedTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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 {
+
+ @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/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..58b6ca856db
--- /dev/null
+++ b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/UsageTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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;
+
+class UsageTest {
+
+ @Test
+ 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/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..f4d82229c06
--- /dev/null
+++ b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/ValueNodeTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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 {
+
+ @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));
+ }
+
+}
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..748334ff93d
--- /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' # for testing
+ - 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