From 3ec821eacb0defc7ae545c186d581b5c359b656d Mon Sep 17 00:00:00 2001 From: JiriOndrusek Date: Mon, 18 Nov 2024 15:12:30 +0100 Subject: [PATCH] fixes #6688: google-secret-manager: extend test coverage --- .../google-secret-manager/README.adoc | 14 ++ .../google-secret-manager/pom.xml | 31 ++++ .../it/GoogleSecretManagerResource.java | 98 ++++++++++-- .../manager/it/GoogleSecretManagerRoutes.java | 36 +++++ .../src/main/resources/application.properties | 20 +++ .../manager/it/GoogleSecretManagerTest.java | 133 ++++++++++++++++- .../it/GoogleSecretManagerTestResource.java | 139 ++++++++++++++++++ 7 files changed, 457 insertions(+), 14 deletions(-) create mode 100644 integration-tests-jvm/google-secret-manager/README.adoc create mode 100644 integration-tests-jvm/google-secret-manager/src/main/java/org/apache/camel/quarkus/component/google/secret/manager/it/GoogleSecretManagerRoutes.java create mode 100644 integration-tests-jvm/google-secret-manager/src/main/resources/application.properties create mode 100644 integration-tests-jvm/google-secret-manager/src/test/java/org/apache/camel/quarkus/component/google/secret/manager/it/GoogleSecretManagerTestResource.java diff --git a/integration-tests-jvm/google-secret-manager/README.adoc b/integration-tests-jvm/google-secret-manager/README.adoc new file mode 100644 index 000000000000..c29df4e79d5d --- /dev/null +++ b/integration-tests-jvm/google-secret-manager/README.adoc @@ -0,0 +1,14 @@ +The integration tests are executed only when valid service credentials are provided. +These tests do not use a mocked backend. + +=== Real Google API + +Below are the environment variables that have to be configured: + +[source,shell] +---- +export GOOGLE_SERVICE_ACCOUNT_KEY= +export GOOGLE_PROJECT= +---- + +If any of the required variables is not defined, the tests are automatically skipped. \ No newline at end of file diff --git a/integration-tests-jvm/google-secret-manager/pom.xml b/integration-tests-jvm/google-secret-manager/pom.xml index 64f9f18c7d29..78dbd7f2bd48 100644 --- a/integration-tests-jvm/google-secret-manager/pom.xml +++ b/integration-tests-jvm/google-secret-manager/pom.xml @@ -58,6 +58,14 @@ io.quarkus quarkus-resteasy + + org.apache.camel.quarkus + camel-quarkus-direct + + + io.quarkus + quarkus-resteasy-jsonb + @@ -70,6 +78,16 @@ rest-assured test + + org.apache.camel.quarkus + camel-quarkus-integration-test-support + test + + + org.awaitility + awaitility + test + @@ -95,6 +113,19 @@ + + org.apache.camel.quarkus + camel-quarkus-direct-deployment + ${project.version} + pom + test + + + * + * + + + diff --git a/integration-tests-jvm/google-secret-manager/src/main/java/org/apache/camel/quarkus/component/google/secret/manager/it/GoogleSecretManagerResource.java b/integration-tests-jvm/google-secret-manager/src/main/java/org/apache/camel/quarkus/component/google/secret/manager/it/GoogleSecretManagerResource.java index 292d02a2a6db..cbec79846a7d 100644 --- a/integration-tests-jvm/google-secret-manager/src/main/java/org/apache/camel/quarkus/component/google/secret/manager/it/GoogleSecretManagerResource.java +++ b/integration-tests-jvm/google-secret-manager/src/main/java/org/apache/camel/quarkus/component/google/secret/manager/it/GoogleSecretManagerResource.java @@ -16,14 +16,28 @@ */ package org.apache.camel.quarkus.component.google.secret.manager.it; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; +import com.google.cloud.secretmanager.v1.SecretVersion; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import org.apache.camel.CamelContext; +import org.apache.camel.Exchange; +import org.apache.camel.ProducerTemplate; +import org.apache.camel.component.google.secret.manager.GoogleSecretManagerOperations; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.logging.Logger; @Path("/google-secret-manager") @@ -32,19 +46,83 @@ public class GoogleSecretManagerResource { private static final Logger LOG = Logger.getLogger(GoogleSecretManagerResource.class); - private static final String COMPONENT_GOOGLE_SECRET_MANAGER = "google-secret-manager"; + @ConfigProperty(name = "cq.google-secrets-manager.path-to-service-account-key") + String accountKey; + + @ConfigProperty(name = "cq.google-secrets-manager.project-name") + String projectName; + @Inject - CamelContext context; + ProducerTemplate producerTemplate; - @Path("/load/component/google-secret-manager") + @Path("/list/{secretName}") @GET @Produces(MediaType.TEXT_PLAIN) - public Response loadComponentGoogleSecretManager() throws Exception { - /* This is an autogenerated test */ - if (context.getComponent(COMPONENT_GOOGLE_SECRET_MANAGER) != null) { - return Response.ok().build(); + public List getSecret(@PathParam("secretName") String secretName) { + SecretManagerServiceClient.ListSecretsPagedResponse secrets = producerTemplate.requestBody("direct:listSecrets", "", + SecretManagerServiceClient.ListSecretsPagedResponse.class); + LinkedList result = new LinkedList<>(); + SecretManagerServiceClient.ListSecretsPage page = secrets.getPage(); + while (page != null) { + page.getValues().iterator().forEachRemaining(s -> result.add(s.getName())); + page = page.getNextPage(); } - LOG.warnf("Could not load [%s] from the Camel context", COMPONENT_GOOGLE_SECRET_MANAGER); - return Response.status(500, COMPONENT_GOOGLE_SECRET_MANAGER + " could not be loaded from the Camel context").build(); + + return result; } + + @Path("/getGcpSecret") + @GET + @Produces(MediaType.TEXT_PLAIN) + public String loadGcpPassword() { + return producerTemplate.requestBody("direct:loadGcpPassword", "", String.class); + } + + @Path("/operation/{operation}") + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response operation(@PathParam("operation") String operation, @QueryParam("body") String body, + Map headers) { + + Exchange ex = producerTemplate.send(String.format("google-secret-manager://%s" + + "?serviceAccountKey=file:%s" + + "&operation=%s", projectName, accountKey, operation), + e -> { + e.getIn().setHeaders(headers == null ? Collections.emptyMap() : headers); + e.getIn().setBody(body == null ? "" : body); + }); + + Object result = null; + switch (GoogleSecretManagerOperations.valueOf(operation)) { + case listSecrets: + + LinkedList listedSecrets = new LinkedList<>(); + SecretManagerServiceClient.ListSecretsPagedResponse response = ex.getIn() + .getBody(SecretManagerServiceClient.ListSecretsPagedResponse.class); + SecretManagerServiceClient.ListSecretsPage page = response.getPage(); + while (page != null) { + page.getValues().iterator().forEachRemaining(s -> listedSecrets.add(s.getName())); + page = page.getNextPage(); + } + + result = listedSecrets; + break; + case createSecret: + SecretVersion createdSecret = ex.getIn().getBody(SecretVersion.class); + result = createdSecret.getName(); + break; + case deleteSecret: + result = true; + break; + case getSecretVersion: + result = ex.getIn().getBody(String.class); + break; + default: + return Response.status(500).build(); + } + + return Response.ok(result).build(); + } + } diff --git a/integration-tests-jvm/google-secret-manager/src/main/java/org/apache/camel/quarkus/component/google/secret/manager/it/GoogleSecretManagerRoutes.java b/integration-tests-jvm/google-secret-manager/src/main/java/org/apache/camel/quarkus/component/google/secret/manager/it/GoogleSecretManagerRoutes.java new file mode 100644 index 000000000000..15074d1863c4 --- /dev/null +++ b/integration-tests-jvm/google-secret-manager/src/main/java/org/apache/camel/quarkus/component/google/secret/manager/it/GoogleSecretManagerRoutes.java @@ -0,0 +1,36 @@ +/* + * 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.camel.quarkus.component.google.secret.manager.it; + +import jakarta.enterprise.context.ApplicationScoped; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.model.language.SimpleExpression; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +@ApplicationScoped +public class GoogleSecretManagerRoutes extends RouteBuilder { + + @ConfigProperty(name = "gcpSecretId") + String gcpSecretId; + + @Override + public void configure() throws Exception { + from("direct:loadGcpPassword") + .setBody(new SimpleExpression("{{gcp:%s@1}}".formatted(gcpSecretId))); + + } +} diff --git a/integration-tests-jvm/google-secret-manager/src/main/resources/application.properties b/integration-tests-jvm/google-secret-manager/src/main/resources/application.properties new file mode 100644 index 000000000000..a563d11879af --- /dev/null +++ b/integration-tests-jvm/google-secret-manager/src/main/resources/application.properties @@ -0,0 +1,20 @@ +## --------------------------------------------------------------------------- +## 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. +## --------------------------------------------------------------------------- +cq.google-secrets-manager.path-to-service-account-key=${GOOGLE_SERVICE_ACCOUNT_KEY} +cq.google-secrets-manager.project-name=${GOOGLE_PROJECT_NAME} +camel.vault.gcp.serviceAccountKey=file:${GOOGLE_SERVICE_ACCOUNT_KEY} +camel.vault.gcp.projectId=${GOOGLE_PROJECT_NAME} diff --git a/integration-tests-jvm/google-secret-manager/src/test/java/org/apache/camel/quarkus/component/google/secret/manager/it/GoogleSecretManagerTest.java b/integration-tests-jvm/google-secret-manager/src/test/java/org/apache/camel/quarkus/component/google/secret/manager/it/GoogleSecretManagerTest.java index d5f03a202865..7bdceb27c55b 100644 --- a/integration-tests-jvm/google-secret-manager/src/test/java/org/apache/camel/quarkus/component/google/secret/manager/it/GoogleSecretManagerTest.java +++ b/integration-tests-jvm/google-secret-manager/src/test/java/org/apache/camel/quarkus/component/google/secret/manager/it/GoogleSecretManagerTest.java @@ -16,19 +16,144 @@ */ package org.apache.camel.quarkus.component.google.secret.manager.it; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import io.quarkus.logging.Log; +import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.apache.camel.component.google.secret.manager.GoogleSecretManagerConstants; +import org.apache.camel.component.google.secret.manager.GoogleSecretManagerOperations; +import org.apache.camel.quarkus.test.mock.backend.MockBackendUtils; +import org.awaitility.Awaitility; +import org.eclipse.microprofile.config.ConfigProvider; +import org.hamcrest.CoreMatchers; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariables; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertTrue; +/** + * Todo use MockBackendUtils + */ @QuarkusTest +@QuarkusTestResource(GoogleSecretManagerTestResource.class) +@EnabledIfEnvironmentVariables({ + @EnabledIfEnvironmentVariable(named = "GOOGLE_SERVICE_ACCOUNT_KEY", matches = ".+"), + @EnabledIfEnvironmentVariable(named = "GOOGLE_PROJECT_NAME", matches = ".+") +}) class GoogleSecretManagerTest { @Test - public void loadComponentGoogleSecretManager() { - /* A simple autogenerated test */ - RestAssured.get("/google-secret-manager/load/component/google-secret-manager") + void secretCreateListDelete() { + final String secretToCreate = "firstSecret!"; + final String secretId = "CQTestSecret" + System.currentTimeMillis(); + String createdName; + + boolean deleted = false; + + try { + //create secret + createdName = createSecret(secretId, secretToCreate); + assertTrue(createdName.contains(secretId)); + + //parse the name without /version/... + String name = createdName.substring(0, createdName.indexOf("/version")); + String version = createdName.substring(createdName.lastIndexOf("/") + 1); + + //get secret + RestAssured.given() + .contentType(ContentType.JSON) + .body(Map.of(GoogleSecretManagerConstants.SECRET_ID, secretId, GoogleSecretManagerConstants.VERSION_ID, + version)) + .post("/google-secret-manager/operation/" + GoogleSecretManagerOperations.getSecretVersion) + .then() + .statusCode(200) + .body(is(secretToCreate)); + + // list secrets + RestAssured.given() + .contentType(ContentType.JSON) + .post("/google-secret-manager/operation/" + GoogleSecretManagerOperations.listSecrets) + .then() + .statusCode(200) + .body(containsString(name)); + + //delete secret + deleteSecret(secretId); + + //verify that the secret is gone + RestAssured.given() + .contentType(ContentType.JSON) + .post("/google-secret-manager/operation/" + GoogleSecretManagerOperations.listSecrets) + .then() + .statusCode(200) + .body(not(containsString(name))); + + deleted = true; + + } finally { + if (!deleted && !MockBackendUtils.startMockBackend(false)) { + String file = ConfigProvider.getConfig().getValue("cq.google-secrets-manager.path-to-service-account-key", + String.class); + String projectName = ConfigProvider.getConfig().getValue("cq.google-secrets-manager.project-name", + String.class); + GoogleSecretManagerTestResource.deleteSecret(secretId, file, projectName); + } + } + } + + @Test + void loadGcpSecretTest() { + String expectedSecret = ConfigProvider.getConfig().getValue("gcpSecretValue", String.class); + + RestAssured + .get("/google-secret-manager/getGcpSecret/") .then() - .statusCode(200); + .statusCode(200) + .body(is(expectedSecret)); } + protected String createSecret(String secretName, String secretValue) { + String createdArn = Awaitility.await() + .pollInterval(5, TimeUnit.SECONDS) + .atMost(1, TimeUnit.MINUTES) + .until(() -> { + try { + return RestAssured.given() + .contentType(ContentType.JSON) + .body(Collections.singletonMap(GoogleSecretManagerConstants.SECRET_ID, secretName)) + .queryParam("body", secretValue) + .post("/google-secret-manager/operation/" + GoogleSecretManagerOperations.createSecret) + .then() + .statusCode(200) + .extract().asString(); + } catch (Exception e) { + return null; + } + }, Objects::nonNull); + + return createdArn; + } + + protected void deleteSecret(String secretId) { + if (secretId != null) { + Log.info("Deleting secret: " + secretId); + RestAssured.given() + .contentType(ContentType.JSON) + .body(Collections.singletonMap(GoogleSecretManagerConstants.SECRET_ID, secretId)) + .post("/google-secret-manager/operation/" + GoogleSecretManagerOperations.deleteSecret) + .then() + .statusCode(200) + .body(CoreMatchers.is("true")); + } + } } diff --git a/integration-tests-jvm/google-secret-manager/src/test/java/org/apache/camel/quarkus/component/google/secret/manager/it/GoogleSecretManagerTestResource.java b/integration-tests-jvm/google-secret-manager/src/test/java/org/apache/camel/quarkus/component/google/secret/manager/it/GoogleSecretManagerTestResource.java new file mode 100644 index 000000000000..4e13c1fd7dac --- /dev/null +++ b/integration-tests-jvm/google-secret-manager/src/test/java/org/apache/camel/quarkus/component/google/secret/manager/it/GoogleSecretManagerTestResource.java @@ -0,0 +1,139 @@ +/* + * 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.camel.quarkus.component.google.secret.manager.it; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Map; + +import com.google.api.gax.core.FixedCredentialsProvider; +import com.google.auth.Credentials; +import com.google.auth.oauth2.ServiceAccountCredentials; +import com.google.cloud.secretmanager.v1.ProjectName; +import com.google.cloud.secretmanager.v1.Replication; +import com.google.cloud.secretmanager.v1.Secret; +import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; +import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings; +import com.google.cloud.secretmanager.v1.SecretName; +import com.google.cloud.secretmanager.v1.SecretPayload; +import com.google.protobuf.ByteString; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import org.apache.camel.quarkus.test.mock.backend.MockBackendUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class GoogleSecretManagerTestResource implements QuarkusTestResourceLifecycleManager { + + private static final Logger LOG = LoggerFactory.getLogger(GoogleSecretManagerTestResource.class); + + private String gcpSecretId; + private String accessFile; + private String projectName; + private boolean realBackendIsUsed = false; + + @Override + public Map start() { + + gcpSecretId = "CQ-GCPTestSecret" + System.currentTimeMillis(); + String gcpSecretValue = "GCP secret value"; + + accessFile = System.getenv("GOOGLE_SERVICE_ACCOUNT_KEY"); + projectName = System.getenv("GOOGLE_PROJECT_NAME"); + + final boolean startMockBackend = MockBackendUtils.startMockBackend(false); + final boolean realCredentialsProvided = accessFile != null && !accessFile.isEmpty() && projectName != null + && !projectName.isEmpty(); + final boolean usingMockBackend = startMockBackend && !realCredentialsProvided; + + if (usingMockBackend) { + //try wiremock + throw new RuntimeException("Mocked test backend is not implemented yet"); + } else { + if (!startMockBackend && !realCredentialsProvided) { + throw new IllegalStateException( + "Set GOOGLE_PROJECT and GOOGLE_SERVICE_ACCOUNT_KEY env vars if you set CAMEL_QUARKUS_START_MOCK_BACKEND=false"); + } + MockBackendUtils.logRealBackendUsed(); + realBackendIsUsed = true; + //create secret for gcp + createSecret(gcpSecretId, gcpSecretValue, accessFile, projectName); + } + return Map.of("gcpSecretId", gcpSecretId, "gcpSecretValue", gcpSecretValue); + } + + static void createSecret(String secretId, String secretValue, String accessFile, String project) { + SecretManagerServiceClient client = null; + try (FileInputStream fis = new FileInputStream(accessFile)) { + + Credentials myCredentials = ServiceAccountCredentials.fromStream(fis); + SecretManagerServiceSettings settings = SecretManagerServiceSettings.newBuilder() + .setCredentialsProvider(FixedCredentialsProvider.create(myCredentials)).build(); + client = SecretManagerServiceClient.create(settings); + + Secret secret = Secret.newBuilder() + .setReplication( + Replication.newBuilder() + .setAutomatic(Replication.Automatic.newBuilder().build()) + .build()) + .build(); + + Secret createdSecret = client.createSecret(ProjectName.of(project), secretId, secret); + + SecretPayload payload = SecretPayload.newBuilder() + .setData(ByteString.copyFromUtf8(secretValue)).build(); + + client.addSecretVersion(createdSecret.getName(), payload); + + } catch (IOException e) { + LOG.error("Unsuccessful creation of secret (%s) for gcp. ".formatted(secretId)); + throw new RuntimeException(e); + } finally { + if (client != null) { + client.close(); + } + } + } + + static void deleteSecret(String secretId, String accessFile, String project) { + + SecretManagerServiceClient client = null; + try (FileInputStream fis = new FileInputStream(accessFile)) { + + Credentials myCredentials = ServiceAccountCredentials.fromStream(fis); + SecretManagerServiceSettings settings = SecretManagerServiceSettings.newBuilder() + .setCredentialsProvider(FixedCredentialsProvider.create(myCredentials)).build(); + client = SecretManagerServiceClient.create(settings); + + client.deleteSecret(SecretName.of(project, secretId)); + } catch (IOException e) { + LOG.error("Unsuccessful creation of secret (%s) for gcp. ".formatted(secretId)); + throw new RuntimeException(e); + } finally { + if (client != null) { + client.close(); + } + } + } + + @Override + public void stop() { + + if (realBackendIsUsed) { + deleteSecret(gcpSecretId, accessFile, projectName); + } + } +}