Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not use CDI beans to read the SM #499

Merged
merged 3 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,10 @@ public interface GcpBootstrapConfiguration {
*/
@WithDefault("true")
boolean accessTokenEnabled();

/**
* Whether to enable the secret manager
*/
@WithDefault("true")
boolean secretManagerEnabled();
}
Original file line number Diff line number Diff line change
@@ -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());
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

# Secret Manager Demo
# You can load secrets from Google Cloud Secret Manager with the ${sm//<SECRET_ID>} syntax.
my.database.password=${sm//integration-test}
%prod.my.database.password=${sm//integration-test}
%test.my.database.password=test
%test.quarkus.google.cloud.secret-manager-enabled=false

## Logging config
%dev.quarkus.google.cloud.logging.enabled=false
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package io.quarkiverse.googlecloudservices.secretmanager.deployment;

import io.quarkiverse.googlecloudservices.secretmanager.runtime.SecretManagerProducer;
import io.quarkiverse.googlecloudservices.secretmanager.runtime.config.SecretManagerClientDestroyer;
import io.quarkiverse.googlecloudservices.secretmanager.runtime.config.SecretManagerConfigBuilder;
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.deployment.annotations.BuildProducer;
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";
Expand All @@ -14,7 +18,13 @@ public FeatureBuildItem feature() {
}

@BuildStep
public AdditionalBeanBuildItem producer() {
return new AdditionalBeanBuildItem(SecretManagerProducer.class);
public void additionalBeans(BuildProducer<AdditionalBeanBuildItem> additionalBeans) {
additionalBeans.produce(new AdditionalBeanBuildItem(SecretManagerProducer.class));
additionalBeans.produce(new AdditionalBeanBuildItem(SecretManagerClientDestroyer.class));
}

@BuildStep
public RunTimeConfigBuilderBuildItem secretManagerConfig() {
return new RunTimeConfigBuilderBuildItem(SecretManagerConfigBuilder.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.quarkiverse.googlecloudservices.secretmanager.runtime.config;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;

import org.eclipse.microprofile.config.ConfigProvider;
import org.eclipse.microprofile.config.spi.ConfigSource;

import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;

import io.quarkus.arc.Unremovable;
import io.quarkus.runtime.ShutdownEvent;
import io.smallrye.config.SmallRyeConfig;

/**
* The {@link SecretManagerConfigSource} keeps a reference to {@link SecretManagerServiceClient} that should be closed
* on shutdown. Unfortunately the Config API does not provide a way to clean up resource, so this is probably the
* easiest way on how to do it.
* <p>
* This will only work the current {@link SmallRyeConfig} registered in the current Classloader. If the application
* registers {@link SmallRyeConfig} instances in another Classloader, or replaces the initial instance by another, the
* close of {@link SecretManagerServiceClient} has to be handled by the application.
*/
@ApplicationScoped
@Unremovable
public class SecretManagerClientDestroyer {
void onStop(@Observes ShutdownEvent ev) {
SmallRyeConfig config = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class);
for (ConfigSource configSource : config.getConfigSources(SecretManagerConfigSource.class)) {
SecretManagerConfigSource secretManagerConfigSource = (SecretManagerConfigSource) configSource;
secretManagerConfigSource.closeClient();
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
package io.quarkiverse.googlecloudservices.secretmanager.runtime.config;

import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

import com.google.api.gax.core.FixedCredentialsProvider;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.secretmanager.v1.AccessSecretVersionResponse;
import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings;
import com.google.cloud.secretmanager.v1.SecretVersionName;

import io.quarkiverse.googlecloudservices.common.GcpBootstrapConfiguration;
import io.smallrye.config.common.AbstractConfigSource;

public class SecretManagerConfigSource extends AbstractConfigSource {
Expand All @@ -16,18 +26,36 @@ public class SecretManagerConfigSource extends AbstractConfigSource {

private static final String CONFIG_SOURCE_NAME = "io.quarkiverse.googlecloudservices.secretmanager.runtime.config";

private final String defaultProject;
private final String projectId;
private final SecretManagerServiceClient client;
private final AtomicBoolean closed;

private final SecretManagerClientProvider clientProvider = new SecretManagerClientProvider();

public SecretManagerConfigSource(String defaultProject) {
public SecretManagerConfigSource(final GcpBootstrapConfiguration gcpConfig, final String projectId) {
super(CONFIG_SOURCE_NAME, SECRET_MANAGER_ORDINAL);
this.defaultProject = defaultProject;
this.projectId = projectId;
if (gcpConfig.secretManagerEnabled()) {
this.client = createClient(gcpConfig, projectId);
this.closed = new AtomicBoolean(false);
} else {
this.client = null;
this.closed = new AtomicBoolean(true);
}
}

@Override
public Map<String, String> 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;
}

if (!closed.get()) {
AccessSecretVersionResponse response = client.accessSecretVersion(secretVersionName);
return response.getPayload().getData().toStringUtf8();
}

return null;
}

@Override
Expand All @@ -36,14 +64,43 @@ public Set<String> 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;
public Map<String, String> getProperties() {
return Collections.emptyMap();
}

void closeClient() {
closed.compareAndSet(false, true);
client.close();
}

private static SecretManagerServiceClient createClient(
final GcpBootstrapConfiguration gcpConfig,
final String projectId) {

try {
return SecretManagerServiceClient.create(
SecretManagerServiceSettings.newBuilder()
.setQuotaProjectId(projectId)
.setCredentialsProvider(FixedCredentialsProvider.create(credentials(gcpConfig)))
.build());
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private static final String CLOUD_OAUTH_SCOPE = "https://www.googleapis.com/auth/cloud-platform";

AccessSecretVersionResponse response = clientProvider.get().accessSecretVersion(secretVersionName);
return response.getPayload().getData().toStringUtf8();
private static GoogleCredentials credentials(final GcpBootstrapConfiguration gcpConfig) throws IOException {
if (gcpConfig.serviceAccountLocation().isPresent()) {
try (FileInputStream is = new FileInputStream(gcpConfig.serviceAccountLocation().get())) {
return GoogleCredentials.fromStream(is).createScoped(CLOUD_OAUTH_SCOPE);
}
} else if (gcpConfig.serviceAccountEncodedKey().isPresent()) {
byte[] decode = Base64.getDecoder().decode(gcpConfig.serviceAccountEncodedKey().get());
try (ByteArrayInputStream is = new ByteArrayInputStream(decode)) {
return GoogleCredentials.fromStream(is).createScoped(CLOUD_OAUTH_SCOPE);
}
}
return GoogleCredentials.getApplicationDefault().createScoped(CLOUD_OAUTH_SCOPE);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,31 @@
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;

import java.util.Optional;

import org.eclipse.microprofile.config.spi.ConfigSource;

import com.google.cloud.ServiceOptions;

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<ConfigSource> 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(gcpConfig, projectId));
}
}
return emptyList();
Expand Down

This file was deleted.

Loading