Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"providers/statsig": "0.2.1",
"providers/multiprovider": "0.0.3",
"providers/ofrep": "0.0.1",
"providers/gcp-parameter-manager": "0.0.1",
"tools/junit-openfeature": "0.2.1",
"tools/flagd-http-connector": "0.0.4",
"tools/flagd-api": "1.0.0",
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<module>providers/optimizely</module>
<module>providers/multiprovider</module>
<module>providers/ofrep</module>
<module>providers/gcp-parameter-manager</module>
<module>tools/flagd-http-connector</module>
</modules>

Expand Down
1 change: 1 addition & 0 deletions providers/gcp-parameter-manager/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

121 changes: 121 additions & 0 deletions providers/gcp-parameter-manager/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# GCP Parameter Manager Provider

An OpenFeature provider that reads feature flags from [Google Cloud Parameter Manager](https://cloud.google.com/secret-manager/parameter-manager/docs/overview), the GCP-native equivalent of AWS SSM Parameter Store.

## Installation

<!-- x-release-please-start-version -->
```xml
<dependency>
<groupId>dev.openfeature.contrib.providers</groupId>
<artifactId>gcp-parameter-manager</artifactId>
<version>0.0.1</version>
</dependency>
```
<!-- x-release-please-end-version -->

## Quick Start

```java
import dev.openfeature.contrib.providers.gcpparametermanager.GcpParameterManagerProvider;
import dev.openfeature.contrib.providers.gcpparametermanager.GcpParameterManagerProviderOptions;
import dev.openfeature.sdk.OpenFeatureAPI;

GcpParameterManagerProviderOptions options = GcpParameterManagerProviderOptions.builder()
.projectId("my-gcp-project")
.build();

OpenFeatureAPI.getInstance().setProvider(new GcpParameterManagerProvider(options));

// Evaluate a boolean flag stored in GCP as parameter "enable-dark-mode"
boolean darkMode = OpenFeatureAPI.getInstance().getClient()
.getBooleanValue("enable-dark-mode", false);
```

## How It Works

Each feature flag is stored as an individual **parameter** in GCP Parameter Manager. The flag key maps directly to the parameter name (with an optional prefix).

Supported raw value formats:

| Flag type | Parameter value example |
|-----------|------------------------|
| Boolean | `true` or `false` |
| Integer | `42` |
| Double | `3.14` |
| String | `dark-mode` |
| Object | `{"color":"blue","level":3}` |

## Authentication

The provider uses [Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/provide-credentials-adc) by default. No explicit credentials are required when running on GCP infrastructure (Cloud Run, GKE, Compute Engine) or when `gcloud auth application-default login` has been run locally.

To use explicit credentials:

```java
import com.google.auth.oauth2.ServiceAccountCredentials;
import java.io.FileInputStream;

GoogleCredentials credentials = ServiceAccountCredentials.fromStream(
new FileInputStream("/path/to/service-account-key.json"));

GcpParameterManagerProviderOptions options = GcpParameterManagerProviderOptions.builder()
.projectId("my-gcp-project")
.credentials(credentials)
.build();
```

## Configuration Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `projectId` | `String` | *(required)* | GCP project ID that owns the parameters |
| `locationId` | `String` | `"global"` | GCP location for the Parameter Manager endpoint (`"global"` or a region such as `"us-central1"`) |
| `credentials` | `GoogleCredentials` | `null` (ADC) | Explicit credentials; falls back to Application Default Credentials when null |
| `cacheExpiry` | `Duration` | `5 minutes` | How long fetched values are cached before re-fetching from GCP |
| `cacheMaxSize` | `int` | `500` | Maximum number of flag values held in the in-memory cache |
| `parameterNamePrefix` | `String` | `null` | Optional prefix prepended to every flag key. E.g. prefix `"ff-"` maps flag `"my-flag"` to parameter `"ff-my-flag"` |

## Advanced Usage

### Regional endpoint

```java
GcpParameterManagerProviderOptions options = GcpParameterManagerProviderOptions.builder()
.projectId("my-gcp-project")
.locationId("us-central1")
.build();
```

### Parameter name prefix

```java
GcpParameterManagerProviderOptions options = GcpParameterManagerProviderOptions.builder()
.projectId("my-gcp-project")
.parameterNamePrefix("feature-flags/")
.build();
```

### Tuning cache for high-throughput scenarios

GCP Parameter Manager has API quotas. Use a longer `cacheExpiry` to reduce quota consumption.

```java
GcpParameterManagerProviderOptions options = GcpParameterManagerProviderOptions.builder()
.projectId("my-gcp-project")
.cacheExpiry(Duration.ofMinutes(10))
.cacheMaxSize(1000)
.build();
```

## Running Integration Tests

Integration tests require real GCP credentials and pre-created test parameters.

1. Configure ADC: `gcloud auth application-default login`
2. Create test parameters in your project (see `GcpParameterManagerProviderIntegrationTest` for the required parameter names)
3. Run:

```bash
GCP_PROJECT_ID=my-project mvn verify -pl providers/gcp-parameter-manager -Dgroups=integration
```
77 changes: 77 additions & 0 deletions providers/gcp-parameter-manager/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.openfeature.contrib</groupId>
<artifactId>parent</artifactId>
<version>[1.0,2.0)</version>
<relativePath>../../pom.xml</relativePath>
</parent>

<groupId>dev.openfeature.contrib.providers</groupId>
<artifactId>gcp-parameter-manager</artifactId>
<version>0.0.1</version> <!--x-release-please-version -->

<properties>
<!-- "-" is not allowed in automatic module names -->
<module-name>${groupId}.gcpparametermanager</module-name>
</properties>

<name>gcp-parameter-manager</name>
<description>GCP Parameter Manager provider for OpenFeature Java SDK</description>
<url>https://openfeature.dev</url>

<developers>
<developer>
<id>openfeaturebot</id>
<name>OpenFeature Bot</name>
<organization>OpenFeature</organization>
<url>https://openfeature.dev/</url>
</developer>
</developers>

<dependencies>
<!-- GCP Parameter Manager client -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-parametermanager</artifactId>
<version>0.31.0</version>
</dependency>

<!-- JSON parsing for structured flag values -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.21.1</version>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.17</version>
Comment thread
mahpatil marked this conversation as resolved.
</dependency>

<!-- test-only logging implementation -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.25.0</version>
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 version 2.25.0 for log4j-slf4j2-impl is likely a typo. The latest stable version is 2.24.3.

Suggested change
<version>2.25.0</version>
<version>2.24.3</version>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

2.25.4 is the latest stable updated.

<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludedGroups>integration</excludedGroups>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package dev.openfeature.contrib.providers.gcpparametermanager;

import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;

/**
* Thread-safe TTL-based in-memory cache for flag values fetched from GCP Parameter Manager.
*
* <p>Entries expire after the configured {@code ttl}. When the cache reaches {@code maxSize},
* the entry with the earliest insertion time is evicted in O(1) via {@link LinkedHashMap}'s
* insertion-order iteration and {@code removeEldestEntry}.
*/
class FlagCache {

private final Map<String, CacheEntry> store;
private final Duration ttl;

FlagCache(Duration ttl, int maxSize) {
this.ttl = ttl;
this.store = Collections.synchronizedMap(
new LinkedHashMap<String, CacheEntry>(16, 0.75f, false) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, CacheEntry> eldest) {
return size() > maxSize;
}
}
);
}

