Skip to content

Commit a3cfe06

Browse files
authored
fix: GcpProjectIdProvider when using Secret Manager (#3664)
fixes #3181 , also fixes #3564 cherry-picked changes from #3383 to allow ci properly run. Adding tests. @PatrickGotthard 's original comment on root cause: #3181 (comment) When using Secret Manager (sm) module, expected behavior for GcpProjectIdProvider should be: - in Spring Bootstrap Phase (where sm if configured), order of discovery: secretmanager.project-id >> gcp.project-id >> default (according to DefaultGcpProjectIdProvider) - in Application Context, GcpProjectIdProvider bean should behave similar to other modules, with no knowledge of secretmanager.project-id, thus: gcp.project-id >> default Current behavior prior to this fix, the discovery order for both cases are: - secretmanager.project-id >> default Fix is to let SecretManagerConfigDataLocationResolver recognizing GcpProperties and **not** promoting GcpProjectIdProvider to application context.
1 parent 3326726 commit a3cfe06

File tree

7 files changed

+130
-35
lines changed

7 files changed

+130
-35
lines changed

docs/src/main/asciidoc/secretmanager.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ This can be overridden using the authentication properties.
4141
| `spring.cloud.gcp.secretmanager.enabled` | Enables the Secret Manager integration. | No | `true`
4242
| `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].
4343
| `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].
44-
| `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].
44+
| `spring.cloud.gcp.secretmanager.project-id` | The default Google Cloud project used to access Secret Manager API for the template and property source. | No | Default to the one in the <<spring-cloud-gcp-core,Spring Framework on Google Cloud Core Module>>.
4545
|`spring.cloud.gcp.secretmanager.allow-default-secret`| Define the behavior when accessing a non-existent secret string/bytes. +
4646
If set to `true`, `null` will be returned when accessing a non-existent secret; otherwise throwing an exception. | No | `false`
4747
|===

spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/core/GcpProperties.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,22 @@
1616

1717
package com.google.cloud.spring.autoconfigure.core;
1818

19+
import static com.google.cloud.spring.autoconfigure.core.GcpProperties.CORE_PROPERTY_PREFIX;
20+
1921
import com.google.cloud.spring.core.Credentials;
2022
import com.google.cloud.spring.core.CredentialsSupplier;
2123
import org.springframework.boot.context.properties.ConfigurationProperties;
2224
import org.springframework.boot.context.properties.NestedConfigurationProperty;
2325
import org.springframework.context.annotation.ImportRuntimeHints;
2426

2527
/** Top-level auto-config settings. */
26-
@ConfigurationProperties("spring.cloud.gcp")
28+
@ConfigurationProperties(CORE_PROPERTY_PREFIX)
2729
@ImportRuntimeHints(CredentialsRuntimeHints.class)
2830
public class GcpProperties implements CredentialsSupplier {
2931

32+
/** Configuration prefix. */
33+
public static final String CORE_PROPERTY_PREFIX = "spring.cloud.gcp";
34+
3035
/** GCP project ID where services are running. */
3136
private String projectId;
3237

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@
1919
import com.google.api.gax.core.CredentialsProvider;
2020
import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
2121
import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings;
22+
import com.google.cloud.spring.autoconfigure.core.GcpContextAutoConfiguration;
2223
import com.google.cloud.spring.core.GcpProjectIdProvider;
2324
import com.google.cloud.spring.core.UserAgentHeaderProvider;
2425
import com.google.cloud.spring.secretmanager.SecretManagerTemplate;
2526
import java.io.IOException;
2627
import org.springframework.boot.autoconfigure.AutoConfiguration;
28+
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
2729
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
2830
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2931
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -39,6 +41,7 @@
3941
@EnableConfigurationProperties(GcpSecretManagerProperties.class)
4042
@ConditionalOnClass(SecretManagerTemplate.class)
4143
@ConditionalOnProperty(value = "spring.cloud.gcp.secretmanager.enabled", matchIfMissing = true)
44+
@AutoConfigureAfter(GcpContextAutoConfiguration.class)
4245
public class GcpSecretManagerAutoConfiguration {
4346

4447
private final GcpProjectIdProvider gcpProjectIdProvider;

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

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
2323
import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings;
24+
import com.google.cloud.spring.autoconfigure.core.GcpProperties;
2425
import com.google.cloud.spring.core.DefaultCredentialsProvider;
2526
import com.google.cloud.spring.core.DefaultGcpProjectIdProvider;
2627
import com.google.cloud.spring.core.GcpProjectIdProvider;
@@ -35,6 +36,7 @@
3536
import org.slf4j.LoggerFactory;
3637
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
3738
import org.springframework.boot.BootstrapRegistry;
39+
import org.springframework.boot.ConfigurableBootstrapContext;
3840
import org.springframework.boot.context.config.ConfigDataLocation;
3941
import org.springframework.boot.context.config.ConfigDataLocationNotFoundException;
4042
import org.springframework.boot.context.config.ConfigDataLocationResolver;
@@ -70,6 +72,8 @@ public List<SecretManagerConfigDataResource> resolve(ConfigDataLocationResolverC
7072
}
7173

7274
private static void registerSecretManagerBeans(ConfigDataLocationResolverContext context) {
75+
// Register the Core properties.
76+
registerBean(context, GcpProperties.class, getGcpProperties(context));
7377
// Register the Secret Manager properties.
7478
registerBean(
7579
context, GcpSecretManagerProperties.class, getSecretManagerProperties(context));
@@ -80,30 +84,41 @@ private static void registerSecretManagerBeans(ConfigDataLocationResolverContext
8084
// lazy register the client solely for unit test.
8185
BootstrapRegistry.InstanceSupplier.from(() -> createSecretManagerClient(context)));
8286
// Register the GCP Project ID provider.
83-
registerAndPromoteBean(
84-
context,
85-
GcpProjectIdProvider.class,
86-
BootstrapRegistry.InstanceSupplier.of(createProjectIdProvider(context)));
87+
registerBean(context, GcpProjectIdProvider.class, createProjectIdProvider(context));
8788
// Register the Secret Manager template.
8889
registerAndPromoteBean(
8990
context,
9091
SecretManagerTemplate.class,
9192
BootstrapRegistry.InstanceSupplier.of(createSecretManagerTemplate(context)));
9293
}
9394

95+
private static GcpProperties getGcpProperties(ConfigDataLocationResolverContext context) {
96+
return context
97+
.getBinder()
98+
.bind(GcpProperties.CORE_PROPERTY_PREFIX, GcpProperties.class)
99+
.orElse(new GcpProperties());
100+
}
101+
94102
private static GcpSecretManagerProperties getSecretManagerProperties(
95103
ConfigDataLocationResolverContext context) {
96104
return context.getBinder()
97105
.bind(GcpSecretManagerProperties.PREFIX, GcpSecretManagerProperties.class)
98106
.orElse(new GcpSecretManagerProperties());
99107
}
100108

101-
private static GcpProjectIdProvider createProjectIdProvider(
102-
ConfigDataLocationResolverContext context) {
103-
GcpSecretManagerProperties properties = context.getBootstrapContext()
104-
.get(GcpSecretManagerProperties.class);
105-
return properties.getProjectId() != null
106-
? properties::getProjectId : new DefaultGcpProjectIdProvider();
109+
@VisibleForTesting
110+
static GcpProjectIdProvider createProjectIdProvider(ConfigDataLocationResolverContext context) {
111+
ConfigurableBootstrapContext bootstrapContext = context.getBootstrapContext();
112+
GcpSecretManagerProperties secretManagerProperties =
113+
bootstrapContext.get(GcpSecretManagerProperties.class);
114+
if (secretManagerProperties.getProjectId() != null) {
115+
return secretManagerProperties::getProjectId;
116+
}
117+
GcpProperties gcpProperties = bootstrapContext.get(GcpProperties.class);
118+
if (gcpProperties.getProjectId() != null) {
119+
return gcpProperties::getProjectId;
120+
}
121+
return new DefaultGcpProjectIdProvider();
107122
}
108123

109124
@VisibleForTesting

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

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import com.google.protobuf.ByteString;
1414
import java.util.stream.Stream;
1515
import org.junit.jupiter.api.BeforeEach;
16-
import org.junit.jupiter.api.Test;
1716
import org.junit.jupiter.params.ParameterizedTest;
1817
import org.junit.jupiter.params.provider.Arguments;
1918
import org.junit.jupiter.params.provider.MethodSource;
@@ -34,17 +33,17 @@ class SecretManagerCompatibilityTests {
3433

3534
static Stream<Arguments> prefixes() {
3635
return Stream.of(
37-
Arguments.of("sm://"),
38-
Arguments.of("sm@")
39-
);
36+
Arguments.of("sm://", "spring.cloud.gcp.project-id="),
37+
Arguments.of("sm://", "spring.cloud.gcp.secretmanager.project-id="),
38+
Arguments.of("sm@", "spring.cloud.gcp.project-id="),
39+
Arguments.of("sm@", "spring.cloud.gcp.secretmanager.project-id="));
4040
}
4141

4242
@BeforeEach
4343
void init() {
4444
application = new SpringApplicationBuilder(SecretManagerCompatibilityTests.class)
4545
.web(WebApplicationType.NONE)
4646
.properties(
47-
"spring.cloud.gcp.secretmanager.project-id=" + PROJECT_NAME,
4847
"spring.cloud.gcp.sql.enabled=false");
4948

5049
client = mock(SecretManagerServiceClient.class);
@@ -77,15 +76,13 @@ void init() {
7776
*/
7877
@ParameterizedTest
7978
@MethodSource("prefixes")
80-
void testConfigurationWhenDefaultSecretIsNotAllowed(String prefix) {
81-
application.properties(
82-
"spring.config.import=" + prefix)
79+
void testConfigurationWhenDefaultSecretIsNotAllowed(String prefix, String projectIdPropertyName) {
80+
application
81+
.properties(projectIdPropertyName + PROJECT_NAME, "spring.config.import=" + prefix)
8382
.addBootstrapRegistryInitializer(
84-
(registry) -> registry.registerIfAbsent(
85-
SecretManagerServiceClient.class,
86-
InstanceSupplier.of(client)
87-
)
88-
);
83+
(registry) ->
84+
registry.registerIfAbsent(
85+
SecretManagerServiceClient.class, InstanceSupplier.of(client)));
8986
try (ConfigurableApplicationContext applicationContext = application.run()) {
9087
ConfigurableEnvironment environment = applicationContext.getEnvironment();
9188
assertThat(environment.getProperty(prefix + "my-secret")).isEqualTo("newSecret");
@@ -96,16 +93,16 @@ void testConfigurationWhenDefaultSecretIsNotAllowed(String prefix) {
9693

9794
@ParameterizedTest
9895
@MethodSource("prefixes")
99-
void testConfigurationWhenDefaultSecretIsAllowed(String prefix) {
100-
application.properties(
96+
void testConfigurationWhenDefaultSecretIsAllowed(String prefix, String projectIdPropertyName) {
97+
application
98+
.properties(
99+
projectIdPropertyName + PROJECT_NAME,
101100
"spring.cloud.gcp.secretmanager.allow-default-secret=true",
102101
"spring.config.import=" + prefix)
103102
.addBootstrapRegistryInitializer(
104-
(registry) -> registry.registerIfAbsent(
105-
SecretManagerServiceClient.class,
106-
InstanceSupplier.of(client)
107-
)
108-
);
103+
(registry) ->
104+
registry.registerIfAbsent(
105+
SecretManagerServiceClient.class, InstanceSupplier.of(client)));
109106
try (ConfigurableApplicationContext applicationContext = application.run()) {
110107
ConfigurableEnvironment environment = applicationContext.getEnvironment();
111108
assertThat(environment.getProperty(prefix + "my-secret")).isEqualTo("newSecret");

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

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
import com.google.api.gax.core.CredentialsProvider;
99
import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
10+
import com.google.cloud.spring.autoconfigure.core.GcpProperties;
11+
import com.google.cloud.spring.core.DefaultGcpProjectIdProvider;
12+
import com.google.cloud.spring.core.GcpProjectIdProvider;
1013
import java.util.List;
1114
import java.util.stream.Stream;
1215
import org.junit.jupiter.api.BeforeEach;
@@ -31,6 +34,8 @@ class SecretManagerConfigDataLocationResolverUnitTests {
3134
private final ConfigDataLocationResolverContext context = mock(
3235
ConfigDataLocationResolverContext.class);
3336
private final DefaultBootstrapContext defaultBootstrapContext = new DefaultBootstrapContext();
37+
private final GcpSecretManagerProperties secretManagerProperties = mock(GcpSecretManagerProperties.class);
38+
private final GcpProperties gcpProperties = mock(GcpProperties.class);
3439

3540
static Stream<Arguments> prefixes() {
3641
return Stream.of(
@@ -76,12 +81,14 @@ void resolveReturnsConfigDataLocation(String prefix) {
7681

7782
@BeforeEach
7883
void registerBean() {
79-
GcpSecretManagerProperties properties = mock(GcpSecretManagerProperties.class);
8084
CredentialsProvider credentialsProvider = mock(CredentialsProvider.class);
8185
SecretManagerServiceClient secretManagerServiceClient = mock(SecretManagerServiceClient.class);
8286

83-
defaultBootstrapContext.register(GcpSecretManagerProperties.class,
84-
BootstrapRegistry.InstanceSupplier.of(properties));
87+
defaultBootstrapContext.register(
88+
GcpProperties.class, BootstrapRegistry.InstanceSupplier.of(gcpProperties));
89+
defaultBootstrapContext.register(
90+
GcpSecretManagerProperties.class,
91+
BootstrapRegistry.InstanceSupplier.of(secretManagerProperties));
8592
defaultBootstrapContext.register(CredentialsProvider.class,
8693
BootstrapRegistry.InstanceSupplier.of(credentialsProvider));
8794
defaultBootstrapContext.register(SecretManagerServiceClient.class,
@@ -90,4 +97,28 @@ void registerBean() {
9097
when(context.getBinder()).thenReturn(new Binder());
9198
when(context.getBootstrapContext()).thenReturn(defaultBootstrapContext);
9299
}
100+
101+
@Test
102+
void testSecretManagerProjectIdTakesPrecedence() {
103+
when(secretManagerProperties.getProjectId()).thenReturn("secret-manager-property-id");
104+
when(gcpProperties.getProjectId()).thenReturn("gcp-project-id");
105+
GcpProjectIdProvider projectIdProvider =
106+
SecretManagerConfigDataLocationResolver.createProjectIdProvider(context);
107+
assertThat(projectIdProvider.getProjectId()).isEqualTo("secret-manager-property-id");
108+
}
109+
110+
@Test
111+
void testProjectIdUseCoreWhenNoSecretManagerProjectId() {
112+
when(gcpProperties.getProjectId()).thenReturn("gcp-project-id");
113+
GcpProjectIdProvider projectIdProvider =
114+
SecretManagerConfigDataLocationResolver.createProjectIdProvider(context);
115+
assertThat(projectIdProvider.getProjectId()).isEqualTo("gcp-project-id");
116+
}
117+
118+
@Test
119+
void testProjectIdFallBackToDefault() {
120+
GcpProjectIdProvider projectIdProvider =
121+
SecretManagerConfigDataLocationResolver.createProjectIdProvider(context);
122+
assertThat(projectIdProvider).isInstanceOf(DefaultGcpProjectIdProvider.class);
123+
}
93124
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.example;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import com.google.cloud.spring.core.GcpProjectIdProvider;
6+
import com.google.cloud.spring.secretmanager.SecretManagerTemplate;
7+
import org.junit.jupiter.api.Test;
8+
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
9+
import org.junit.jupiter.api.extension.ExtendWith;
10+
import org.springframework.beans.factory.annotation.Autowired;
11+
import org.springframework.boot.test.context.SpringBootTest;
12+
import org.springframework.test.context.TestPropertySource;
13+
import org.springframework.test.context.aot.DisabledInAotMode;
14+
import org.springframework.test.context.junit.jupiter.SpringExtension;
15+
16+
@EnabledIfSystemProperty(named = "it.secretmanager", matches = "true")
17+
@ExtendWith(SpringExtension.class)
18+
@SpringBootTest(
19+
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
20+
classes = SecretManagerApplication.class)
21+
@TestPropertySource(
22+
properties = {
23+
"spring.cloud.gcp.secretmanager.project-id=spring-cloud-gcp-ci",
24+
"spring.cloud.gcp.project-id=gcp-project-id"
25+
})
26+
@DisabledInAotMode
27+
class SecretManagerSamplePropertyIdTest {
28+
29+
@Autowired private SecretManagerTemplate secretManagerTemplate;
30+
31+
@Autowired private GcpProjectIdProvider gcpProjectIdProvider;
32+
33+
// This test verifies propertyId is correctly recognized.
34+
// When secretmanager.project-id and project gcp.project-id are both set,
35+
// secretManagerTemplate should recognize secretmanager.project-id
36+
// but gcpProjectIdProvider should only capture gcp.project-id
37+
@Test
38+
void testProjectIdCorrect() {
39+
String projectId = secretManagerTemplate.getProjectId();
40+
assertThat(projectId).isEqualTo("spring-cloud-gcp-ci");
41+
String gcpProjectId = gcpProjectIdProvider.getProjectId();
42+
assertThat(gcpProjectId).isEqualTo("gcp-project-id");
43+
}
44+
}

0 commit comments

Comments
 (0)