feat: add GCP Secret Manager OpenFeature provider#1772
feat: add GCP Secret Manager OpenFeature provider#1772mahpatil wants to merge 6 commits intoopen-feature:mainfrom
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a new OpenFeature provider for Google Cloud Secret Manager, enabling feature flags to be managed as GCP secrets. The changes include the core provider implementation with caching and value conversion, along with comprehensive unit and integration tests, and a sample application. Feedback suggests adding an initial entry to the new module's CHANGELOG.md, refining the spotbugs-exclusions.xml to only include relevant exclusions, and improving the testability of FlagCache by injecting a Clock instance instead of relying on Instant.now() and Thread.sleep().
Adds a new OpenFeature provider for GCP Secret Manager that enables reading feature flags from GCP Secret Manager secrets. Includes the provider implementation with flag caching, value conversion, unit tests, integration tests, and a sample application demonstrating usage. Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
1c0fd8d to
0d10a01
Compare
Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Mahesh Patil <17205424+mahpatil@users.noreply.github.com>
…more deterministic in tests Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
…/java-sdk-contrib into feat/gcp-secret-manager
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a new OpenFeature provider for Google Cloud Secret Manager, enabling feature flags to be stored and managed as secrets. The implementation includes a TTL-based in-memory cache to optimize API usage and a converter to handle various OpenFeature data types. Feedback identifies several improvement opportunities, including fixing duplicate entries in the changelog, addressing a race condition and a potential 'thundering herd' issue in the caching logic, and strengthening input validation for the secret version configuration.
| # Changelog | ||
| # Changelog | ||
|
|
||
| ## 0.0.1 | ||
|
|
||
| ### ✨ New Features | ||
|
|
||
| * Initial release of the GCP Secret Manager OpenFeature provider. | ||
| ## 0.0.1 | ||
|
|
||
| ### ✨ New Features | ||
| * Initial release of the GCP Secret Manager OpenFeature provider. |
There was a problem hiding this comment.
The changelog contains duplicate entries for version 0.0.1. Please consolidate the content.
| # Changelog | |
| # Changelog | |
| ## 0.0.1 | |
| ### ✨ New Features | |
| * Initial release of the GCP Secret Manager OpenFeature provider. | |
| ## 0.0.1 | |
| ### ✨ New Features | |
| * Initial release of the GCP Secret Manager OpenFeature provider. | |
| # Changelog | |
| ## 0.0.1 | |
| ### ✨ New Features | |
| * Initial release of the GCP Secret Manager OpenFeature provider. |
| Optional<String> get(String key) { | ||
| CacheEntry entry = store.get(key); | ||
| if (entry == null) { | ||
| return Optional.empty(); | ||
| } | ||
| if (entry.isExpired()) { | ||
| store.remove(key); | ||
| return Optional.empty(); | ||
| } | ||
| return Optional.of(entry.value); | ||
| } |
There was a problem hiding this comment.
The get method has a check-then-act race condition. Although store is a synchronized map, the sequence of get followed by remove is not atomic. Multiple threads could identify an entry as expired and attempt to remove it simultaneously. Synchronizing on the store instance ensures atomicity for this operation.
Optional<String> get(String key) {
synchronized (store) {
CacheEntry entry = store.get(key);
if (entry == null) {
return Optional.empty();
}
if (entry.isExpired()) {
store.remove(key);
return Optional.empty();
}
return Optional.of(entry.value);
}
}| return cache.get(secretName).orElseGet(() -> { | ||
| String value = fetchFromGcp(secretName); | ||
| cache.put(secretName, value); | ||
| return value; | ||
| }); |
There was a problem hiding this comment.
This implementation is susceptible to a 'thundering herd' problem. If multiple threads request an expired or missing flag simultaneously, they will all trigger a call to fetchFromGcp, potentially exhausting GCP Secret Manager quotas. Using a synchronized block with double-checked locking prevents redundant API calls.
| return cache.get(secretName).orElseGet(() -> { | |
| String value = fetchFromGcp(secretName); | |
| cache.put(secretName, value); | |
| return value; | |
| }); | |
| Optional<String> cached = cache.get(secretName); | |
| if (cached.isPresent()) { | |
| return cached.get(); | |
| } | |
| synchronized (this) { | |
| return cache.get(secretName).orElseGet(() -> { | |
| String value = fetchFromGcp(secretName); | |
| cache.put(secretName, value); | |
| return value; | |
| }); | |
| } |
| public void validate() { | ||
| if (projectId == null || projectId.trim().isEmpty()) { | ||
| throw new IllegalArgumentException("GcpSecretManagerProviderOptions: projectId must not be blank"); | ||
| } | ||
| } |
There was a problem hiding this comment.
The validate method should also check that secretVersion is not null or blank, as it is a required parameter for constructing the GCP secret path.
public void validate() {
if (projectId == null || projectId.trim().isEmpty()) {
throw new IllegalArgumentException("GcpSecretManagerProviderOptions: projectId must not be blank");
}
if (secretVersion == null || secretVersion.trim().isEmpty()) {
throw new IllegalArgumentException("GcpSecretManagerProviderOptions: secretVersion must not be blank");
}
}| if ("true".equalsIgnoreCase(trimmed)) { | ||
| return Boolean.TRUE; | ||
| } | ||
| if ("false".equalsIgnoreCase(trimmed)) { | ||
| return Boolean.FALSE; | ||
| } |
There was a problem hiding this comment.
Why not Boolean.parseBoolean()?
| import org.junit.jupiter.api.Test; | ||
|
|
||
| @DisplayName("FlagCache") | ||
| class FlagCacheTest { |
There was a problem hiding this comment.
I'd suggest adding a test that lets a cache entry time out while a get operation and an insert operation are concurrently ongoing. This way we can make sure that a removal of a timed out value does not remove the newly added value by accident. Maybe VmLens can help with that (see our existing VmLens tests).
| @Test | ||
| @DisplayName("converts numeric string to Integer") | ||
| void numericString() { | ||
| assertThat(FlagValueConverter.convert("42", Integer.class)).isEqualTo(42); |
There was a problem hiding this comment.
Maybe we should also add a test for negative numbers
| @Test | ||
| @DisplayName("converts numeric string to Double") | ||
| void numericString() { | ||
| assertThat(FlagValueConverter.convert("3.14", Double.class)).isEqualTo(3.14); |
There was a problem hiding this comment.
Do we also want to support the exponential format, e.g. 3141.5e-3? We should add tests accordingly
|
|
||
| @AfterEach | ||
| void tearDown() { | ||
| if (provider != null) { |
There was a problem hiding this comment.
When is the provider null?
Summary
samples/gcp-secret-manager-sample/with setup/teardown scriptsProvider Details
providers/gcp-secret-managerGcpSecretManagerProviderTest plan
FlagCache,FlagValueConverter, andGcpSecretManagerProviderGcpSecretManagerProviderIntegrationTest) requires a real GCP project — setGCP_PROJECT_IDenv var to runsamples/gcp-secret-manager-sample/README.mdto run end-to-end