From fd10322c9f23ce26bfb2d93f98fbc52e936abf94 Mon Sep 17 00:00:00 2001 From: Mahesh Patil Date: Wed, 3 Jun 2026 13:48:13 +0100 Subject: [PATCH 01/15] Add GCP parameter manager classes Signed-off-by: Mahesh Patil --- providers/gcp/CHANGELOG.md | 7 + providers/gcp/README.md | 26 ++- providers/gcp/pom.xml | 24 ++- .../gcp/GcpParameterManagerProvider.java | 176 ++++++++++++++++++ .../GcpParameterManagerProviderOptions.java | 90 +++++++++ .../gcp/GcpSecretManagerProvider.java | 4 +- .../gcp/ParameterManagerClientFactory.java | 34 ++++ providers/gcp/version.txt | 2 +- 8 files changed, 352 insertions(+), 11 deletions(-) create mode 100644 providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java create mode 100644 providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderOptions.java create mode 100644 providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/ParameterManagerClientFactory.java diff --git a/providers/gcp/CHANGELOG.md b/providers/gcp/CHANGELOG.md index 7b09cce026..63e85b9bb0 100644 --- a/providers/gcp/CHANGELOG.md +++ b/providers/gcp/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog + +## [0.0.3] + +### ✨ New Features + +* Add support for GCP Parameter Manager. + ## 0.0.1 ### ✨ New Features diff --git a/providers/gcp/README.md b/providers/gcp/README.md index 5d7fab28b6..8d3531d85f 100644 --- a/providers/gcp/README.md +++ b/providers/gcp/README.md @@ -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 @@ -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; @@ -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.GcpParameterManagerProviderOptions; +import dev.openfeature.sdk.OpenFeatureAPI; + +GcpProviderOptions options = GcpParameterManagerProviderOptions.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: @@ -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() diff --git a/providers/gcp/pom.xml b/providers/gcp/pom.xml index 101a070436..c59e132201 100644 --- a/providers/gcp/pom.xml +++ b/providers/gcp/pom.xml @@ -15,7 +15,7 @@ dev.openfeature.contrib.providers gcp - 0.0.1 + 0.0.2 @@ -35,20 +35,34 @@ + + + + com.google.cloud + libraries-bom + 26.83.0 + pom + import + + + + com.google.cloud google-cloud-secretmanager - 2.57.0 + + + com.google.cloud + google-cloud-parametermanager - + org.slf4j diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java new file mode 100644 index 0000000000..1a61f6b3ac --- /dev/null +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java @@ -0,0 +1,176 @@ +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.Metadata; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.Reason; +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. + * + *

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 + * {@link GcpParameterManagerProviderOptions#getParameterNamePrefix()}). + * + *

Flag values are read as strings and parsed to the requested type. Supported raw value + * formats: + *

    + *
  • Boolean: {@code "true"} / {@code "false"} (case-insensitive)
  • + *
  • Integer: numeric string, e.g. {@code "42"}
  • + *
  • Double: numeric string, e.g. {@code "3.14"}
  • + *
  • String: any string value
  • + *
  • Object: JSON string that is parsed into an OpenFeature {@link Value}
  • + *
+ * + *

Results are cached in-process for the duration configured in + * {@link GcpParameterManagerProviderOptions#getCacheExpiry()}. + * + *

Example: + *

{@code
+ * GcpParameterManagerProviderOptions opts = GcpParameterManagerProviderOptions.builder()
+ *     .projectId("my-gcp-project")
+ *     .build();
+ * OpenFeatureAPI.getInstance().setProvider(new GcpParameterManagerProvider(opts));
+ * }
+ */ +@Slf4j +public class GcpParameterManagerProvider implements FeatureProvider { + + static final String PROVIDER_NAME = "GCP Parameter Manager Provider"; + + private final GcpParameterManagerProviderOptions options; + private ParameterManagerClient client; + private FlagCache cache; + + /** + * 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(GcpParameterManagerProviderOptions options) { + this.options = options; + } + + /** + * Package-private constructor allowing injection of a pre-built client for testing. + */ + GcpParameterManagerProvider(GcpParameterManagerProviderOptions options, ParameterManagerClient client) { + this.options = options; + this.client = client; + } + + @Override + public Metadata getMetadata() { + return () -> PROVIDER_NAME; + } + + @Override + public void initialize(EvaluationContext evaluationContext) throws Exception { + options.validate(); + if (client == null) { + client = ParameterManagerClientFactory.create(options); + } + cache = new FlagCache(options.getCacheExpiry(), options.getCacheMaxSize()); + log.info("GcpParameterManagerProvider initialized for project '{}'", options.getProjectId()); + } + + @Override + public void shutdown() { + if (client != null) { + try { + client.close(); + } catch (Exception e) { + log.warn("Error closing ParameterManagerClient", e); + } + client = null; + } + log.info("GcpParameterManagerProvider shut down"); + } + + @Override + public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { + return evaluate(key, Boolean.class); + } + + @Override + public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { + return evaluate(key, String.class); + } + + @Override + public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { + return evaluate(key, Integer.class); + } + + @Override + public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { + return evaluate(key, Double.class); + } + + @Override + public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) { + return evaluate(key, Value.class); + } + + // ------------------------------------------------------------------------- + // Internal helpers + // ------------------------------------------------------------------------- + + private ProviderEvaluation evaluate(String key, Class targetType) { + String rawValue = fetchWithCache(key); + T value = FlagValueConverter.convert(rawValue, targetType); + return ProviderEvaluation.builder() + .value(value) + .reason(Reason.CACHED.toString()) + .build(); + } + + private String fetchWithCache(String key) { + String paramName = buildParameterName(key); + return cache.get(paramName).orElseGet(() -> { + String value = fetchFromGcp(paramName); + cache.put(paramName, value); + return value; + }); + } + + /** + * Applies the configured prefix (if any) and returns the GCP parameter name for the flag. + */ + private String buildParameterName(String flagKey) { + String prefix = options.getParameterNamePrefix(); + return (prefix != null && !prefix.isEmpty()) ? prefix + flagKey : flagKey; + } + + /** + * Fetches the latest version of the named parameter from GCP Parameter Manager. + * + * @param parameterName the GCP parameter name (without project/location path) + * @return the rendered string value of the parameter + * @throws FlagNotFoundError when the parameter does not exist + * @throws GeneralError for any other GCP API error + */ + private String fetchFromGcp(String parameterName) { + try { + ParameterVersionName versionName = ParameterVersionName.of( + options.getProjectId(), options.getLocationId(), parameterName, options.getParameterVersion()); + log.debug("Fetching parameter '{}' from GCP", versionName); + 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); + } + } +} diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderOptions.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderOptions.java new file mode 100644 index 0000000000..ea6a9d8927 --- /dev/null +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderOptions.java @@ -0,0 +1,90 @@ +package dev.openfeature.contrib.providers.gcp; + +import com.google.auth.oauth2.GoogleCredentials; +import java.time.Duration; +import lombok.Builder; +import lombok.Getter; + +/** + * Configuration options for {@link GcpParameterManagerProvider}. + * + *

Example usage: + *

{@code
+ * GcpParameterManagerProviderOptions opts = GcpParameterManagerProviderOptions.builder()
+ *     .projectId("my-gcp-project")
+ *     .locationId("us-central1")
+ *     .cacheExpiry(Duration.ofMinutes(2))
+ *     .build();
+ * }
+ */ +@Getter +@Builder +public class GcpParameterManagerProviderOptions { + + /** + * GCP project ID that owns the parameters. Required. + * Example: "my-gcp-project" or numeric project number "123456789". + */ + private final String projectId; + + /** + * GCP location for the Parameter Manager endpoint. Optional. + * Use "global" (default) for the global endpoint, or a region such as "us-central1" + * when parameters are stored regionally. + */ + @Builder.Default + private final String locationId = "global"; + + /** + * Explicit Google credentials to use when creating the Parameter Manager client. + * When {@code null} (default), Application Default Credentials (ADC) are used + * automatically by the GCP client library. + */ + private final GoogleCredentials credentials; + + /** + * How long a fetched parameter value is retained in the in-memory cache before + * the next evaluation triggers a fresh GCP API call. + * + *

GCP Parameter Manager has API quotas. Set this to at least + * {@code Duration.ofSeconds(30)} in high-throughput scenarios. + * + *

Default: 5 minutes. + */ + @Builder.Default + private final Duration cacheExpiry = Duration.ofMinutes(5); + + /** + * Maximum number of distinct parameter names held in the cache at once. + * When the cache is full, the oldest entry is evicted before inserting a new one. + * Default: 500. + */ + @Builder.Default + private final int cacheMaxSize = 500; + + /** + * The parameter version to retrieve. Defaults to {@code "latest"}. + * Override with a specific version number (e.g. {@code "3"}) for pinned deployments + * where you want consistent behaviour regardless of parameter updates. + */ + @Builder.Default + private final String parameterVersion = "latest"; + + /** + * Optional prefix prepended to every flag key before constructing the GCP + * parameter name. For example, setting {@code parameterNamePrefix = "ff-"} maps + * flag key {@code "my-flag"} to parameter name {@code "ff-my-flag"}. + */ + private final String parameterNamePrefix; + + /** + * Validates that required options are present and well-formed. + * + * @throws IllegalArgumentException when {@code projectId} is null or blank + */ + public void validate() { + if (projectId == null || projectId.trim().isEmpty()) { + throw new IllegalArgumentException("GcpParameterManagerProviderOptions: projectId must not be blank"); + } + } +} diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProvider.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProvider.java index becf030904..ccc1f4905a 100644 --- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProvider.java +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProvider.java @@ -20,7 +20,7 @@ * *

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 configured via - * {@link GcpProviderOptions}'s {@code namePrefix}). + * {@link GcpProviderOptions#getNamePrefix()}). * *

Flag values are read as UTF-8 strings from the secret payload and parsed to the requested * type. Supported raw value formats: @@ -33,7 +33,7 @@ * * *

Results are cached in-process for the duration configured in - * {@link GcpProviderOptions}'s {@code cacheExpiry}. + * {@link GcpProviderOptions#getCacheExpiry()}. * *

Example: *

{@code
diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/ParameterManagerClientFactory.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/ParameterManagerClientFactory.java
new file mode 100644
index 0000000000..42193dba38
--- /dev/null
+++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/ParameterManagerClientFactory.java
@@ -0,0 +1,34 @@
+package dev.openfeature.contrib.providers.gcp;
+
+import com.google.api.gax.core.FixedCredentialsProvider;
+import com.google.cloud.parametermanager.v1.ParameterManagerClient;
+import com.google.cloud.parametermanager.v1.ParameterManagerSettings;
+import java.io.IOException;
+
+/**
+ * Factory for creating a {@link ParameterManagerClient}, separated to allow injection
+ * of mock clients in unit tests.
+ */
+final class ParameterManagerClientFactory {
+
+    private ParameterManagerClientFactory() {}
+
+    /**
+     * Creates a new {@link ParameterManagerClient} using the provided options.
+     *
+     * 

When {@link GcpParameterManagerProviderOptions#getCredentials()} is non-null, those + * credentials are used explicitly. Otherwise, the GCP client library falls back to + * Application Default Credentials (ADC) automatically. + * + * @param options the provider options + * @return a configured {@link ParameterManagerClient} + * @throws IOException if the client cannot be created + */ + static ParameterManagerClient create(GcpParameterManagerProviderOptions options) throws IOException { + ParameterManagerSettings.Builder settingsBuilder = ParameterManagerSettings.newBuilder(); + if (options.getCredentials() != null) { + settingsBuilder.setCredentialsProvider(FixedCredentialsProvider.create(options.getCredentials())); + } + return ParameterManagerClient.create(settingsBuilder.build()); + } +} diff --git a/providers/gcp/version.txt b/providers/gcp/version.txt index 8acdd82b76..4e379d2bfe 100644 --- a/providers/gcp/version.txt +++ b/providers/gcp/version.txt @@ -1 +1 @@ -0.0.1 +0.0.2 From ef5e25beb3eb6ab0280ec21fadd3770e58e0078c Mon Sep 17 00:00:00 2001 From: Mahesh Patil Date: Wed, 3 Jun 2026 13:48:13 +0100 Subject: [PATCH 02/15] Add GCP parameter manager classes Signed-off-by: Mahesh Patil --- providers/gcp/CHANGELOG.md | 7 + providers/gcp/README.md | 26 ++- providers/gcp/pom.xml | 24 ++- .../gcp/GcpParameterManagerProvider.java | 176 ++++++++++++++++++ .../GcpParameterManagerProviderOptions.java | 90 +++++++++ .../gcp/GcpSecretManagerProvider.java | 4 +- .../gcp/ParameterManagerClientFactory.java | 34 ++++ providers/gcp/version.txt | 2 +- 8 files changed, 352 insertions(+), 11 deletions(-) create mode 100644 providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java create mode 100644 providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderOptions.java create mode 100644 providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/ParameterManagerClientFactory.java diff --git a/providers/gcp/CHANGELOG.md b/providers/gcp/CHANGELOG.md index 7b09cce026..63e85b9bb0 100644 --- a/providers/gcp/CHANGELOG.md +++ b/providers/gcp/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog + +## [0.0.3] + +### ✨ New Features + +* Add support for GCP Parameter Manager. + ## 0.0.1 ### ✨ New Features diff --git a/providers/gcp/README.md b/providers/gcp/README.md index 5d7fab28b6..8d3531d85f 100644 --- a/providers/gcp/README.md +++ b/providers/gcp/README.md @@ -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 @@ -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; @@ -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.GcpParameterManagerProviderOptions; +import dev.openfeature.sdk.OpenFeatureAPI; + +GcpProviderOptions options = GcpParameterManagerProviderOptions.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: @@ -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() diff --git a/providers/gcp/pom.xml b/providers/gcp/pom.xml index 101a070436..c59e132201 100644 --- a/providers/gcp/pom.xml +++ b/providers/gcp/pom.xml @@ -15,7 +15,7 @@ dev.openfeature.contrib.providers gcp - 0.0.1 + 0.0.2 @@ -35,20 +35,34 @@ + + + + com.google.cloud + libraries-bom + 26.83.0 + pom + import + + + + com.google.cloud google-cloud-secretmanager - 2.57.0 + + + com.google.cloud + google-cloud-parametermanager - + org.slf4j diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java new file mode 100644 index 0000000000..1a61f6b3ac --- /dev/null +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java @@ -0,0 +1,176 @@ +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.Metadata; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.Reason; +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. + * + *

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 + * {@link GcpParameterManagerProviderOptions#getParameterNamePrefix()}). + * + *

Flag values are read as strings and parsed to the requested type. Supported raw value + * formats: + *

    + *
  • Boolean: {@code "true"} / {@code "false"} (case-insensitive)
  • + *
  • Integer: numeric string, e.g. {@code "42"}
  • + *
  • Double: numeric string, e.g. {@code "3.14"}
  • + *
  • String: any string value
  • + *
  • Object: JSON string that is parsed into an OpenFeature {@link Value}
  • + *
+ * + *

Results are cached in-process for the duration configured in + * {@link GcpParameterManagerProviderOptions#getCacheExpiry()}. + * + *

Example: + *

{@code
+ * GcpParameterManagerProviderOptions opts = GcpParameterManagerProviderOptions.builder()
+ *     .projectId("my-gcp-project")
+ *     .build();
+ * OpenFeatureAPI.getInstance().setProvider(new GcpParameterManagerProvider(opts));
+ * }
+ */ +@Slf4j +public class GcpParameterManagerProvider implements FeatureProvider { + + static final String PROVIDER_NAME = "GCP Parameter Manager Provider"; + + private final GcpParameterManagerProviderOptions options; + private ParameterManagerClient client; + private FlagCache cache; + + /** + * 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(GcpParameterManagerProviderOptions options) { + this.options = options; + } + + /** + * Package-private constructor allowing injection of a pre-built client for testing. + */ + GcpParameterManagerProvider(GcpParameterManagerProviderOptions options, ParameterManagerClient client) { + this.options = options; + this.client = client; + } + + @Override + public Metadata getMetadata() { + return () -> PROVIDER_NAME; + } + + @Override + public void initialize(EvaluationContext evaluationContext) throws Exception { + options.validate(); + if (client == null) { + client = ParameterManagerClientFactory.create(options); + } + cache = new FlagCache(options.getCacheExpiry(), options.getCacheMaxSize()); + log.info("GcpParameterManagerProvider initialized for project '{}'", options.getProjectId()); + } + + @Override + public void shutdown() { + if (client != null) { + try { + client.close(); + } catch (Exception e) { + log.warn("Error closing ParameterManagerClient", e); + } + client = null; + } + log.info("GcpParameterManagerProvider shut down"); + } + + @Override + public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { + return evaluate(key, Boolean.class); + } + + @Override + public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { + return evaluate(key, String.class); + } + + @Override + public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { + return evaluate(key, Integer.class); + } + + @Override + public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { + return evaluate(key, Double.class); + } + + @Override + public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) { + return evaluate(key, Value.class); + } + + // ------------------------------------------------------------------------- + // Internal helpers + // ------------------------------------------------------------------------- + + private ProviderEvaluation evaluate(String key, Class targetType) { + String rawValue = fetchWithCache(key); + T value = FlagValueConverter.convert(rawValue, targetType); + return ProviderEvaluation.builder() + .value(value) + .reason(Reason.CACHED.toString()) + .build(); + } + + private String fetchWithCache(String key) { + String paramName = buildParameterName(key); + return cache.get(paramName).orElseGet(() -> { + String value = fetchFromGcp(paramName); + cache.put(paramName, value); + return value; + }); + } + + /** + * Applies the configured prefix (if any) and returns the GCP parameter name for the flag. + */ + private String buildParameterName(String flagKey) { + String prefix = options.getParameterNamePrefix(); + return (prefix != null && !prefix.isEmpty()) ? prefix + flagKey : flagKey; + } + + /** + * Fetches the latest version of the named parameter from GCP Parameter Manager. + * + * @param parameterName the GCP parameter name (without project/location path) + * @return the rendered string value of the parameter + * @throws FlagNotFoundError when the parameter does not exist + * @throws GeneralError for any other GCP API error + */ + private String fetchFromGcp(String parameterName) { + try { + ParameterVersionName versionName = ParameterVersionName.of( + options.getProjectId(), options.getLocationId(), parameterName, options.getParameterVersion()); + log.debug("Fetching parameter '{}' from GCP", versionName); + 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); + } + } +} diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderOptions.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderOptions.java new file mode 100644 index 0000000000..ea6a9d8927 --- /dev/null +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderOptions.java @@ -0,0 +1,90 @@ +package dev.openfeature.contrib.providers.gcp; + +import com.google.auth.oauth2.GoogleCredentials; +import java.time.Duration; +import lombok.Builder; +import lombok.Getter; + +/** + * Configuration options for {@link GcpParameterManagerProvider}. + * + *

Example usage: + *

{@code
+ * GcpParameterManagerProviderOptions opts = GcpParameterManagerProviderOptions.builder()
+ *     .projectId("my-gcp-project")
+ *     .locationId("us-central1")
+ *     .cacheExpiry(Duration.ofMinutes(2))
+ *     .build();
+ * }
+ */ +@Getter +@Builder +public class GcpParameterManagerProviderOptions { + + /** + * GCP project ID that owns the parameters. Required. + * Example: "my-gcp-project" or numeric project number "123456789". + */ + private final String projectId; + + /** + * GCP location for the Parameter Manager endpoint. Optional. + * Use "global" (default) for the global endpoint, or a region such as "us-central1" + * when parameters are stored regionally. + */ + @Builder.Default + private final String locationId = "global"; + + /** + * Explicit Google credentials to use when creating the Parameter Manager client. + * When {@code null} (default), Application Default Credentials (ADC) are used + * automatically by the GCP client library. + */ + private final GoogleCredentials credentials; + + /** + * How long a fetched parameter value is retained in the in-memory cache before + * the next evaluation triggers a fresh GCP API call. + * + *

GCP Parameter Manager has API quotas. Set this to at least + * {@code Duration.ofSeconds(30)} in high-throughput scenarios. + * + *

Default: 5 minutes. + */ + @Builder.Default + private final Duration cacheExpiry = Duration.ofMinutes(5); + + /** + * Maximum number of distinct parameter names held in the cache at once. + * When the cache is full, the oldest entry is evicted before inserting a new one. + * Default: 500. + */ + @Builder.Default + private final int cacheMaxSize = 500; + + /** + * The parameter version to retrieve. Defaults to {@code "latest"}. + * Override with a specific version number (e.g. {@code "3"}) for pinned deployments + * where you want consistent behaviour regardless of parameter updates. + */ + @Builder.Default + private final String parameterVersion = "latest"; + + /** + * Optional prefix prepended to every flag key before constructing the GCP + * parameter name. For example, setting {@code parameterNamePrefix = "ff-"} maps + * flag key {@code "my-flag"} to parameter name {@code "ff-my-flag"}. + */ + private final String parameterNamePrefix; + + /** + * Validates that required options are present and well-formed. + * + * @throws IllegalArgumentException when {@code projectId} is null or blank + */ + public void validate() { + if (projectId == null || projectId.trim().isEmpty()) { + throw new IllegalArgumentException("GcpParameterManagerProviderOptions: projectId must not be blank"); + } + } +} diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProvider.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProvider.java index becf030904..ccc1f4905a 100644 --- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProvider.java +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProvider.java @@ -20,7 +20,7 @@ * *

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 configured via - * {@link GcpProviderOptions}'s {@code namePrefix}). + * {@link GcpProviderOptions#getNamePrefix()}). * *

Flag values are read as UTF-8 strings from the secret payload and parsed to the requested * type. Supported raw value formats: @@ -33,7 +33,7 @@ * * *

Results are cached in-process for the duration configured in - * {@link GcpProviderOptions}'s {@code cacheExpiry}. + * {@link GcpProviderOptions#getCacheExpiry()}. * *

Example: *

{@code
diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/ParameterManagerClientFactory.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/ParameterManagerClientFactory.java
new file mode 100644
index 0000000000..42193dba38
--- /dev/null
+++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/ParameterManagerClientFactory.java
@@ -0,0 +1,34 @@
+package dev.openfeature.contrib.providers.gcp;
+
+import com.google.api.gax.core.FixedCredentialsProvider;
+import com.google.cloud.parametermanager.v1.ParameterManagerClient;
+import com.google.cloud.parametermanager.v1.ParameterManagerSettings;
+import java.io.IOException;
+
+/**
+ * Factory for creating a {@link ParameterManagerClient}, separated to allow injection
+ * of mock clients in unit tests.
+ */
+final class ParameterManagerClientFactory {
+
+    private ParameterManagerClientFactory() {}
+
+    /**
+     * Creates a new {@link ParameterManagerClient} using the provided options.
+     *
+     * 

When {@link GcpParameterManagerProviderOptions#getCredentials()} is non-null, those + * credentials are used explicitly. Otherwise, the GCP client library falls back to + * Application Default Credentials (ADC) automatically. + * + * @param options the provider options + * @return a configured {@link ParameterManagerClient} + * @throws IOException if the client cannot be created + */ + static ParameterManagerClient create(GcpParameterManagerProviderOptions options) throws IOException { + ParameterManagerSettings.Builder settingsBuilder = ParameterManagerSettings.newBuilder(); + if (options.getCredentials() != null) { + settingsBuilder.setCredentialsProvider(FixedCredentialsProvider.create(options.getCredentials())); + } + return ParameterManagerClient.create(settingsBuilder.build()); + } +} diff --git a/providers/gcp/version.txt b/providers/gcp/version.txt index 8acdd82b76..4e379d2bfe 100644 --- a/providers/gcp/version.txt +++ b/providers/gcp/version.txt @@ -1 +1 @@ -0.0.1 +0.0.2 From 9f2305cef429cbb9028559a1d02db2bdba581ad1 Mon Sep 17 00:00:00 2001 From: Mahesh Patil Date: Wed, 3 Jun 2026 14:13:32 +0100 Subject: [PATCH 03/15] Update pom dependencies to use bom based on review comment Signed-off-by: Mahesh Patil --- providers/gcp/pom.xml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/providers/gcp/pom.xml b/providers/gcp/pom.xml index c59e132201..2c4759f2e3 100644 --- a/providers/gcp/pom.xml +++ b/providers/gcp/pom.xml @@ -44,6 +44,13 @@ pom import + + com.fasterxml.jackson + jackson-bom + 2.21.1 + pom + import + @@ -59,10 +66,10 @@ - + org.slf4j From c24e55349b66dbed3df1167e7cb622e22044613a Mon Sep 17 00:00:00 2001 From: Mahesh Patil Date: Wed, 3 Jun 2026 15:43:17 +0100 Subject: [PATCH 04/15] feat/updates to parameter manager options & provider classes 1. add location id to options as gcp params require location 2. fixed parameter manager classes to use common gcp classes post refactor Signed-off-by: Mahesh Patil --- .../gcp/GcpParameterManagerProvider.java | 10 +-- .../GcpParameterManagerProviderOptions.java | 90 ------------------- .../providers/gcp/GcpProviderOptions.java | 10 ++- .../gcp/ParameterManagerClientFactory.java | 2 +- 4 files changed, 14 insertions(+), 98 deletions(-) delete mode 100644 providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderOptions.java diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java index 1a61f6b3ac..ec68e0c54b 100644 --- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java @@ -47,7 +47,7 @@ public class GcpParameterManagerProvider implements FeatureProvider { static final String PROVIDER_NAME = "GCP Parameter Manager Provider"; - private final GcpParameterManagerProviderOptions options; + private final GcpProviderOptions options; private ParameterManagerClient client; private FlagCache cache; @@ -57,14 +57,14 @@ public class GcpParameterManagerProvider implements FeatureProvider { * * @param options provider configuration; must not be null */ - public GcpParameterManagerProvider(GcpParameterManagerProviderOptions options) { + public GcpParameterManagerProvider(GcpProviderOptions options) { this.options = options; } /** * Package-private constructor allowing injection of a pre-built client for testing. */ - GcpParameterManagerProvider(GcpParameterManagerProviderOptions options, ParameterManagerClient client) { + GcpParameterManagerProvider(GcpProviderOptions options, ParameterManagerClient client) { this.options = options; this.client = client; } @@ -148,7 +148,7 @@ private String fetchWithCache(String key) { * Applies the configured prefix (if any) and returns the GCP parameter name for the flag. */ private String buildParameterName(String flagKey) { - String prefix = options.getParameterNamePrefix(); + String prefix = options.getNamePrefix(); return (prefix != null && !prefix.isEmpty()) ? prefix + flagKey : flagKey; } @@ -163,7 +163,7 @@ private String buildParameterName(String flagKey) { private String fetchFromGcp(String parameterName) { try { ParameterVersionName versionName = ParameterVersionName.of( - options.getProjectId(), options.getLocationId(), parameterName, options.getParameterVersion()); + options.getProjectId(), options.getLocationId(), parameterName, options.getVersion()); log.debug("Fetching parameter '{}' from GCP", versionName); RenderParameterVersionResponse response = client.renderParameterVersion(versionName); return response.getRenderedPayload().toStringUtf8(); diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderOptions.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderOptions.java deleted file mode 100644 index ea6a9d8927..0000000000 --- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderOptions.java +++ /dev/null @@ -1,90 +0,0 @@ -package dev.openfeature.contrib.providers.gcp; - -import com.google.auth.oauth2.GoogleCredentials; -import java.time.Duration; -import lombok.Builder; -import lombok.Getter; - -/** - * Configuration options for {@link GcpParameterManagerProvider}. - * - *

Example usage: - *

{@code
- * GcpParameterManagerProviderOptions opts = GcpParameterManagerProviderOptions.builder()
- *     .projectId("my-gcp-project")
- *     .locationId("us-central1")
- *     .cacheExpiry(Duration.ofMinutes(2))
- *     .build();
- * }
- */ -@Getter -@Builder -public class GcpParameterManagerProviderOptions { - - /** - * GCP project ID that owns the parameters. Required. - * Example: "my-gcp-project" or numeric project number "123456789". - */ - private final String projectId; - - /** - * GCP location for the Parameter Manager endpoint. Optional. - * Use "global" (default) for the global endpoint, or a region such as "us-central1" - * when parameters are stored regionally. - */ - @Builder.Default - private final String locationId = "global"; - - /** - * Explicit Google credentials to use when creating the Parameter Manager client. - * When {@code null} (default), Application Default Credentials (ADC) are used - * automatically by the GCP client library. - */ - private final GoogleCredentials credentials; - - /** - * How long a fetched parameter value is retained in the in-memory cache before - * the next evaluation triggers a fresh GCP API call. - * - *

GCP Parameter Manager has API quotas. Set this to at least - * {@code Duration.ofSeconds(30)} in high-throughput scenarios. - * - *

Default: 5 minutes. - */ - @Builder.Default - private final Duration cacheExpiry = Duration.ofMinutes(5); - - /** - * Maximum number of distinct parameter names held in the cache at once. - * When the cache is full, the oldest entry is evicted before inserting a new one. - * Default: 500. - */ - @Builder.Default - private final int cacheMaxSize = 500; - - /** - * The parameter version to retrieve. Defaults to {@code "latest"}. - * Override with a specific version number (e.g. {@code "3"}) for pinned deployments - * where you want consistent behaviour regardless of parameter updates. - */ - @Builder.Default - private final String parameterVersion = "latest"; - - /** - * Optional prefix prepended to every flag key before constructing the GCP - * parameter name. For example, setting {@code parameterNamePrefix = "ff-"} maps - * flag key {@code "my-flag"} to parameter name {@code "ff-my-flag"}. - */ - private final String parameterNamePrefix; - - /** - * Validates that required options are present and well-formed. - * - * @throws IllegalArgumentException when {@code projectId} is null or blank - */ - public void validate() { - if (projectId == null || projectId.trim().isEmpty()) { - throw new IllegalArgumentException("GcpParameterManagerProviderOptions: projectId must not be blank"); - } - } -} diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpProviderOptions.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpProviderOptions.java index 95949f1a56..cdb68e9248 100644 --- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpProviderOptions.java +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpProviderOptions.java @@ -10,9 +10,9 @@ * *

Example usage: *

{@code
- * GcpSecretManagerProviderOptions opts = GcpSecretManagerProviderOptions.builder()
+ * GcpProviderOptions opts = GcpProviderOptions.builder()
  *     .projectId("my-gcp-project")
- *     .secretVersion("latest")
+ *     .version("latest")
  *     .cacheExpiry(Duration.ofMinutes(2))
  *     .build();
  * }
@@ -42,6 +42,12 @@ public class GcpProviderOptions { @Builder.Default private final String version = "latest"; + + /** + * Optional location required for ParameterManager, ignored by SecretManager + */ + private final String locationId; + /** * How long a fetched secret value is retained in the in-memory cache before * the next evaluation triggers a fresh GCP API call. diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/ParameterManagerClientFactory.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/ParameterManagerClientFactory.java index 42193dba38..c71ff2e0ee 100644 --- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/ParameterManagerClientFactory.java +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/ParameterManagerClientFactory.java @@ -24,7 +24,7 @@ private ParameterManagerClientFactory() {} * @return a configured {@link ParameterManagerClient} * @throws IOException if the client cannot be created */ - static ParameterManagerClient create(GcpParameterManagerProviderOptions options) throws IOException { + static ParameterManagerClient create(GcpProviderOptions options) throws IOException { ParameterManagerSettings.Builder settingsBuilder = ParameterManagerSettings.newBuilder(); if (options.getCredentials() != null) { settingsBuilder.setCredentialsProvider(FixedCredentialsProvider.create(options.getCredentials())); From 3587f3516e465258b5dffc4151d6bc7f504cb20b Mon Sep 17 00:00:00 2001 From: Mahesh Patil Date: Wed, 3 Jun 2026 18:40:21 +0100 Subject: [PATCH 05/15] feat/fixes to parameter manager tests Signed-off-by: Mahesh Patil --- .../gcp/GcpParameterManagerProvider.java | 25 +- .../gcp/GcpParameterManagerProviderTest.java | 269 ++++++++++++++++++ 2 files changed, 287 insertions(+), 7 deletions(-) create mode 100644 providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderTest.java diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java index ec68e0c54b..808630b092 100644 --- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java @@ -1,5 +1,7 @@ package dev.openfeature.contrib.providers.gcp; +import java.util.Optional; + import com.google.api.gax.rpc.NotFoundException; import com.google.cloud.parametermanager.v1.ParameterManagerClient; import com.google.cloud.parametermanager.v1.ParameterVersionName; @@ -131,17 +133,25 @@ private ProviderEvaluation evaluate(String key, Class targetType) { T value = FlagValueConverter.convert(rawValue, targetType); return ProviderEvaluation.builder() .value(value) - .reason(Reason.CACHED.toString()) + .reason(Reason.STATIC.toString()) .build(); } private String fetchWithCache(String key) { String paramName = buildParameterName(key); - return cache.get(paramName).orElseGet(() -> { - String value = fetchFromGcp(paramName); - cache.put(paramName, value); - return value; - }); + Optional cached = cache.get(paramName); + if (cached.isPresent()) { + log.debug("Fetching from cache parameter '{}'", key); + return cached.get(); + } + synchronized (this) { + return cache.get(paramName).orElseGet(() -> { + String value = fetchFromGcp(paramName); + cache.put(paramName, value); + return value; + }); + } + } /** @@ -162,9 +172,10 @@ private String buildParameterName(String flagKey) { */ private String fetchFromGcp(String parameterName) { try { + log.info("Fetching parameter from GCP name '{}'", parameterName); ParameterVersionName versionName = ParameterVersionName.of( options.getProjectId(), options.getLocationId(), parameterName, options.getVersion()); - log.debug("Fetching parameter '{}' from GCP", versionName); + log.info("Fetching parameter from GCP version {}", parameterName, versionName); RenderParameterVersionResponse response = client.renderParameterVersion(versionName); return response.getRenderedPayload().toStringUtf8(); } catch (NotFoundException e) { diff --git a/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderTest.java b/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderTest.java new file mode 100644 index 0000000000..9c2d96bee2 --- /dev/null +++ b/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderTest.java @@ -0,0 +1,269 @@ +package dev.openfeature.contrib.providers.gcp; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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 com.google.protobuf.ByteString; +import dev.openfeature.sdk.ImmutableContext; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.Reason; +import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.exceptions.FlagNotFoundError; +import dev.openfeature.sdk.exceptions.GeneralError; +import dev.openfeature.sdk.exceptions.ParseError; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@DisplayName("GcpParameterManagerProvider") +@ExtendWith(MockitoExtension.class) +class GcpParameterManagerProviderTest { + + @Mock + private ParameterManagerClient mockClient; + + private GcpProviderOptions options; + private GcpParameterManagerProvider provider; + + @BeforeEach + void setUp() throws Exception { + options = GcpProviderOptions.builder() + .projectId("test-project") + .locationId("europe-west1") + .build(); + provider = new GcpParameterManagerProvider(options, mockClient); + provider.initialize(new ImmutableContext()); + } + + private void stubParameter(String value) { + // Build a mock response + RenderParameterVersionResponse response = RenderParameterVersionResponse.newBuilder() + .setRenderedPayload(ByteString.copyFromUtf8(value)) + .build(); + when(mockClient.renderParameterVersion(any(ParameterVersionName.class))).thenReturn(response); + + } + + private void stubParameterNotFound() { + when(mockClient.renderParameterVersion(any(ParameterVersionName.class))).thenThrow(NotFoundException.class); + } + + private void stubParameterError(String message) { + when(mockClient.renderParameterVersion(any(ParameterVersionName.class))).thenThrow(new RuntimeException(message)); + } + + @Nested + @DisplayName("Metadata") + class MetadataTests { + + @Test + @DisplayName("returns the correct provider name") + void providerName() { + assertThat(provider.getMetadata().getName()).isEqualTo(GcpParameterManagerProvider.PROVIDER_NAME); + } + } + + @Nested + @DisplayName("Initialization") + class InitializationTests { + + @Test + @DisplayName("throws IllegalArgumentException when projectId is blank") + void blankProjectIdThrows() { + GcpProviderOptions badOpts = + GcpProviderOptions.builder().projectId("").build(); + GcpParameterManagerProvider badProvider = new GcpParameterManagerProvider(badOpts, mockClient); + assertThatThrownBy(() -> badProvider.initialize(new ImmutableContext())) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("throws IllegalArgumentException when projectId is null") + void nullProjectIdThrows() { + GcpProviderOptions badOpts = GcpProviderOptions.builder().build(); + GcpParameterManagerProvider badProvider = new GcpParameterManagerProvider(badOpts, mockClient); + assertThatThrownBy(() -> badProvider.initialize(new ImmutableContext())) + .isInstanceOf(IllegalArgumentException.class); + } + } + + @Nested + @DisplayName("Boolean evaluation") + class BooleanEvaluation { + + @Test + @DisplayName("returns true for value 'true'") + void trueValue() { + stubParameter("true"); + ProviderEvaluation result = + provider.getBooleanEvaluation("bool-flag", false, new ImmutableContext()); + assertThat(result.getValue()).isTrue(); + assertThat(result.getReason()).isEqualTo(Reason.STATIC.toString()); + } + + @Test + @DisplayName("returns false for value 'false'") + void falseValue() { + stubParameter("false"); + ProviderEvaluation result = + provider.getBooleanEvaluation("bool-flag", true, new ImmutableContext()); + assertThat(result.getValue()).isFalse(); + } + + @Test + @DisplayName("throws ParseError for malformed boolean value") + void malformedBooleanThrows() { + stubParameter("not-a-bool"); + assertThatThrownBy(() -> provider.getBooleanEvaluation("bool-flag", false, new ImmutableContext())) + .isInstanceOf(ParseError.class); + } + } + + @Nested + @DisplayName("String evaluation") + class StringEvaluation { + + @Test + @DisplayName("returns string value as-is") + void stringValue() { + stubParameter("dark-mode"); + ProviderEvaluation result = + provider.getStringEvaluation("str-flag", "light-mode", new ImmutableContext()); + assertThat(result.getValue()).isEqualTo("dark-mode"); + } + } + + @Nested + @DisplayName("Integer evaluation") + class IntegerEvaluation { + + @Test + @DisplayName("parses numeric string to Integer") + void integerValue() { + stubParameter("42"); + ProviderEvaluation result = provider.getIntegerEvaluation("int-flag", 0, new ImmutableContext()); + assertThat(result.getValue()).isEqualTo(42); + } + + @Test + @DisplayName("throws ParseError for non-numeric value") + void nonNumericThrows() { + stubParameter("abc"); + assertThatThrownBy(() -> provider.getIntegerEvaluation("int-flag", 0, new ImmutableContext())) + .isInstanceOf(ParseError.class); + } + } + + @Nested + @DisplayName("Double evaluation") + class DoubleEvaluation { + + @Test + @DisplayName("parses numeric string to Double") + void doubleValue() { + stubParameter("3.14"); + ProviderEvaluation result = + provider.getDoubleEvaluation("double-flag", 0.0, new ImmutableContext()); + assertThat(result.getValue()).isEqualTo(3.14); + } + } + + @Nested + @DisplayName("Object evaluation") + class ObjectEvaluation { + + @Test + @DisplayName("parses JSON string to Value/Structure") + void jsonValue() { + stubParameter("{\"color\":\"blue\",\"count\":3}"); + ProviderEvaluation result = + provider.getObjectEvaluation("obj-flag", new Value(), new ImmutableContext()); + assertThat(result.getValue().asStructure()).isNotNull(); + assertThat(result.getValue().asStructure().getValue("color").asString()) + .isEqualTo("blue"); + } + } + + @Nested + @DisplayName("Error handling") + class ErrorHandling { + + @Test + @DisplayName("throws FlagNotFoundError when parameter does not exist") + void flagNotFound() { + stubParameterNotFound(); + assertThatThrownBy(() -> provider.getBooleanEvaluation("missing-flag", false, new ImmutableContext())) + .isInstanceOf(FlagNotFoundError.class); + } + + @Test + @DisplayName("throws GeneralError on unexpected GCP API exception") + void gcpApiError() { + stubParameterError("Connection refused"); + assertThatThrownBy(() -> provider.getBooleanEvaluation("flag", false, new ImmutableContext())) + .isInstanceOf(GeneralError.class); + } + } + + @Nested + @DisplayName("Caching") + class CachingTests { + + @Test + @DisplayName("cache hit: GCP client called only once for two consecutive evaluations") + void cacheHit() { + stubParameter("true"); + provider.getBooleanEvaluation("cached-flag", false, new ImmutableContext()); + provider.getBooleanEvaluation("cached-flag", false, new ImmutableContext()); + verify(mockClient, times(1)).renderParameterVersion(any(ParameterVersionName.class)); + } + } + + @Nested + @DisplayName("Secret name prefix") + class PrefixTests { + + @Test + @DisplayName("prefix is prepended to the flag key when building parameter name") + void prefixApplied() { + GcpProviderOptions prefixedOpts = GcpProviderOptions.builder() + .projectId("test-project") + .namePrefix("ff-") + .build(); + stubParameter("true"); + GcpParameterManagerProvider prefixedProvider = new GcpParameterManagerProvider(prefixedOpts, mockClient); + try { + prefixedProvider.initialize(new ImmutableContext()); + } catch (Exception e) { + throw new RuntimeException(e); + } + ProviderEvaluation result = + prefixedProvider.getBooleanEvaluation("my-flag", false, new ImmutableContext()); + assertThat(result.getValue()).isTrue(); + } + } + + @Nested + @DisplayName("Lifecycle") + class LifecycleTests { + + @Test + @DisplayName("shutdown() closes the GCP client") + void shutdownClosesClient() { + provider.shutdown(); + verify(mockClient, times(1)).close(); + } + } +} From 12338ffc8021b0973b0a441365786bf9ea35b479 Mon Sep 17 00:00:00 2001 From: Mahesh Patil Date: Fri, 5 Jun 2026 18:23:42 +0100 Subject: [PATCH 06/15] Refactor by creating abstract clases to reduce duplication Signed-off-by: Mahesh Patil --- .../providers/gcp/AbstractGcpProvider.java | 120 +++++++++ .../gcp/GcpParameterManagerProvider.java | 110 ++------ .../gcp/GcpSecretManagerProvider.java | 106 ++------ .../gcp/AbstractGcpProviderTest.java | 229 ++++++++++++++++ .../gcp/GcpParameterManagerProviderTest.java | 252 ++---------------- .../gcp/GcpSecretManagerProviderTest.java | 245 ++--------------- 6 files changed, 438 insertions(+), 624 deletions(-) create mode 100644 providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProvider.java create mode 100644 providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProviderTest.java diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProvider.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProvider.java new file mode 100644 index 0000000000..88a099054c --- /dev/null +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProvider.java @@ -0,0 +1,120 @@ +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 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 getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { + return evaluate(key, Boolean.class); + } + + @Override + public final ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { + return evaluate(key, String.class); + } + + @Override + public final ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { + return evaluate(key, Integer.class); + } + + @Override + public final ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { + return evaluate(key, Double.class); + } + + @Override + public final ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) { + return evaluate(key, Value.class); + } + + // ------------------------------------------------------------------------- + // Internal helpers + // ------------------------------------------------------------------------- + + protected ProviderEvaluation evaluate(String key, Class targetType) { + String rawValue = fetchWithCache(key); + T value = FlagValueConverter.convert(rawValue, targetType); + return ProviderEvaluation.builder() + .value(value) + .reason(Reason.STATIC.toString()) + .build(); + } + + protected String fetchWithCache(String key) { + String name = buildName(key); + Optional cached = cache.get(name); + if (cached.isPresent()) { + log.debug("Fetching from cache name '{}'", key); + return cached.get(); + } + synchronized (this) { + 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); +} diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java index 808630b092..4e64c6a783 100644 --- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java @@ -45,14 +45,10 @@ * }
*/ @Slf4j -public class GcpParameterManagerProvider implements FeatureProvider { +public class GcpParameterManagerProvider extends AbstractGcpProvider { static final String PROVIDER_NAME = "GCP Parameter Manager Provider"; - private final GcpProviderOptions options; - private ParameterManagerClient client; - private FlagCache cache; - /** * Creates a new provider using the given options. The GCP client is created lazily * during {@link #initialize(EvaluationContext)}. @@ -60,122 +56,50 @@ public class GcpParameterManagerProvider implements FeatureProvider { * @param options provider configuration; must not be null */ public GcpParameterManagerProvider(GcpProviderOptions options) { - this.options = options; + super(options); } /** * Package-private constructor allowing injection of a pre-built client for testing. */ GcpParameterManagerProvider(GcpProviderOptions options, ParameterManagerClient client) { - this.options = options; - this.client = client; - } - - @Override - public Metadata getMetadata() { - return () -> PROVIDER_NAME; + super(options, client); } @Override - public void initialize(EvaluationContext evaluationContext) throws Exception { - options.validate(); - if (client == null) { - client = ParameterManagerClientFactory.create(options); - } - cache = new FlagCache(options.getCacheExpiry(), options.getCacheMaxSize()); - log.info("GcpParameterManagerProvider initialized for project '{}'", options.getProjectId()); + protected String getProviderName() { + return PROVIDER_NAME; } @Override - public void shutdown() { - if (client != null) { - try { - client.close(); - } catch (Exception e) { - log.warn("Error closing ParameterManagerClient", e); - } - client = null; - } - log.info("GcpParameterManagerProvider shut down"); + protected void createClient() throws Exception { + this.client = ParameterManagerClientFactory.create(options); } @Override - public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { - return evaluate(key, Boolean.class); + protected void closeClient() throws Exception { + this.client.close(); } @Override - public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { - return evaluate(key, String.class); - } - - @Override - public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { - return evaluate(key, Integer.class); + public void initialize(EvaluationContext evaluationContext) throws Exception { + super.initialize(evaluationContext); + log.info("{} initialized via initialize()", getProviderName()); } @Override - public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { - return evaluate(key, Double.class); + public void shutdown() { + super.shutdown(); + log.info("{} shutdown via shutdown()", getProviderName()); } @Override - public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) { - return evaluate(key, Value.class); - } - - // ------------------------------------------------------------------------- - // Internal helpers - // ------------------------------------------------------------------------- - - private ProviderEvaluation evaluate(String key, Class targetType) { - String rawValue = fetchWithCache(key); - T value = FlagValueConverter.convert(rawValue, targetType); - return ProviderEvaluation.builder() - .value(value) - .reason(Reason.STATIC.toString()) - .build(); - } - - private String fetchWithCache(String key) { - String paramName = buildParameterName(key); - Optional cached = cache.get(paramName); - if (cached.isPresent()) { - log.debug("Fetching from cache parameter '{}'", key); - return cached.get(); - } - synchronized (this) { - return cache.get(paramName).orElseGet(() -> { - String value = fetchFromGcp(paramName); - cache.put(paramName, value); - return value; - }); - } - - } - - /** - * Applies the configured prefix (if any) and returns the GCP parameter name for the flag. - */ - private String buildParameterName(String flagKey) { - String prefix = options.getNamePrefix(); - return (prefix != null && !prefix.isEmpty()) ? prefix + flagKey : flagKey; - } - - /** - * Fetches the latest version of the named parameter from GCP Parameter Manager. - * - * @param parameterName the GCP parameter name (without project/location path) - * @return the rendered string value of the parameter - * @throws FlagNotFoundError when the parameter does not exist - * @throws GeneralError for any other GCP API error - */ - private String fetchFromGcp(String parameterName) { + protected String fetchFromGcp(String parameterName) { try { log.info("Fetching parameter from GCP name '{}'", parameterName); ParameterVersionName versionName = ParameterVersionName.of( options.getProjectId(), options.getLocationId(), parameterName, options.getVersion()); - log.info("Fetching parameter from GCP version {}", parameterName, versionName); + log.info("Fetching parameter from GCP version {}", parameterName); RenderParameterVersionResponse response = client.renderParameterVersion(versionName); return response.getRenderedPayload().toStringUtf8(); } catch (NotFoundException e) { diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProvider.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProvider.java index ccc1f4905a..39526fef31 100644 --- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProvider.java +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProvider.java @@ -44,14 +44,10 @@ * }
*/ @Slf4j -public class GcpSecretManagerProvider implements FeatureProvider { +public class GcpSecretManagerProvider extends AbstractGcpProvider { static final String PROVIDER_NAME = "GCP Secret Manager Provider"; - private final GcpProviderOptions options; - private SecretManagerServiceClient client; - private FlagCache cache; - /** * Creates a new provider using the given options. The GCP client is created lazily * during {@link #initialize(EvaluationContext)}. @@ -59,115 +55,45 @@ public class GcpSecretManagerProvider implements FeatureProvider { * @param options provider configuration; must not be null */ public GcpSecretManagerProvider(GcpProviderOptions options) { - this.options = options; + super(options); } /** * Package-private constructor allowing injection of a pre-built client for testing. */ GcpSecretManagerProvider(GcpProviderOptions options, SecretManagerServiceClient client) { - this.options = options; - this.client = client; - } - - @Override - public Metadata getMetadata() { - return () -> PROVIDER_NAME; + super(options, client); } @Override - public void initialize(EvaluationContext evaluationContext) throws Exception { - options.validate(); - if (client == null) { - client = SecretManagerClientFactory.create(options); - } - cache = new FlagCache(options.getCacheExpiry(), options.getCacheMaxSize()); - log.info("GcpSecretManagerProvider initialized for project '{}'", options.getProjectId()); + protected String getProviderName() { + return PROVIDER_NAME; } @Override - public void shutdown() { - if (client != null) { - try { - client.close(); - } catch (Exception e) { - log.warn("Error closing SecretManagerServiceClient", e); - } - client = null; - } - log.info("GcpSecretManagerProvider shut down"); + protected void createClient() throws Exception { + this.client = SecretManagerClientFactory.create(options); } @Override - public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { - return evaluate(key, Boolean.class); + protected void closeClient() throws Exception { + this.client.close(); } @Override - public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { - return evaluate(key, String.class); - } - - @Override - public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { - return evaluate(key, Integer.class); + public void initialize(EvaluationContext evaluationContext) throws Exception { + super.initialize(evaluationContext); + log.info("{} initialized via initialize()", getProviderName()); } @Override - public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { - return evaluate(key, Double.class); + public void shutdown() { + super.shutdown(); + log.info("{} shutdown via shutdown()", getProviderName()); } @Override - public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) { - return evaluate(key, Value.class); - } - - // ------------------------------------------------------------------------- - // Internal helpers - // ------------------------------------------------------------------------- - - private ProviderEvaluation evaluate(String key, Class targetType) { - String rawValue = fetchWithCache(key); - T value = FlagValueConverter.convert(rawValue, targetType); - return ProviderEvaluation.builder() - .value(value) - .reason(Reason.STATIC.toString()) - .build(); - } - - private String fetchWithCache(String key) { - String secretName = buildSecretName(key); - Optional 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; - }); - } - } - - /** - * Applies the configured prefix (if any) and returns the GCP secret name for the flag. - */ - private String buildSecretName(String flagKey) { - String prefix = options.getNamePrefix(); - return (prefix != null && !prefix.isEmpty()) ? prefix + flagKey : flagKey; - } - - /** - * Accesses the configured version of the named secret from GCP Secret Manager. - * - * @param secretName the GCP secret name (without project path) - * @return the UTF-8 string value of the secret payload - * @throws FlagNotFoundError when the secret does not exist - * @throws GeneralError for any other GCP API error - */ - private String fetchFromGcp(String secretName) { + protected String fetchFromGcp(String secretName) { try { SecretVersionName versionName = SecretVersionName.of(options.getProjectId(), secretName, options.getVersion()); diff --git a/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProviderTest.java b/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProviderTest.java new file mode 100644 index 0000000000..86802838d6 --- /dev/null +++ b/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProviderTest.java @@ -0,0 +1,229 @@ +package dev.openfeature.contrib.providers.gcp; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import dev.openfeature.sdk.ImmutableContext; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.Reason; +import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.exceptions.FlagNotFoundError; +import dev.openfeature.sdk.exceptions.GeneralError; +import dev.openfeature.sdk.exceptions.ParseError; +import dev.openfeature.sdk.FeatureProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("GCP provider shared behavior") +abstract class AbstractGcpProviderTest { + + protected GcpProviderOptions options; + protected FeatureProvider provider; + + protected abstract FeatureProvider createProvider(GcpProviderOptions options); + protected abstract FeatureProvider createProvider(GcpProviderOptions options, Object client); + protected abstract void stubFetchSuccess(String value); + protected abstract void stubFetchNotFound(); + protected abstract void stubFetchError(String message); + protected abstract void verifyFetchCalled(int times); + protected abstract void verifyClientClosed(int times); + protected abstract String getProviderName(); + protected abstract GcpProviderOptions.GcpProviderOptionsBuilder newOptionsBuilder(); + + @BeforeEach + void setUp() throws Exception { + options = newOptionsBuilder().build(); + provider = createProvider(options); + provider.initialize(new ImmutableContext()); + } + + @Nested + @DisplayName("Metadata") + class MetadataTests { + + @Test + @DisplayName("returns the correct provider name") + void providerName() { + assertThat(provider.getMetadata().getName()).isEqualTo(getProviderName()); + } + } + + @Nested + @DisplayName("Initialization") + class InitializationTests { + + @Test + @DisplayName("throws IllegalArgumentException when projectId is blank") + void blankProjectIdThrows() { + GcpProviderOptions badOpts = newOptionsBuilder().projectId("").build(); + FeatureProvider badProvider = createProvider(badOpts); + assertThatThrownBy(() -> badProvider.initialize(new ImmutableContext())) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("throws IllegalArgumentException when projectId is null") + void nullProjectIdThrows() { + GcpProviderOptions badOpts = newOptionsBuilder().projectId(null).build(); + FeatureProvider badProvider = createProvider(badOpts); + assertThatThrownBy(() -> badProvider.initialize(new ImmutableContext())) + .isInstanceOf(IllegalArgumentException.class); + } + } + + @Nested + @DisplayName("Boolean evaluation") + class BooleanEvaluation { + + @Test + @DisplayName("returns true for value 'true'") + void trueValue() { + stubFetchSuccess("true"); + ProviderEvaluation result = provider.getBooleanEvaluation("bool-flag", false, new ImmutableContext()); + assertThat(result.getValue()).isTrue(); + assertThat(result.getReason()).isEqualTo(Reason.STATIC.toString()); + } + + @Test + @DisplayName("returns false for value 'false'") + void falseValue() { + stubFetchSuccess("false"); + ProviderEvaluation result = provider.getBooleanEvaluation("bool-flag", true, new ImmutableContext()); + assertThat(result.getValue()).isFalse(); + } + + @Test + @DisplayName("throws ParseError for malformed boolean value") + void malformedBooleanThrows() { + stubFetchSuccess("not-a-bool"); + assertThatThrownBy(() -> provider.getBooleanEvaluation("bool-flag", false, new ImmutableContext())) + .isInstanceOf(ParseError.class); + } + } + + @Nested + @DisplayName("String evaluation") + class StringEvaluation { + + @Test + @DisplayName("returns string value as-is") + void stringValue() { + stubFetchSuccess("dark-mode"); + ProviderEvaluation result = provider.getStringEvaluation("str-flag", "light-mode", new ImmutableContext()); + assertThat(result.getValue()).isEqualTo("dark-mode"); + } + } + + @Nested + @DisplayName("Integer evaluation") + class IntegerEvaluation { + + @Test + @DisplayName("parses numeric string to Integer") + void integerValue() { + stubFetchSuccess("42"); + ProviderEvaluation result = provider.getIntegerEvaluation("int-flag", 0, new ImmutableContext()); + assertThat(result.getValue()).isEqualTo(42); + } + + @Test + @DisplayName("throws ParseError for non-numeric value") + void nonNumericThrows() { + stubFetchSuccess("abc"); + assertThatThrownBy(() -> provider.getIntegerEvaluation("int-flag", 0, new ImmutableContext())) + .isInstanceOf(ParseError.class); + } + } + + @Nested + @DisplayName("Double evaluation") + class DoubleEvaluation { + + @Test + @DisplayName("parses numeric string to Double") + void doubleValue() { + stubFetchSuccess("3.14"); + ProviderEvaluation result = provider.getDoubleEvaluation("double-flag", 0.0, new ImmutableContext()); + assertThat(result.getValue()).isEqualTo(3.14); + } + } + + @Nested + @DisplayName("Object evaluation") + class ObjectEvaluation { + + @Test + @DisplayName("parses JSON string to Value/Structure") + void jsonValue() { + stubFetchSuccess("{\"color\":\"blue\",\"count\":3}"); + ProviderEvaluation result = provider.getObjectEvaluation("obj-flag", new Value(), new ImmutableContext()); + assertThat(result.getValue().asStructure()).isNotNull(); + assertThat(result.getValue().asStructure().getValue("color").asString()).isEqualTo("blue"); + } + } + + @Nested + @DisplayName("Error handling") + class ErrorHandling { + + @Test + @DisplayName("throws FlagNotFoundError when value does not exist") + void flagNotFound() { + stubFetchNotFound(); + assertThatThrownBy(() -> provider.getBooleanEvaluation("missing-flag", false, new ImmutableContext())) + .isInstanceOf(FlagNotFoundError.class); + } + + @Test + @DisplayName("throws GeneralError on unexpected API exception") + void apiError() { + stubFetchError("Connection refused"); + assertThatThrownBy(() -> provider.getBooleanEvaluation("flag", false, new ImmutableContext())) + .isInstanceOf(GeneralError.class); + } + } + + @Nested + @DisplayName("Caching") + class CachingTests { + + @Test + @DisplayName("cache hit: API called only once for repeated evaluations") + void cacheHit() { + stubFetchSuccess("true"); + provider.getBooleanEvaluation("cached-flag", false, new ImmutableContext()); + provider.getBooleanEvaluation("cached-flag", false, new ImmutableContext()); + verifyFetchCalled(1); + } + } + + @Nested + @DisplayName("Name prefix") + class PrefixTests { + + @Test + @DisplayName("prefix is prepended to the flag key when building the resource name") + void prefixApplied() throws Exception { + GcpProviderOptions prefixedOpts = newOptionsBuilder().namePrefix("ff-").build(); + FeatureProvider prefixedProvider = createProvider(prefixedOpts); + stubFetchSuccess("true"); + prefixedProvider.initialize(new ImmutableContext()); + ProviderEvaluation result = prefixedProvider.getBooleanEvaluation("my-flag", false, new ImmutableContext()); + assertThat(result.getValue()).isTrue(); + } + } + + @Nested + @DisplayName("Lifecycle") + class LifecycleTests { + + @Test + @DisplayName("shutdown() closes the client") + void shutdownClosesClient() { + provider.shutdown(); + verifyClientClosed(1); + } + } +} diff --git a/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderTest.java b/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderTest.java index 9c2d96bee2..f59009eea3 100644 --- a/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderTest.java +++ b/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderTest.java @@ -1,7 +1,5 @@ package dev.openfeature.contrib.providers.gcp; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -12,258 +10,66 @@ import com.google.cloud.parametermanager.v1.ParameterVersionName; import com.google.cloud.parametermanager.v1.RenderParameterVersionResponse; import com.google.protobuf.ByteString; -import dev.openfeature.sdk.ImmutableContext; -import dev.openfeature.sdk.ProviderEvaluation; -import dev.openfeature.sdk.Reason; -import dev.openfeature.sdk.Value; -import dev.openfeature.sdk.exceptions.FlagNotFoundError; -import dev.openfeature.sdk.exceptions.GeneralError; -import dev.openfeature.sdk.exceptions.ParseError; -import org.junit.jupiter.api.BeforeEach; +import dev.openfeature.sdk.FeatureProvider; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @DisplayName("GcpParameterManagerProvider") @ExtendWith(MockitoExtension.class) -class GcpParameterManagerProviderTest { +class GcpParameterManagerProviderTest extends AbstractGcpProviderTest { @Mock private ParameterManagerClient mockClient; - private GcpProviderOptions options; - private GcpParameterManagerProvider provider; + @Override + protected FeatureProvider createProvider(GcpProviderOptions options) { + return new GcpParameterManagerProvider(options, mockClient); + } - @BeforeEach - void setUp() throws Exception { - options = GcpProviderOptions.builder() - .projectId("test-project") - .locationId("europe-west1") - .build(); - provider = new GcpParameterManagerProvider(options, mockClient); - provider.initialize(new ImmutableContext()); + @Override + protected FeatureProvider createProvider(GcpProviderOptions options, Object client) { + return new GcpParameterManagerProvider(options, (ParameterManagerClient) client); } - private void stubParameter(String value) { - // Build a mock response + @Override + protected void stubFetchSuccess(String value) { RenderParameterVersionResponse response = RenderParameterVersionResponse.newBuilder() .setRenderedPayload(ByteString.copyFromUtf8(value)) .build(); when(mockClient.renderParameterVersion(any(ParameterVersionName.class))).thenReturn(response); - } - private void stubParameterNotFound() { + @Override + protected void stubFetchNotFound() { when(mockClient.renderParameterVersion(any(ParameterVersionName.class))).thenThrow(NotFoundException.class); } - private void stubParameterError(String message) { + @Override + protected void stubFetchError(String message) { when(mockClient.renderParameterVersion(any(ParameterVersionName.class))).thenThrow(new RuntimeException(message)); } - @Nested - @DisplayName("Metadata") - class MetadataTests { - - @Test - @DisplayName("returns the correct provider name") - void providerName() { - assertThat(provider.getMetadata().getName()).isEqualTo(GcpParameterManagerProvider.PROVIDER_NAME); - } - } - - @Nested - @DisplayName("Initialization") - class InitializationTests { - - @Test - @DisplayName("throws IllegalArgumentException when projectId is blank") - void blankProjectIdThrows() { - GcpProviderOptions badOpts = - GcpProviderOptions.builder().projectId("").build(); - GcpParameterManagerProvider badProvider = new GcpParameterManagerProvider(badOpts, mockClient); - assertThatThrownBy(() -> badProvider.initialize(new ImmutableContext())) - .isInstanceOf(IllegalArgumentException.class); - } - - @Test - @DisplayName("throws IllegalArgumentException when projectId is null") - void nullProjectIdThrows() { - GcpProviderOptions badOpts = GcpProviderOptions.builder().build(); - GcpParameterManagerProvider badProvider = new GcpParameterManagerProvider(badOpts, mockClient); - assertThatThrownBy(() -> badProvider.initialize(new ImmutableContext())) - .isInstanceOf(IllegalArgumentException.class); - } - } - - @Nested - @DisplayName("Boolean evaluation") - class BooleanEvaluation { - - @Test - @DisplayName("returns true for value 'true'") - void trueValue() { - stubParameter("true"); - ProviderEvaluation result = - provider.getBooleanEvaluation("bool-flag", false, new ImmutableContext()); - assertThat(result.getValue()).isTrue(); - assertThat(result.getReason()).isEqualTo(Reason.STATIC.toString()); - } - - @Test - @DisplayName("returns false for value 'false'") - void falseValue() { - stubParameter("false"); - ProviderEvaluation result = - provider.getBooleanEvaluation("bool-flag", true, new ImmutableContext()); - assertThat(result.getValue()).isFalse(); - } - - @Test - @DisplayName("throws ParseError for malformed boolean value") - void malformedBooleanThrows() { - stubParameter("not-a-bool"); - assertThatThrownBy(() -> provider.getBooleanEvaluation("bool-flag", false, new ImmutableContext())) - .isInstanceOf(ParseError.class); - } - } - - @Nested - @DisplayName("String evaluation") - class StringEvaluation { - - @Test - @DisplayName("returns string value as-is") - void stringValue() { - stubParameter("dark-mode"); - ProviderEvaluation result = - provider.getStringEvaluation("str-flag", "light-mode", new ImmutableContext()); - assertThat(result.getValue()).isEqualTo("dark-mode"); - } - } - - @Nested - @DisplayName("Integer evaluation") - class IntegerEvaluation { - - @Test - @DisplayName("parses numeric string to Integer") - void integerValue() { - stubParameter("42"); - ProviderEvaluation result = provider.getIntegerEvaluation("int-flag", 0, new ImmutableContext()); - assertThat(result.getValue()).isEqualTo(42); - } - - @Test - @DisplayName("throws ParseError for non-numeric value") - void nonNumericThrows() { - stubParameter("abc"); - assertThatThrownBy(() -> provider.getIntegerEvaluation("int-flag", 0, new ImmutableContext())) - .isInstanceOf(ParseError.class); - } - } - - @Nested - @DisplayName("Double evaluation") - class DoubleEvaluation { - - @Test - @DisplayName("parses numeric string to Double") - void doubleValue() { - stubParameter("3.14"); - ProviderEvaluation result = - provider.getDoubleEvaluation("double-flag", 0.0, new ImmutableContext()); - assertThat(result.getValue()).isEqualTo(3.14); - } - } - - @Nested - @DisplayName("Object evaluation") - class ObjectEvaluation { - - @Test - @DisplayName("parses JSON string to Value/Structure") - void jsonValue() { - stubParameter("{\"color\":\"blue\",\"count\":3}"); - ProviderEvaluation result = - provider.getObjectEvaluation("obj-flag", new Value(), new ImmutableContext()); - assertThat(result.getValue().asStructure()).isNotNull(); - assertThat(result.getValue().asStructure().getValue("color").asString()) - .isEqualTo("blue"); - } - } - - @Nested - @DisplayName("Error handling") - class ErrorHandling { - - @Test - @DisplayName("throws FlagNotFoundError when parameter does not exist") - void flagNotFound() { - stubParameterNotFound(); - assertThatThrownBy(() -> provider.getBooleanEvaluation("missing-flag", false, new ImmutableContext())) - .isInstanceOf(FlagNotFoundError.class); - } - - @Test - @DisplayName("throws GeneralError on unexpected GCP API exception") - void gcpApiError() { - stubParameterError("Connection refused"); - assertThatThrownBy(() -> provider.getBooleanEvaluation("flag", false, new ImmutableContext())) - .isInstanceOf(GeneralError.class); - } + @Override + protected void verifyFetchCalled(int times) { + verify(mockClient, times(times)).renderParameterVersion(any(ParameterVersionName.class)); } - @Nested - @DisplayName("Caching") - class CachingTests { - - @Test - @DisplayName("cache hit: GCP client called only once for two consecutive evaluations") - void cacheHit() { - stubParameter("true"); - provider.getBooleanEvaluation("cached-flag", false, new ImmutableContext()); - provider.getBooleanEvaluation("cached-flag", false, new ImmutableContext()); - verify(mockClient, times(1)).renderParameterVersion(any(ParameterVersionName.class)); - } + @Override + protected void verifyClientClosed(int times) { + verify(mockClient, times(times)).close(); } - @Nested - @DisplayName("Secret name prefix") - class PrefixTests { - - @Test - @DisplayName("prefix is prepended to the flag key when building parameter name") - void prefixApplied() { - GcpProviderOptions prefixedOpts = GcpProviderOptions.builder() - .projectId("test-project") - .namePrefix("ff-") - .build(); - stubParameter("true"); - GcpParameterManagerProvider prefixedProvider = new GcpParameterManagerProvider(prefixedOpts, mockClient); - try { - prefixedProvider.initialize(new ImmutableContext()); - } catch (Exception e) { - throw new RuntimeException(e); - } - ProviderEvaluation result = - prefixedProvider.getBooleanEvaluation("my-flag", false, new ImmutableContext()); - assertThat(result.getValue()).isTrue(); - } + @Override + protected String getProviderName() { + return GcpParameterManagerProvider.PROVIDER_NAME; } - @Nested - @DisplayName("Lifecycle") - class LifecycleTests { - - @Test - @DisplayName("shutdown() closes the GCP client") - void shutdownClosesClient() { - provider.shutdown(); - verify(mockClient, times(1)).close(); - } + @Override + protected GcpProviderOptions.GcpProviderOptionsBuilder newOptionsBuilder() { + return GcpProviderOptions.builder() + .projectId("test-project") + .locationId("europe-west1"); } } diff --git a/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProviderTest.java b/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProviderTest.java index 5e6618ee3b..b976883f9a 100644 --- a/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProviderTest.java +++ b/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProviderTest.java @@ -1,7 +1,5 @@ package dev.openfeature.contrib.providers.gcp; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -13,39 +11,31 @@ import com.google.cloud.secretmanager.v1.SecretPayload; import com.google.cloud.secretmanager.v1.SecretVersionName; import com.google.protobuf.ByteString; -import dev.openfeature.sdk.ImmutableContext; -import dev.openfeature.sdk.ProviderEvaluation; -import dev.openfeature.sdk.Reason; -import dev.openfeature.sdk.Value; -import dev.openfeature.sdk.exceptions.FlagNotFoundError; -import dev.openfeature.sdk.exceptions.GeneralError; -import dev.openfeature.sdk.exceptions.ParseError; -import org.junit.jupiter.api.BeforeEach; +import dev.openfeature.sdk.FeatureProvider; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @DisplayName("GcpSecretManagerProvider") @ExtendWith(MockitoExtension.class) -class GcpSecretManagerProviderTest { +class GcpSecretManagerProviderTest extends AbstractGcpProviderTest { @Mock private SecretManagerServiceClient mockClient; - private GcpProviderOptions options; - private GcpSecretManagerProvider provider; + @Override + protected FeatureProvider createProvider(GcpProviderOptions options) { + return new GcpSecretManagerProvider(options, mockClient); + } - @BeforeEach - void setUp() throws Exception { - options = GcpProviderOptions.builder().projectId("test-project").build(); - provider = new GcpSecretManagerProvider(options, mockClient); - provider.initialize(new ImmutableContext()); + @Override + protected FeatureProvider createProvider(GcpProviderOptions options, Object client) { + return new GcpSecretManagerProvider(options, (SecretManagerServiceClient) client); } - private void stubSecret(String value) { + @Override + protected void stubFetchSuccess(String value) { AccessSecretVersionResponse response = AccessSecretVersionResponse.newBuilder() .setPayload(SecretPayload.newBuilder() .setData(ByteString.copyFromUtf8(value)) @@ -54,214 +44,33 @@ private void stubSecret(String value) { when(mockClient.accessSecretVersion(any(SecretVersionName.class))).thenReturn(response); } - private void stubSecretNotFound() { + @Override + protected void stubFetchNotFound() { when(mockClient.accessSecretVersion(any(SecretVersionName.class))).thenThrow(NotFoundException.class); } - private void stubSecretError(String message) { + @Override + protected void stubFetchError(String message) { when(mockClient.accessSecretVersion(any(SecretVersionName.class))).thenThrow(new RuntimeException(message)); } - @Nested - @DisplayName("Metadata") - class MetadataTests { - - @Test - @DisplayName("returns the correct provider name") - void providerName() { - assertThat(provider.getMetadata().getName()).isEqualTo(GcpSecretManagerProvider.PROVIDER_NAME); - } - } - - @Nested - @DisplayName("Initialization") - class InitializationTests { - - @Test - @DisplayName("throws IllegalArgumentException when projectId is blank") - void blankProjectIdThrows() { - GcpProviderOptions badOpts = - GcpProviderOptions.builder().projectId("").build(); - GcpSecretManagerProvider badProvider = new GcpSecretManagerProvider(badOpts, mockClient); - assertThatThrownBy(() -> badProvider.initialize(new ImmutableContext())) - .isInstanceOf(IllegalArgumentException.class); - } - - @Test - @DisplayName("throws IllegalArgumentException when projectId is null") - void nullProjectIdThrows() { - GcpProviderOptions badOpts = GcpProviderOptions.builder().build(); - GcpSecretManagerProvider badProvider = new GcpSecretManagerProvider(badOpts, mockClient); - assertThatThrownBy(() -> badProvider.initialize(new ImmutableContext())) - .isInstanceOf(IllegalArgumentException.class); - } - } - - @Nested - @DisplayName("Boolean evaluation") - class BooleanEvaluation { - - @Test - @DisplayName("returns true for secret value 'true'") - void trueValue() { - stubSecret("true"); - ProviderEvaluation result = - provider.getBooleanEvaluation("bool-flag", false, new ImmutableContext()); - assertThat(result.getValue()).isTrue(); - assertThat(result.getReason()).isEqualTo(Reason.STATIC.toString()); - } - - @Test - @DisplayName("returns false for secret value 'false'") - void falseValue() { - stubSecret("false"); - ProviderEvaluation result = - provider.getBooleanEvaluation("bool-flag", true, new ImmutableContext()); - assertThat(result.getValue()).isFalse(); - } - - @Test - @DisplayName("throws ParseError for malformed boolean value") - void malformedBooleanThrows() { - stubSecret("not-a-bool"); - assertThatThrownBy(() -> provider.getBooleanEvaluation("bool-flag", false, new ImmutableContext())) - .isInstanceOf(ParseError.class); - } + @Override + protected void verifyFetchCalled(int times) { + verify(mockClient, times(times)).accessSecretVersion(any(SecretVersionName.class)); } - @Nested - @DisplayName("String evaluation") - class StringEvaluation { - - @Test - @DisplayName("returns string value as-is") - void stringValue() { - stubSecret("dark-mode"); - ProviderEvaluation result = - provider.getStringEvaluation("str-flag", "light-mode", new ImmutableContext()); - assertThat(result.getValue()).isEqualTo("dark-mode"); - } + @Override + protected void verifyClientClosed(int times) { + verify(mockClient, times(times)).close(); } - @Nested - @DisplayName("Integer evaluation") - class IntegerEvaluation { - - @Test - @DisplayName("parses numeric string to Integer") - void integerValue() { - stubSecret("42"); - ProviderEvaluation result = provider.getIntegerEvaluation("int-flag", 0, new ImmutableContext()); - assertThat(result.getValue()).isEqualTo(42); - } - - @Test - @DisplayName("throws ParseError for non-numeric value") - void nonNumericThrows() { - stubSecret("abc"); - assertThatThrownBy(() -> provider.getIntegerEvaluation("int-flag", 0, new ImmutableContext())) - .isInstanceOf(ParseError.class); - } + @Override + protected String getProviderName() { + return GcpSecretManagerProvider.PROVIDER_NAME; } - @Nested - @DisplayName("Double evaluation") - class DoubleEvaluation { - - @Test - @DisplayName("parses numeric string to Double") - void doubleValue() { - stubSecret("3.14"); - ProviderEvaluation result = - provider.getDoubleEvaluation("double-flag", 0.0, new ImmutableContext()); - assertThat(result.getValue()).isEqualTo(3.14); - } - } - - @Nested - @DisplayName("Object evaluation") - class ObjectEvaluation { - - @Test - @DisplayName("parses JSON string to Value/Structure") - void jsonValue() { - stubSecret("{\"color\":\"blue\",\"count\":3}"); - ProviderEvaluation result = - provider.getObjectEvaluation("obj-flag", new Value(), new ImmutableContext()); - assertThat(result.getValue().asStructure()).isNotNull(); - assertThat(result.getValue().asStructure().getValue("color").asString()) - .isEqualTo("blue"); - } - } - - @Nested - @DisplayName("Error handling") - class ErrorHandling { - - @Test - @DisplayName("throws FlagNotFoundError when secret does not exist") - void flagNotFound() { - stubSecretNotFound(); - assertThatThrownBy(() -> provider.getBooleanEvaluation("missing-flag", false, new ImmutableContext())) - .isInstanceOf(FlagNotFoundError.class); - } - - @Test - @DisplayName("throws GeneralError on unexpected GCP API exception") - void gcpApiError() { - stubSecretError("Connection refused"); - assertThatThrownBy(() -> provider.getBooleanEvaluation("flag", false, new ImmutableContext())) - .isInstanceOf(GeneralError.class); - } - } - - @Nested - @DisplayName("Caching") - class CachingTests { - - @Test - @DisplayName("cache hit: GCP client called only once for two consecutive evaluations") - void cacheHit() { - stubSecret("true"); - provider.getBooleanEvaluation("cached-flag", false, new ImmutableContext()); - provider.getBooleanEvaluation("cached-flag", false, new ImmutableContext()); - verify(mockClient, times(1)).accessSecretVersion(any(SecretVersionName.class)); - } - } - - @Nested - @DisplayName("Secret name prefix") - class PrefixTests { - - @Test - @DisplayName("prefix is prepended to the flag key when building secret name") - void prefixApplied() { - GcpProviderOptions prefixedOpts = GcpProviderOptions.builder() - .projectId("test-project") - .namePrefix("ff-") - .build(); - stubSecret("true"); - GcpSecretManagerProvider prefixedProvider = new GcpSecretManagerProvider(prefixedOpts, mockClient); - try { - prefixedProvider.initialize(new ImmutableContext()); - } catch (Exception e) { - throw new RuntimeException(e); - } - ProviderEvaluation result = - prefixedProvider.getBooleanEvaluation("my-flag", false, new ImmutableContext()); - assertThat(result.getValue()).isTrue(); - } - } - - @Nested - @DisplayName("Lifecycle") - class LifecycleTests { - - @Test - @DisplayName("shutdown() closes the GCP client") - void shutdownClosesClient() { - provider.shutdown(); - verify(mockClient, times(1)).close(); - } + @Override + protected GcpProviderOptions.GcpProviderOptionsBuilder newOptionsBuilder() { + return GcpProviderOptions.builder().projectId("test-project"); } } From c3eb60380d9245bcb283bf19427b0a26680676ef Mon Sep 17 00:00:00 2001 From: Mahesh Patil Date: Fri, 5 Jun 2026 18:25:27 +0100 Subject: [PATCH 07/15] Fix spotless Signed-off-by: Mahesh Patil --- .../providers/gcp/AbstractGcpProvider.java | 15 +++++--- .../gcp/GcpParameterManagerProvider.java | 5 --- .../providers/gcp/GcpProviderOptions.java | 1 - .../gcp/GcpSecretManagerProvider.java | 4 --- .../gcp/AbstractGcpProviderTest.java | 34 ++++++++++++++----- .../gcp/GcpParameterManagerProviderTest.java | 7 ++-- 6 files changed, 39 insertions(+), 27 deletions(-) diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProvider.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProvider.java index 88a099054c..543110306a 100644 --- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProvider.java +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProvider.java @@ -54,22 +54,26 @@ public void shutdown() { } @Override - public final ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { + public final ProviderEvaluation getBooleanEvaluation( + String key, Boolean defaultValue, EvaluationContext ctx) { return evaluate(key, Boolean.class); } @Override - public final ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { + public final ProviderEvaluation getStringEvaluation( + String key, String defaultValue, EvaluationContext ctx) { return evaluate(key, String.class); } @Override - public final ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { + public final ProviderEvaluation getIntegerEvaluation( + String key, Integer defaultValue, EvaluationContext ctx) { return evaluate(key, Integer.class); } @Override - public final ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { + public final ProviderEvaluation getDoubleEvaluation( + String key, Double defaultValue, EvaluationContext ctx) { return evaluate(key, Double.class); } @@ -114,7 +118,10 @@ protected String buildName(String 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); } diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java index 4e64c6a783..3e05a33edf 100644 --- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java @@ -1,16 +1,11 @@ package dev.openfeature.contrib.providers.gcp; -import java.util.Optional; - 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.Metadata; -import dev.openfeature.sdk.ProviderEvaluation; -import dev.openfeature.sdk.Reason; import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.FlagNotFoundError; import dev.openfeature.sdk.exceptions.GeneralError; diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpProviderOptions.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpProviderOptions.java index cdb68e9248..2907a46654 100644 --- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpProviderOptions.java +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpProviderOptions.java @@ -42,7 +42,6 @@ public class GcpProviderOptions { @Builder.Default private final String version = "latest"; - /** * Optional location required for ParameterManager, ignored by SecretManager */ diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProvider.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProvider.java index 39526fef31..ce04e36d0f 100644 --- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProvider.java +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProvider.java @@ -6,13 +6,9 @@ import com.google.cloud.secretmanager.v1.SecretVersionName; 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 dev.openfeature.sdk.exceptions.FlagNotFoundError; import dev.openfeature.sdk.exceptions.GeneralError; -import java.util.Optional; import lombok.extern.slf4j.Slf4j; /** diff --git a/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProviderTest.java b/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProviderTest.java index 86802838d6..0691e1b41a 100644 --- a/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProviderTest.java +++ b/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProviderTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import dev.openfeature.sdk.FeatureProvider; import dev.openfeature.sdk.ImmutableContext; import dev.openfeature.sdk.ProviderEvaluation; import dev.openfeature.sdk.Reason; @@ -10,7 +11,6 @@ import dev.openfeature.sdk.exceptions.FlagNotFoundError; import dev.openfeature.sdk.exceptions.GeneralError; import dev.openfeature.sdk.exceptions.ParseError; -import dev.openfeature.sdk.FeatureProvider; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -23,13 +23,21 @@ abstract class AbstractGcpProviderTest { protected FeatureProvider provider; protected abstract FeatureProvider createProvider(GcpProviderOptions options); + protected abstract FeatureProvider createProvider(GcpProviderOptions options, Object client); + protected abstract void stubFetchSuccess(String value); + protected abstract void stubFetchNotFound(); + protected abstract void stubFetchError(String message); + protected abstract void verifyFetchCalled(int times); + protected abstract void verifyClientClosed(int times); + protected abstract String getProviderName(); + protected abstract GcpProviderOptions.GcpProviderOptionsBuilder newOptionsBuilder(); @BeforeEach @@ -81,7 +89,8 @@ class BooleanEvaluation { @DisplayName("returns true for value 'true'") void trueValue() { stubFetchSuccess("true"); - ProviderEvaluation result = provider.getBooleanEvaluation("bool-flag", false, new ImmutableContext()); + ProviderEvaluation result = + provider.getBooleanEvaluation("bool-flag", false, new ImmutableContext()); assertThat(result.getValue()).isTrue(); assertThat(result.getReason()).isEqualTo(Reason.STATIC.toString()); } @@ -90,7 +99,8 @@ void trueValue() { @DisplayName("returns false for value 'false'") void falseValue() { stubFetchSuccess("false"); - ProviderEvaluation result = provider.getBooleanEvaluation("bool-flag", true, new ImmutableContext()); + ProviderEvaluation result = + provider.getBooleanEvaluation("bool-flag", true, new ImmutableContext()); assertThat(result.getValue()).isFalse(); } @@ -111,7 +121,8 @@ class StringEvaluation { @DisplayName("returns string value as-is") void stringValue() { stubFetchSuccess("dark-mode"); - ProviderEvaluation result = provider.getStringEvaluation("str-flag", "light-mode", new ImmutableContext()); + ProviderEvaluation result = + provider.getStringEvaluation("str-flag", "light-mode", new ImmutableContext()); assertThat(result.getValue()).isEqualTo("dark-mode"); } } @@ -145,7 +156,8 @@ class DoubleEvaluation { @DisplayName("parses numeric string to Double") void doubleValue() { stubFetchSuccess("3.14"); - ProviderEvaluation result = provider.getDoubleEvaluation("double-flag", 0.0, new ImmutableContext()); + ProviderEvaluation result = + provider.getDoubleEvaluation("double-flag", 0.0, new ImmutableContext()); assertThat(result.getValue()).isEqualTo(3.14); } } @@ -158,9 +170,11 @@ class ObjectEvaluation { @DisplayName("parses JSON string to Value/Structure") void jsonValue() { stubFetchSuccess("{\"color\":\"blue\",\"count\":3}"); - ProviderEvaluation result = provider.getObjectEvaluation("obj-flag", new Value(), new ImmutableContext()); + ProviderEvaluation result = + provider.getObjectEvaluation("obj-flag", new Value(), new ImmutableContext()); assertThat(result.getValue().asStructure()).isNotNull(); - assertThat(result.getValue().asStructure().getValue("color").asString()).isEqualTo("blue"); + assertThat(result.getValue().asStructure().getValue("color").asString()) + .isEqualTo("blue"); } } @@ -206,11 +220,13 @@ class PrefixTests { @Test @DisplayName("prefix is prepended to the flag key when building the resource name") void prefixApplied() throws Exception { - GcpProviderOptions prefixedOpts = newOptionsBuilder().namePrefix("ff-").build(); + GcpProviderOptions prefixedOpts = + newOptionsBuilder().namePrefix("ff-").build(); FeatureProvider prefixedProvider = createProvider(prefixedOpts); stubFetchSuccess("true"); prefixedProvider.initialize(new ImmutableContext()); - ProviderEvaluation result = prefixedProvider.getBooleanEvaluation("my-flag", false, new ImmutableContext()); + ProviderEvaluation result = + prefixedProvider.getBooleanEvaluation("my-flag", false, new ImmutableContext()); assertThat(result.getValue()).isTrue(); } } diff --git a/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderTest.java b/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderTest.java index f59009eea3..4716fcc02a 100644 --- a/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderTest.java +++ b/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderTest.java @@ -48,7 +48,8 @@ protected void stubFetchNotFound() { @Override protected void stubFetchError(String message) { - when(mockClient.renderParameterVersion(any(ParameterVersionName.class))).thenThrow(new RuntimeException(message)); + when(mockClient.renderParameterVersion(any(ParameterVersionName.class))) + .thenThrow(new RuntimeException(message)); } @Override @@ -68,8 +69,6 @@ protected String getProviderName() { @Override protected GcpProviderOptions.GcpProviderOptionsBuilder newOptionsBuilder() { - return GcpProviderOptions.builder() - .projectId("test-project") - .locationId("europe-west1"); + return GcpProviderOptions.builder().projectId("test-project").locationId("europe-west1"); } } From 28e9364f5f3b88adfc8980ce229c63fe5215ef1a Mon Sep 17 00:00:00 2001 From: Mahesh Patil Date: Fri, 5 Jun 2026 19:19:20 +0100 Subject: [PATCH 08/15] Update gcp samples to include parameter manager Signed-off-by: Mahesh Patil --- samples/gcp/README.md | 37 ++++++-- samples/gcp/pom.xml | 6 +- samples/gcp/setup.sh | 91 +++++++++++++------ .../samples/gcp/AbstractGcpSampleApp.java | 68 ++++++++++++++ .../gcp/ParameterManagerSampleApp.java | 51 +++++++++++ .../samples/gcp/SecretManagerSampleApp.java | 74 +-------------- samples/gcp/teardown.sh | 44 +++++++-- 7 files changed, 253 insertions(+), 118 deletions(-) create mode 100644 samples/gcp/src/main/java/dev/openfeature/contrib/samples/gcp/AbstractGcpSampleApp.java create mode 100644 samples/gcp/src/main/java/dev/openfeature/contrib/samples/gcp/ParameterManagerSampleApp.java diff --git a/samples/gcp/README.md b/samples/gcp/README.md index d27981e465..52426413ad 100644 --- a/samples/gcp/README.md +++ b/samples/gcp/README.md @@ -1,9 +1,10 @@ # GCP — OpenFeature Sample -A runnable Java application demonstrating the [GCP Secret Manager OpenFeature provider](../../providers/gcp). +A runnable Java application demonstrating the [GCP Secret Manager](../../providers/gcp) and +[GCP Parameter Manager](../../providers/gcp) OpenFeature providers. -It evaluates five feature flags (covering every supported type) that are stored as secrets in -Google Cloud Secret Manager. +It evaluates five feature flags (covering every supported type) that are stored with the +`of-sample-` prefix in either Google Cloud Secret Manager or Google Cloud Parameter Manager. ## Feature Flags Used @@ -57,26 +58,30 @@ mvn install -DskipTests -P '!deploy' This installs the provider JAR to your local Maven repository (`~/.m2`). -## Step 4 — Create the feature-flag secrets +## Step 4 — Create the feature-flag secrets or parameters ```bash cd samples/gcp -bash setup.sh +bash setup.sh # Creates secrets (default: Secret Manager) +# OR +bash setup.sh parameter-manager # Creates parameters in Parameter Manager ``` You should see output like: ``` -Creating sample feature-flag secrets in project: my-gcp-project +Creating sample feature-flag secret-manager in project: my-gcp-project [CREATED] of-sample-dark-mode [VERSION] of-sample-dark-mode → true [CREATED] of-sample-banner-text ... -✓ All secrets created successfully. +✓ All secret-manager entries created successfully. ``` ## Step 5 — Run the sample +By default the module runs the Secret Manager sample: + ```bash mvn exec:java ``` @@ -87,6 +92,15 @@ The app reads `GCP_PROJECT_ID` from the environment. You can also pass it explic mvn exec:java -DGCP_PROJECT_ID=my-gcp-project ``` +### Run the Parameter Manager sample + +```bash +mvn exec:java -Dexec.mainClass=dev.openfeature.contrib.samples.gcp.ParameterManagerSampleApp +``` + +This uses the same sample flag names and prefix. If you want to evaluate with Parameter Manager, +create the sample parameters in your project under `of-sample-`. + ### Expected output ``` @@ -124,7 +138,9 @@ Express checkout : true ## Step 6 — Clean up ```bash -bash teardown.sh +bash teardown.sh # Deletes secrets (default) +# OR +bash teardown.sh parameter-manager # Deletes Parameter Manager parameters ``` --- @@ -147,8 +163,9 @@ Re-run the sample to see the new value (cache expires after 30 seconds in this s | Error | Cause | Fix | |---|---|---| | `GCP_PROJECT_ID is not set` | Env var missing | `export GCP_PROJECT_ID=my-project` | -| `FlagNotFoundError` | Secret doesn't exist | Run `setup.sh` first | -| `PERMISSION_DENIED` | Missing IAM role | Grant `roles/secretmanager.secretAccessor` | +| `FlagNotFoundError` | Secret/parameter doesn't exist | Run `setup.sh` first (or `setup.sh parameter-manager`) | +| `PERMISSION_DENIED` | Missing IAM role | Grant `roles/secretmanager.secretAccessor` or `roles/secretmanager.parameterAccessor` | | `UNAUTHENTICATED` | No credentials | Run `gcloud auth application-default login` | | `secretmanager.googleapis.com is not enabled` | API disabled | Run Step 1 | | `Could not find artifact ...gcp` | Provider not installed | Run Step 3 | +| `Invalid choice: 'parameter-manager'` | gcloud alpha components missing | Run `gcloud components install alpha` | diff --git a/samples/gcp/pom.xml b/samples/gcp/pom.xml index 664ceef427..9a79bccfbd 100644 --- a/samples/gcp/pom.xml +++ b/samples/gcp/pom.xml @@ -23,6 +23,7 @@ ${java.version} ${java.version} UTF-8 + dev.openfeature.contrib.samples.gcp.SecretManagerSampleApp ${env.GCP_PROJECT_ID}
@@ -32,7 +33,7 @@ dev.openfeature.contrib.providers gcp - 0.0.1 + 0.0.2 @@ -57,8 +58,7 @@ exec-maven-plugin 3.5.0 - dev.openfeature.contrib.samples.gcpsecretmanager.SecretManagerSampleApp + ${exec.mainClass} GCP_PROJECT_ID diff --git a/samples/gcp/setup.sh b/samples/gcp/setup.sh index f66e9b0e6c..035f46a8a1 100644 --- a/samples/gcp/setup.sh +++ b/samples/gcp/setup.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# setup.sh — Creates the five sample feature-flag secrets in GCP Secret Manager. +# setup.sh — Creates the five sample feature-flag secrets or parameters in GCP. # # Prerequisites: # - gcloud CLI installed and authenticated (gcloud auth application-default login) @@ -8,63 +8,100 @@ # # Usage: # export GCP_PROJECT_ID=my-gcp-project -# bash setup.sh +# bash setup.sh [secret-manager|parameter-manager] +# # Default: secret-manager set -euo pipefail PROJECT="${GCP_PROJECT_ID:?Please set GCP_PROJECT_ID (e.g. export GCP_PROJECT_ID=my-project)}" +BACKEND="${1:-secret-manager}" -echo "Creating sample feature-flag secrets in project: ${PROJECT}" -echo "All secrets are prefixed with 'of-sample-' to match the sample app." +if [[ "$BACKEND" != "secret-manager" && "$BACKEND" != "parameter-manager" ]]; then + echo "ERROR: Backend must be 'secret-manager' or 'parameter-manager'. Got: $BACKEND" + exit 1 +fi + +echo "Creating sample feature-flag ${BACKEND} in project: ${PROJECT}" +echo "All entries are prefixed with 'of-sample-' to match the sample app." echo "" # ──────────────────────────────────────────────────────────────────────────────── -create_secret() { +create_entry() { local name="$1" local value="$2" local full_name="of-sample-${name}" - # Create the secret resource (idempotent — ignores "already exists") - if gcloud secrets describe "${full_name}" --project="${PROJECT}" &>/dev/null; then - echo " [EXISTS] ${full_name} — adding new version" - else - gcloud secrets create "${full_name}" \ + if [[ "$BACKEND" == "secret-manager" ]]; then + # Create or update Secret Manager secret + if gcloud secrets describe "${full_name}" --project="${PROJECT}" &>/dev/null; then + echo " [EXISTS] ${full_name} — adding new version" + else + gcloud secrets create "${full_name}" \ + --project="${PROJECT}" \ + --replication-policy=automatic \ + --quiet + echo " [CREATED] ${full_name}" + fi + echo -n "${value}" | gcloud secrets versions add "${full_name}" \ --project="${PROJECT}" \ - --replication-policy=automatic \ + --data-file=- \ --quiet - echo " [CREATED] ${full_name}" - fi + echo " [VERSION] ${full_name} → ${value}" + elif [[ "$BACKEND" == "parameter-manager" ]]; then + # Create or update Parameter Manager parameter + if gcloud parametermanager parameters describe "${full_name}" --location=global --project="${PROJECT}" &>/dev/null 2>&1; then + echo " [EXISTS] ${full_name} — updating value" + gcloud parametermanager parameters update "${full_name}" \ + --location=global \ + --project="${PROJECT}" \ + --data="${value}" \ + --quiet || echo " [WARN] Could not update parameter (may require gcloud alpha components)" + else + gcloud parametermanager parameters create "${full_name}" \ + --location=global \ + --project="${PROJECT}" \ + --parameter-format=UNFORMATTED \ + --quiet || echo " [WARN] Could not create parameter (may require gcloud alpha components)" + echo " [CREATED] ${full_name}" + + gcloud parametermanager parameters versions create VERSION_ID \ + --parameter="${full_name}" \ + --project="${PROJECT}" \ + --location=global \ + --payload-data="${value}" - # Add a secret version with the flag value - echo -n "${value}" | gcloud secrets versions add "${full_name}" \ - --project="${PROJECT}" \ - --data-file=- \ - --quiet - echo " [VERSION] ${full_name} → ${value}" + + fi + echo " [SET] ${full_name} → ${value}" + fi } # ──────────────────────────────────────────────────────────────────────────────── # Boolean flag: dark UI theme toggle -create_secret "dark-mode" "true" +create_entry "dark-mode" "true" # String flag: hero banner text -create_secret "banner-text" "Welcome! 10% off today only" +create_entry "banner-text" "Welcome! 10% off today only" # Integer flag: maximum items in cart -create_secret "max-cart-items" "25" +create_entry "max-cart-items" "25" # Double flag: discount multiplier (10%) -create_secret "discount-rate" "0.10" +create_entry "discount-rate" "0.10" # Object flag: structured checkout configuration (JSON) -create_secret "checkout-config" '{"paymentMethods":["card","paypal"],"expressCheckout":true,"maxRetries":3}' +create_entry "checkout-config" '{"paymentMethods":["card","paypal"],"expressCheckout":true,"maxRetries":3}' echo "" -echo "✓ All secrets created successfully." +echo "✓ All ${BACKEND} entries created successfully." echo "" echo "Next steps:" echo " 1. Authenticate: gcloud auth application-default login" echo " 2. Run the sample:" echo " cd samples/gcp" -echo " mvn exec:java # GCP_PROJECT_ID must still be set in your shell" -echo " 3. To clean up: bash teardown.sh" +if [[ "$BACKEND" == "secret-manager" ]]; then + echo " mvn exec:java # Uses Secret Manager (default)" +else + echo " mvn exec:java -Dexec.mainClass=dev.openfeature.contrib.samples.gcp.ParameterManagerSampleApp" +fi +echo " 3. To clean up: bash teardown.sh $BACKEND" diff --git a/samples/gcp/src/main/java/dev/openfeature/contrib/samples/gcp/AbstractGcpSampleApp.java b/samples/gcp/src/main/java/dev/openfeature/contrib/samples/gcp/AbstractGcpSampleApp.java new file mode 100644 index 0000000000..435d779528 --- /dev/null +++ b/samples/gcp/src/main/java/dev/openfeature/contrib/samples/gcp/AbstractGcpSampleApp.java @@ -0,0 +1,68 @@ +package dev.openfeature.contrib.samples.gcp; + +import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.MutableContext; +import dev.openfeature.sdk.Value; + +abstract class AbstractGcpSampleApp { + + static final String PREFIX = "of-sample-"; + + static String resolveProjectId(String[] args) { + if (args.length > 0 && !args[0].isBlank()) { + return args[0]; + } + + String fromEnv = System.getenv("GCP_PROJECT_ID"); + if (fromEnv != null && !fromEnv.isBlank()) { + return fromEnv; + } + + String fromProp = System.getProperty("GCP_PROJECT_ID"); + if (fromProp != null && !fromProp.isBlank()) { + return fromProp; + } + + System.err.println("ERROR: GCP_PROJECT_ID is not set."); + System.err.println("Usage: export GCP_PROJECT_ID=my-project && mvn exec:java"); + System.err.println(" or: mvn exec:java -DGCP_PROJECT_ID=my-project"); + System.exit(1); + return null; + } + + static void printHeader(String title) { + System.out.println(); + System.out.println("── " + title + " " + "─".repeat(Math.max(0, 50 - title.length()))); + } + + static void evaluateCommonFlags(Client client, MutableContext ctx) { + printHeader("Boolean Flag » dark-mode"); + boolean darkMode = client.getBooleanValue("dark-mode", false, ctx); + System.out.println("Value : " + darkMode); + System.out.println("Effect : " + (darkMode ? "Dark theme activated" : "Light theme active")); + + printHeader("String Flag » banner-text"); + String bannerText = client.getStringValue("banner-text", "Welcome!", ctx); + System.out.println("Value : " + bannerText); + + printHeader("Integer Flag » max-cart-items"); + int maxCartItems = client.getIntegerValue("max-cart-items", 10, ctx); + System.out.println("Value : " + maxCartItems); + System.out.println("Effect : Cart is capped at " + maxCartItems + " items"); + + printHeader("Double Flag » discount-rate"); + double discountRate = client.getDoubleValue("discount-rate", 0.0, ctx); + System.out.printf("Value : %.2f%n", discountRate); + System.out.printf("Effect : %.0f%% discount applied to cart total%n", discountRate * 100); + + printHeader("Object Flag » checkout-config"); + Value checkoutConfig = client.getObjectValue("checkout-config", new Value(), ctx); + System.out.println("Value : " + checkoutConfig); + if (checkoutConfig.isStructure()) { + Value paymentMethods = checkoutConfig.asStructure().getValue("paymentMethods"); + Value expressCheckout = checkoutConfig.asStructure().getValue("expressCheckout"); + System.out.println("Payment methods : " + paymentMethods); + System.out.println("Express checkout : " + expressCheckout); + } + } +} diff --git a/samples/gcp/src/main/java/dev/openfeature/contrib/samples/gcp/ParameterManagerSampleApp.java b/samples/gcp/src/main/java/dev/openfeature/contrib/samples/gcp/ParameterManagerSampleApp.java new file mode 100644 index 0000000000..43fc1fcccc --- /dev/null +++ b/samples/gcp/src/main/java/dev/openfeature/contrib/samples/gcp/ParameterManagerSampleApp.java @@ -0,0 +1,51 @@ +package dev.openfeature.contrib.samples.gcp; + +import dev.openfeature.contrib.providers.gcp.GcpParameterManagerProvider; +import dev.openfeature.contrib.providers.gcp.GcpProviderOptions; +import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.MutableContext; +import dev.openfeature.sdk.OpenFeatureAPI; +import java.time.Duration; + +/** + * Sample application demonstrating the GCP Parameter Manager OpenFeature provider. + * + *

This app evaluates the same five feature flags as the Secret Manager sample, but reads + * values from GCP Parameter Manager parameters with names prefixed by {@code of-sample-}. + */ +public class ParameterManagerSampleApp extends AbstractGcpSampleApp { + + public static void main(String[] args) throws Exception { + String projectId = resolveProjectId(args); + + System.out.println("======================================================="); + System.out.println(" GCP Parameter Manager — OpenFeature Sample"); + System.out.println("======================================================="); + System.out.println("Project : " + projectId); + System.out.println("Prefix : " + PREFIX); + System.out.println(); + + GcpProviderOptions options = GcpProviderOptions.builder() + .projectId(projectId) + .namePrefix(PREFIX) + .cacheExpiry(Duration.ofSeconds(30)) + .build(); + + GcpParameterManagerProvider provider = new GcpParameterManagerProvider(options); + OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + api.setProviderAndWait(provider); + Client client = api.getClient(); + + MutableContext ctx = new MutableContext(); + ctx.add("userId", "user-42"); + + evaluateCommonFlags(client, ctx); + + System.out.println(); + System.out.println("======================================================="); + System.out.println(" All flags evaluated successfully."); + System.out.println("======================================================="); + + api.shutdown(); + } +} diff --git a/samples/gcp/src/main/java/dev/openfeature/contrib/samples/gcp/SecretManagerSampleApp.java b/samples/gcp/src/main/java/dev/openfeature/contrib/samples/gcp/SecretManagerSampleApp.java index 838ece1d34..d168d7652c 100644 --- a/samples/gcp/src/main/java/dev/openfeature/contrib/samples/gcp/SecretManagerSampleApp.java +++ b/samples/gcp/src/main/java/dev/openfeature/contrib/samples/gcp/SecretManagerSampleApp.java @@ -1,7 +1,7 @@ package dev.openfeature.contrib.samples.gcp; import dev.openfeature.contrib.providers.gcp.GcpSecretManagerProvider; -import dev.openfeature.contrib.providers.gcp.GcpSecretManagerProviderOptions; +import dev.openfeature.contrib.providers.gcp.GcpProviderOptions; import dev.openfeature.sdk.Client; import dev.openfeature.sdk.MutableContext; import dev.openfeature.sdk.OpenFeatureAPI; @@ -26,9 +26,7 @@ * mvn exec:java * */ -public class SecretManagerSampleApp { - - private static final String PREFIX = "of-sample-"; +public class SecretManagerSampleApp extends AbstractGcpSampleApp { public static void main(String[] args) throws Exception { String projectId = resolveProjectId(args); @@ -40,57 +38,22 @@ public static void main(String[] args) throws Exception { System.out.println("Prefix : " + PREFIX); System.out.println(); - // Build provider options - GcpSecretManagerProviderOptions options = GcpSecretManagerProviderOptions.builder() + GcpProviderOptions options = GcpProviderOptions.builder() .projectId(projectId) .namePrefix(PREFIX) // secrets are named "of-sample-" - .secretVersion("latest") + .version("latest") .cacheExpiry(Duration.ofSeconds(30)) .build(); - // Register the provider with OpenFeature GcpSecretManagerProvider provider = new GcpSecretManagerProvider(options); OpenFeatureAPI api = OpenFeatureAPI.getInstance(); api.setProviderAndWait(provider); Client client = api.getClient(); - // Evaluation context (optional — demonstrates passing user context) MutableContext ctx = new MutableContext(); ctx.add("userId", "user-42"); - // ── Boolean flag ──────────────────────────────────────────────────────────── - printHeader("Boolean Flag » dark-mode"); - boolean darkMode = client.getBooleanValue("dark-mode", false, ctx); - System.out.println("Value : " + darkMode); - System.out.println("Effect : " + (darkMode ? "Dark theme activated" : "Light theme active")); - - // ── String flag ───────────────────────────────────────────────────────────── - printHeader("String Flag » banner-text"); - String bannerText = client.getStringValue("banner-text", "Welcome!", ctx); - System.out.println("Value : " + bannerText); - - // ── Integer flag ───────────────────────────────────────────────────────────── - printHeader("Integer Flag » max-cart-items"); - int maxCartItems = client.getIntegerValue("max-cart-items", 10, ctx); - System.out.println("Value : " + maxCartItems); - System.out.println("Effect : Cart is capped at " + maxCartItems + " items"); - - // ── Double flag ────────────────────────────────────────────────────────────── - printHeader("Double Flag » discount-rate"); - double discountRate = client.getDoubleValue("discount-rate", 0.0, ctx); - System.out.printf("Value : %.2f%n", discountRate); - System.out.printf("Effect : %.0f%% discount applied to cart total%n", discountRate * 100); - - // ── Object flag (JSON) ─────────────────────────────────────────────────────── - printHeader("Object Flag » checkout-config"); - Value checkoutConfig = client.getObjectValue("checkout-config", new Value(), ctx); - System.out.println("Value : " + checkoutConfig); - if (checkoutConfig.isStructure()) { - Value paymentMethods = checkoutConfig.asStructure().getValue("paymentMethods"); - Value expressCheckout = checkoutConfig.asStructure().getValue("expressCheckout"); - System.out.println("Payment methods : " + paymentMethods); - System.out.println("Express checkout : " + expressCheckout); - } + evaluateCommonFlags(client, ctx); System.out.println(); System.out.println("======================================================="); @@ -99,31 +62,4 @@ public static void main(String[] args) throws Exception { api.shutdown(); } - - private static String resolveProjectId(String[] args) { - // 1. CLI argument - if (args.length > 0 && !args[0].isBlank()) { - return args[0]; - } - // 2. Environment variable - String fromEnv = System.getenv("GCP_PROJECT_ID"); - if (fromEnv != null && !fromEnv.isBlank()) { - return fromEnv; - } - // 3. System property (set via -DGCP_PROJECT_ID=... or exec plugin config) - String fromProp = System.getProperty("GCP_PROJECT_ID"); - if (fromProp != null && !fromProp.isBlank()) { - return fromProp; - } - System.err.println("ERROR: GCP_PROJECT_ID is not set."); - System.err.println("Usage: export GCP_PROJECT_ID=my-project && mvn exec:java"); - System.err.println(" or: mvn exec:java -DGCP_PROJECT_ID=my-project"); - System.exit(1); - return null; // unreachable - } - - private static void printHeader(String title) { - System.out.println(); - System.out.println("── " + title + " " + "─".repeat(Math.max(0, 50 - title.length()))); - } } diff --git a/samples/gcp/teardown.sh b/samples/gcp/teardown.sh index ec5997e777..8c26efd1cc 100644 --- a/samples/gcp/teardown.sh +++ b/samples/gcp/teardown.sh @@ -1,26 +1,52 @@ #!/usr/bin/env bash -# teardown.sh — Deletes the sample feature-flag secrets from GCP Secret Manager. +# teardown.sh — Deletes the sample feature-flag secrets or parameters from GCP. # # Usage: # export GCP_PROJECT_ID=my-gcp-project -# bash teardown.sh +# bash teardown.sh [secret-manager|parameter-manager] +# # Default: secret-manager set -euo pipefail PROJECT="${GCP_PROJECT_ID:?Please set GCP_PROJECT_ID (e.g. export GCP_PROJECT_ID=my-project)}" +BACKEND="${1:-secret-manager}" -echo "Deleting sample secrets from project: ${PROJECT}" +if [[ "$BACKEND" != "secret-manager" && "$BACKEND" != "parameter-manager" ]]; then + echo "ERROR: Backend must be 'secret-manager' or 'parameter-manager'. Got: $BACKEND" + exit 1 +fi + +echo "Deleting sample ${BACKEND} entries from project: ${PROJECT}" echo "" for name in dark-mode banner-text max-cart-items discount-rate checkout-config; do full_name="of-sample-${name}" - if gcloud secrets describe "${full_name}" --project="${PROJECT}" &>/dev/null; then - gcloud secrets delete "${full_name}" --project="${PROJECT}" --quiet - echo " [DELETED] ${full_name}" - else - echo " [SKIP] ${full_name} (not found)" + if [[ "$BACKEND" == "secret-manager" ]]; then + if gcloud secrets describe "${full_name}" --project="${PROJECT}" &>/dev/null; then + gcloud secrets delete "${full_name}" --project="${PROJECT}" --quiet + echo " [DELETED] ${full_name}" + else + echo " [SKIP] ${full_name} (not found)" + fi + elif [[ "$BACKEND" == "parameter-manager" ]]; then + if gcloud parametermanager parameters describe "${full_name}" --location=global --project="${PROJECT}" &>/dev/null 2>&1; then + for v in $(gcloud parametermanager parameters versions list \ + --parameter="${full_name}" --location=global \ + --format="value(name.basename())"); do + gcloud parametermanager parameters versions delete "$v" \ + --parameter="${full_name}" --location=global --quiet + done && \ + + gcloud parametermanager parameters delete "${full_name}" \ + --location=global \ + --project="${PROJECT}" \ + --quiet || echo " [WARN] Could not delete parameter (may require gcloud alpha components)" + echo " [DELETED] ${full_name}" + else + echo " [SKIP] ${full_name} (not found)" + fi fi done echo "" -echo "✓ Cleanup complete." +echo "✓ ${BACKEND} cleanup complete." From b60bda4a31c44824f6695c36f6cf03747575a8a7 Mon Sep 17 00:00:00 2001 From: Mahesh Patil Date: Sat, 6 Jun 2026 11:36:35 +0100 Subject: [PATCH 09/15] Fix build issues due to use of GcpParameterManagerProviderOptions Signed-off-by: Mahesh Patil --- providers/gcp/README.md | 4 ++-- .../contrib/providers/gcp/GcpParameterManagerProvider.java | 6 +++--- .../providers/gcp/ParameterManagerClientFactory.java | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/providers/gcp/README.md b/providers/gcp/README.md index 8d3531d85f..968ebf19d2 100644 --- a/providers/gcp/README.md +++ b/providers/gcp/README.md @@ -38,10 +38,10 @@ boolean darkMode = OpenFeatureAPI.getInstance().getClient() ### GCP Parameter Manager ```java import dev.openfeature.contrib.providers.gcp.GcpParameterManagerProvider; -import dev.openfeature.contrib.providers.gcp.GcpParameterManagerProviderOptions; +import dev.openfeature.contrib.providers.gcp.GcpProviderOptions; import dev.openfeature.sdk.OpenFeatureAPI; -GcpProviderOptions options = GcpParameterManagerProviderOptions.builder() +GcpProviderOptions options = GcpProviderOptions.builder() .projectId("my-gcp-project") .build(); diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java index 3e05a33edf..a4cb24be7c 100644 --- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java @@ -16,7 +16,7 @@ * *

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 - * {@link GcpParameterManagerProviderOptions#getParameterNamePrefix()}). + * {@link GcpProviderOptions#getParameterNamePrefix()}). * *

Flag values are read as strings and parsed to the requested type. Supported raw value * formats: @@ -29,11 +29,11 @@ * * *

Results are cached in-process for the duration configured in - * {@link GcpParameterManagerProviderOptions#getCacheExpiry()}. + * {@link GcpProviderOptions#getCacheExpiry()}. * *

Example: *

{@code
- * GcpParameterManagerProviderOptions opts = GcpParameterManagerProviderOptions.builder()
+ * GcpProviderOptions opts = GcpProviderOptions.builder()
  *     .projectId("my-gcp-project")
  *     .build();
  * OpenFeatureAPI.getInstance().setProvider(new GcpParameterManagerProvider(opts));
diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/ParameterManagerClientFactory.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/ParameterManagerClientFactory.java
index c71ff2e0ee..9a0b0f3dab 100644
--- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/ParameterManagerClientFactory.java
+++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/ParameterManagerClientFactory.java
@@ -16,7 +16,7 @@ private ParameterManagerClientFactory() {}
     /**
      * Creates a new {@link ParameterManagerClient} using the provided options.
      *
-     * 

When {@link GcpParameterManagerProviderOptions#getCredentials()} is non-null, those + *

When {@link GcpProviderOptions#getCredentials()} is non-null, those * credentials are used explicitly. Otherwise, the GCP client library falls back to * Application Default Credentials (ADC) automatically. * From 02d853b0d0c882a14dab807741b5074aeb40353c Mon Sep 17 00:00:00 2001 From: Mahesh Patil Date: Sat, 6 Jun 2026 11:44:21 +0100 Subject: [PATCH 10/15] Fix review comments1. setup.sh was updating parameter manager instead of creating new versoin2. locationId default to prevent null pointers3. update references to stale GcpParameterManagerOptions Signed-off-by: Mahesh Patil --- .../providers/gcp/GcpParameterManagerProvider.java | 2 -- .../contrib/providers/gcp/GcpProviderOptions.java | 2 +- samples/gcp/setup.sh | 10 +++++----- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java index a4cb24be7c..6b6a69cd6a 100644 --- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java @@ -91,10 +91,8 @@ public void shutdown() { @Override protected String fetchFromGcp(String parameterName) { try { - log.info("Fetching parameter from GCP name '{}'", parameterName); ParameterVersionName versionName = ParameterVersionName.of( options.getProjectId(), options.getLocationId(), parameterName, options.getVersion()); - log.info("Fetching parameter from GCP version {}", parameterName); RenderParameterVersionResponse response = client.renderParameterVersion(versionName); return response.getRenderedPayload().toStringUtf8(); } catch (NotFoundException e) { diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpProviderOptions.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpProviderOptions.java index 2907a46654..3e3f77044a 100644 --- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpProviderOptions.java +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpProviderOptions.java @@ -45,7 +45,7 @@ public class GcpProviderOptions { /** * Optional location required for ParameterManager, ignored by SecretManager */ - private final String locationId; + private final String locationId = "global"; /** * How long a fetched secret value is retained in the in-memory cache before diff --git a/samples/gcp/setup.sh b/samples/gcp/setup.sh index 035f46a8a1..fa4ada8e23 100644 --- a/samples/gcp/setup.sh +++ b/samples/gcp/setup.sh @@ -50,12 +50,12 @@ create_entry() { elif [[ "$BACKEND" == "parameter-manager" ]]; then # Create or update Parameter Manager parameter if gcloud parametermanager parameters describe "${full_name}" --location=global --project="${PROJECT}" &>/dev/null 2>&1; then - echo " [EXISTS] ${full_name} — updating value" - gcloud parametermanager parameters update "${full_name}" \ - --location=global \ + echo " [EXISTS] ${full_name} — create new version" + gcloud parametermanager parameters versions create VERSION_ID \ + --parameter="${full_name}" \ --project="${PROJECT}" \ - --data="${value}" \ - --quiet || echo " [WARN] Could not update parameter (may require gcloud alpha components)" + --location=global \ + --payload-data="${value}" else gcloud parametermanager parameters create "${full_name}" \ --location=global \ From 6e0952c9b6d4890f9ca523af60da9522e33131fe Mon Sep 17 00:00:00 2001 From: Mahesh Patil Date: Sat, 6 Jun 2026 11:57:01 +0100 Subject: [PATCH 11/15] Fix comments risky loop in samples teardown.sh, synch on map instead of this ffor performance reasons Signed-off-by: Mahesh Patil --- .../contrib/providers/gcp/AbstractGcpProvider.java | 2 +- .../java/dev/openfeature/contrib/providers/gcp/FlagCache.java | 1 + .../openfeature/contrib/providers/gcp/GcpProviderOptions.java | 1 + samples/gcp/teardown.sh | 4 ++-- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProvider.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProvider.java index 543110306a..dd059da5c5 100644 --- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProvider.java +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProvider.java @@ -102,7 +102,7 @@ protected String fetchWithCache(String key) { log.debug("Fetching from cache name '{}'", key); return cached.get(); } - synchronized (this) { + synchronized (cache) { return cache.get(name).orElseGet(() -> { String value = fetchFromGcp(name); cache.put(name, value); diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/FlagCache.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/FlagCache.java index cf53d8684f..17de3ec15a 100644 --- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/FlagCache.java +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/FlagCache.java @@ -7,6 +7,7 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; /** * Thread-safe TTL-based in-memory cache for flag values fetched from GCP services. diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpProviderOptions.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpProviderOptions.java index 3e3f77044a..b0baf21831 100644 --- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpProviderOptions.java +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpProviderOptions.java @@ -45,6 +45,7 @@ public class GcpProviderOptions { /** * Optional location required for ParameterManager, ignored by SecretManager */ + @Builder.Default private final String locationId = "global"; /** diff --git a/samples/gcp/teardown.sh b/samples/gcp/teardown.sh index 8c26efd1cc..eaabddae88 100644 --- a/samples/gcp/teardown.sh +++ b/samples/gcp/teardown.sh @@ -32,9 +32,9 @@ for name in dark-mode banner-text max-cart-items discount-rate checkout-config; if gcloud parametermanager parameters describe "${full_name}" --location=global --project="${PROJECT}" &>/dev/null 2>&1; then for v in $(gcloud parametermanager parameters versions list \ --parameter="${full_name}" --location=global \ - --format="value(name.basename())"); do + --format="value(name.basename())" 2>/dev/null || true); do gcloud parametermanager parameters versions delete "$v" \ - --parameter="${full_name}" --location=global --quiet + --parameter="${full_name}" --location=global --quiet || true done && \ gcloud parametermanager parameters delete "${full_name}" \ From 36885e28721f151c1c52a457d7e69737ce81f20f Mon Sep 17 00:00:00 2001 From: Mahesh Patil <17205424+mahpatil@users.noreply.github.com> Date: Sat, 6 Jun 2026 11:58:22 +0100 Subject: [PATCH 12/15] Update samples/gcp/teardown.sh 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> --- samples/gcp/teardown.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/gcp/teardown.sh b/samples/gcp/teardown.sh index eaabddae88..b6a51a6a70 100644 --- a/samples/gcp/teardown.sh +++ b/samples/gcp/teardown.sh @@ -33,14 +33,14 @@ for name in dark-mode banner-text max-cart-items discount-rate checkout-config; for v in $(gcloud parametermanager parameters versions list \ --parameter="${full_name}" --location=global \ --format="value(name.basename())" 2>/dev/null || true); do - gcloud parametermanager parameters versions delete "$v" \ - --parameter="${full_name}" --location=global --quiet || true - done && \ + gcloud parametermanager parameters versions delete "$v" \ + --parameter="${full_name}" --location=global --quiet || true + done gcloud parametermanager parameters delete "${full_name}" \ --location=global \ --project="${PROJECT}" \ - --quiet || echo " [WARN] Could not delete parameter (may require gcloud alpha components)" + --quiet || echo " [WARN] Could not delete parameter" echo " [DELETED] ${full_name}" else echo " [SKIP] ${full_name} (not found)" From a91756e60275df9abd6486399ee3f951c2490532 Mon Sep 17 00:00:00 2001 From: Mahesh Patil Date: Sat, 6 Jun 2026 12:08:46 +0100 Subject: [PATCH 13/15] Fix java21 javadoc issues @link to @code Signed-off-by: Mahesh Patil --- .../contrib/providers/gcp/GcpParameterManagerProvider.java | 5 +++-- .../contrib/providers/gcp/GcpSecretManagerProvider.java | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java index 6b6a69cd6a..42baf905b6 100644 --- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProvider.java @@ -16,7 +16,7 @@ * *

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 - * {@link GcpProviderOptions#getParameterNamePrefix()}). + * {@code GcpProviderOptions#getNamePrefix()}). * *

Flag values are read as strings and parsed to the requested type. Supported raw value * formats: @@ -29,12 +29,13 @@ * * *

Results are cached in-process for the duration configured in - * {@link GcpProviderOptions#getCacheExpiry()}. + * {@code GcpProviderOptions#getCacheExpiry()}. * *

Example: *

{@code
  * GcpProviderOptions opts = GcpProviderOptions.builder()
  *     .projectId("my-gcp-project")
+ *     .locationId("global") // optional, defaults to "global"
  *     .build();
  * OpenFeatureAPI.getInstance().setProvider(new GcpParameterManagerProvider(opts));
  * }
diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProvider.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProvider.java index ce04e36d0f..c235f4bdaa 100644 --- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProvider.java +++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProvider.java @@ -16,7 +16,7 @@ * *

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 configured via - * {@link GcpProviderOptions#getNamePrefix()}). + * {@code GcpProviderOptions#getNamePrefix()}). * *

Flag values are read as UTF-8 strings from the secret payload and parsed to the requested * type. Supported raw value formats: @@ -29,7 +29,7 @@ * * *

Results are cached in-process for the duration configured in - * {@link GcpProviderOptions#getCacheExpiry()}. + * {@code GcpProviderOptions#getCacheExpiry()}. * *

Example: *

{@code

From 67b7c3191931cf0ac4ac5292f8f50e82b9507476 Mon Sep 17 00:00:00 2001
From: Mahesh Patil 
Date: Sat, 6 Jun 2026 12:14:28 +0100
Subject: [PATCH 14/15] Fix trailing period javadoc GcpProviderOptions

Signed-off-by: Mahesh Patil 
---
 .../openfeature/contrib/providers/gcp/GcpProviderOptions.java   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpProviderOptions.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpProviderOptions.java
index b0baf21831..5be526a879 100644
--- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpProviderOptions.java
+++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/GcpProviderOptions.java
@@ -43,7 +43,7 @@ public class GcpProviderOptions {
     private final String version = "latest";
 
     /**
-     * Optional location required for ParameterManager, ignored by SecretManager
+     * Optional location required for ParameterManager, ignored by SecretManager.
      */
     @Builder.Default
     private final String locationId = "global";

From d17d145a873069a4507d0cd368e714a28969338a Mon Sep 17 00:00:00 2001
From: Mahesh Patil 
Date: Sat, 6 Jun 2026 12:25:12 +0100
Subject: [PATCH 15/15] Fix spotless FlagCache

Signed-off-by: Mahesh Patil 
---
 .../java/dev/openfeature/contrib/providers/gcp/FlagCache.java    | 1 -
 1 file changed, 1 deletion(-)

diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/FlagCache.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/FlagCache.java
index 17de3ec15a..cf53d8684f 100644
--- a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/FlagCache.java
+++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/FlagCache.java
@@ -7,7 +7,6 @@
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Optional;
-import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * Thread-safe TTL-based in-memory cache for flag values fetched from GCP services.