Skip to content

Commit 42dcccb

Browse files
authored
feat: added regional secret support for secret-manager (#3365)
Issue: #3331 Description: Added the support for regional secret creation/updation, deletion and fetch for secret manager service. Updated the autoconfigure/secretmanager to create the regional client and secretmanager module to support the regional secret operation. Added the optional property location in GcpSecretManagerProperties.java which will take region from application.properties file. Whenever the location is available, it will use to perform the operation on regional secret. If not provided the global stack will be served. Updated the documentation for this additional property in the docs/src/main/asciidoc/secretmanager.adoc Added the sample application for regional secret operations. Added/Updated the unit and integration tests. Note: Fixed the integration test testUpdateSecrets in the file secretmanager/it/SecretManagerTemplateIntegrationTests.java Performed the below mentioned manual unit tests to validate the working of the global and regional secret operations. Create secret with only secretId and payload Create secret with existing secretId and payload Create secret with secretId, payload and valid projectID Create secret with secretId, payload and invalid projectID (project on which user doesn't have access) Read an existing secret with secretId Read a non-existing secret with secretId Read a secret with secretId and existing version Read a secret with secretId and disabled version Read a secret with secretId and destroyed version Read a secret with secretId and non-existing version Read a secret with secretId and valid project Read a secret with secretId and invalid project (project on which user doesn't have access) Read a secret with secretId and existing version and valid project Read a secret with secretId and existing version and invalid project Read a secret with secretId and non-existing version and valid project Read a secret with secretId and non-existing version and invalid project Update an existing secret with only secretId and payload Update an existing secret with secretId, payload and valid projectID Update an existing secret with secretId, payload and invalid projectID (project on which user doesn't have access) Delete an existing secret with secretId Delete a non-existing secret with secretId Delete a secret with secretId and valid projectId Delete a secret with secretId and invalid projectId Enable an existing secret and valid version Enable an existing secret without version Enable an existing secret and invalid version Enable a non-existing secret Disable an existing secret and valid version Disable an existing secret without version Disable an existing secret and invalid version Disable a non-existing secret Check if secret exists for existing secret Check if secret exists for non-existing secret Check if secret exists for existing secret & valid projectId Check if secret exists for existing secret & invalid projectId Read secret with secretId and other project using service account Inject secret with the @value annotation More information about regional secrets: https://cloud.google.com/secret-manager/regional-secrets/data-residency
1 parent 3441f06 commit 42dcccb

25 files changed

+1799
-166
lines changed

docs/src/main/asciidoc/secretmanager.adoc

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,15 @@ This allows you to specify and load secrets from Google Cloud Secret Manager as
5656

5757
The Secret Manager config data resource uses the following syntax to specify secrets:
5858

59+
== Global Secrets
60+
The following formats apply to **global secrets**, where secrets are stored without specifying a region.
61+
5962
[source]
6063
----
6164
# 1. Long form - specify the project ID, secret ID, and version
6265
sm@projects/<project-id>/secrets/<secret-id>/versions/<version-id>}
6366
64-
# 2. Long form - specify project ID, secret ID, and use latest version
67+
# 2. Long form - specify project ID, secret ID, and use latest version
6568
sm@projects/<project-id>/secrets/<secret-id>
6669
6770
# 3. Short form - specify project ID, secret ID, and version
@@ -78,6 +81,28 @@ sm@<secret-id>/<version>
7881
sm@<secret-id>
7982
----
8083

84+
== Regional Secrets
85+
The following formats apply to **regional secrets**, where secrets are stored in a specific Google Cloud region.
86+
For more details, see https://cloud.google.com/secret-manager/regional-secrets/data-residency[Google Cloud Regional Secrets].
87+
88+
[source]
89+
----
90+
# 6. Long form - specify project ID, location ID, secret ID, and version
91+
sm@projects/<project-id>/locations/<location-id>/secrets/<secret-id>/versions/<version-id>
92+
93+
# 7. Long form - specify project ID, location ID, secret ID, and use latest version
94+
sm@projects/<project-id>/locations/<location-id>/secrets/<secret-id>
95+
96+
# 8. Short form - specify project ID, location ID, secret ID, and version
97+
sm@<project-id>/<location-id>/<secret-id>/<version-id>
98+
99+
# 9. Short form - specify location ID, secret ID, version and use default project
100+
sm@locations/<location-id>/<secret-id>/<version>
101+
102+
# 10. Shortest form - specify location ID, secret ID, and use default project and latest version
103+
sm@locations/<location-id>/<secret-id>
104+
----
105+
81106
You can use this syntax in the following places:
82107

