-
Notifications
You must be signed in to change notification settings - Fork 339
feat: added regional secret support for secret-manager #3365
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
Changes from 22 commits
b45b0fc
c75d512
9557d43
6f23874
9f6bf38
e47d196
ca13e36
50c0f80
1bcd340
9deaa78
a35f8f2
0f45864
e175a35
64215f5
ab33af2
4bbf654
56f3ead
5db414a
ead04ca
fa72e52
e83f844
2208a5c
7683612
65f6e17
d3dca72
9bfe41a
edfc0a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
/* | ||
* Copyright 2025 Google LLC | ||
* | ||
* 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 | ||
* | ||
* https://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 com.google.cloud.spring.autoconfigure.secretmanager; | ||
|
||
import com.google.api.gax.core.CredentialsProvider; | ||
import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; | ||
import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings; | ||
import com.google.cloud.spring.core.UserAgentHeaderProvider; | ||
import com.google.cloud.spring.secretmanager.SecretManagerServiceClientFactory; | ||
import java.io.IOException; | ||
import java.util.Map; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
import javax.annotation.Nullable; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.util.ObjectUtils; | ||
|
||
/** | ||
* A default implementation of the {@link SecretManagerServiceClientFactory} interface. | ||
* | ||
* <p>This factory provides a caching layer for {@link SecretManagerServiceClient} instances. | ||
* Clients are created using the provided {@link CredentialsProvider} and a {@link | ||
* UserAgentHeaderProvider} that adds the Spring Cloud GCP agent header to the client. | ||
* | ||
*/ | ||
@Component | ||
public class DefaultSecretManagerServiceClientFactory implements SecretManagerServiceClientFactory { | ||
|
||
/** | ||
* Default value for the latest version of the secret. | ||
*/ | ||
public static final String GLOBAL_LOCATION = "global"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. GLOBAL_LOCATION seems only used within this class, does it needs to be public? We try to restrict public access when possible to avoid future breaking changes. Also see this defined separately in SecretManagerTemplate below There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Used constant from the |
||
|
||
private final CredentialsProvider credentialsProvider; | ||
private final Map<String, SecretManagerServiceClient> clientCache = new ConcurrentHashMap<>(); | ||
|
||
public DefaultSecretManagerServiceClientFactory(CredentialsProvider credentialsProvider, SecretManagerServiceClient client) { | ||
zhumin8 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
this.credentialsProvider = credentialsProvider; | ||
this.clientCache.putIfAbsent(GLOBAL_LOCATION, client); | ||
} | ||
|
||
@Override | ||
public SecretManagerServiceClient getClient(@Nullable String location) { | ||
if (ObjectUtils.isEmpty(location)) { | ||
location = GLOBAL_LOCATION; | ||
} | ||
return clientCache.computeIfAbsent(location, loc -> { | ||
try { | ||
SecretManagerServiceSettings.Builder settings = SecretManagerServiceSettings.newBuilder() | ||
.setCredentialsProvider(credentialsProvider) | ||
.setHeaderProvider(new UserAgentHeaderProvider(SecretManagerConfigDataLoader.class)); | ||
if (!loc.equals(GLOBAL_LOCATION)) { | ||
settings.setEndpoint(String.format("secretmanager.%s.rep.googleapis.com:443", loc)); | ||
} | ||
return SecretManagerServiceClient.create(settings.build()); | ||
} catch (IOException e) { | ||
throw new RuntimeException( | ||
"Failed to create SecretManagerServiceClient for location: " + loc, e); | ||
} | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ | |
import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings; | ||
import com.google.cloud.spring.core.GcpProjectIdProvider; | ||
import com.google.cloud.spring.core.UserAgentHeaderProvider; | ||
import com.google.cloud.spring.secretmanager.SecretManagerServiceClientFactory; | ||
import com.google.cloud.spring.secretmanager.SecretManagerTemplate; | ||
import java.io.IOException; | ||
import org.springframework.boot.autoconfigure.AutoConfiguration; | ||
|
@@ -30,6 +31,7 @@ | |
import org.springframework.boot.context.properties.EnableConfigurationProperties; | ||
import org.springframework.context.annotation.Bean; | ||
|
||
|
||
/** | ||
* Autoconfiguration for GCP Secret Manager. | ||
* | ||
|
@@ -60,22 +62,33 @@ public GcpSecretManagerAutoConfiguration( | |
|
||
@Bean | ||
@ConditionalOnMissingBean | ||
public SecretManagerServiceClient secretManagerClient() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Customers may already be using this one or defining a custom There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated. Also checked sample application by using the |
||
throws IOException { | ||
public SecretManagerServiceClient secretManagerClient() throws IOException { | ||
SecretManagerServiceSettings settings = | ||
SecretManagerServiceSettings.newBuilder() | ||
.setCredentialsProvider(this.credentialsProvider) | ||
.setHeaderProvider( | ||
new UserAgentHeaderProvider(GcpSecretManagerAutoConfiguration.class)) | ||
.setHeaderProvider(new UserAgentHeaderProvider(GcpSecretManagerAutoConfiguration.class)) | ||
.build(); | ||
|
||
return SecretManagerServiceClient.create(settings); | ||
} | ||
|
||
@Bean | ||
@ConditionalOnMissingBean | ||
public SecretManagerTemplate secretManagerTemplate(SecretManagerServiceClient client) { | ||
return new SecretManagerTemplate(client, this.gcpProjectIdProvider) | ||
.setAllowDefaultSecretValue(this.properties.isAllowDefaultSecret()); | ||
public SecretManagerServiceClientFactory clientFactory(SecretManagerServiceClient client) { | ||
return new DefaultSecretManagerServiceClientFactory(this.credentialsProvider, client); | ||
} | ||
|
||
|
||
@Bean | ||
@ConditionalOnMissingBean | ||
public SecretManagerTemplate secretManagerTemplate( | ||
SecretManagerServiceClient client, SecretManagerServiceClientFactory clientFactory) { | ||
if (clientFactory != null) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you sure Spring Boot will not throw an an exception if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I have changed it with |
||
return new SecretManagerTemplate(clientFactory, this.gcpProjectIdProvider) | ||
.setAllowDefaultSecretValue(this.properties.isAllowDefaultSecret()); | ||
} else { | ||
return new SecretManagerTemplate(client, this.gcpProjectIdProvider) | ||
.setAllowDefaultSecretValue(this.properties.isAllowDefaultSecret()); | ||
} | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,18 +19,20 @@ | |
import static com.google.cloud.spring.secretmanager.SecretManagerSyntaxUtils.getMatchedPrefixes; | ||
import static com.google.cloud.spring.secretmanager.SecretManagerSyntaxUtils.warnIfUsingDeprecatedSyntax; | ||
|
||
import autovalue.shaded.com.google.common.annotations.VisibleForTesting; | ||
import com.google.api.gax.core.CredentialsProvider; | ||
import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; | ||
import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings; | ||
import com.google.cloud.spring.core.DefaultCredentialsProvider; | ||
import com.google.cloud.spring.core.DefaultGcpProjectIdProvider; | ||
import com.google.cloud.spring.core.GcpProjectIdProvider; | ||
import com.google.cloud.spring.core.UserAgentHeaderProvider; | ||
import com.google.cloud.spring.secretmanager.SecretManagerServiceClientFactory; | ||
import com.google.cloud.spring.secretmanager.SecretManagerTemplate; | ||
import java.io.IOException; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.Optional; | ||
import org.apache.arrow.util.VisibleForTesting; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; | ||
|
@@ -44,42 +46,52 @@ | |
public class SecretManagerConfigDataLocationResolver implements | ||
ConfigDataLocationResolver<SecretManagerConfigDataResource> { | ||
|
||
private static final Logger logger = LoggerFactory.getLogger(SecretManagerConfigDataLocationResolver.class); | ||
private static final Logger logger = | ||
LoggerFactory.getLogger(SecretManagerConfigDataLocationResolver.class); | ||
|
||
/** | ||
* A static client to avoid creating another client after refreshing. | ||
*/ | ||
private static SecretManagerServiceClient secretManagerServiceClient; | ||
/** | ||
* A static client factory to avoid creating another client after refreshing. | ||
*/ | ||
private static SecretManagerServiceClientFactory secretManagerServiceClientFactory; | ||
|
||
@Override | ||
public boolean isResolvable(ConfigDataLocationResolverContext context, | ||
ConfigDataLocation location) { | ||
public boolean isResolvable( | ||
ConfigDataLocationResolverContext context, ConfigDataLocation location) { | ||
Optional<String> matchedPrefix = getMatchedPrefixes(location::hasPrefix); | ||
warnIfUsingDeprecatedSyntax(logger, matchedPrefix.orElse("")); | ||
return matchedPrefix.isPresent(); | ||
} | ||
|
||
@Override | ||
public List<SecretManagerConfigDataResource> resolve(ConfigDataLocationResolverContext context, | ||
ConfigDataLocation location) | ||
public List<SecretManagerConfigDataResource> resolve( | ||
ConfigDataLocationResolverContext context, ConfigDataLocation location) | ||
throws ConfigDataLocationNotFoundException, ConfigDataResourceNotFoundException { | ||
registerSecretManagerBeans(context); | ||
|
||
return Collections.singletonList( | ||
new SecretManagerConfigDataResource(location)); | ||
return Collections.singletonList(new SecretManagerConfigDataResource(location)); | ||
} | ||
|
||
private static void registerSecretManagerBeans(ConfigDataLocationResolverContext context) { | ||
// Register the Secret Manager properties. | ||
registerBean( | ||
context, GcpSecretManagerProperties.class, getSecretManagerProperties(context)); | ||
registerBean(context, GcpSecretManagerProperties.class, getSecretManagerProperties(context)); | ||
// Register the CredentialsProvider. | ||
registerBean(context, CredentialsProvider.class, getCredentialsProvider(context)); | ||
// Register the Secret Manager client factory. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @abheda-crest Sorry this was missed in earlier reviews. I believe the root cause is here: This SecretManagerConfigDataLocationResolver is used regardless if secretmanager dependency is present. When not present, for example if you run the vision sample app, this code Note that secretmanager is optional dependency for autoconfig. You will need to guard this logic to only run when secretmanager is present. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have fixed the issue |
||
registerAndPromoteBean( | ||
context, | ||
SecretManagerServiceClientFactory.class, | ||
BootstrapRegistry.InstanceSupplier.from( | ||
() -> createSecretManagerServiceClientFactory(context))); | ||
// Register the Secret Manager client. | ||
registerAndPromoteBean( | ||
context, | ||
SecretManagerServiceClient.class, | ||
// lazy register the client solely for unit test. | ||
BootstrapRegistry.InstanceSupplier.from(() -> createSecretManagerClient(context))); | ||
// Register the GCP Project ID provider. | ||
registerAndPromoteBean( | ||
context, | ||
GcpProjectIdProvider.class, | ||
|
@@ -98,12 +110,27 @@ private static GcpSecretManagerProperties getSecretManagerProperties( | |
.orElse(new GcpSecretManagerProperties()); | ||
} | ||
|
||
private static CredentialsProvider getCredentialsProvider( | ||
ConfigDataLocationResolverContext context) { | ||
try { | ||
GcpSecretManagerProperties properties = | ||
context.getBootstrapContext().get(GcpSecretManagerProperties.class); | ||
return context.getBinder() | ||
.bind(GcpSecretManagerProperties.PREFIX, CredentialsProvider.class) | ||
.orElse(new DefaultCredentialsProvider(properties)); | ||
} catch (IOException e) { | ||
throw new RuntimeException( | ||
"Failed to create the Secret Manager Client Factory for ConfigData loading.", e); | ||
} | ||
} | ||
|
||
private static GcpProjectIdProvider createProjectIdProvider( | ||
ConfigDataLocationResolverContext context) { | ||
GcpSecretManagerProperties properties = context.getBootstrapContext() | ||
.get(GcpSecretManagerProperties.class); | ||
return properties.getProjectId() != null | ||
? properties::getProjectId : new DefaultGcpProjectIdProvider(); | ||
? properties::getProjectId | ||
: new DefaultGcpProjectIdProvider(); | ||
} | ||
|
||
@VisibleForTesting | ||
|
@@ -132,19 +159,39 @@ static synchronized SecretManagerServiceClient createSecretManagerClient( | |
} | ||
} | ||
|
||
@VisibleForTesting | ||
static synchronized SecretManagerServiceClientFactory createSecretManagerServiceClientFactory( | ||
ConfigDataLocationResolverContext context) { | ||
if (secretManagerServiceClientFactory != null) { | ||
return secretManagerServiceClientFactory; | ||
} | ||
SecretManagerServiceClient client = context.getBootstrapContext() | ||
.get(SecretManagerServiceClient.class); | ||
return new DefaultSecretManagerServiceClientFactory( | ||
context.getBootstrapContext().get(CredentialsProvider.class), client); | ||
} | ||
|
||
private static SecretManagerTemplate createSecretManagerTemplate( | ||
ConfigDataLocationResolverContext context) { | ||
SecretManagerServiceClient client = context.getBootstrapContext() | ||
.get(SecretManagerServiceClient.class); | ||
SecretManagerServiceClientFactory clientFactory = context.getBootstrapContext() | ||
.get(SecretManagerServiceClientFactory.class); | ||
GcpProjectIdProvider projectIdProvider = context.getBootstrapContext() | ||
.get(GcpProjectIdProvider.class); | ||
GcpSecretManagerProperties properties = context.getBootstrapContext() | ||
.get(GcpSecretManagerProperties.class); | ||
|
||
return new SecretManagerTemplate(client, projectIdProvider) | ||
.setAllowDefaultSecretValue(properties.isAllowDefaultSecret()); | ||
if (clientFactory != null) { | ||
return new SecretManagerTemplate(clientFactory, projectIdProvider) | ||
.setAllowDefaultSecretValue(properties.isAllowDefaultSecret()); | ||
} else { | ||
return new SecretManagerTemplate(client, projectIdProvider) | ||
.setAllowDefaultSecretValue(properties.isAllowDefaultSecret()); | ||
} | ||
} | ||
|
||
|
||
/** | ||
* Registers a bean in the Bootstrap Registry. | ||
* | ||
|
@@ -153,7 +200,8 @@ private static SecretManagerTemplate createSecretManagerTemplate( | |
*/ | ||
private static <T> void registerBean( | ||
ConfigDataLocationResolverContext context, Class<T> type, T instance) { | ||
context.getBootstrapContext() | ||
context | ||
.getBootstrapContext() | ||
.registerIfAbsent(type, BootstrapRegistry.InstanceSupplier.of(instance)); | ||
} | ||
|
||
|
@@ -162,7 +210,8 @@ private static <T> void registerBean( | |
* application context. | ||
*/ | ||
private static <T> void registerAndPromoteBean( | ||
ConfigDataLocationResolverContext context, Class<T> type, | ||
ConfigDataLocationResolverContext context, | ||
Class<T> type, | ||
BootstrapRegistry.InstanceSupplier<T> supplier) { | ||
context.getBootstrapContext().registerIfAbsent(type, supplier); | ||
context.getBootstrapContext().addCloseListener(event -> { | ||
|
@@ -179,4 +228,10 @@ private static <T> void registerAndPromoteBean( | |
static void setSecretManagerServiceClient(SecretManagerServiceClient client) { | ||
secretManagerServiceClient = client; | ||
} | ||
|
||
@VisibleForTesting | ||
static void setSecretManagerServiceClientFactory( | ||
SecretManagerServiceClientFactory clientFactory) { | ||
secretManagerServiceClientFactory = clientFactory; | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.