diff --git a/docs/src/main/asciidoc/secretmanager.adoc b/docs/src/main/asciidoc/secretmanager.adoc
index a769c31a5a..c73d7d060a 100644
--- a/docs/src/main/asciidoc/secretmanager.adoc
+++ b/docs/src/main/asciidoc/secretmanager.adoc
@@ -42,6 +42,7 @@ This can be overridden using the authentication properties.
| `spring.cloud.gcp.secretmanager.credentials.location` | OAuth2 credentials for authenticating to the Google Cloud Secret Manager API. | No | By default, infers credentials from https://cloud.google.com/docs/authentication/production[Application Default Credentials].
| `spring.cloud.gcp.secretmanager.credentials.encoded-key` | Base64-encoded contents of OAuth2 account private key for authenticating to the Google Cloud Secret Manager API. | No | By default, infers credentials from https://cloud.google.com/docs/authentication/production[Application Default Credentials].
| `spring.cloud.gcp.secretmanager.project-id` | The default Google Cloud project used to access Secret Manager API for the template and property source. | No | By default, infers the project from https://cloud.google.com/docs/authentication/production[Application Default Credentials].
+| spring.cloud.gcp.secretmanager.location | Defines the region of the Secret Manager where your secrets are stored, specifically when using a regional stack. This option is particularly useful for applications that need to access secrets from a specific geographical location. | No | By default, the global stack will be utilized.
|`spring.cloud.gcp.secretmanager.allow-default-secret`| Define the behavior when accessing a non-existent secret string/bytes. +
If set to `true`, `null` will be returned when accessing a non-existent secret; otherwise throwing an exception. | No | `false`
|===
diff --git a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/GcpSecretManagerAutoConfiguration.java b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/GcpSecretManagerAutoConfiguration.java
index 365ef6739f..83a68a204b 100644
--- a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/GcpSecretManagerAutoConfiguration.java
+++ b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/GcpSecretManagerAutoConfiguration.java
@@ -76,6 +76,7 @@ public SecretManagerServiceClient secretManagerClient()
@ConditionalOnMissingBean
public SecretManagerTemplate secretManagerTemplate(SecretManagerServiceClient client) {
return new SecretManagerTemplate(client, this.gcpProjectIdProvider)
- .setAllowDefaultSecretValue(this.properties.isAllowDefaultSecret());
+ .setAllowDefaultSecretValue(this.properties.isAllowDefaultSecret())
+ .setLocation(this.properties.getLocation());
}
}
diff --git a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/GcpSecretManagerProperties.java b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/GcpSecretManagerProperties.java
index 852533d36b..363adc7976 100644
--- a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/GcpSecretManagerProperties.java
+++ b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/GcpSecretManagerProperties.java
@@ -21,6 +21,7 @@
import com.google.cloud.spring.core.Credentials;
import com.google.cloud.spring.core.CredentialsSupplier;
import com.google.cloud.spring.core.GcpScope;
+import java.util.Optional;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
@@ -43,6 +44,13 @@ public class GcpSecretManagerProperties implements CredentialsSupplier {
*/
private String projectId;
+ /**
+ * Defines the region of the secrets when Regional Stack is used.
+ *
+ *
When not specified, the secret manager will use the Global Stack.
+ */
+ private Optional location = Optional.empty();
+
/**
* Whether the secret manager will allow a default secret value when accessing a non-existing
* secret.
@@ -71,4 +79,12 @@ public boolean isAllowDefaultSecret() {
public void setAllowDefaultSecret(boolean allowDefaultSecret) {
this.allowDefaultSecret = allowDefaultSecret;
}
+
+ public Optional getLocation() {
+ return location;
+ }
+
+ public void setLocation(String location) {
+ this.location = Optional.ofNullable(location).filter(loc -> !loc.isEmpty());
+ }
}
diff --git a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLoader.java b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLoader.java
index e49bdea27b..098701b01b 100644
--- a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLoader.java
+++ b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLoader.java
@@ -40,7 +40,14 @@ public ConfigData load(
GcpProjectIdProvider projectIdProvider = context.getBootstrapContext()
.get(GcpProjectIdProvider.class);
- return new ConfigData(Collections.singleton(new SecretManagerPropertySource(
- "spring-cloud-gcp-secret-manager", secretManagerTemplate, projectIdProvider)));
+ GcpSecretManagerProperties properties = context.getBootstrapContext()
+ .get(GcpSecretManagerProperties.class);
+
+ SecretManagerPropertySource secretManagerPropertySource = new SecretManagerPropertySource(
+ "spring-cloud-gcp-secret-manager", secretManagerTemplate, projectIdProvider);
+
+ secretManagerPropertySource.setLocation(properties.getLocation());
+
+ return new ConfigData(Collections.singleton(secretManagerPropertySource));
}
}
diff --git a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLocationResolver.java b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLocationResolver.java
index f75c9a1d1f..efe2970c2e 100644
--- a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLocationResolver.java
+++ b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLocationResolver.java
@@ -46,6 +46,10 @@ public class SecretManagerConfigDataLocationResolver implements
* A static client to avoid creating another client after refreshing.
*/
private static SecretManagerServiceClient secretManagerServiceClient;
+ /**
+ * A static endpoint format for regional client creation.
+ */
+ private static final String ENDPOINT_FORMAT = "secretmanager.%s.rep.googleapis.com:443";
@Override
public boolean isResolvable(ConfigDataLocationResolverContext context,
@@ -112,12 +116,15 @@ static synchronized SecretManagerServiceClient createSecretManagerClient(
.get(GcpSecretManagerProperties.class);
DefaultCredentialsProvider credentialsProvider =
new DefaultCredentialsProvider(properties);
- SecretManagerServiceSettings settings = SecretManagerServiceSettings.newBuilder()
- .setCredentialsProvider(credentialsProvider)
- .setHeaderProvider(
- new UserAgentHeaderProvider(SecretManagerConfigDataLoader.class))
- .build();
- secretManagerServiceClient = SecretManagerServiceClient.create(settings);
+ SecretManagerServiceSettings.Builder settings =
+ SecretManagerServiceSettings.newBuilder()
+ .setCredentialsProvider(credentialsProvider)
+ .setHeaderProvider(new UserAgentHeaderProvider(SecretManagerConfigDataLoader.class));
+
+ properties.getLocation().ifPresent(location ->
+ settings.setEndpoint(String.format(ENDPOINT_FORMAT, properties.getLocation().get())));
+
+ secretManagerServiceClient = SecretManagerServiceClient.create(settings.build());
return secretManagerServiceClient;
} catch (IOException e) {
@@ -136,7 +143,8 @@ private static SecretManagerTemplate createSecretManagerTemplate(
.get(GcpSecretManagerProperties.class);
return new SecretManagerTemplate(client, projectIdProvider)
- .setAllowDefaultSecretValue(properties.isAllowDefaultSecret());
+ .setAllowDefaultSecretValue(properties.isAllowDefaultSecret())
+ .setLocation(properties.getLocation());
}
/**
diff --git a/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/secretmanager/GcpSecretManagerAutoConfigurationUnitTests.java b/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/secretmanager/GcpSecretManagerAutoConfigurationUnitTests.java
index f90ab28286..2357b4e3c3 100644
--- a/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/secretmanager/GcpSecretManagerAutoConfigurationUnitTests.java
+++ b/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/secretmanager/GcpSecretManagerAutoConfigurationUnitTests.java
@@ -74,6 +74,15 @@ void testSecretManagerTemplateExists() {
.isNotNull());
}
+ @Test
+ void testLocationWithSecretManagerProperties() {
+ contextRunner
+ .withPropertyValues("spring.cloud.gcp.secretmanager.location=us-central1")
+ .run(
+ ctx -> assertThat(ctx.getBean(SecretManagerTemplate.class)
+ .getLocation()).isEqualTo("us-central1"));
+ }
+
static class TestConfig {
@Bean
diff --git a/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLoaderUnitTests.java b/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLoaderUnitTests.java
index 38e054af11..d1069665b0 100644
--- a/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLoaderUnitTests.java
+++ b/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLoaderUnitTests.java
@@ -1,14 +1,19 @@
package com.google.cloud.spring.autoconfigure.secretmanager;
import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.cloud.spring.core.GcpProjectIdProvider;
+import com.google.cloud.spring.secretmanager.SecretManagerPropertySource;
import com.google.cloud.spring.secretmanager.SecretManagerTemplate;
-import org.junit.jupiter.api.Test;
+import java.util.Optional;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.boot.ConfigurableBootstrapContext;
+import org.springframework.boot.context.config.ConfigData;
import org.springframework.boot.context.config.ConfigDataLoaderContext;
import org.springframework.boot.context.config.ConfigDataLocation;
@@ -21,18 +26,30 @@ class SecretManagerConfigDataLoaderUnitTests {
private final ConfigDataLoaderContext loaderContext = mock(ConfigDataLoaderContext.class);
private final GcpProjectIdProvider idProvider = mock(GcpProjectIdProvider.class);
private final SecretManagerTemplate template = mock(SecretManagerTemplate.class);
+ private final GcpSecretManagerProperties properties = mock(GcpSecretManagerProperties.class);
private final ConfigurableBootstrapContext bootstrapContext = mock(
ConfigurableBootstrapContext.class);
private final SecretManagerConfigDataLoader loader = new SecretManagerConfigDataLoader();
- @Test
- void loadIncorrectResourceThrowsException() {
+ @ParameterizedTest
+ @CsvSource({
+ "regional-fake, us-central1",
+ "fake, "
+ })
+ void loadIncorrectResourceThrowsException(String resourceName, String location) {
when(loaderContext.getBootstrapContext()).thenReturn(bootstrapContext);
when(bootstrapContext.get(GcpProjectIdProvider.class)).thenReturn(idProvider);
when(bootstrapContext.get(SecretManagerTemplate.class)).thenReturn(template);
+ when(bootstrapContext.get(GcpSecretManagerProperties.class)).thenReturn(properties);
when(template.secretExists(anyString(), anyString())).thenReturn(false);
+ when(properties.getLocation()).thenReturn(Optional.ofNullable(location));
SecretManagerConfigDataResource resource = new SecretManagerConfigDataResource(
- ConfigDataLocation.of("fake"));
- assertThatCode(() -> loader.load(loaderContext, resource)).doesNotThrowAnyException();
+ ConfigDataLocation.of(resourceName));
+ assertThatCode(() -> {
+ ConfigData configData = loader.load(loaderContext, resource);
+ SecretManagerPropertySource propertySource =
+ (SecretManagerPropertySource) configData.getPropertySources().get(0);
+ assertThat(Optional.ofNullable(location)).isEqualTo(propertySource.getLocation());
+ }).doesNotThrowAnyException();
}
}
diff --git a/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerRegionalCompatibilityTests.java b/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerRegionalCompatibilityTests.java
new file mode 100644
index 0000000000..02187f53bd
--- /dev/null
+++ b/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerRegionalCompatibilityTests.java
@@ -0,0 +1,107 @@
+package com.google.cloud.spring.autoconfigure.secretmanager;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.api.gax.rpc.NotFoundException;
+import com.google.cloud.secretmanager.v1.AccessSecretVersionResponse;
+import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
+import com.google.cloud.secretmanager.v1.SecretPayload;
+import com.google.cloud.secretmanager.v1.SecretVersionName;
+import com.google.protobuf.ByteString;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.BootstrapRegistry.InstanceSupplier;
+import org.springframework.boot.WebApplicationType;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.env.ConfigurableEnvironment;
+
+
+/**
+ * Unit tests to check compatibility of Secret Manager for regional endpoints.
+ */
+class SecretManagerRegionalCompatibilityTests {
+
+ private static final String PROJECT_NAME = "regional-secret-manager-project";
+ private static final String LOCATION = "us-central1";
+ private SpringApplicationBuilder application;
+ private SecretManagerServiceClient client;
+
+ @BeforeEach
+ void init() {
+ application = new SpringApplicationBuilder(SecretManagerRegionalCompatibilityTests.class)
+ .web(WebApplicationType.NONE)
+ .properties(
+ "spring.cloud.gcp.secretmanager.project-id=" + PROJECT_NAME,
+ "spring.cloud.gcp.sql.enabled=false",
+ "spring.cloud.gcp.secretmanager.location=" + LOCATION);
+
+ client = mock(SecretManagerServiceClient.class);
+ SecretVersionName secretVersionName =
+ SecretVersionName.newProjectLocationSecretSecretVersionBuilder()
+ .setProject(PROJECT_NAME)
+ .setLocation(LOCATION)
+ .setSecret("my-reg-secret")
+ .setSecretVersion("latest")
+ .build();
+ when(client.accessSecretVersion(secretVersionName))
+ .thenReturn(
+ AccessSecretVersionResponse.newBuilder()
+ .setPayload(
+ SecretPayload.newBuilder().setData(ByteString.copyFromUtf8("newRegSecret")))
+ .build());
+ secretVersionName =
+ SecretVersionName.newProjectLocationSecretSecretVersionBuilder()
+ .setProject(PROJECT_NAME)
+ .setLocation(LOCATION)
+ .setSecret("fake-reg-secret")
+ .setSecretVersion("latest")
+ .build();
+ when(client.accessSecretVersion(secretVersionName))
+ .thenThrow(NotFoundException.class);
+ }
+
+ /**
+ * Users with the new configuration (i.e., using `spring.config.import`) should get {@link
+ * com.google.cloud.spring.secretmanager.SecretManagerTemplate} autoconfiguration and properties
+ * resolved.
+ */
+ @Test
+ void testRegionalConfigurationWhenDefaultSecretIsNotAllowed() {
+ application.properties(
+ "spring.config.import=sm://")
+ .addBootstrapRegistryInitializer(
+ (registry) -> registry.registerIfAbsent(
+ SecretManagerServiceClient.class,
+ InstanceSupplier.of(client)
+ )
+ );
+ try (ConfigurableApplicationContext applicationContext = application.run()) {
+ ConfigurableEnvironment environment = applicationContext.getEnvironment();
+ assertThat(environment.getProperty("sm://my-reg-secret")).isEqualTo("newRegSecret");
+ assertThatThrownBy(() -> environment.getProperty("sm://fake-reg-secret"))
+ .isExactlyInstanceOf(NotFoundException.class);
+ }
+ }
+
+ @Test
+ void testRegionalConfigurationWhenDefaultSecretIsAllowed() {
+ application.properties(
+ "spring.cloud.gcp.secretmanager.allow-default-secret=true",
+ "spring.config.import=sm://")
+ .addBootstrapRegistryInitializer(
+ (registry) -> registry.registerIfAbsent(
+ SecretManagerServiceClient.class,
+ InstanceSupplier.of(client)
+ )
+ );
+ try (ConfigurableApplicationContext applicationContext = application.run()) {
+ ConfigurableEnvironment environment = applicationContext.getEnvironment();
+ assertThat(environment.getProperty("sm://my-reg-secret")).isEqualTo("newRegSecret");
+ assertThat(environment.getProperty("sm://fake-reg-secret")).isNull();
+ }
+ }
+}
diff --git a/spring-cloud-gcp-samples/pom.xml b/spring-cloud-gcp-samples/pom.xml
index 44cf2e2fdc..bb524e0f62 100644
--- a/spring-cloud-gcp-samples/pom.xml
+++ b/spring-cloud-gcp-samples/pom.xml
@@ -80,6 +80,7 @@
spring-cloud-gcp-data-firestore-samplespring-cloud-gcp-bigquery-samplespring-cloud-gcp-security-firebase-sample
+ spring-cloud-gcp-secretmanager-regional-samplespring-cloud-gcp-secretmanager-samplespring-cloud-gcp-kotlin-samplesspring-cloud-gcp-metrics-sample
@@ -103,6 +104,7 @@
spring-cloud-gcp-integration-storage-samplespring-cloud-gcp-trace-samplespring-cloud-gcp-vision-api-sample
+ spring-cloud-gcp-secretmanager-regional-samplespring-cloud-gcp-secretmanager-samplespring-cloud-gcp-vision-ocr-demospring-cloud-gcp-data-spanner-template-sample
diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-regional-sample/README.adoc b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-regional-sample/README.adoc
new file mode 100644
index 0000000000..4d3fbede2b
--- /dev/null
+++ b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-regional-sample/README.adoc
@@ -0,0 +1,63 @@
+= Spring Framework on Google Cloud Secret Manager Regional Sample Application
+
+This code sample demonstrates how to use the Spring Framework on Google Cloud Secret Manager integration.
+The sample demonstrates how one can access Secret Manager regional secrets through a `@ConfigurationProperties` class and also through `@Value` annotations on fields.
+
+== Running the Sample
+
+image:http://gstatic.com/cloudssh/images/open-btn.svg[link=https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2FGoogleCloudPlatform%2Fspring-cloud-gcp&cloudshell_open_in_editor=spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-regional-sample/README.adoc]
+
+1. Create a Google Cloud project with https://cloud.google.com/billing/docs/how-to/modify-project#enable-billing[billing enabled], if you don't have one already.
+
+2. Enable the Secret Manager API from the "APIs & Services" menu of the Google Cloud Console.
+This can be done using the `gcloud` command line tool:
++
+[source]
+----
+gcloud services enable secretmanager.googleapis.com
+----
+
+3. Authenticate in one of two ways:
+
+a. Use the Google Cloud SDK to https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login[authenticate with application default credentials].
+This method is convenient but should only be used in local development.
+b. https://cloud.google.com/iam/docs/creating-managing-service-accounts[Create a new service account], download its private key and point the `spring.cloud.gcp.secretmanager.credentials.location` property to it.
++
+Such as: `spring.cloud.gcp.secretmanager.credentials.location=file:/path/to/creds.json`
+
+4. Using the https://console.cloud.google.com/security/secret-manager;regionalSecret[Secret Manager UI in Cloud Console], create a new regional secret named `application-secret` at us-central1 location and set it to any value.
+Instructions for using the Secret Manager UI can be found in the https://cloud.google.com/secret-manager/regional-secrets/create-regional-secret[Secret Manager documentation].
+
+5. Make sure that the `spring.cloud.gcp.secretmanager.location` property points to the desired location for the regional secret. For this sample, we have kept the location as us-central1.
++
+Such as: `spring.cloud.gcp.secretmanager.location=us-central1`
+
+6. Run `$ mvn clean install` from the root directory of the project.
+
+7. Run `$ mvn spring-boot:run` command from the same directory as this sample's `pom.xml` file.
+
+8. Go to http://localhost:8080 in your browser or use the `Web Preview` button in Cloud Shell to preview the app
+on port 8080. Your secret value is injected into your application through the `WebController` and you will see it
+displayed.
++
+[source]
+----
+applicationSecret: Hello regional world.
+----
++
+You will also see some web forms that allow you to create, read, and update regional secrets in Secret Manager.
+This is done by using the `SecretManagerTemplate`.
++
+Finally, you can view all of your regional secrets using the https://console.cloud.google.com/security/secret-manager;regionalSecret[Secret Manager Cloud Console UI], which is the source of truth for all of your secrets in Secret Manager.
+
+9. Refresh the secrets without restarting the application:
+
+a. After running the application, change your secrets using https://console.cloud.google.com/security/secret-manager;regionalSecret[Secret Manager Cloud Console UI].
+
+b. To refresh the secret, send the following command to your server from which hosting the application:
++
+[source]
+----
+curl -X POST http://localhost:8080/actuator/refresh
+----
+Note that only `@ConfigurationProperties` annotated with `@RefreshScope` got the updated value.
diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-regional-sample/pom.xml b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-regional-sample/pom.xml
new file mode 100644
index 0000000000..5ab856c894
--- /dev/null
+++ b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-regional-sample/pom.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+ spring-cloud-gcp-samples
+ com.google.cloud
+ 5.8.1-SNAPSHOT
+
+
+ 4.0.0
+ spring-cloud-gcp-secretmanager-regional-sample
+ Spring Framework on Google Cloud Code Sample - Secret Manager Regional Secrets
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+
+ com.google.cloud
+ spring-cloud-gcp-starter-secretmanager
+
+
+
+ org.apache.commons
+ commons-lang3
+
+
+
+
+ junit
+ junit
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+
+ com.google.cloud
+ spring-cloud-gcp-dependencies
+ ${project.version}
+ pom
+ import
+
+
+
+
+
+
+
+ maven-deploy-plugin
+
+ true
+
+
+
+
+
diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-regional-sample/src/main/java/com/example/RegionalSecretConfiguration.java b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-regional-sample/src/main/java/com/example/RegionalSecretConfiguration.java
new file mode 100644
index 0000000000..78412b86ca
--- /dev/null
+++ b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-regional-sample/src/main/java/com/example/RegionalSecretConfiguration.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017-2024 the original author or authors.
+ *
+ * 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.example;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+
+@ConfigurationProperties("application")
+@RefreshScope
+public class RegionalSecretConfiguration {
+
+ private String secret;
+
+ public void setSecret(String secret) {
+ this.secret = secret;
+ }
+
+ public String getSecret() {
+ return secret;
+ }
+}
diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-regional-sample/src/main/java/com/example/SecretManagerRegionalApplication.java b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-regional-sample/src/main/java/com/example/SecretManagerRegionalApplication.java
new file mode 100644
index 0000000000..dca5a16373
--- /dev/null
+++ b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-regional-sample/src/main/java/com/example/SecretManagerRegionalApplication.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017-2024 the original author or authors.
+ *
+ * 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.example;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+
+@SpringBootApplication
+@EnableConfigurationProperties(RegionalSecretConfiguration.class)
+public class SecretManagerRegionalApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(SecretManagerRegionalApplication.class, args);
+ }
+}
diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-regional-sample/src/main/java/com/example/SecretManagerRegionalWebController.java b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-regional-sample/src/main/java/com/example/SecretManagerRegionalWebController.java
new file mode 100644
index 0000000000..fe9c197091
--- /dev/null
+++ b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-regional-sample/src/main/java/com/example/SecretManagerRegionalWebController.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2017-2024 the original author or authors.
+ *
+ * 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.example;
+
+import com.google.cloud.spring.secretmanager.SecretManagerTemplate;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.util.HtmlUtils;
+
+@Controller
+public class SecretManagerRegionalWebController {
+
+ private static final String INDEX_PAGE = "index.html";
+ private static final String APPLICATION_SECRET_FROM_VALUE = "applicationSecretFromValue";
+
+ private final SecretManagerTemplate secretManagerTemplate;
+ // Application secrets can be accessed using configuration properties class,
+ // secret can be refreshed when decorated with @RefreshScope on the class.
+ private final RegionalSecretConfiguration configuration;
+
+ // For the default value takes place, there should be no property called `application-fake`
+ // in property files.
+ @Value("${${sm://application-fake}:DEFAULT}")
+ private String defaultSecret;
+ // Application secrets can be accessed using @Value syntax.
+ @Value("${sm://application-secret}")
+ private String appSecretFromValue;
+
+ public SecretManagerRegionalWebController(SecretManagerTemplate secretManagerTemplate,
+ RegionalSecretConfiguration configuration
+ ) {
+ this.secretManagerTemplate = secretManagerTemplate;
+ this.configuration = configuration;
+ }
+
+ @GetMapping("/")
+ public ModelAndView renderIndex(ModelMap map) {
+ map.put("applicationDefaultSecret", defaultSecret);
+ map.put(APPLICATION_SECRET_FROM_VALUE, appSecretFromValue);
+ map.put("applicationSecretFromConfigurationProperties", configuration.getSecret());
+ return new ModelAndView(INDEX_PAGE, map);
+ }
+
+ @GetMapping("/getSecret")
+ @ResponseBody
+ public String getSecret(
+ @RequestParam String secretId,
+ @RequestParam(required = false) String version,
+ @RequestParam(required = false) String projectId,
+ ModelMap map) {
+
+ if (StringUtils.isEmpty(version)) {
+ version = SecretManagerTemplate.LATEST_VERSION;
+ }
+
+ String secretPayload;
+ if (StringUtils.isEmpty(projectId)) {
+ secretPayload =
+ this.secretManagerTemplate.getSecretString("sm://" + secretId + "/" + version);
+ } else {
+ secretPayload =
+ this.secretManagerTemplate.getSecretString(
+ "sm://" + projectId + "/" + secretId + "/" + version);
+ }
+
+ return "Secret ID: "
+ + HtmlUtils.htmlEscape(secretId)
+ + " | Value: "
+ + secretPayload
+ + "
Go back";
+ }
+
+ @PostMapping("/createSecret")
+ public ModelAndView createSecret(
+ @RequestParam String secretId,
+ @RequestParam String secretPayload,
+ @RequestParam(required = false) String projectId,
+ ModelMap map) {
+
+ if (StringUtils.isEmpty(projectId)) {
+ this.secretManagerTemplate.createSecret(secretId, secretPayload);
+ } else {
+ this.secretManagerTemplate.createSecret(secretId, secretPayload.getBytes(), projectId);
+ }
+
+ map.put(APPLICATION_SECRET_FROM_VALUE, this.appSecretFromValue);
+ map.put("message", "Secret created!");
+ return new ModelAndView(INDEX_PAGE, map);
+ }
+
+ @PostMapping("/deleteSecret")
+ public ModelAndView deleteSecret(
+ @RequestParam String secretId,
+ @RequestParam(required = false) String projectId,
+ ModelMap map) {
+ if (StringUtils.isEmpty(projectId)) {
+ this.secretManagerTemplate.deleteSecret(secretId);
+ } else {
+ this.secretManagerTemplate.deleteSecret(secretId, projectId);
+ }
+ map.put(APPLICATION_SECRET_FROM_VALUE, this.appSecretFromValue);
+ map.put("message", "Secret deleted!");
+ return new ModelAndView(INDEX_PAGE, map);
+ }
+}
diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-regional-sample/src/main/resources/application.properties b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-regional-sample/src/main/resources/application.properties
new file mode 100644
index 0000000000..8a1d708724
--- /dev/null
+++ b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-regional-sample/src/main/resources/application.properties
@@ -0,0 +1,18 @@
+# You can directly load the secret into a variable, as in this example
+# This demonstrates multiple ways of loading the same application secret using template syntax.
+#
+# Please refer to the Spring Cloud GCP Secret Manager reference documentation for the full protocol syntax.
+
+# You can also specify a secret from another project.
+# example.property=${sm://MY_PROJECT/MY_SECRET_ID/MY_VERSION}
+
+# Using SpEL, you can reference an environment variable and fallback to a secret if it is missing.
+# example.secret=${MY_ENV_VARIABLE:${sm://application-secret/latest}}
+
+management.endpoints.web.exposure.include=refresh
+# enable external resource from GCP Secret Manager.
+spring.config.import=sm://
+application.secret=${sm://application-secret}
+# enable default secret value when accessing non-exited secret.
+spring.cloud.gcp.secretmanager.allow-default-secret=true
+spring.cloud.gcp.secretmanager.location=us-central1
diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-regional-sample/src/main/resources/templates/index.html b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-regional-sample/src/main/resources/templates/index.html
new file mode 100644
index 0000000000..520fec5875
--- /dev/null
+++ b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-regional-sample/src/main/resources/templates/index.html
@@ -0,0 +1,135 @@
+
+
+
+
+
+ Google Cloud Secret Manager Regional Secrets Demo
+
+
+
+
+
+
Secret Manager Regional Secrets Demo with Spring Cloud GCP
+
+
+
Secret Manager Property Source
+ At the bootstrap phase, we loaded the following regional secret from us-central1 location into the application context:
+
+
+ Default Application secret if not found:[[${applicationDefaultSecret}]]
+ Application secret from @Value:[[${applicationSecretFromValue}]]
+ Application secret from @ConfigurationProperties:[[${applicationSecretFromConfigurationProperties}]]
+
+
+
+
Create, Read, Update and Delete Secrets
+
+
+ Using the form below, you can create/update regional secrets in Secret Manager, read and delete them.
+ In the controller code, the SecretManagerTemplate is being used to do these operations.
+
+
+
+ Here, We have set the location for the regional secrets as us-central1 in the application properties.
+ If we want to use different location, then we can set that particular location in the application properties.
+
+
+
+ NOTE: In practice, you never want to allow your secrets to be visible as plaintext.
+ This is just a demonstration!
+
+
+
+
Get Regional Secret by Secret ID
+
+ Get a regional secret by secret ID from Secret Manager. You will receive an error if you
+ try to get a secret ID that does not already exist.
+
+
+
+
+
+
Create/Update Regional Secret
+
+ This will create a regional secret if the provided secret ID does not exist.
+ Otherwise, it will create version under the provided secret ID.
+
+
+
+
+
+
+
Delete Regional Secret
+
+ This will delete a regional secret if the provided secret ID exists.
+