diff --git a/common/deployment/src/main/java/io/quarkiverse/googlecloudservices/common/deployment/CommonBuildSteps.java b/common/deployment/src/main/java/io/quarkiverse/googlecloudservices/common/deployment/CommonBuildSteps.java index 3c68991c..0b9aff8f 100644 --- a/common/deployment/src/main/java/io/quarkiverse/googlecloudservices/common/deployment/CommonBuildSteps.java +++ b/common/deployment/src/main/java/io/quarkiverse/googlecloudservices/common/deployment/CommonBuildSteps.java @@ -3,9 +3,11 @@ import io.quarkiverse.googlecloudservices.common.GcpConfigHolder; import io.quarkiverse.googlecloudservices.common.GcpCredentialProducer; import io.quarkiverse.googlecloudservices.common.GcpCredentialProviderProducer; +import io.quarkiverse.googlecloudservices.common.GcpDefaultsConfigBuilder; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; +import io.quarkus.deployment.builditem.RunTimeConfigBuilderBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageConfigBuildItem; public class CommonBuildSteps { @@ -22,6 +24,11 @@ public ExtensionSslNativeSupportBuildItem ssl() { return new ExtensionSslNativeSupportBuildItem("google-cloud-common"); } + @BuildStep + public RunTimeConfigBuilderBuildItem defaultsConfig() { + return new RunTimeConfigBuilderBuildItem(GcpDefaultsConfigBuilder.class); + } + /** * Work around for https://github.com/quarkusio/quarkus/issues/25501 until * https://github.com/oracle/graal/issues/4543 gets resolved diff --git a/common/runtime/src/main/java/io/quarkiverse/googlecloudservices/common/GcpDefaultsConfigBuilder.java b/common/runtime/src/main/java/io/quarkiverse/googlecloudservices/common/GcpDefaultsConfigBuilder.java new file mode 100644 index 00000000..b5f664de --- /dev/null +++ b/common/runtime/src/main/java/io/quarkiverse/googlecloudservices/common/GcpDefaultsConfigBuilder.java @@ -0,0 +1,11 @@ +package io.quarkiverse.googlecloudservices.common; + +import io.quarkus.runtime.configuration.ConfigBuilder; +import io.smallrye.config.SmallRyeConfigBuilder; + +public class GcpDefaultsConfigBuilder implements ConfigBuilder { + @Override + public SmallRyeConfigBuilder configBuilder(final SmallRyeConfigBuilder builder) { + return builder.withSources(new GcpDefaultsConfigSourceFactory()); + } +} diff --git a/common/runtime/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceFactory b/common/runtime/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceFactory deleted file mode 100644 index d9e9a1ca..00000000 --- a/common/runtime/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceFactory +++ /dev/null @@ -1 +0,0 @@ -io.quarkiverse.googlecloudservices.common.GcpDefaultsConfigSourceFactory diff --git a/secret-manager/deployment/src/main/java/io/quarkiverse/googlecloudservices/secretmanager/deployment/SecretManagerBuildSteps.java b/secret-manager/deployment/src/main/java/io/quarkiverse/googlecloudservices/secretmanager/deployment/SecretManagerBuildSteps.java index 21944d0f..dbbac307 100644 --- a/secret-manager/deployment/src/main/java/io/quarkiverse/googlecloudservices/secretmanager/deployment/SecretManagerBuildSteps.java +++ b/secret-manager/deployment/src/main/java/io/quarkiverse/googlecloudservices/secretmanager/deployment/SecretManagerBuildSteps.java @@ -1,9 +1,11 @@ package io.quarkiverse.googlecloudservices.secretmanager.deployment; import io.quarkiverse.googlecloudservices.secretmanager.runtime.SecretManagerProducer; +import io.quarkiverse.googlecloudservices.secretmanager.runtime.config.SecretManagerConfigBuilder; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.RunTimeConfigBuilderBuildItem; public class SecretManagerBuildSteps { private static final String FEATURE = "google-cloud-secret-manager"; @@ -17,4 +19,9 @@ public FeatureBuildItem feature() { public AdditionalBeanBuildItem producer() { return new AdditionalBeanBuildItem(SecretManagerProducer.class); } + + @BuildStep + public RunTimeConfigBuilderBuildItem secretManagerConfig() { + return new RunTimeConfigBuilderBuildItem(SecretManagerConfigBuilder.class); + } } diff --git a/secret-manager/runtime/src/main/java/io/quarkiverse/googlecloudservices/secretmanager/runtime/config/SecretManagerClientProvider.java b/secret-manager/runtime/src/main/java/io/quarkiverse/googlecloudservices/secretmanager/runtime/config/SecretManagerClientProvider.java deleted file mode 100644 index d15e6d54..00000000 --- a/secret-manager/runtime/src/main/java/io/quarkiverse/googlecloudservices/secretmanager/runtime/config/SecretManagerClientProvider.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.quarkiverse.googlecloudservices.secretmanager.runtime.config; - -import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; - -import io.quarkus.arc.Arc; - -/** - * Provides an instance of {@link SecretManagerServiceClient} from the Quarkus application context - * with concurrency protection. - */ -class SecretManagerClientProvider { - - private volatile SecretManagerServiceClient client = null; - - synchronized SecretManagerServiceClient get() { - if (client == null) { - // Retrieve the Secret Manager client in the context. - client = Arc.container().instance(SecretManagerServiceClient.class).get(); - } - return client; - } -} diff --git a/secret-manager/runtime/src/main/java/io/quarkiverse/googlecloudservices/secretmanager/runtime/config/SecretManagerConfigBuilder.java b/secret-manager/runtime/src/main/java/io/quarkiverse/googlecloudservices/secretmanager/runtime/config/SecretManagerConfigBuilder.java new file mode 100644 index 00000000..6d20419c --- /dev/null +++ b/secret-manager/runtime/src/main/java/io/quarkiverse/googlecloudservices/secretmanager/runtime/config/SecretManagerConfigBuilder.java @@ -0,0 +1,11 @@ +package io.quarkiverse.googlecloudservices.secretmanager.runtime.config; + +import io.quarkus.runtime.configuration.ConfigBuilder; +import io.smallrye.config.SmallRyeConfigBuilder; + +public class SecretManagerConfigBuilder implements ConfigBuilder { + @Override + public SmallRyeConfigBuilder configBuilder(final SmallRyeConfigBuilder builder) { + return builder.withSources(new SecretManagerConfigSourceFactory()); + } +} diff --git a/secret-manager/runtime/src/main/java/io/quarkiverse/googlecloudservices/secretmanager/runtime/config/SecretManagerConfigSource.java b/secret-manager/runtime/src/main/java/io/quarkiverse/googlecloudservices/secretmanager/runtime/config/SecretManagerConfigSource.java index 622c7105..77f56932 100644 --- a/secret-manager/runtime/src/main/java/io/quarkiverse/googlecloudservices/secretmanager/runtime/config/SecretManagerConfigSource.java +++ b/secret-manager/runtime/src/main/java/io/quarkiverse/googlecloudservices/secretmanager/runtime/config/SecretManagerConfigSource.java @@ -4,30 +4,33 @@ import java.util.Map; import java.util.Set; -import com.google.cloud.secretmanager.v1.AccessSecretVersionResponse; import com.google.cloud.secretmanager.v1.SecretVersionName; -import io.smallrye.config.common.AbstractConfigSource; +import io.smallrye.config.common.MapBackedConfigSource; -public class SecretManagerConfigSource extends AbstractConfigSource { +public class SecretManagerConfigSource extends MapBackedConfigSource { /** The ordinal is set to < 100 (which is the default) so that this config source is retrieved from last. */ private static final int SECRET_MANAGER_ORDINAL = 50; private static final String CONFIG_SOURCE_NAME = "io.quarkiverse.googlecloudservices.secretmanager.runtime.config"; - private final String defaultProject; + private final String projectId; - private final SecretManagerClientProvider clientProvider = new SecretManagerClientProvider(); - - public SecretManagerConfigSource(String defaultProject) { - super(CONFIG_SOURCE_NAME, SECRET_MANAGER_ORDINAL); - this.defaultProject = defaultProject; + public SecretManagerConfigSource(final Map properties, final String projectId) { + super(CONFIG_SOURCE_NAME, properties, SECRET_MANAGER_ORDINAL); + this.projectId = projectId; } @Override - public Map getProperties() { - return Collections.emptyMap(); + public String getValue(String propertyName) { + SecretVersionName secretVersionName = SecretManagerConfigUtils.getSecretVersionName(propertyName, projectId); + if (secretVersionName == null) { + // The propertyName is not in the form "${sm//...}" so return null. + return null; + } + + return super.getProperties().get(secretVersionName.toString()); } @Override @@ -36,14 +39,7 @@ public Set getPropertyNames() { } @Override - public String getValue(String propertyName) { - SecretVersionName secretVersionName = SecretManagerConfigUtils.getSecretVersionName(propertyName, defaultProject); - if (secretVersionName == null) { - // The propertyName is not in the form "${sm//...}" so return null. - return null; - } - - AccessSecretVersionResponse response = clientProvider.get().accessSecretVersion(secretVersionName); - return response.getPayload().getData().toStringUtf8(); + public Map getProperties() { + return Collections.emptyMap(); } } diff --git a/secret-manager/runtime/src/main/java/io/quarkiverse/googlecloudservices/secretmanager/runtime/config/SecretManagerConfigSourceFactory.java b/secret-manager/runtime/src/main/java/io/quarkiverse/googlecloudservices/secretmanager/runtime/config/SecretManagerConfigSourceFactory.java index b37753e6..bb85d4dc 100644 --- a/secret-manager/runtime/src/main/java/io/quarkiverse/googlecloudservices/secretmanager/runtime/config/SecretManagerConfigSourceFactory.java +++ b/secret-manager/runtime/src/main/java/io/quarkiverse/googlecloudservices/secretmanager/runtime/config/SecretManagerConfigSourceFactory.java @@ -3,30 +3,96 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; -import java.util.Optional; +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; import org.eclipse.microprofile.config.spi.ConfigSource; +import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.ServiceOptions; +import com.google.cloud.secretmanager.v1.AccessSecretVersionResponse; +import com.google.cloud.secretmanager.v1.ProjectName; +import com.google.cloud.secretmanager.v1.Secret; +import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; +import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings; +import com.google.cloud.secretmanager.v1.SecretVersion; +import com.google.cloud.secretmanager.v1.SecretVersionName; +import io.quarkiverse.googlecloudservices.common.GcpBootstrapConfiguration; import io.smallrye.config.ConfigSourceContext; import io.smallrye.config.ConfigSourceFactory; -import io.smallrye.config.ConfigValue; -import io.smallrye.config.Converters; +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; public class SecretManagerConfigSourceFactory implements ConfigSourceFactory { + @Override public Iterable getConfigSources(final ConfigSourceContext context) { - ConfigValue enableMetadataServer = context.getValue("quarkus.google.cloud.enable-metadata-server"); - if (enableMetadataServer.getValue() != null) { - if (Converters.getImplicitConverter(Boolean.class).convert(enableMetadataServer.getValue())) { - String projectId = Optional.ofNullable(context.getValue("quarkus.google.cloud.project-id").getValue()) - .orElse(ServiceOptions.getDefaultProjectId()); - if (projectId != null) { - return singletonList(new SecretManagerConfigSource(projectId)); - } + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(new ConfigSourceContext.ConfigSourceContextConfigSource(context)) + .withMapping(GcpBootstrapConfiguration.class) + .withMappingIgnore("quarkus.**") + .build(); + + GcpBootstrapConfiguration gcpConfig = config.getConfigMapping(GcpBootstrapConfiguration.class); + + if (gcpConfig.enableMetadataServer()) { + String projectId = gcpConfig.projectId().orElse(ServiceOptions.getDefaultProjectId()); + if (projectId != null) { + return singletonList(new SecretManagerConfigSource(getProperties(gcpConfig, projectId), projectId)); } } return emptyList(); } + + private Map getProperties(final GcpBootstrapConfiguration config, final String projectId) { + try { + SecretManagerServiceClient client = SecretManagerServiceClient.create( + SecretManagerServiceSettings.newBuilder() + .setQuotaProjectId(projectId) + .setCredentialsProvider(() -> credentials(config)) + .build()); + + Map properties = new HashMap<>(); + for (Secret secret : client.listSecrets(ProjectName.of(projectId)).iterateAll()) { + boolean latest = true; + for (SecretVersion secretVersion : client.listSecretVersions(secret.getName()).iterateAll()) { + AccessSecretVersionResponse accessSecretVersion = client.accessSecretVersion(secretVersion.getName()); + properties.put(secretVersion.getName(), accessSecretVersion.getPayload().getData().toStringUtf8()); + if (latest) { + SecretVersionName secretVersionName = SecretVersionName.parse(secretVersion.getName()); + properties.put(SecretVersionName + .of(secretVersionName.getProject(), secretVersionName.getSecret(), "latest").toString(), + accessSecretVersion.getPayload().getData().toStringUtf8()); + latest = false; + } + } + } + + client.close(); + return properties; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static final String CLOUD_OAUTH_SCOPE = "https://www.googleapis.com/auth/cloud-platform"; + + private static GoogleCredentials credentials(final GcpBootstrapConfiguration config) throws IOException { + if (config.serviceAccountLocation().isPresent()) { + try (FileInputStream is = new FileInputStream(config.serviceAccountLocation().get())) { + return GoogleCredentials.fromStream(is).createScoped(CLOUD_OAUTH_SCOPE); + } + } else if (config.serviceAccountEncodedKey().isPresent()) { + byte[] decode = Base64.getDecoder().decode(config.serviceAccountEncodedKey().get()); + try (ByteArrayInputStream is = new ByteArrayInputStream(decode)) { + return GoogleCredentials.fromStream(is).createScoped(CLOUD_OAUTH_SCOPE); + } + } + return GoogleCredentials.getApplicationDefault().createScoped(CLOUD_OAUTH_SCOPE); + } } diff --git a/secret-manager/runtime/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceFactory b/secret-manager/runtime/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceFactory deleted file mode 100644 index b6b50db8..00000000 --- a/secret-manager/runtime/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceFactory +++ /dev/null @@ -1 +0,0 @@ -io.quarkiverse.googlecloudservices.secretmanager.runtime.config.SecretManagerConfigSourceFactory