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

Added support for regional secrets via Google Cloud Secret Manager #1202

Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@
import io.micronaut.context.annotation.BootstrapContextCompatible;
import io.micronaut.context.annotation.Factory;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.util.StringUtils;
import io.micronaut.gcp.Modules;
import io.micronaut.gcp.UserAgentHeaderProvider;

import io.micronaut.gcp.secretmanager.configuration.SecretManagerConfigurationProperties;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import java.io.IOException;
Expand All @@ -43,6 +46,22 @@
@BootstrapContextCompatible
public class SecretManagerFactory {

private static final String REGIONAL_ENDPOINT = "secretmanager.%s.rep.googleapis.com:443";
private final SecretManagerConfigurationProperties configurationProperties;

/**
*
* @param configurationProperties SecretManager Configuration Properties
*/
@Inject
public SecretManagerFactory(SecretManagerConfigurationProperties configurationProperties) {
sdelamo marked this conversation as resolved.
Show resolved Hide resolved
this.configurationProperties = configurationProperties;
}

@Deprecated
public SecretManagerFactory() {
this(new SecretManagerConfigurationProperties());
}

/**
* Creates a {@link SecretManagerServiceClient} instance.
Expand All @@ -55,7 +74,11 @@ public class SecretManagerFactory {
public SecretManagerServiceClient secretManagerServiceClient(@Named(Modules.SECRET_MANAGER) CredentialsProvider credentialsProvider,
@Named(Modules.SECRET_MANAGER) TransportChannelProvider transportChannelProvider) {
try {
SecretManagerServiceSettings settings = SecretManagerServiceSettings.newBuilder()
SecretManagerServiceSettings.Builder builder = SecretManagerServiceSettings.newBuilder();
if (configurationProperties != null && StringUtils.isNotEmpty(configurationProperties.getLocation())) {
builder.setEndpoint(String.format(REGIONAL_ENDPOINT, configurationProperties.getLocation()));
}
SecretManagerServiceSettings settings = builder
.setCredentialsProvider(credentialsProvider)
.setTransportChannelProvider(transportChannelProvider)
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@
import io.micronaut.context.annotation.BootstrapContextCompatible;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.util.StringUtils;
import io.micronaut.gcp.GoogleCloudConfiguration;
import io.micronaut.gcp.secretmanager.configuration.SecretManagerConfigurationProperties;
import io.micronaut.scheduling.TaskExecutors;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import org.slf4j.Logger;
Expand All @@ -44,18 +47,44 @@
@Requires(classes = SecretManagerServiceClient.class)
public class DefaultSecretManagerClient implements SecretManagerClient {

private static final Logger LOGGER = LoggerFactory.getLogger(DefaultSecretManagerClient.class);
private static final Logger LOG = LoggerFactory.getLogger(DefaultSecretManagerClient.class);
private final SecretManagerServiceClient client;
private final GoogleCloudConfiguration googleCloudConfiguration;
private final ExecutorService executorService;
private final SecretManagerConfigurationProperties configurationProperties;

/**
*
* @param client SecretManagerServiceClient
* @param googleCloudConfiguration Google Cloud Configuration
* @param executorService IO ExecutorService
* @param configurationProperties Secret Manager Configuration Properties
*/
@Inject
public DefaultSecretManagerClient(
SecretManagerServiceClient client,
GoogleCloudConfiguration googleCloudConfiguration,
@Nullable @Named(TaskExecutors.IO) ExecutorService executorService) {
@Nullable @Named(TaskExecutors.BLOCKING) ExecutorService executorService,
SecretManagerConfigurationProperties configurationProperties) {
this.client = client;
this.googleCloudConfiguration = googleCloudConfiguration;
this.executorService = executorService != null ? executorService : Executors.newSingleThreadExecutor() ;
this.configurationProperties = configurationProperties;
}

/**
*
* @param client SecretManagerServiceClient
* @param googleCloudConfiguration Google Cloud Configuration
* @param executorService IO ExecutorService
* @deprecated Use {@link #DefaultSecretManagerClient(SecretManagerServiceClient, GoogleCloudConfiguration, ExecutorService, SecretManagerConfigurationProperties)} instead.
*/
@Deprecated
public DefaultSecretManagerClient(
SecretManagerServiceClient client,
GoogleCloudConfiguration googleCloudConfiguration,
@Nullable @Named(TaskExecutors.IO) ExecutorService executorService) {
this(client, googleCloudConfiguration, executorService, new SecretManagerConfigurationProperties());
}

@Override
Expand All @@ -70,8 +99,15 @@ public Mono<VersionedSecret> getSecret(String secretId, String version) {

@Override
public Mono<VersionedSecret> getSecret(String secretId, String version, String projectId) {
LOGGER.debug("Fetching secret: projects/{}/secrets/{}/{}", projectId, secretId, version);
SecretVersionName secretVersionName = SecretVersionName.of(projectId, secretId, version);
if (LOG.isDebugEnabled()) {
if (StringUtils.isNotEmpty(configurationProperties.getLocation())) {
LOG.debug("Fetching secret: projects/{}/locations/{}/secrets/{}/{}", projectId, configurationProperties.getLocation(), secretId, version);
} else {
LOG.debug("Fetching secret: projects/{}/secrets/{}/{}", projectId, secretId, version);
}
}

SecretVersionName secretVersionName = secretVersionName(projectId, secretId, version);
AccessSecretVersionRequest request = AccessSecretVersionRequest.newBuilder()
.setName(secretVersionName.toString())
.build();
Expand All @@ -89,7 +125,22 @@ public Mono<VersionedSecret> getSecret(String secretId, String version, String p
});

return mono
.map(response -> new VersionedSecret(secretId, projectId, version, response.getPayload().getData().toByteArray()))
.map(response -> versionedSecret(secretId, version, projectId, response))
.onErrorResume(throwable -> Mono.empty());
}

private SecretVersionName secretVersionName(String projectId, String secretId, String version) {
return StringUtils.isEmpty(configurationProperties.getLocation())
? SecretVersionName.of(projectId, secretId, version)
: SecretVersionName.ofProjectLocationSecretSecretVersionName(projectId, configurationProperties.getLocation(), secretId, version);
}

private VersionedSecret versionedSecret(String secretId,
String version,
String projectId,
AccessSecretVersionResponse response) {
return StringUtils.isEmpty(configurationProperties.getLocation())
? new VersionedSecret(secretId, projectId, version, response.getPayload().getData().toByteArray())
: new VersionedSecret(secretId, projectId, version, response.getPayload().getData().toByteArray(), configurationProperties.getLocation());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package io.micronaut.gcp.secretmanager.client;

import io.micronaut.core.annotation.Nullable;

/**
* A wrapper class around {@link com.google.cloud.secretmanager.v1.AccessSecretVersionResponse} with secret information.
*
Expand All @@ -28,11 +30,19 @@ public class VersionedSecret {
private final byte[] contents;
private final String version;

@Nullable
private final String location;

public VersionedSecret(String name, String projectId, String version, byte[] contents) {
this(name, projectId, version, contents, null);
}

public VersionedSecret(String name, String projectId, String version, byte[] contents, String location) {
this.name = name;
this.projectId = projectId;
this.contents = contents;
this.version = version;
this.location = location;
}

/**
Expand Down Expand Up @@ -66,4 +76,14 @@ public String getVersion() {
public String getProjectId() {
return projectId;
}

/**
*
* @return location
* @since 5.8.0
*/
@Nullable
public String getLocation() {
return location;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

import io.micronaut.context.annotation.BootstrapContextCompatible;
import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.gcp.GoogleCloudConfiguration;
import jakarta.validation.constraints.Pattern;

import java.util.HashSet;
import java.util.LinkedHashSet;
Expand All @@ -34,10 +36,73 @@ public class SecretManagerConfigurationProperties {
public static final String PREFIX = GoogleCloudConfiguration.PREFIX + ".secret-manager";
private static final boolean DEFAULT_DEFAULT_CONFIG_ENABLED = true;

/**
* Secret Manager Locations.
* <a href="https://cloud.google.com/secret-manager/docs/locations">Secret Manger Locations</a>
*
*/
// Locations in Asia Pacific
private static final String DELHI = "asia-south2";
private static final String HONG_KONG = "asia-east2";
private static final String JAKARTA = "asia-southeast2";
private static final String MELBOURNE = "australia-southeast2";
private static final String MUMBAI = "asia-south1";
private static final String OSAKA = "asia-northeast2";
private static final String SEOUL = "asia-northeast3";
private static final String SINGAPORE = "asia-southeast1";
private static final String SYDNEY = "australia-southeast1";
private static final String TAIWAN = "asia-east1";
private static final String TOKYO = "asia-northeast1";

// Locations in Europe
private static final String BELGIUM = "europe-west1";
private static final String BERLIN = "europe-west10";
private static final String FINLAND = "europe-north1";
private static final String FRANKFURT = "europe-west3";
private static final String LONDON = "europe-west2";
private static final String MILAN = "europe-west8";
private static final String NETHERLANDS = "europe-west4";
private static final String PARIS = "europe-west9";
private static final String TURIN = "europe-west12";
private static final String WARSAW = "europe-central2";
private static final String ZURICH = "europe-west6";

// Locations in North America
private static final String COLUMBUS = "us-east5";
private static final String DALLAS = "us-south1";
private static final String IOWA = "us-central1";
private static final String LAS_VEGAS = "us-west4";
private static final String LOS_ANGELES = "us-west2";
private static final String MONTREAL = "northamerica-northeast1";
private static final String NORTHERN_VIRGINIA = "us-east4";
private static final String OREGON = "us-west1";
private static final String SALT_LAKE_CITY = "us-west3";
private static final String SOUTH_CAROLINA = "us-east1";
private static final String TORONTO = "northamerica-northeast2";

// Locations in South America
private static final String SANTIAGO = "southamerica-west1";
private static final String SAO_PAULO = "southamerica-east1";

// Locations in Middle East
private static final String DAMMAM = "me-central2";
private static final String DOHA = "me-central1";
private static final String TEL_AVIV = "me-west1";

// Locations in Africa
private static final String JOHANNESBURG = "africa-south1";

private Set<String> customConfigs = new LinkedHashSet<>();
private Set<String> keys = new HashSet<>();
private boolean defaultConfigEnabled = DEFAULT_DEFAULT_CONFIG_ENABLED;

@Pattern(regexp = DELHI + "|" + HONG_KONG + "|" + JAKARTA + "|" + MELBOURNE + "|" + MUMBAI + "|" + OSAKA + "|" + SEOUL + "|" + SINGAPORE + "|" + SYDNEY + "|" + TAIWAN + "|" + TOKYO
+ "|" + BELGIUM + "|" + BERLIN + "|" + FINLAND + "|" + FRANKFURT + "|" + LONDON + "|" + MILAN + "|" + NETHERLANDS + "|" + PARIS + "|" + TURIN + "|" + WARSAW + "|" + ZURICH
+ "|" + COLUMBUS + "|" + DALLAS + "|" + IOWA + "|" + LAS_VEGAS + "|" + LOS_ANGELES + "|" + MONTREAL + "|" + NORTHERN_VIRGINIA + "|" + OREGON + "|" + SALT_LAKE_CITY + "|" + SOUTH_CAROLINA + "|" + TORONTO
+ "|" + SANTIAGO + "|" + SAO_PAULO + "|" + DAMMAM + "|" + DOHA + "|" + TEL_AVIV + "|" + JOHANNESBURG)
@Nullable
private String location;

/**
*
* @return Custom config files to be included as property sources.
Expand Down Expand Up @@ -87,4 +152,24 @@ public boolean isDefaultConfigEnabled() {
public void setDefaultConfigEnabled(boolean defaultConfigEnabled) {
this.defaultConfigEnabled = defaultConfigEnabled;
}

/**
* Specifies the location of the regional secrets used to create a {@link com.google.cloud.secretmanager.v1.SecretManagerServiceClient} specific to the location endpoint.
* If not provided, the client will be created using the global endpoint.
* It must be one of the available location for the regional endpoints.
* See <a href="https://cloud.google.com/secret-manager/docs/locations">Secret Manager locations</a> for more information.
* @return Location of the regional secrets
*/
@Nullable
public String getLocation() {
return location;
}

/**
*
* @param location Sets the location of the regional secrets
*/
public void setLocation(@Nullable String location) {
this.location = location;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.micronaut.gcp.secretmanager

import io.micronaut.context.ApplicationContext
import io.micronaut.gcp.secretmanager.client.SecretManagerClient
import reactor.core.publisher.Mono
import spock.lang.Specification

class LocationSecretManagerClientSpec extends Specification {

void "missing regional secret"() {
ApplicationContext context = ApplicationContext.run(["spec.name" : "LocationSecretManagerClientSpec", "gcp.projectId" : "first-gcp-project", "gcp.secret-manager.location" : "us-central1"])
def client = context.getBean(SecretManagerClient)
when:
def result = Mono.from(client.getSecret("notFound", "latest", "first-gcp-project")).block()
then:
!result
}

void "fetch single regional secret"() {
ApplicationContext context = ApplicationContext.run(["spec.name" : "LocationSecretManagerClientSpec", "gcp.projectId" : "first-gcp-project", "gcp.secret-manager.location" : "us-central1"])
def client = context.getBean(SecretManagerClient)
when:
def result = Mono.from(client.getSecret("application", "latest", "first-gcp-project")).block()
then:
result.name == "application"
result.contents
}

}
Loading
Loading