This is an automated email from the ASF dual-hosted git repository.

thenatog pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 518f413d9f NIFI-9637: Adding GcpSecretManagerParameterProvider
518f413d9f is described below

commit 518f413d9fa12e7fdf62c7ef33aefe33d1a2d596
Author: Joe Gresock <[email protected]>
AuthorDate: Sat Sep 10 10:35:21 2022 -0400

    NIFI-9637: Adding GcpSecretManagerParameterProvider
    
    Signed-off-by: Nathan Gough <[email protected]>
    
    This closes #6394.
---
 .../nifi-gcp-bundle/nifi-gcp-nar/pom.xml           |   5 +
 .../nifi-gcp-parameter-providers/pom.xml           |  84 +++++++++
 .../gcp/GcpSecretManagerParameterProvider.java     | 200 ++++++++++++++++++++
 .../org.apache.nifi.parameter.ParameterProvider    |  16 ++
 .../additionalDetails.html                         |  55 ++++++
 .../gcp/TestGcpSecretManagerParameterProvider.java | 208 +++++++++++++++++++++
 .../org.mockito.plugins.MockMaker                  |  15 ++
 nifi-nar-bundles/nifi-gcp-bundle/pom.xml           |   1 +
 8 files changed, 584 insertions(+)

diff --git a/nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-nar/pom.xml 
b/nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-nar/pom.xml
index 168756d348..b1f69f73b8 100644
--- a/nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-nar/pom.xml
+++ b/nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-nar/pom.xml
@@ -41,5 +41,10 @@
             <artifactId>nifi-gcp-processors</artifactId>
             <version>1.18.0-SNAPSHOT</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-gcp-parameter-providers</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+        </dependency>
     </dependencies>
 </project>
diff --git 
a/nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-parameter-providers/pom.xml 
b/nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-parameter-providers/pom.xml
new file mode 100644
index 0000000000..91ac70afa5
--- /dev/null
+++ b/nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-parameter-providers/pom.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements. See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License. You may obtain a copy of the License at
+  http://www.apache.org/licenses/LICENSE-2.0
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
https://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi</groupId>
+        <artifactId>nifi-gcp-bundle</artifactId>
+        <version>1.18.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>nifi-gcp-parameter-providers</artifactId>
+    <packaging>jar</packaging>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-utils</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-gcp-services-api</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>jcl-over-slf4j</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.google.cloud</groupId>
+            <artifactId>google-cloud-secretmanager</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>commons-logging</groupId>
+                    <artifactId>commons-logging</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.google.auth</groupId>
+            <artifactId>google-auth-library-oauth2-http</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.google.code.findbugs</groupId>
+                    <artifactId>jsr305</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>commons-logging</groupId>
+                    <artifactId>commons-logging</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-expression-language</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-mock</artifactId>
+            <version>1.18.0-SNAPSHOT</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git 
a/nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-parameter-providers/src/main/java/org/apache/nifi/parameter/gcp/GcpSecretManagerParameterProvider.java
 