83108
1. In your `application.properties` file:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.spring.autoconfigure.secretmanager;
18+
19+
import static com.google.cloud.spring.secretmanager.SecretManagerTemplate.GLOBAL_LOCATION;
20+
21+
import com.google.api.gax.core.CredentialsProvider;
22+
import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
23+
import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings;
24+
import com.google.cloud.spring.core.UserAgentHeaderProvider;
25+
import com.google.cloud.spring.secretmanager.SecretManagerServiceClientFactory;
26+
import java.io.IOException;
27+
import java.util.Map;
28+
import java.util.concurrent.ConcurrentHashMap;
29+
import javax.annotation.Nullable;
30+
import org.springframework.stereotype.Component;
31+
import org.springframework.util.ObjectUtils;
32+
33+
/**
34+
* A default implementation of the {@link SecretManagerServiceClientFactory} interface.
35+
*
36+
* <p>This factory provides a caching layer for {@link SecretManagerServiceClient} instances.
37+
* Clients are created using the provided {@link CredentialsProvider} and a {@link
38+
* UserAgentHeaderProvider} that adds the Spring Cloud GCP agent header to the client.
39+
*
40+
*/
41+
@Component
42+
public class DefaultSecretManagerServiceClientFactory implements SecretManagerServiceClientFactory {
43+
44+
private final CredentialsProvider credentialsProvider;
45+
private final Map<String, SecretManagerServiceClient> clientCache = new ConcurrentHashMap<>();
46+
47+
DefaultSecretManagerServiceClientFactory(CredentialsProvider credentialsProvider, SecretManagerServiceClient client) {
48+
this.credentialsProvider = credentialsProvider;
49+
this.clientCache.putIfAbsent(GLOBAL_LOCATION, client);
50+
}
51+
52+
@Override
53+
public SecretManagerServiceClient getClient(@Nullable String location) {
54+
if (ObjectUtils.isEmpty(location)) {
55+
location = GLOBAL_LOCATION;
56+
}
57+
return clientCache.computeIfAbsent(location, loc -> {
58+
try {
59+
SecretManagerServiceSettings.Builder settings = SecretManagerServiceSettings.newBuilder()
60+
.setCredentialsProvider(credentialsProvider)
61+
.setHeaderProvider(new UserAgentHeaderProvider(SecretManagerConfigDataLoader.class));
62+
if (!loc.equals(GLOBAL_LOCATION)) {
63+
settings.setEndpoint(String.format("secretmanager.%s.rep.googleapis.com:443", loc));
64+
}
65+
return SecretManagerServiceClient.create(settings.build());
66+
} catch (IOException e) {
67+
throw new RuntimeException(
68+
"Failed to create SecretManagerServiceClient for location: " + loc, e);
69+
}
70+
});
71+
}
72+
}

spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/GcpSecretManagerAutoConfiguration.java

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222
import com.google.cloud.spring.autoconfigure.core.GcpContextAutoConfiguration;
2323
import com.google.cloud.spring.core.GcpProjectIdProvider;
2424
import com.google.cloud.spring.core.UserAgentHeaderProvider;
25+
import com.google.cloud.spring.secretmanager.SecretManagerServiceClientFactory;
2526
import com.google.cloud.spring.secretmanager.SecretManagerTemplate;
2627
import java.io.IOException;
28+
import org.springframework.beans.factory.ObjectProvider;
2729
import org.springframework.boot.autoconfigure.AutoConfiguration;
2830
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
2931
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -32,6 +34,7 @@
3234
import org.springframework.boot.context.properties.EnableConfigurationProperties;
3335
import org.springframework.context.annotation.Bean;
3436