/**
* Returns the cached value for {@code key} if present and not expired.
*
* @param key the cache key
* @return an {@link Optional} containing the cached string, or empty if absent/expired
*/
Optional<String> get(String key) {
CacheEntry entry = store.get(key);
if (entry == null) {
return Optional.empty();
}
if (entry.isExpired()) {
store.remove(key, entry);
return Optional.empty();
}
Comment thread
mahpatil marked this conversation as resolved.
return Optional.of(entry.value);
}

/**
* Stores {@code value} under {@code key}. Eviction of the oldest entry (when the cache is
* full) is handled automatically by the underlying {@link LinkedHashMap}.
*
* @param key the cache key
* @param value the value to cache
*/
void put(String key, String value) {
store.put(key, new CacheEntry(value, ttl));
}

/**
* Removes the entry for {@code key}, forcing re-fetch on next access.
*
* @param key the cache key to invalidate
*/
void invalidate(String key) {
store.remove(key);
}

/** Removes all entries from the cache. */
void clear() {
store.clear();
}

private static final class CacheEntry {

final String value;
final Instant expiresAt;

CacheEntry(String value, Duration ttl) {
this.value = value;
this.expiresAt = Instant.now().plus(ttl);
}

boolean isExpired() {
return Instant.now().isAfter(expiresAt);
}
}
}
Loading
Loading