b/nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-parameter-providers/src/main/java/org/apache/nifi/parameter/gcp/GcpSecretManagerParameterProvider.java
new file mode 100644
index 0000000000..19ffa06c56
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-parameter-providers/src/main/java/org/apache/nifi/parameter/gcp/GcpSecretManagerParameterProvider.java
@@ -0,0 +1,200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.parameter.gcp;
+
+import com.google.cloud.secretmanager.v1.AccessSecretVersionResponse;
+import com.google.cloud.secretmanager.v1.ProjectName;
+import com.google.cloud.secretmanager.v1.Secret;
+import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
+import 
com.google.cloud.secretmanager.v1.SecretManagerServiceClient.ListSecretsPage;
+import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings;
+import com.google.cloud.secretmanager.v1.SecretVersionName;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.gcp.credentials.service.GCPCredentialsService;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.parameter.AbstractParameterProvider;
+import org.apache.nifi.parameter.Parameter;
+import org.apache.nifi.parameter.ParameterDescriptor;
+import org.apache.nifi.parameter.ParameterGroup;
+import org.apache.nifi.parameter.VerifiableParameterProvider;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * Reads secrets from GCP Secret Manager to provide parameter values.  Secrets 
must be created similar to the following GCP cli command: <br/><br/>
+ * <code>gcp secretsmanager create-secret --name "[Context]" --secret-string 
'{ "[Param]": "[secretValue]", "[Param2]": "[secretValue2]" }'</code> <br/><br/>
+ *
+ */
+
+@Tags({"gcp", "secret", "manager"})
+@CapabilityDescription("Fetches parameters from GCP Secret Manager.  Each 
secret becomes a Parameter, which can be mapped to a Parameter Group " +
+        "by adding a GCP label named 'group-name'.")
+public class GcpSecretManagerParameterProvider extends 
AbstractParameterProvider implements VerifiableParameterProvider {
+    private static final Logger logger = 
LoggerFactory.getLogger(GcpSecretManagerParameterProvider.class);
+
+    public static final PropertyDescriptor GROUP_NAME_PATTERN = new 
PropertyDescriptor.Builder()
+            .name("group-name-pattern")
+            .displayName("Group Name Pattern")
+            .description("A Regular Expression matching on the 'group-name' 
label value that identifies Secrets whose parameters should be fetched. " +
+                    "Any secrets without a 'group-name' label value that 
matches this Regex will not be fetched.")
+            .addValidator(StandardValidators.REGULAR_EXPRESSION_VALIDATOR)
+            .required(true)
+            .defaultValue(".*")
+            .build();
+
+    public static final PropertyDescriptor PROJECT_ID = new PropertyDescriptor
+            .Builder().name("gcp-project-id")
+            .displayName("Project ID")
+            .description("Google Cloud Project ID")
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .required(true)
+            .build();
+
+    /**
+     * Links to the {@link GCPCredentialsService} which provides credentials 
for this particular processor.
+     */
+    public static final PropertyDescriptor GCP_CREDENTIALS_PROVIDER_SERVICE = 
new PropertyDescriptor.Builder()
+            .name("gcp-credentials-provider-service")
+            .displayName("GCP Credentials Provider Service")
+            .description("The Controller Service used to obtain Google Cloud 
Platform credentials.")
+            .required(true)
+            .identifiesControllerService(GCPCredentialsService.class)
+            .build();
+
+    private static final String GROUP_NAME_LABEL = "group-name";
+    private static final String SECRETS_PATH = "secrets/";
+    private static final List<PropertyDescriptor> PROPERTIES = 
Collections.unmodifiableList(Arrays.asList(
+            GROUP_NAME_PATTERN,
+            PROJECT_ID,
+            GCP_CREDENTIALS_PROVIDER_SERVICE
+    ));
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTIES;
+    }
+
+    @Override
+    public List<ParameterGroup> fetchParameters(final ConfigurationContext 
context) throws IOException {
+        final Map<String, ParameterGroup> providedParameterGroups = new 
HashMap<>();
+        final SecretManagerServiceClient secretsManager = 
this.configureClient(context);
+        final ProjectName projectName = 
ProjectName.of(context.getProperty(PROJECT_ID).getValue());
+
+        final SecretManagerServiceClient.ListSecretsPagedResponse 
pagedResponse = secretsManager.listSecrets(projectName);
+        ListSecretsPage page = pagedResponse.getPage();
+        do {
+            for (final Secret secret : page.getValues()) {
+                final String contextName = 
secret.getLabelsOrDefault(GROUP_NAME_LABEL, null);
+                if (contextName == null) {
+                    getLogger().debug("Secret [{}] does not have the {} label, 
and will be skipped", secret.getName(), GROUP_NAME_LABEL);
+                    continue;
+                }
+                final String secretName = 
StringUtils.substringAfter(secret.getName(), SECRETS_PATH);
+
+                fetchSecret(secretsManager, context, secretName, contextName, 
providedParameterGroups);
+            }
+            if (page.hasNextPage()) {
+                page = page.getNextPage();
+            }
+        } while (page.hasNextPage());
+        return new ArrayList<>(providedParameterGroups.values());
+    }
+
+    @Override
+    public List<ConfigVerificationResult> verify(final ConfigurationContext 
context, final ComponentLog verificationLogger) {
+        final List<ConfigVerificationResult> results = new ArrayList<>();
+
+        try {
+            final List<ParameterGroup> parameterGroups = 
fetchParameters(context);
+            int count = 0;
+            for (final ParameterGroup group : parameterGroups) {
+                count += group.getParameters().size();
+            }
+            results.add(new ConfigVerificationResult.Builder()
+                    .outcome(ConfigVerificationResult.Outcome.SUCCESSFUL)
+                    .verificationStepName("Fetch Parameters")
+                    .explanation(String.format("Fetched secret keys [%d] as 
parameters within groups [%d]",
+                            count, parameterGroups.size()))
+                    .build());
+        } catch (final Exception e) {
+            verificationLogger.error("Failed to fetch parameters", e);
+            results.add(new ConfigVerificationResult.Builder()
+                    .outcome(ConfigVerificationResult.Outcome.FAILED)
+                    .verificationStepName("Fetch Parameters")
+                    .explanation("Failed to fetch parameters: " + 
e.getMessage())
+                    .build());
+        }
+        return results;
+    }
+
+    private void fetchSecret(final SecretManagerServiceClient secretsManager, 
final ConfigurationContext context, final String secretName,
+                                                     final String groupName, 
final Map<String, ParameterGroup> providedParameterGroups) {
+        final Pattern groupNamePattern = 
Pattern.compile(context.getProperty(GROUP_NAME_PATTERN).getValue());
+        final String projectId = context.getProperty(PROJECT_ID).getValue();
+
+        if (!groupNamePattern.matcher(groupName).matches()) {
+            logger.debug("Secret [{}] label [{}] does not match the group name 
pattern {}", secretName, groupName, groupNamePattern);
+            return;
+        }
+
+        final SecretVersionName secretVersionName = 
SecretVersionName.of(projectId, secretName, "latest");
+
+        // Access the secret version.
+        final AccessSecretVersionResponse response = 
secretsManager.accessSecretVersion(secretVersionName);
+        final String parameterValue = 
response.getPayload().getData().toStringUtf8();
+
+        final Parameter parameter = createParameter(secretName, 
parameterValue);
+
+        if (parameter != null) {
+            final ParameterGroup group = providedParameterGroups
+                    .computeIfAbsent(groupName, key -> new 
ParameterGroup(groupName, new ArrayList<>()));
+            final List<Parameter> updatedParameters = new 
ArrayList<>(group.getParameters());
+            updatedParameters.add(parameter);
+            providedParameterGroups.put(groupName, new 
ParameterGroup(groupName, updatedParameters));
+        }
+    }
+
+    private Parameter createParameter(final String parameterName, final String 
parameterValue) {
+        final ParameterDescriptor parameterDescriptor = new 
ParameterDescriptor.Builder().name(parameterName).build();
+        return new Parameter(parameterDescriptor, parameterValue, null, true);
+    }
+
+    SecretManagerServiceClient configureClient(final ConfigurationContext 
context) throws IOException {
+        final GCPCredentialsService credentialsService = 
context.getProperty(GCP_CREDENTIALS_PROVIDER_SERVICE).asControllerService(GCPCredentialsService.class);
+
+        final SecretManagerServiceClient client = 
SecretManagerServiceClient.create(SecretManagerServiceSettings
+                .newBuilder().setCredentialsProvider(() -> 
credentialsService.getGoogleCredentials())
+                .build());
+
+        return client;
+    }
+}
diff --git 
a/nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-parameter-providers/src/main/resources/META-INF/services/org.apache.nifi.parameter.ParameterProvider
 
b/nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-parameter-providers/src/main/resources/META-INF/services/org.apache.nifi.parameter.ParameterProvider
new file mode 100644
index 0000000000..29aa40d470
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-parameter-providers/src/main/resources/META-INF/services/org.apache.nifi.parameter.ParameterProvider
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+org.apache.nifi.parameter.gcp.GcpSecretManagerParameterProvider
+
diff --git 
a/nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-parameter-providers/src/main/resources/docs/org.apache.nifi.parameter.gcp.GcpSecretManagerParameterProvider/additionalDetails.html
 
b/nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-parameter-providers/src/main/resources/docs/org.apache.nifi.parameter.gcp.GcpSecretManagerParameterProvider/additionalDetails.html
new file mode 100644
index 0000000000..bc32d72cb4
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-parameter-providers/src/main/resources/docs/org.apache.nifi.parameter.gcp.GcpSecretManagerParameterProvider/additionalDetails.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html lang="en">
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+      http://www.apache.org/licenses/LICENSE-2.0
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<head>
+    <meta charset="utf-8" />
+    <title>GcpSecretManagerParameterProvider</title>
+
+    <link rel="stylesheet" href="../../../../../css/component-usage.css" 
type="text/css" />
+</head>
+<body>
+    <h3>Mapping GCP Secrets to Parameter Contexts</h3>
+
+    <p>
+        The GcpSecretManagerParameterProvider maps a Secret to a Parameter, 
which can be grouped by adding a "group-name" label.
+        To create a compatible secret from the GCP Console:
+    </p>
+
+    <ol>
+        <li>From the Secret Manager service, click the "Create Secret" 
button</li>
+        <li>Enter the Secret name.  This is the name of a parameter.  Enter a 
value.</li>
+        <li>Under "Labels", add a label with a Key of "group-name" and a value 
of the intended Parameter Group name.</li>
+    </ol>
+
+    <p>
+        Alternatively, from the command line, run a command like the following:
+    </p>
+
+    <pre>
+printf "[Parameter Value]" | gcloud secrets create 
--labels=group-name="[Parameter Group Name]" "[Parameter Name]" --data-file=-
+    </pre>
+
+    <p>In this example, [Parameter Group Name] should be the intended name of 
the Parameter Group, [Parameter Name] should be
+    the parameter name, and [Parameter Value] should be the value of the 
parameter.</p>
+
+    <h3>Configuring the Parameter Provider</h3>
+
+    <p>
+        GCP Secrets must be explicitly matched in the "Group Name Pattern" 
property in order for them to be fetched.  This
+        prevents more than the intended Secrets from being pulled into NiFi.
+    </p>
+</body>
+</html>
diff --git 
a/nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-parameter-providers/src/test/java/org/apache/nifi/parameter/gcp/TestGcpSecretManagerParameterProvider.java
 
b/nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-parameter-providers/src/test/java/org/apache/nifi/parameter/gcp/TestGcpSecretManagerParameterProvider.java
new file mode 100644
index 0000000000..e4983e0ff6
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-parameter-providers/src/test/java/org/apache/nifi/parameter/gcp/TestGcpSecretManagerParameterProvider.java
@@ -0,0 +1,208 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.parameter.gcp;
+
+import com.google.cloud.secretmanager.v1.AccessSecretVersionResponse;
+import com.google.cloud.secretmanager.v1.ProjectName;
+import com.google.cloud.secretmanager.v1.Secret;
+import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
+import 
com.google.cloud.secretmanager.v1.SecretManagerServiceClient.ListSecretsPage;
+import 
com.google.cloud.secretmanager.v1.SecretManagerServiceClient.ListSecretsPagedResponse;
+import com.google.cloud.secretmanager.v1.SecretPayload;
+import com.google.cloud.secretmanager.v1.SecretVersionName;
+import com.google.protobuf.ByteString;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.parameter.Parameter;
+import org.apache.nifi.parameter.ParameterDescriptor;
+import org.apache.nifi.parameter.ParameterGroup;
+import org.apache.nifi.parameter.VerifiableParameterProvider;
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.util.MockComponentLog;
+import org.apache.nifi.util.MockConfigurationContext;
+import org.apache.nifi.util.MockParameterProviderInitializationContext;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+public class TestGcpSecretManagerParameterProvider {
+
+    final List<Parameter> mySecretParameters = Arrays.asList(
+            parameter("paramA", "valueA"),
+            parameter("paramB", "valueB"),
+            parameter("otherC", "valueOther"),
+            parameter("paramD", "valueD"),
+            parameter("nonSensitiveE", "valueE"),
+            parameter("otherF", "valueF")
+    );
+    final List<Parameter> otherSecretParameters = Arrays.asList(
+            parameter("paramG", "valueG"),
+            parameter("otherH", "valueOther")
+    );
+    final List<Parameter> unrelatedParameters = 
Collections.singletonList(parameter("paramK", "unused"));
+    final List<ParameterGroup> mockParameterGroups = Arrays.asList(
+            new ParameterGroup("MySecret", mySecretParameters),
+            new ParameterGroup("OtherSecret", otherSecretParameters),
+            new ParameterGroup("Unrelated", unrelatedParameters) // will not 
be picked up
+    );
+
+    @Test
+    public void testFetchParametersWithNoSecrets() throws 
InitializationException, IOException {
+        final List<ParameterGroup> expectedGroups = 
Collections.singletonList(new ParameterGroup("MySecret", 
Collections.emptyList()));
+        runProviderTest(mockSecretManagerClient(expectedGroups), 0, 
ConfigVerificationResult.Outcome.SUCCESSFUL);
+    }
+
+    @Test
+    public void testFetchParameters() throws InitializationException, 
IOException {
+        runProviderTest(mockSecretManagerClient(mockParameterGroups), 8, 
ConfigVerificationResult.Outcome.SUCCESSFUL);
+    }
+
+    @Test
+    public void testFetchParametersListFailure() throws 
InitializationException, IOException {
+        final SecretManagerServiceClient mockSecretsManager = 
mock(SecretManagerServiceClient.class);
+        
when(mockSecretsManager.listSecrets(any(ProjectName.class))).thenThrow(new 
RuntimeException("Fake exception"));
+        runProviderTest(mockSecretsManager, 0, 
ConfigVerificationResult.Outcome.FAILED);
+    }
+
+    @Test
+    public void testFetchParametersGetSecretFailure() throws 
InitializationException, IOException {
+        final SecretManagerServiceClient mockSecretsManager = 
mock(SecretManagerServiceClient.class);
+        final ListSecretsPagedResponse listSecretsPagedResponse = 
mock(ListSecretsPagedResponse.class);
+        final ListSecretsPage page = mock(ListSecretsPage.class);
+        when(listSecretsPagedResponse.getPage()).thenReturn(page);
+
+        when(page.hasNextPage()).thenReturn(false);
+        final Secret secret = mock(Secret.class);
+        when(secret.getName()).thenReturn("paramA");
+        when(secret.getLabelsOrDefault("group-name", 
null)).thenReturn("Secret");
+        when(page.getValues()).thenReturn(Collections.singletonList(secret));
+
+        
when(mockSecretsManager.accessSecretVersion(any(SecretVersionName.class))).thenThrow(new
 RuntimeException("Fake exception"));
+        runProviderTest(mockSecretsManager, 0, 
ConfigVerificationResult.Outcome.FAILED);
+    }
+
+    private GcpSecretManagerParameterProvider getParameterProvider() {
+        return spy(new GcpSecretManagerParameterProvider());
+    }
+
+    private SecretManagerServiceClient mockSecretManagerClient(final 
List<ParameterGroup> mockGroups) {
+        final SecretManagerServiceClient secretManager = 
mock(SecretManagerServiceClient.class);
+
+        final ListSecretsPagedResponse listSecretsPagedResponse = 
mock(ListSecretsPagedResponse.class);
+        
when(secretManager.listSecrets(any(ProjectName.class))).thenReturn(listSecretsPagedResponse);
+
+        boolean mockedFirstPage = false;
+        ListSecretsPage currentPage;
+        ListSecretsPage previousPage = null;
+        for (final Iterator<ParameterGroup> it = mockGroups.iterator(); 
it.hasNext(); ) {
+            final ParameterGroup group = it.next();
+            currentPage = mock(ListSecretsPage.class);
+            if (mockedFirstPage) {
+                when(previousPage.getNextPage()).thenReturn(currentPage);
+            } else {
+                
when(listSecretsPagedResponse.getPage()).thenReturn(currentPage);
+                mockedFirstPage = true;
+            }
+            final List<Secret> values = new ArrayList<>();
+            values.addAll(group.getParameters().stream()
+                    .map(parameter -> mockSecret(secretManager, 
group.getGroupName(), parameter))
+                    .collect(Collectors.toList()));
+            when(currentPage.getValues()).thenReturn(values);
+            if (it.hasNext()) {
+                when(currentPage.hasNextPage()).thenReturn(true);
+                previousPage = currentPage;
+            } else {
+                when(currentPage.hasNextPage()).thenReturn(false);
+            }
+        }
+
+        return secretManager;
+    }
+
+    final Secret mockSecret(final SecretManagerServiceClient secretManager, 
final String groupName, final Parameter parameter) {
+        final Secret secret = mock(Secret.class);
+
+        final String parameterName = parameter.getDescriptor().getName();
+        when(secret.getName()).thenReturn("projects/project/secrets/" + 
parameterName);
+        when(secret.getLabelsOrDefault("group-name", 
null)).thenReturn(groupName);
+
+        final AccessSecretVersionResponse response = 
mock(AccessSecretVersionResponse.class);
+        
doReturn(response).when(secretManager).accessSecretVersion(argThat((SecretVersionName
 secretVersionName) -> secretVersionName.getSecret().equals(parameterName)));
+        final SecretPayload payload = mock(SecretPayload.class);
+        final ByteString data = ByteString.copyFromUtf8(parameter.getValue());
+        when(payload.getData()).thenReturn(data);
+        when(response.getPayload()).thenReturn(payload);
+        return secret;
+    }
+
+    private List<ParameterGroup> runProviderTest(final 
SecretManagerServiceClient secretsManager, final int expectedCount,
+                                                 final 
ConfigVerificationResult.Outcome expectedOutcome) throws 
InitializationException, IOException {
+
+        final GcpSecretManagerParameterProvider parameterProvider = 
getParameterProvider();
+        
doReturn(secretsManager).when(parameterProvider).configureClient(any());
+        final MockParameterProviderInitializationContext initContext = new 
MockParameterProviderInitializationContext("id", "name",
+                new MockComponentLog("providerId", parameterProvider));
+        parameterProvider.initialize(initContext);
+
+        final Map<PropertyDescriptor, String> properties = new HashMap<>();
+        properties.put(GcpSecretManagerParameterProvider.GROUP_NAME_PATTERN, 
".*Secret");
+        properties.put(GcpSecretManagerParameterProvider.PROJECT_ID, 
"my-project");
+        final MockConfigurationContext mockConfigurationContext = new 
MockConfigurationContext(properties, null);
+
+        List<ParameterGroup> parameterGroups = new ArrayList<>();
+        // Verify parameter fetching
+        if (expectedOutcome == ConfigVerificationResult.Outcome.FAILED) {
+            assertThrows(RuntimeException.class, () -> 
parameterProvider.fetchParameters(mockConfigurationContext));
+        } else {
+            parameterGroups = 
parameterProvider.fetchParameters(mockConfigurationContext);
+            final int count = (int) parameterGroups.stream()
+                    .flatMap(group -> group.getParameters().stream())
+                    .count();
+            assertEquals(expectedCount, count);
+        }
+
+        // Verify config verification
+        final List<ConfigVerificationResult> results = 
((VerifiableParameterProvider) 
parameterProvider).verify(mockConfigurationContext, initContext.getLogger());
+
+        assertEquals(1, results.size());
+        assertEquals(expectedOutcome, results.get(0).getOutcome());
+
+        return parameterGroups;
+    }
+
+    private static Parameter parameter(final String name, final String value) {
+        return new Parameter(new 
ParameterDescriptor.Builder().name(name).build(), value);
+    }
+
+}
diff --git 
a/nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-parameter-providers/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
 
b/nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-parameter-providers/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..dc5c0f7aa2
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-parameter-providers/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1,15 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+mock-maker-inline
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-gcp-bundle/pom.xml 
b/nifi-nar-bundles/nifi-gcp-bundle/pom.xml
index 59633ada9d..67f62059ad 100644
--- a/nifi-nar-bundles/nifi-gcp-bundle/pom.xml
+++ b/nifi-nar-bundles/nifi-gcp-bundle/pom.xml
@@ -45,6 +45,7 @@
         <module>nifi-gcp-services-api</module>
         <module>nifi-gcp-services-api-nar</module>
         <module>nifi-gcp-processors</module>
+        <module>nifi-gcp-parameter-providers</module>
         <module>nifi-gcp-nar</module>
     </modules>
 </project>

Reply via email to