37+
3538
/**
3639
* Autoconfiguration for GCP Secret Manager.
3740
*
@@ -63,22 +66,36 @@ public GcpSecretManagerAutoConfiguration(
6366

6467
@Bean
6568
@ConditionalOnMissingBean
66-
public SecretManagerServiceClient secretManagerClient()
67-
throws IOException {
69+
public SecretManagerServiceClient secretManagerClient() throws IOException {
6870
SecretManagerServiceSettings settings =
6971
SecretManagerServiceSettings.newBuilder()
7072
.setCredentialsProvider(this.credentialsProvider)
71-
.setHeaderProvider(
72-
new UserAgentHeaderProvider(GcpSecretManagerAutoConfiguration.class))
73+
.setHeaderProvider(new UserAgentHeaderProvider(GcpSecretManagerAutoConfiguration.class))
7374
.build();
74-
7575
return SecretManagerServiceClient.create(settings);
7676
}
7777

7878
@Bean
7979
@ConditionalOnMissingBean
80-
public SecretManagerTemplate secretManagerTemplate(SecretManagerServiceClient client) {
81-
return new SecretManagerTemplate(client, this.gcpProjectIdProvider)
82-
.setAllowDefaultSecretValue(this.properties.isAllowDefaultSecret());
80+
public SecretManagerServiceClientFactory clientFactory(SecretManagerServiceClient client) {
81+
return new DefaultSecretManagerServiceClientFactory(this.credentialsProvider, client);
82+
}
83+
84+
85+
@Bean
86+
@ConditionalOnMissingBean
87+
public SecretManagerTemplate secretManagerTemplate(
88+
SecretManagerServiceClient client, ObjectProvider<SecretManagerServiceClientFactory> clientFactoryProvider) {
89+
90+
SecretManagerServiceClientFactory clientFactory = clientFactoryProvider.getIfAvailable();
91+
92+
if (clientFactory != null) {
93+
return new SecretManagerTemplate(clientFactory, this.gcpProjectIdProvider)
94+
.setAllowDefaultSecretValue(this.properties.isAllowDefaultSecret());
95+
} else {
96+
return new SecretManagerTemplate(client, this.gcpProjectIdProvider)
97+
.setAllowDefaultSecretValue(this.properties.isAllowDefaultSecret());
98+
}
8399
}
100+
84101
}

spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLoader.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ public ConfigData load(
4040
GcpProjectIdProvider projectIdProvider = context.getBootstrapContext()
4141
.get(GcpProjectIdProvider.class);
4242

43-
return new ConfigData(Collections.singleton(new SecretManagerPropertySource(
44-
"spring-cloud-gcp-secret-manager", secretManagerTemplate, projectIdProvider)));
43+
SecretManagerPropertySource secretManagerPropertySource = new SecretManagerPropertySource(
44+
"spring-cloud-gcp-secret-manager", secretManagerTemplate, projectIdProvider);
45+
return new ConfigData(Collections.singleton(secretManagerPropertySource));
4546
}
4647
}

spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLocationResolver.java

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@
1919
import static com.google.cloud.spring.secretmanager.SecretManagerSyntaxUtils.getMatchedPrefixes;
2020
import static com.google.cloud.spring.secretmanager.SecretManagerSyntaxUtils.warnIfUsingDeprecatedSyntax;
2121

22+
import com.google.api.gax.core.CredentialsProvider;
2223
import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
2324
import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings;
2425
import com.google.cloud.spring.autoconfigure.core.GcpProperties;
2526
import com.google.cloud.spring.core.DefaultCredentialsProvider;
2627
import com.google.cloud.spring.core.DefaultGcpProjectIdProvider;
2728
import com.google.cloud.spring.core.GcpProjectIdProvider;
2829
import com.google.cloud.spring.core.UserAgentHeaderProvider;
30+
import com.google.cloud.spring.secretmanager.SecretManagerServiceClientFactory;
2931
import com.google.cloud.spring.secretmanager.SecretManagerTemplate;
3032
import java.io.IOException;
3133
import java.util.Collections;
@@ -52,6 +54,10 @@ public class SecretManagerConfigDataLocationResolver implements
5254
* A static client to avoid creating another client after refreshing.
5355
*/
5456
private static SecretManagerServiceClient secretManagerServiceClient;
57+
/**
58+
* A static client factory to avoid creating another client after refreshing.
59+
*/
60+
private static SecretManagerServiceClientFactory secretManagerServiceClientFactory;
5561

