Skip to content

feat: add GCP Secret Manager OpenFeature provider#1772

Open
mahpatil wants to merge 6 commits intoopen-feature:mainfrom
mahpatil:feat/gcp-secret-manager
Open

feat: add GCP Secret Manager OpenFeature provider#1772
mahpatil wants to merge 6 commits intoopen-feature:mainfrom
mahpatil:feat/gcp-secret-manager

Conversation

@mahpatil
Copy link
Copy Markdown

@mahpatil mahpatil commented Apr 9, 2026

Summary

  • Adds a new OpenFeature provider for GCP Secret Manager that reads feature flags from GCP Secret Manager secrets
  • Includes flag caching, value conversion for all OpenFeature types (bool, string, int, double, object), and background polling for updates
  • Adds a sample application under samples/gcp-secret-manager-sample/ with setup/teardown scripts

Provider Details

  • Package: providers/gcp-secret-manager
  • Class: GcpSecretManagerProvider
  • Supports all OpenFeature flag types via structured or plain-text secret values
  • Configurable poll interval and GCP project settings

Test plan

  • Unit tests for FlagCache, FlagValueConverter, and GcpSecretManagerProvider
  • Integration test (GcpSecretManagerProviderIntegrationTest) requires a real GCP project — set GCP_PROJECT_ID env var to run
  • Sample app: follow samples/gcp-secret-manager-sample/README.md to run end-to-end

@mahpatil mahpatil requested a review from a team as a code owner April 9, 2026 05:18
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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().

Comment thread providers/gcp-secret-manager/CHANGELOG.md
Comment thread providers/gcp-secret-manager/spotbugs-exclusions.xml Outdated
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>
@mahpatil mahpatil force-pushed the feat/gcp-secret-manager branch from 1c0fd8d to 0d10a01 Compare April 11, 2026 20:44
mahpatil and others added 5 commits April 11, 2026 22:00
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>
@aepfli
Copy link
Copy Markdown
Member

aepfli commented Apr 13, 2026

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1 to +12
# 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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The changelog contains duplicate entries for version 0.0.1. Please consolidate the content.

Suggested change
# 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.

Comment on lines +46 to +56
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);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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);
        }
    }

Comment on lines +140 to +144
return cache.get(secretName).orElseGet(() -> {
String value = fetchFromGcp(secretName);
cache.put(secretName, value);
return value;
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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;
});
}

Comment on lines +78 to +82
public void validate() {
if (projectId == null || projectId.trim().isEmpty()) {
throw new IllegalArgumentException("GcpSecretManagerProviderOptions: projectId must not be blank");
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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");
        }
    }

Comment on lines +76 to +81
if ("true".equalsIgnoreCase(trimmed)) {
return Boolean.TRUE;
}
if ("false".equalsIgnoreCase(trimmed)) {
return Boolean.FALSE;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not Boolean.parseBoolean()?

import org.junit.jupiter.api.Test;

@DisplayName("FlagCache")
class FlagCacheTest {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When is the provider null?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants