Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions providers/gcp/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog


## [0.0.3]

### ✨ New Features

* Add support for GCP Parameter Manager.

## 0.0.1

### ✨ New Features
Expand Down
26 changes: 23 additions & 3 deletions providers/gcp/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# GCP Provider

An OpenFeature provider that reads feature flags from Google Cloud. Currently supports [Google Cloud Secret Manager](https://cloud.google.com/secret-manager), purpose-built for secrets requiring versioning, rotation, and fine-grained IAM access control.
An OpenFeature provider that reads feature flags from Google Cloud. Currently supports the following
1. [Google Cloud Secret Manager](https://cloud.google.com/secret-manager), purpose-built for secrets requiring versioning, rotation, and fine-grained IAM access control.
2. [Google Cloud Parameter Manager](https://cloud.google.com/secret-manager/parameter-manager/docs/overview), the GCP-native equivalent of AWS SSM Parameter Store.

## Installation

Expand All @@ -16,6 +18,7 @@ An OpenFeature provider that reads feature flags from Google Cloud. Currently su

## Quick Start

### GCP Secret Manager
```java
import dev.openfeature.contrib.providers.gcp.GcpSecretManagerProvider;
import dev.openfeature.contrib.providers.gcp.GcpSecretManagerProviderOptions;
Expand All @@ -32,9 +35,26 @@ boolean darkMode = OpenFeatureAPI.getInstance().getClient()
.getBooleanValue("enable-dark-mode", false);
```

### GCP Parameter Manager
```java
import dev.openfeature.contrib.providers.gcp.GcpParameterManagerProvider;
import dev.openfeature.contrib.providers.gcp.GcpProviderOptions;
import dev.openfeature.sdk.OpenFeatureAPI;

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

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

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

## How It Works

Each feature flag is stored as an individual **secret** in GCP Secret Manager. The flag key maps directly to the secret name (with an optional prefix). The `latest` version is accessed by default.
Each feature flag is stored as an individual **secret** in GCP Secret Manager or **parameter** in GCP Parameter Manager. The flag key maps directly to the secret name (with an optional prefix). The `latest` version is accessed by default.

Supported raw value formats:

Expand Down Expand Up @@ -78,7 +98,7 @@ GcpSecretManagerProviderOptions options = GcpSecretManagerProviderOptions.builde

## Advanced Usage

### Pinning to a specific secret version
### Pinning to a specific version

```java
GcpSecretManagerProviderOptions options = GcpSecretManagerProviderOptions.builder()
Expand Down
27 changes: 24 additions & 3 deletions providers/gcp/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

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

<properties>
<!-- "-" is not allowed in automatic module names -->
Expand All @@ -35,19 +35,40 @@
</developer>
</developers>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>libraries-bom</artifactId>
<version>26.83.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>2.21.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<!-- GCP Secret Manager client -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-secretmanager</artifactId>
<version>2.57.0</version>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-parametermanager</artifactId>
</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>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package dev.openfeature.contrib.providers.gcp;

import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.FeatureProvider;
import dev.openfeature.sdk.Metadata;
import dev.openfeature.sdk.ProviderEvaluation;
import dev.openfeature.sdk.Reason;
import dev.openfeature.sdk.Value;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;

@Slf4j
abstract class AbstractGcpProvider<C> implements FeatureProvider {

protected final GcpProviderOptions options;
protected C client;
protected FlagCache cache;

AbstractGcpProvider(GcpProviderOptions options) {
this.options = options;
}

AbstractGcpProvider(GcpProviderOptions options, C client) {
this.options = options;
this.client = client;
}

@Override
public Metadata getMetadata() {
return () -> getProviderName();
}

@Override
public void initialize(EvaluationContext evaluationContext) throws Exception {
options.validate();
if (client == null) {
createClient();
}
cache = new FlagCache(options.getCacheExpiry(), options.getCacheMaxSize());
log.info("{} initialized for project '{}'", getProviderName(), options.getProjectId());
}

@Override
public void shutdown() {
if (client != null) {
try {
closeClient();
} catch (Exception e) {
log.warn("Error closing client for {}", getProviderName(), e);
}
client = null;
}
log.info("{} shut down", getProviderName());
}

@Override
public final ProviderEvaluation<Boolean> getBooleanEvaluation(
String key, Boolean defaultValue, EvaluationContext ctx) {
return evaluate(key, Boolean.class);
}

@Override
public final ProviderEvaluation<String> getStringEvaluation(
String key, String defaultValue, EvaluationContext ctx) {
return evaluate(key, String.class);
}

@Override
public final ProviderEvaluation<Integer> getIntegerEvaluation(
String key, Integer defaultValue, EvaluationContext ctx) {
return evaluate(key, Integer.class);
}

@Override
public final ProviderEvaluation<Double> getDoubleEvaluation(
String key, Double defaultValue, EvaluationContext ctx) {
return evaluate(key, Double.class);
}

@Override
public final ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
return evaluate(key, Value.class);
}

// -------------------------------------------------------------------------
// Internal helpers
// -------------------------------------------------------------------------

protected <T> ProviderEvaluation<T> evaluate(String key, Class<T> targetType) {
String rawValue = fetchWithCache(key);
T value = FlagValueConverter.convert(rawValue, targetType);
return ProviderEvaluation.<T>builder()
.value(value)
.reason(Reason.STATIC.toString())
.build();
}

protected String fetchWithCache(String key) {
String name = buildName(key);
Optional<String> cached = cache.get(name);
if (cached.isPresent()) {
log.debug("Fetching from cache name '{}'", key);
return cached.get();
}
synchronized (cache) {
return cache.get(name).orElseGet(() -> {
String value = fetchFromGcp(name);
cache.put(name, value);
return value;
});
}
}

protected String buildName(String flagKey) {
String prefix = options.getNamePrefix();
return (prefix != null && !prefix.isEmpty()) ? prefix + flagKey : flagKey;
}

// Subclasses must implement these
protected abstract String getProviderName();

protected abstract void createClient() throws Exception;

protected abstract void closeClient() throws Exception;

protected abstract String fetchFromGcp(String name);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package dev.openfeature.contrib.providers.gcp;

import com.google.api.gax.rpc.NotFoundException;
import com.google.cloud.parametermanager.v1.ParameterManagerClient;
import com.google.cloud.parametermanager.v1.ParameterVersionName;
import com.google.cloud.parametermanager.v1.RenderParameterVersionResponse;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.FeatureProvider;
import dev.openfeature.sdk.Value;
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
import dev.openfeature.sdk.exceptions.GeneralError;
import lombok.extern.slf4j.Slf4j;

/**
* OpenFeature {@link FeatureProvider} backed by Google Cloud Parameter Manager.
*
* <p>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 configured via
* {@code GcpProviderOptions#getNamePrefix()}).
*
* <p>Flag values are read as strings and parsed to the requested type. Supported raw value
* formats:
* <ul>
* <li>Boolean: {@code "true"} / {@code "false"} (case-insensitive)</li>
* <li>Integer: numeric string, e.g. {@code "42"}</li>
* <li>Double: numeric string, e.g. {@code "3.14"}</li>
* <li>String: any string value</li>
* <li>Object: JSON string that is parsed into an OpenFeature {@link Value}</li>
* </ul>
*
* <p>Results are cached in-process for the duration configured in
* {@code GcpProviderOptions#getCacheExpiry()}.
*
* <p>Example:
* <pre>{@code
* GcpProviderOptions opts = GcpProviderOptions.builder()
* .projectId("my-gcp-project")
* .locationId("global") // optional, defaults to "global"
* .build();
* OpenFeatureAPI.getInstance().setProvider(new GcpParameterManagerProvider(opts));
* }</pre>
*/
@Slf4j
public class GcpParameterManagerProvider extends AbstractGcpProvider<ParameterManagerClient> {

static final String PROVIDER_NAME = "GCP Parameter Manager Provider";

/**
* Creates a new provider using the given options. The GCP client is created lazily
* during {@link #initialize(EvaluationContext)}.
*
* @param options provider configuration; must not be null
*/
public GcpParameterManagerProvider(GcpProviderOptions options) {
super(options);
}

/**
* Package-private constructor allowing injection of a pre-built client for testing.
*/
GcpParameterManagerProvider(GcpProviderOptions options, ParameterManagerClient client) {
super(options, client);
}

@Override
protected String getProviderName() {
return PROVIDER_NAME;
}

@Override
protected void createClient() throws Exception {
this.client = ParameterManagerClientFactory.create(options);
}

@Override
protected void closeClient() throws Exception {
this.client.close();
}

@Override
public void initialize(EvaluationContext evaluationContext) throws Exception {
super.initialize(evaluationContext);
log.info("{} initialized via initialize()", getProviderName());
}

@Override
public void shutdown() {
super.shutdown();
log.info("{} shutdown via shutdown()", getProviderName());
}

@Override
protected String fetchFromGcp(String parameterName) {
try {
ParameterVersionName versionName = ParameterVersionName.of(
options.getProjectId(), options.getLocationId(), parameterName, options.getVersion());
RenderParameterVersionResponse response = client.renderParameterVersion(versionName);
return response.getRenderedPayload().toStringUtf8();
} catch (NotFoundException e) {
throw new FlagNotFoundError("Parameter not found: " + parameterName);
} catch (Exception e) {
throw new GeneralError("Error fetching parameter '" + parameterName + "': " + e.getMessage(), e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
*
* <p>Example usage:
* <pre>{@code
* GcpSecretManagerProviderOptions opts = GcpSecretManagerProviderOptions.builder()
* GcpProviderOptions opts = GcpProviderOptions.builder()
* .projectId("my-gcp-project")
* .secretVersion("latest")
* .version("latest")
* .cacheExpiry(Duration.ofMinutes(2))
* .build();
* }</pre>
Expand Down Expand Up @@ -42,6 +42,12 @@ public class GcpProviderOptions {
@Builder.Default
private final String version = "latest";

/**
* Optional location required for ParameterManager, ignored by SecretManager.
*/
@Builder.Default
private final String locationId = "global";

/**
* How long a fetched secret value is retained in the in-memory cache before
* the next evaluation triggers a fresh GCP API call.
Expand Down
Loading
Loading