5662
/**
5763
* Checks if the property can be resolved by the Secret Manager resolver.
@@ -104,8 +110,15 @@ private static void registerSecretManagerBeans(ConfigDataLocationResolverContext
104110
// Register the Core properties.
105111
registerBean(context, GcpProperties.class, getGcpProperties(context));
106112
// Register the Secret Manager properties.
107-
registerBean(
108-
context, GcpSecretManagerProperties.class, getSecretManagerProperties(context));
113+
registerBean(context, GcpSecretManagerProperties.class, getSecretManagerProperties(context));
114+
// Register the CredentialsProvider.
115+
registerBean(context, CredentialsProvider.class, getCredentialsProvider(context));
116+
// Register the Secret Manager client factory.
117+
registerAndPromoteBean(
118+
context,
119+
SecretManagerServiceClientFactory.class,
120+
BootstrapRegistry.InstanceSupplier.from(
121+
() -> createSecretManagerServiceClientFactory(context)));
109122
// Register the Secret Manager client.
110123
registerAndPromoteBean(
111124
context,
@@ -135,6 +148,20 @@ private static GcpSecretManagerProperties getSecretManagerProperties(
135148
.orElse(new GcpSecretManagerProperties());
136149
}
137150

151+
private static CredentialsProvider getCredentialsProvider(
152+
ConfigDataLocationResolverContext context) {
153+
try {
154+
GcpSecretManagerProperties properties =
155+
context.getBootstrapContext().get(GcpSecretManagerProperties.class);
156+
return context.getBinder()
157+
.bind(GcpSecretManagerProperties.PREFIX, CredentialsProvider.class)
158+
.orElse(new DefaultCredentialsProvider(properties));
159+
} catch (IOException e) {
160+
throw new RuntimeException(
161+
"Failed to create the Secret Manager Client Factory for ConfigData loading.", e);
162+
}
163+
}
164+
138165
@VisibleForTesting
139166
static GcpProjectIdProvider createProjectIdProvider(ConfigDataLocationResolverContext context) {
140167
ConfigurableBootstrapContext bootstrapContext = context.getBootstrapContext();
@@ -176,17 +203,36 @@ static synchronized SecretManagerServiceClient createSecretManagerClient(
176203
}
177204
}
178205

206+
@VisibleForTesting
207+
static synchronized SecretManagerServiceClientFactory createSecretManagerServiceClientFactory(
208+
ConfigDataLocationResolverContext context) {
209+
if (secretManagerServiceClientFactory != null) {
210+
return secretManagerServiceClientFactory;
211+
}
212+
SecretManagerServiceClient client = context.getBootstrapContext()
213+
.get(SecretManagerServiceClient.class);
214+
return new DefaultSecretManagerServiceClientFactory(
215+
context.getBootstrapContext().get(CredentialsProvider.class), client);
216+
}
217+
179218
private static SecretManagerTemplate createSecretManagerTemplate(
180219
ConfigDataLocationResolverContext context) {
181220
SecretManagerServiceClient client = context.getBootstrapContext()
182221
.get(SecretManagerServiceClient.class);
222+
SecretManagerServiceClientFactory clientFactory = context.getBootstrapContext()
223+
.get(SecretManagerServiceClientFactory.class);
183224
GcpProjectIdProvider projectIdProvider = context.getBootstrapContext()
184225
.get(GcpProjectIdProvider.class);
185226
GcpSecretManagerProperties properties = context.getBootstrapContext()
186227
.get(GcpSecretManagerProperties.class);
187228

188-
return new SecretManagerTemplate(client, projectIdProvider)
189-
.setAllowDefaultSecretValue(properties.isAllowDefaultSecret());
229+
if (clientFactory != null) {
230+
return new SecretManagerTemplate(clientFactory, projectIdProvider)
231+
.setAllowDefaultSecretValue(properties.isAllowDefaultSecret());
232+
} else {
233+
return new SecretManagerTemplate(client, projectIdProvider)
234+
.setAllowDefaultSecretValue(properties.isAllowDefaultSecret());
235+
}
190236
}
191237

192238
/**
@@ -223,4 +269,10 @@ private static <T> void registerAndPromoteBean(
223269
static void setSecretManagerServiceClient(SecretManagerServiceClient client) {
224270
secretManagerServiceClient = client;
225271
}
272+
273+
@VisibleForTesting
274+
static void setSecretManagerServiceClientFactory(
275+
SecretManagerServiceClientFactory clientFactory) {
276+
secretManagerServiceClientFactory = clientFactory;
277+
}
226278
}

spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/secretmanager/GcpSecretManagerAutoConfigurationUnitTests.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,12 @@
1717
package com.google.cloud.spring.autoconfigure.secretmanager;
1818

1919
import static org.assertj.core.api.Assertions.assertThat;
20-
import static org.mockito.Mockito.mock;
2120

2221
import com.google.api.gax.core.CredentialsProvider;
23-
import com.google.auth.Credentials;
2422
import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
2523
import com.google.cloud.spring.autoconfigure.TestUtils;
2624
import com.google.cloud.spring.autoconfigure.core.GcpContextAutoConfiguration;
25+
import com.google.cloud.spring.secretmanager.SecretManagerServiceClientFactory;
2726
import com.google.cloud.spring.secretmanager.SecretManagerTemplate;
2827
import org.junit.jupiter.api.BeforeEach;
2928
import org.junit.jupiter.api.Test;
@@ -67,6 +66,13 @@ void testSecretManagerServiceClientExists() {
6766
.isNotNull());
6867
}
6968

69+
@Test
70+
void testSecretManagerServiceClientFactoryExists() {
71+
contextRunner.run(
72+
ctx -> assertThat(ctx.getBean(SecretManagerServiceClientFactory.class))
73+
.isNotNull());
74+
}
75+
7076
@Test
7177
void testSecretManagerTemplateExists() {
7278
contextRunner.run(

spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLoaderUnitTests.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
import static org.mockito.Mockito.mock;
66
import static org.mockito.Mockito.when;
77

8+
import com.google.api.gax.core.CredentialsProvider;
89
import com.google.cloud.spring.core.GcpProjectIdProvider;
910
import com.google.cloud.spring.secretmanager.SecretManagerTemplate;
10-
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.params.ParameterizedTest;
12+
import org.junit.jupiter.params.provider.CsvSource;
1113
import org.springframework.boot.ConfigurableBootstrapContext;
1214
import org.springframework.boot.context.config.ConfigDataLoaderContext;
1315
import org.springframework.boot.context.config.ConfigDataLocation;
@@ -21,15 +23,23 @@ class SecretManagerConfigDataLoaderUnitTests {
2123
private final ConfigDataLoaderContext loaderContext = mock(ConfigDataLoaderContext.class);
2224
private final GcpProjectIdProvider idProvider = mock(GcpProjectIdProvider.class);
2325
private final SecretManagerTemplate template = mock(SecretManagerTemplate.class);
26+
private final GcpSecretManagerProperties properties = mock(GcpSecretManagerProperties.class);
27+
private final CredentialsProvider credentialsProvider = mock(CredentialsProvider.class);
2428
private final ConfigurableBootstrapContext bootstrapContext = mock(
2529
ConfigurableBootstrapContext.class);
2630
private final SecretManagerConfigDataLoader loader = new SecretManagerConfigDataLoader();
2731

28-
@Test
29-
void loadIncorrectResourceThrowsException() {
32+
@ParameterizedTest
33+
@CsvSource({
34+
"regional-fake, us-central1",
35+
"fake, "
36+
})
37+
void loadIncorrectResourceThrowsException(String resourceName, String location) {
3038
when(loaderContext.getBootstrapContext()).thenReturn(bootstrapContext);
3139
when(bootstrapContext.get(GcpProjectIdProvider.class)).thenReturn(idProvider);
3240
when(bootstrapContext.get(SecretManagerTemplate.class)).thenReturn(template);
41+
when(bootstrapContext.get(GcpSecretManagerProperties.class)).thenReturn(properties);
42+
when(bootstrapContext.get(CredentialsProvider.class)).thenReturn(credentialsProvider);
3343
when(template.secretExists(anyString(), anyString())).thenReturn(false);
3444
SecretManagerConfigDataResource resource = new SecretManagerConfigDataResource(
3545
ConfigDataLocation.of("fake"));

0 commit comments

Comments
 (0)