From 47732520cdbd128e530ed16d2e01ecd43208962f Mon Sep 17 00:00:00 2001 From: Tomas Langer Date: Fri, 30 Sep 2022 11:34:50 +0200 Subject: [PATCH 1/8] Vault integration test --- etc/scripts/test-integ-vault.sh | 0 pom.xml | 8 + .../helidon/reactive/webclient/WebClient.java | 24 ++- tests/integration/pom.xml | 7 +- .../vault/mp/TestKubernetesAuth.java | 179 ------------------ tests/integration/vault/pom.xml | 4 +- tests/integration/vault/se/pom.xml | 48 ++++- .../vault/hcp/reactive/K8sExample.java | 103 ---------- .../vault/hcp/reactive/ReactiveVaultMain.java | 32 +--- .../se/src/main/resources/application.yaml | 14 -- .../vault/hcp/reactive/VaultTest.java | 117 ++++++++++++ 11 files changed, 197 insertions(+), 339 deletions(-) mode change 100755 => 100644 etc/scripts/test-integ-vault.sh delete mode 100644 tests/integration/vault/mp/src/test/java/io/helidon/tests/integration/vault/mp/TestKubernetesAuth.java delete mode 100644 tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/K8sExample.java create mode 100644 tests/integration/vault/se/src/test/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultTest.java diff --git a/etc/scripts/test-integ-vault.sh b/etc/scripts/test-integ-vault.sh old mode 100755 new mode 100644 diff --git a/pom.xml b/pom.xml index a25cb230746..d10a7943961 100644 --- a/pom.xml +++ b/pom.xml @@ -75,6 +75,7 @@ 3.0.1 3.1.12 3.0.0-M5 + 1.17.4 7.5 2.12.5 5.0.11 @@ -1172,6 +1173,13 @@ classgraph ${version.lib.classgraph} + + org.testcontainers + testcontainers-bom + ${version.lib.testcontainers} + pom + import + diff --git a/reactive/webclient/webclient/src/main/java/io/helidon/reactive/webclient/WebClient.java b/reactive/webclient/webclient/src/main/java/io/helidon/reactive/webclient/WebClient.java index 0a445dad183..1e8073576ce 100644 --- a/reactive/webclient/webclient/src/main/java/io/helidon/reactive/webclient/WebClient.java +++ b/reactive/webclient/webclient/src/main/java/io/helidon/reactive/webclient/WebClient.java @@ -259,7 +259,17 @@ public Builder config(Config config) { * @return updated builder instance */ public Builder connectTimeout(long amount, TimeUnit unit) { - configuration.connectTimeout(Duration.of(amount, unit.toChronoUnit())); + return connectTimeout(Duration.of(amount, unit.toChronoUnit())); + } + + /** + * Sets new connection timeout. + * + * @param timeout connection timeout + * @return updated builder instance + */ + public Builder connectTimeout(Duration timeout) { + configuration.connectTimeout(timeout); return this; } @@ -271,7 +281,17 @@ public Builder connectTimeout(long amount, TimeUnit unit) { * @return updated builder instance */ public Builder readTimeout(long amount, TimeUnit unit) { - configuration.readTimeout(Duration.of(amount, unit.toChronoUnit())); + return readTimeout(Duration.of(amount, unit.toChronoUnit())); + } + + /** + * Sets new read timeout. + * + * @param timeout read timeout + * @return updated builder instance + */ + public Builder readTimeout(Duration timeout) { + configuration.readTimeout(timeout); return this; } diff --git a/tests/integration/pom.xml b/tests/integration/pom.xml index b6dad7d103e..3577ae9bc56 100644 --- a/tests/integration/pom.xml +++ b/tests/integration/pom.xml @@ -57,6 +57,7 @@ mp-bean-validation restclient native-image + vault @@ -75,11 +76,5 @@ dbclient - - vault-it - - vault - - diff --git a/tests/integration/vault/mp/src/test/java/io/helidon/tests/integration/vault/mp/TestKubernetesAuth.java b/tests/integration/vault/mp/src/test/java/io/helidon/tests/integration/vault/mp/TestKubernetesAuth.java deleted file mode 100644 index b9b9bb5dc4a..00000000000 --- a/tests/integration/vault/mp/src/test/java/io/helidon/tests/integration/vault/mp/TestKubernetesAuth.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. - * - * Licensed 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 io.helidon.tests.integration.vault.mp; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Map; -import java.util.Optional; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.inject.literal.NamedLiteral; -import jakarta.enterprise.inject.se.SeContainer; -import jakarta.inject.Inject; - -import io.helidon.config.mp.MpConfigSources; -import io.helidon.config.yaml.mp.YamlMpConfigSource; -import io.helidon.integrations.vault.Vault; -import io.helidon.integrations.vault.auths.k8s.ConfigureK8s; -import io.helidon.integrations.vault.auths.k8s.CreateRole; -import io.helidon.integrations.vault.auths.k8s.K8sAuthRx; -import io.helidon.integrations.vault.cdi.VaultCdiExtension; -import io.helidon.integrations.vault.cdi.VaultName; -import io.helidon.integrations.vault.cdi.VaultPath; -import io.helidon.integrations.vault.secrets.kv2.Kv2Secret; -import io.helidon.integrations.vault.secrets.kv2.Kv2Secrets; -import io.helidon.integrations.vault.secrets.kv2.Kv2SecretsRx; -import io.helidon.integrations.vault.sys.EnableEngine; -import io.helidon.integrations.vault.sys.Sys; -import io.helidon.microprofile.config.ConfigCdiExtension; -import io.helidon.microprofile.tests.junit5.AddBean; -import io.helidon.microprofile.tests.junit5.AddExtension; -import io.helidon.microprofile.tests.junit5.Configuration; -import io.helidon.microprofile.tests.junit5.DisableDiscovery; -import io.helidon.microprofile.tests.junit5.HelidonTest; - -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.spi.ConfigProviderResolver; -import org.eclipse.microprofile.config.spi.ConfigSource; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; - -import static io.helidon.common.testing.junit5.OptionalMatcher.optionalEmpty; -import static io.helidon.common.testing.junit5.OptionalMatcher.optionalPresent; -import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.MatcherAssert.assertThat; - -@HelidonTest(resetPerTest = true) -@DisableDiscovery -@AddExtension(VaultCdiExtension.class) -@AddExtension(ConfigCdiExtension.class) -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -@Configuration(useExisting = true) -class TestKubernetesAuth { - @BeforeAll - static void setupConfig() { - ConfigProviderResolver resolver = ConfigProviderResolver.instance(); - Config config = resolver.getBuilder() - .withSources(MpConfigSources.systemProperties()) - .withSources(MpConfigSources.create(Map.of("mp.initializer.allow", "true"))) - .withSources(YamlMpConfigSource.classPath("vault-application.yaml").toArray(new ConfigSource[0])) - .build(); - resolver.registerConfig(config, null); - } - - @Test - @Order(0) - void setupK8s(SeContainer container) { - Sys sys = container.select(Sys.class).get(); - Vault vault = container.select(Vault.class).get(); - sys.enableAuth(K8sAuthRx.AUTH_METHOD); - - K8sAuthRx k8sAuth = vault.auth(K8sAuthRx.AUTH_METHOD); - - //kubernetes.default.svc - k8sAuth.configure(ConfigureK8s.Request.builder() - .address("https://10.96.0.1")) - .await(); - - sys.createPolicy("admin", VaultPolicy.POLICY); - k8sAuth.createRole(CreateRole.Request.builder() - .roleName("my-role") - .addBoundServiceAccountName("*") - .addBoundServiceAccountNamespace(findNamespace()) - .addTokenPolicy("admin")) - .await(); - } - - private static String findNamespace() { - Path path = Paths.get("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); - if (Files.exists(path)) { - try { - return Files.readString(path); - } catch (IOException e) { - throw new IllegalStateException("Could not read k8s namespace", e); - } - } - return "default"; - } - - @Test - @Order(1) - void initializeEngines(SeContainer container) { - Sys sys = container.select(Sys.class, NamedLiteral.of("k8s")).get(); - sys.enableEngine(EnableEngine.Request.builder() - .engine(Kv2SecretsRx.ENGINE) - .path("kv")); - } - - @Test - @Order(2) - @AddBean(SecretsHolder.class) - void testSecrets(SeContainer container) { - Kv2Secrets secrets = container.select(SecretsHolder.class).get().secrets(); - secrets.create("nested/path/secret", Map.of("first", "a value")); - - try { - Optional maybeSecret = secrets.get("nested/path/secret"); - assertThat(maybeSecret, is(optionalPresent())); - Kv2Secret theSecret = maybeSecret.get(); - assertThat(theSecret.value("first"), optionalValue(is("a value"))); - assertThat(theSecret.metadata().version(), is(1)); - assertThat(theSecret.metadata().deleted(), is(false)); - assertThat(theSecret.metadata().destroyed(), is(false)); - assertThat(theSecret.metadata().deletedTime(), is(optionalEmpty())); - assertThat(theSecret.metadata().createdTime(), notNullValue()); - } finally { - // we want to remote the secret even if validation fails - // delete latest version - secrets.delete("nested/path/secret", 1); - // destroy it - secrets.destroy("nested/path/secret", 1); - // delete all versions and history for this secret - secrets.deleteAll("nested/path/secret"); - } - } - - @Test - @Order(100) - void removeEngines(SeContainer container) { - // using token authentication again - Sys sys = container.select(Sys.class).get(); - sys.disableEngine("kv"); - sys.deletePolicy("admin"); - sys.disableAuth(K8sAuthRx.AUTH_METHOD.defaultPath()); - } - - @ApplicationScoped - private static class SecretsHolder { - @Inject - @VaultPath("kv") - @VaultName("k8s") - Kv2Secrets secrets; - - Kv2Secrets secrets() { - return secrets; - } - } -} diff --git a/tests/integration/vault/pom.xml b/tests/integration/vault/pom.xml index 6fcb81cde60..8e3c258059a 100644 --- a/tests/integration/vault/pom.xml +++ b/tests/integration/vault/pom.xml @@ -30,7 +30,9 @@ helidon-tests-integration-vault-project Helidon Integration Tests Vault Project - Tests for Hashicorp Vault, using pod with Vault and mysql to test all options + + Tests for Hashicorp Vault, requires Vault and mysql to test most options, except for k8s integration + se diff --git a/tests/integration/vault/se/pom.xml b/tests/integration/vault/se/pom.xml index 4009208c1ed..99643622f8a 100644 --- a/tests/integration/vault/se/pom.xml +++ b/tests/integration/vault/se/pom.xml @@ -56,10 +56,6 @@ io.helidon.integrations.vault.auths helidon-integrations-vault-auths-approle - - io.helidon.integrations.vault.auths - helidon-integrations-vault-auths-k8s - io.helidon.integrations.vault.secrets helidon-integrations-vault-secrets-kv1 @@ -76,10 +72,54 @@ io.helidon.integrations.vault.secrets helidon-integrations-vault-secrets-transit + + io.helidon.integrations.vault.secrets + helidon-integrations-vault-secrets-database + io.helidon.integrations.vault.sys helidon-integrations-vault-sys + + org.junit.jupiter + junit-jupiter-api + test + + + org.testcontainers + junit-jupiter + test + + + org.hamcrest + hamcrest-all + test + + + org.testcontainers + mysql + test + + + org.testcontainers + vault + test + + + org.slf4j + slf4j-jdk14 + test + + + mysql + mysql-connector-java + test + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + diff --git a/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/K8sExample.java b/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/K8sExample.java deleted file mode 100644 index e934b8160b4..00000000000 --- a/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/K8sExample.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2021 Oracle and/or its affiliates. - * - * Licensed 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 io.helidon.examples.integrations.vault.hcp.reactive; - -import java.util.Map; -import java.util.function.Function; - -import io.helidon.common.reactive.Single; -import io.helidon.config.Config; -import io.helidon.integrations.common.rest.ApiResponse; -import io.helidon.integrations.vault.Vault; -import io.helidon.integrations.vault.auths.k8s.ConfigureK8s; -import io.helidon.integrations.vault.auths.k8s.CreateRole; -import io.helidon.integrations.vault.auths.k8s.K8sAuthRx; -import io.helidon.integrations.vault.secrets.kv2.Kv2Secret; -import io.helidon.integrations.vault.secrets.kv2.Kv2SecretsRx; -import io.helidon.integrations.vault.sys.SysRx; - -class K8sExample { - private static final String SECRET_PATH = "k8s/example/secret"; - private static final String POLICY_NAME = "k8s_policy"; - - private final Vault tokenVault; - private final String k8sAddress; - private final Config config; - private final SysRx sys; - - private Vault k8sVault; - - K8sExample(Vault tokenVault, Config config) { - this.tokenVault = tokenVault; - this.sys = tokenVault.sys(SysRx.API); - this.k8sAddress = config.get("cluster-address").asString().get(); - this.config = config; - } - - public Single run() { - /* - The following tasks must be run before we authenticate - */ - return enableK8sAuth() - // Now we can login using k8s - must run within a k8s cluster (or you need the k8s configuration files locally) - .flatMapSingle(ignored -> workWithSecrets()) - // Now back to token based Vault, as we will clean up - .flatMapSingle(ignored -> disableK8sAuth()) - .map(ignored -> "k8s example finished successfully."); - } - - private Single workWithSecrets() { - Kv2SecretsRx secrets = k8sVault.secrets(Kv2SecretsRx.ENGINE); - - return secrets.create(SECRET_PATH, Map.of("secret-key", "secretValue", - "secret-user", "username")) - .flatMapSingle(ignored -> secrets.get(SECRET_PATH)) - .peek(secret -> { - if (secret.isPresent()) { - Kv2Secret kv2Secret = secret.get(); - System.out.println("k8s first secret: " + kv2Secret.value("secret-key")); - System.out.println("k8s second secret: " + kv2Secret.value("secret-user")); - } else { - System.out.println("k8s secret not found"); - } - }).flatMapSingle(ignored -> secrets.deleteAll(SECRET_PATH)); - } - - private Single disableK8sAuth() { - return sys.deletePolicy(POLICY_NAME) - .flatMapSingle(ignored -> sys.disableAuth(K8sAuthRx.AUTH_METHOD.defaultPath())); - } - - private Single enableK8sAuth() { - // enable the method - return sys.enableAuth(K8sAuthRx.AUTH_METHOD) - // add policy - .flatMapSingle(ignored -> sys.createPolicy(POLICY_NAME, VaultPolicy.POLICY)) - .flatMapSingle(ignored -> tokenVault.auth(K8sAuthRx.AUTH_METHOD) - .configure(ConfigureK8s.Request.builder() - .address(k8sAddress))) - .flatMapSingle(ignored -> tokenVault.auth(K8sAuthRx.AUTH_METHOD) - // this must be the same role name as is defined in application.yaml - .createRole(CreateRole.Request.builder() - .roleName("my-role") - .addBoundServiceAccountName("*") - .addBoundServiceAccountNamespace("default") - .addTokenPolicy(POLICY_NAME))) - .peek(ignored -> k8sVault = Vault.create(config)) - .map(Function.identity()); - } -} diff --git a/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/ReactiveVaultMain.java b/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/ReactiveVaultMain.java index 4f9b53da9ab..451514b6ddc 100644 --- a/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/ReactiveVaultMain.java +++ b/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/ReactiveVaultMain.java @@ -47,13 +47,9 @@ private ReactiveVaultMain() { public static void main(String[] args) { LogConfig.configureRuntime(); - // as I cannot share my secret configuration, let's combine the configuration - // from my home directory with the one compiled into the jar - // when running this example, you can either update the application.yaml in resources directory - // or use the same approach - Config config = buildConfig(); + Config config = Config.create(); - // we have three configurations available + // we have two configurations available // 1. Token based authentication Vault tokenVault = Vault.builder() .config(config.get("vault.token")) @@ -62,16 +58,9 @@ public static void main(String[] args) { .build(); // 2. App role based authentication - must be created after we obtain the role id an token - // 3. Kubernetes (k8s) based authentication (requires to run on k8s) - must be created after we create - // the authentication method - // the tokenVault is using the root token and can be used to enable engines and // other authentication mechanisms - CompletionAwaitable k8sFuture = new K8sExample(tokenVault, config.get("vault.k8s")) - .run() - .forSingle(System.out::println); - CompletionAwaitable appRoleFuture = new AppRoleExample(tokenVault, config.get("vault.approle")) .run() .forSingle(System.out::println); @@ -101,13 +90,6 @@ public static void main(String[] args) { e.printStackTrace(); } - try { - k8sFuture.await(); - } catch (Exception e) { - System.err.println("Kubernetes example failed"); - e.printStackTrace(); - } - String baseAddress = "http://localhost:" + webServer.port() + "/"; System.out.println("Server started on " + baseAddress); System.out.println(); @@ -135,14 +117,4 @@ public static void main(String[] args) { System.out.println("\tcurl -i -X DELETE " + baseAddress + "transit/keys"); System.out.println("\t" + baseAddress + "transit/disable"); } - - private static Config buildConfig() { - return Config.builder() - .sources( - // you can use this file to override the defaults that are built-in - file(System.getProperty("user.home") + "/helidon/conf/examples.yaml").optional(), - // in jar file (see src/main/resources/application.yaml) - classpath("application.yaml")) - .build(); - } } diff --git a/tests/integration/vault/se/src/main/resources/application.yaml b/tests/integration/vault/se/src/main/resources/application.yaml index 294350d518b..5fe7c28a61f 100644 --- a/tests/integration/vault/se/src/main/resources/application.yaml +++ b/tests/integration/vault/se/src/main/resources/application.yaml @@ -19,20 +19,6 @@ server.port: 8080 vault: properties: address: "http://localhost:8200" - k8s: - address: "${vault.properties.address}" - # please change this to your k8s cluster address - cluster-address: "https://kubernetes.docker.internal:6443" - auth: - k8s: - enabled: true - # this role is created in the code, must be the same value - token-role: my-role - service-account-token: "${vault.properties.k8s.service-account-token}" - app-role: - enabled: false - token: - enabled: false token: token: "myroot" address: "${vault.properties.address}" diff --git a/tests/integration/vault/se/src/test/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultTest.java b/tests/integration/vault/se/src/test/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultTest.java new file mode 100644 index 00000000000..9e6519b7d7d --- /dev/null +++ b/tests/integration/vault/se/src/test/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultTest.java @@ -0,0 +1,117 @@ +package io.helidon.examples.integrations.vault.hcp.reactive; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import io.helidon.common.http.Http; +import io.helidon.integrations.vault.Secret; +import io.helidon.integrations.vault.Vault; +import io.helidon.integrations.vault.secrets.cubbyhole.CreateCubbyhole; +import io.helidon.integrations.vault.secrets.cubbyhole.CubbyholeSecretsRx; +import io.helidon.integrations.vault.secrets.cubbyhole.DeleteCubbyhole; +import io.helidon.logging.common.LogConfig; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.vault.VaultContainer; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalPresent; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@Testcontainers(disabledWithoutDocker = true) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class VaultTest { + private static final Duration TIMEOUT = Duration.ofSeconds(5); + private static final DockerImageName MYSQL_IMAGE = DockerImageName.parse("mysql:8"); + private static final DockerImageName HCP_VAULT_IMAGE = DockerImageName.parse("vault:1.11.3"); + + @Container + private static final MySQLContainer mySql = new MySQLContainer(MYSQL_IMAGE) + .withUsername("user") + .withPassword("password") + .withDatabaseName("pokemon"); + + @Container + private static final VaultContainer vaultContainer = new VaultContainer<>(HCP_VAULT_IMAGE) + .withVaultToken("myroot") + .dependsOn(mySql); + + // it seems that test containers run each method on a different test class instance + private static Vault tokenVault; + + @BeforeAll + static void setup() { + LogConfig.configureRuntime(); + } + + @Test + @Order(1) + void validateContainers() { + assertThat("MySQL must be running", mySql.isRunning(), is(true)); + assertThat("Vault must be running", vaultContainer.isRunning(), is(true)); + } + + @Test + @Order(2) + void prepareVaultAndSecrets() { + tokenVault = Vault.builder() + .address("http://localhost:" + vaultContainer.getMappedPort(8200)) + .token("myroot") + .updateWebClient(it -> it.connectTimeout(TIMEOUT) + .readTimeout(TIMEOUT)) + .build(); + // TODO approle + // vaultSys = tokenVault.sys(SysRx.API); + // + // kv1 = tokenVault.secrets(Kv1SecretsRx.ENGINE); + // kv2 = tokenVault.secrets(Kv2SecretsRx.ENGINE); + // database = tokenVault.secrets(DbSecretsRx.ENGINE); + // transit = tokenVault.secrets(TransitSecretsRx.ENGINE); + } + + @Test + @Order(3) + void testCubbyhole() { + CubbyholeSecretsRx cubbyhole = tokenVault.secrets(CubbyholeSecretsRx.ENGINE); + String secretPath = "first/secret"; + + // create secret + CreateCubbyhole.Response createResponse = cubbyhole.create(secretPath, Map.of("key", "secretValue")).await(TIMEOUT); + assertThat("Create secret, got response status: " + createResponse.status(), + createResponse.status().family(), + is(Http.Status.Family.SUCCESSFUL)); + + // get secret + Optional maybeSecret = cubbyhole.get(secretPath) + .await(TIMEOUT); + + assertThat(maybeSecret, optionalPresent()); + Secret secret = maybeSecret.get(); + assertThat(secret.path(), is(secretPath)); + assertThat(secret.values(), is(Map.of("key", "secretValue"))); + + DeleteCubbyhole.Response deleteResponse = cubbyhole.delete(secretPath).await(TIMEOUT); + assertThat("Delete secret, got response status: " + deleteResponse.status(), + deleteResponse.status().family(), + is(Http.Status.Family.SUCCESSFUL)); + + List secretList = cubbyhole.list().await(TIMEOUT); + assertThat(secretList, is(List.of())); + } + + @Test + @Order(3) + void testDatabase() { + int mysqlPort = mySql.getMappedPort(3306); + } +} From 1db71b37493f2906494624711ed64819175ad5b6 Mon Sep 17 00:00:00 2001 From: Tomas Langer Date: Fri, 30 Sep 2022 16:07:50 +0200 Subject: [PATCH 2/8] Vault integration test based on testcontainers --- tests/integration/vault/mp/README.md | 26 - tests/integration/vault/mp/pom.xml | 109 ---- .../integration/vault/mp/VaultPolicy.java | 79 --- .../mp/src/main/resources/META-INF/beans.xml | 25 - .../mp/src/main/resources/logging.properties | 27 - .../integration/vault/mp/TestDbSecrets.java | 152 ----- .../src/test/resources/vault-application.yaml | 52 -- tests/integration/vault/pom.xml | 96 +++- tests/integration/vault/se/README.md | 26 - tests/integration/vault/se/pom.xml | 138 ----- .../vault/hcp/reactive/AppRoleExample.java | 124 ----- .../vault/hcp/reactive/CubbyholeService.java | 65 --- .../vault/hcp/reactive/Kv1Service.java | 87 --- .../vault/hcp/reactive/Kv2Service.java | 75 --- .../vault/hcp/reactive/ReactiveVaultMain.java | 120 ---- .../vault/hcp/reactive/TransitService.java | 193 ------- .../vault/hcp/reactive/VaultTest.java | 117 ---- .../vault/hcp/reactive/VaultPolicy.java | 0 .../vault/hcp/reactive/VaultTest.java | 527 ++++++++++++++++++ .../test}/resources/logging.properties | 0 20 files changed, 613 insertions(+), 1425 deletions(-) delete mode 100644 tests/integration/vault/mp/README.md delete mode 100644 tests/integration/vault/mp/pom.xml delete mode 100644 tests/integration/vault/mp/src/main/java/io/helidon/tests/integration/vault/mp/VaultPolicy.java delete mode 100644 tests/integration/vault/mp/src/main/resources/META-INF/beans.xml delete mode 100644 tests/integration/vault/mp/src/main/resources/logging.properties delete mode 100644 tests/integration/vault/mp/src/test/java/io/helidon/tests/integration/vault/mp/TestDbSecrets.java delete mode 100644 tests/integration/vault/mp/src/test/resources/vault-application.yaml delete mode 100644 tests/integration/vault/se/README.md delete mode 100644 tests/integration/vault/se/pom.xml delete mode 100644 tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/AppRoleExample.java delete mode 100644 tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/CubbyholeService.java delete mode 100644 tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/Kv1Service.java delete mode 100644 tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/Kv2Service.java delete mode 100644 tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/ReactiveVaultMain.java delete mode 100644 tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/TransitService.java delete mode 100644 tests/integration/vault/se/src/test/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultTest.java rename tests/integration/vault/{se/src/main => src/test}/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultPolicy.java (100%) create mode 100644 tests/integration/vault/src/test/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultTest.java rename tests/integration/vault/{se/src/main => src/test}/resources/logging.properties (100%) diff --git a/tests/integration/vault/mp/README.md b/tests/integration/vault/mp/README.md deleted file mode 100644 index 3e2aa9c8b90..00000000000 --- a/tests/integration/vault/mp/README.md +++ /dev/null @@ -1,26 +0,0 @@ -HCP Vault Integration with Reactive APIs ---- - -This example expects an empty Vault. It uses the token to create all required resources. - -To run this example: - -1. Run a docker image with a known root token - -```shell -docker run --cap-add=IPC_LOCK -e VAULT_DEV_ROOT_TOKEN_ID=myroot -d --name=vault -p8200:8200 vault -``` - -2. Build this application - -```shell -mvn clean package -``` - -3. Start this application - -```shell -java -jar ./target/ -``` - -4. Exercise the endpoints \ No newline at end of file diff --git a/tests/integration/vault/mp/pom.xml b/tests/integration/vault/mp/pom.xml deleted file mode 100644 index ea1131adee0..00000000000 --- a/tests/integration/vault/mp/pom.xml +++ /dev/null @@ -1,109 +0,0 @@ - - - - - 4.0.0 - - io.helidon.tests.integration.vault - helidon-tests-integration-vault-project - 4.0.0-SNAPSHOT - - - helidon-tests-integration-vault-mp - - Helidon Integration Tests Vault MP - Integration test with Vault. - - - - io.helidon.microprofile.bundles - helidon-microprofile - - - io.helidon.integrations.vault - helidon-integrations-vault-cdi - - - io.helidon.config - helidon-config-yaml-mp - - - io.helidon.integrations.vault.auths - helidon-integrations-vault-auths-token - - - io.helidon.integrations.vault.auths - helidon-integrations-vault-auths-approle - - - io.helidon.integrations.vault.auths - helidon-integrations-vault-auths-k8s - - - io.helidon.integrations.vault.secrets - helidon-integrations-vault-secrets-kv1 - - - io.helidon.integrations.vault.secrets - helidon-integrations-vault-secrets-kv2 - - - io.helidon.integrations.vault.secrets - helidon-integrations-vault-secrets-database - - - io.helidon.integrations.vault.secrets - helidon-integrations-vault-secrets-cubbyhole - - - io.helidon.integrations.vault.secrets - helidon-integrations-vault-secrets-transit - - - io.helidon.integrations.vault.sys - helidon-integrations-vault-sys - - - mysql - mysql-connector-java - test - - - org.hamcrest - hamcrest-all - test - - - org.junit.jupiter - junit-jupiter-api - test - - - io.helidon.microprofile.tests - helidon-microprofile-tests-junit5 - test - - - io.helidon.config - helidon-config-testing - test - - - diff --git a/tests/integration/vault/mp/src/main/java/io/helidon/tests/integration/vault/mp/VaultPolicy.java b/tests/integration/vault/mp/src/main/java/io/helidon/tests/integration/vault/mp/VaultPolicy.java deleted file mode 100644 index 0a2aefd7978..00000000000 --- a/tests/integration/vault/mp/src/main/java/io/helidon/tests/integration/vault/mp/VaultPolicy.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2021 Oracle and/or its affiliates. - * - * Licensed 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 io.helidon.tests.integration.vault.mp; - -final class VaultPolicy { - private VaultPolicy() { - } - static final String POLICY = "# Enable and manage authentication methods\n" - + "path \"auth/*\"\n" - + "{\n" - + " capabilities = [\"create\", \"update\", \"delete\", \"sudo\"]\n" - + "}\n" - + "\n" - + "# Create, update, and delete auth methods\n" - + "path \"sys/auth/*\"\n" - + "{\n" - + " capabilities = [\"create\", \"update\", \"delete\", \"sudo\"]\n" - + "}\n" - + "\n" - + "# List auth methods\n" - + "path \"sys/auth\"\n" - + "{\n" - + " capabilities = [\"read\"]\n" - + "}\n" - + "\n" - + "# Enable and manage the key/value secrets engine at `secret/` path\n" - + "\n" - + "# List, create, update, and delete key/value secrets\n" - + "path \"secret/*\"\n" - + "{\n" - + " capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\", \"sudo\"]\n" - + "}\n" - + "\n" - + "path \"kv1/*\"\n" - + "{\n" - + " capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\", \"sudo\"]\n" - + "}\n" - + "\n" - + "path \"cubbyhole/*\"\n" - + "{\n" - + " capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\", \"sudo\"]\n" - + "}\n" - + "\n" - + "path \"database/*\"\n" - + "{\n" - + " capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\", \"sudo\"]\n" - + "}\n" - + "\n" - + "path \"kv/*\"\n" - + "{\n" - + " capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\", \"sudo\"]\n" - + "}\n" - + "\n" - + "# Manage secrets engines\n" - + "path \"sys/mounts/*\"\n" - + "{\n" - + " capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\", \"sudo\"]\n" - + "}\n" - + "\n" - + "# List existing secrets engines.\n" - + "path \"sys/mounts\"\n" - + "{\n" - + " capabilities = [\"read\"]\n" - + "}\n"; -} diff --git a/tests/integration/vault/mp/src/main/resources/META-INF/beans.xml b/tests/integration/vault/mp/src/main/resources/META-INF/beans.xml deleted file mode 100644 index dbf3e648c1e..00000000000 --- a/tests/integration/vault/mp/src/main/resources/META-INF/beans.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - diff --git a/tests/integration/vault/mp/src/main/resources/logging.properties b/tests/integration/vault/mp/src/main/resources/logging.properties deleted file mode 100644 index f8802f90626..00000000000 --- a/tests/integration/vault/mp/src/main/resources/logging.properties +++ /dev/null @@ -1,27 +0,0 @@ -# -# Copyright (c) 2021, 2022 Oracle and/or its affiliates. -# -# Licensed 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. -# - -# Send messages to the console -handlers=io.helidon.logging.jul.HelidonConsoleHandler - -# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread -java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n - -# Global logging level. Can be overridden by specific loggers -.level=INFO - -io.helidon.level=INFO -io.helidon.integrations.level=INFO diff --git a/tests/integration/vault/mp/src/test/java/io/helidon/tests/integration/vault/mp/TestDbSecrets.java b/tests/integration/vault/mp/src/test/java/io/helidon/tests/integration/vault/mp/TestDbSecrets.java deleted file mode 100644 index 6c5d1cc2505..00000000000 --- a/tests/integration/vault/mp/src/test/java/io/helidon/tests/integration/vault/mp/TestDbSecrets.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. - * - * Licensed 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 io.helidon.tests.integration.vault.mp; - -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; -import java.time.Duration; -import java.util.Map; -import java.util.Optional; - -import jakarta.inject.Inject; - -import io.helidon.config.mp.MpConfigSources; -import io.helidon.config.yaml.mp.YamlMpConfigSource; -import io.helidon.integrations.vault.cdi.VaultCdiExtension; -import io.helidon.integrations.vault.secrets.database.DbCreateRole; -import io.helidon.integrations.vault.secrets.database.DbCredentials; -import io.helidon.integrations.vault.secrets.database.DbSecrets; -import io.helidon.integrations.vault.secrets.database.MySqlConfigureRequest; -import io.helidon.integrations.vault.sys.Sys; -import io.helidon.microprofile.config.ConfigCdiExtension; -import io.helidon.microprofile.tests.junit5.AddExtension; -import io.helidon.microprofile.tests.junit5.Configuration; -import io.helidon.microprofile.tests.junit5.DisableDiscovery; -import io.helidon.microprofile.tests.junit5.HelidonTest; - -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.eclipse.microprofile.config.spi.ConfigProviderResolver; -import org.eclipse.microprofile.config.spi.ConfigSource; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; - -import static io.helidon.common.testing.junit5.OptionalMatcher.optionalPresent; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.MatcherAssert.assertThat; - -@HelidonTest -@DisableDiscovery -@AddExtension(VaultCdiExtension.class) -@AddExtension(ConfigCdiExtension.class) -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -@Configuration(useExisting = true) -class TestDbSecrets { - @Inject - DbSecrets db; - - @Inject - Sys vault; - - @Inject - @ConfigProperty(name = "vault.db.host", defaultValue = "localhost") - String dbHost; - - @BeforeAll - static void setupConfig() { - ConfigProviderResolver resolver = ConfigProviderResolver.instance(); - Config config = resolver.getBuilder() - .withSources(MpConfigSources.systemProperties()) - .withSources(MpConfigSources.create(Map.of("mp.initializer.allow", "true"))) - .withSources(YamlMpConfigSource.classPath("vault-application.yaml").toArray(new ConfigSource[0])) - .build(); - resolver.registerConfig(config, null); - } - - @Test - @Order(1) - void initializeEngines() { - vault.enableEngine(DbSecrets.ENGINE); - } - - @Test - @Order(2) - void testAddDatabase() { - db.configure(MySqlConfigureRequest.builder("{{username}}:{{password}}@tcp(" + dbHost + ":3306)/") - .name("mysql") - .username("root") - .password("root") - .maxOpenConnections(5) - .maxConnectionLifetime(Duration.ofMinutes(1)) - .addAllowedRole("readonly")); - } - - @Test - @Order(3) - void testAddRole() { - db.createRole(DbCreateRole.Request.builder() - .name("readonly") - .dbName("mysql") - .addCreationStatement("CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'") - .addCreationStatement("GRANT SELECT ON *.* TO '{{name}}'@'%'")); - } - - @Test - @Order(4) - void testGetSecrets() throws SQLException { - Optional maybeCreds = db.get("readonly"); - - assertThat(maybeCreds, is(optionalPresent())); - - DbCredentials mysql = maybeCreds.get(); - - String username = mysql.username(); - String password = mysql.password(); - - String address = "jdbc:mysql://localhost:3306/"; - com.mysql.jdbc.Driver.class.getName(); - try (Connection conn = DriverManager.getConnection(address, username, password)) { - assertThat(conn, notNullValue()); - } - } - - @Test - @Order(5) - void testRemoveRole() { - db.deleteRole("readonly"); - } - - @Test - @Order(6) - @Disabled - void testRemoveDatabase() { - // we cannot do this, as then the credentials failed to get revoked when we disable the engine - db.delete("mysql"); - } - - @Test - @Order(100) - void removeEngines() { - vault.disableEngine(DbSecrets.ENGINE); - } -} diff --git a/tests/integration/vault/mp/src/test/resources/vault-application.yaml b/tests/integration/vault/mp/src/test/resources/vault-application.yaml deleted file mode 100644 index fcc73dcec49..00000000000 --- a/tests/integration/vault/mp/src/test/resources/vault-application.yaml +++ /dev/null @@ -1,52 +0,0 @@ -# -# Copyright (c) 2021 Oracle and/or its affiliates. -# -# Licensed 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. -# - -server.port: 8080 - -vault: - default: - address: "http://localhost:8200" - token: "myroot" - auth: - app-role: - enabled: false - token: - enabled: true - k8s: - enabled: false - app-role: - address: "http://localhost:8200" - auth: - app-role: - enabled: true - role-id: "${vault.properties.app-role.role-id}" - secret-id: "${vault.properties.app-role.secret-id}" - k8s: - enabled: false - token: - enabled: false - k8s: - address: "http://localhost:8200" - auth: - app-role: - enabled: false - k8s: - enabled: true - # this role is created in the code, must be the same value - token-role: my-role - token: - enabled: false - diff --git a/tests/integration/vault/pom.xml b/tests/integration/vault/pom.xml index 8e3c258059a..7d697e32114 100644 --- a/tests/integration/vault/pom.xml +++ b/tests/integration/vault/pom.xml @@ -17,25 +17,101 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + https://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 helidon-tests-integration io.helidon.tests.integration 4.0.0-SNAPSHOT - 4.0.0 - pom io.helidon.tests.integration.vault - helidon-tests-integration-vault-project - Helidon Integration Tests Vault Project + helidon-tests-integration-vault + Helidon Integration Tests Vault - Tests for Hashicorp Vault, requires Vault and mysql to test most options, except for k8s integration + Tests for Hashicorp Vault, requires Vault and mysql to test most options, except for k8s integration. + Based on test containers - - se - mp - + + + + io.helidon.integrations.vault + helidon-integrations-vault + + + io.helidon.integrations.vault.auths + helidon-integrations-vault-auths-token + + + io.helidon.integrations.vault.auths + helidon-integrations-vault-auths-approle + + + io.helidon.integrations.vault.secrets + helidon-integrations-vault-secrets-kv1 + + + io.helidon.integrations.vault.secrets + helidon-integrations-vault-secrets-kv2 + + + io.helidon.integrations.vault.secrets + helidon-integrations-vault-secrets-cubbyhole + + + io.helidon.integrations.vault.secrets + helidon-integrations-vault-secrets-transit + + + io.helidon.integrations.vault.secrets + helidon-integrations-vault-secrets-database + + + io.helidon.integrations.vault.sys + helidon-integrations-vault-sys + + + org.junit.jupiter + junit-jupiter-api + test + + + org.testcontainers + junit-jupiter + test + + + org.hamcrest + hamcrest-all + test + + + org.testcontainers + mysql + test + + + org.testcontainers + vault + test + + + org.slf4j + slf4j-jdk14 + test + + + mysql + mysql-connector-java + test + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + \ No newline at end of file diff --git a/tests/integration/vault/se/README.md b/tests/integration/vault/se/README.md deleted file mode 100644 index 3e2aa9c8b90..00000000000 --- a/tests/integration/vault/se/README.md +++ /dev/null @@ -1,26 +0,0 @@ -HCP Vault Integration with Reactive APIs ---- - -This example expects an empty Vault. It uses the token to create all required resources. - -To run this example: - -1. Run a docker image with a known root token - -```shell -docker run --cap-add=IPC_LOCK -e VAULT_DEV_ROOT_TOKEN_ID=myroot -d --name=vault -p8200:8200 vault -``` - -2. Build this application - -```shell -mvn clean package -``` - -3. Start this application - -```shell -java -jar ./target/ -``` - -4. Exercise the endpoints \ No newline at end of file diff --git a/tests/integration/vault/se/pom.xml b/tests/integration/vault/se/pom.xml deleted file mode 100644 index 99643622f8a..00000000000 --- a/tests/integration/vault/se/pom.xml +++ /dev/null @@ -1,138 +0,0 @@ - - - - - 4.0.0 - - io.helidon.tests.integration.vault - helidon-tests-integration-vault-project - 4.0.0-SNAPSHOT - - - helidon-tests-integration-vault-se - - Helidon Integration Tests Vault SE - Integration test with Vault. - - - io.helidon.examples.integrations.vault.hcp.reactive.ReactiveVaultMain - - - - - io.helidon.reactive.webserver - helidon-reactive-webserver - - - io.helidon.integrations.vault - helidon-integrations-vault - - - io.helidon.config - helidon-config-yaml - - - io.helidon.integrations.vault.auths - helidon-integrations-vault-auths-token - - - io.helidon.integrations.vault.auths - helidon-integrations-vault-auths-approle - - - io.helidon.integrations.vault.secrets - helidon-integrations-vault-secrets-kv1 - - - io.helidon.integrations.vault.secrets - helidon-integrations-vault-secrets-kv2 - - - io.helidon.integrations.vault.secrets - helidon-integrations-vault-secrets-cubbyhole - - - io.helidon.integrations.vault.secrets - helidon-integrations-vault-secrets-transit - - - io.helidon.integrations.vault.secrets - helidon-integrations-vault-secrets-database - - - io.helidon.integrations.vault.sys - helidon-integrations-vault-sys - - - org.junit.jupiter - junit-jupiter-api - test - - - org.testcontainers - junit-jupiter - test - - - org.hamcrest - hamcrest-all - test - - - org.testcontainers - mysql - test - - - org.testcontainers - vault - test - - - org.slf4j - slf4j-jdk14 - test - - - mysql - mysql-connector-java - test - - - io.helidon.common.testing - helidon-common-testing-junit5 - test - - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - copy-libs - - - - - - diff --git a/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/AppRoleExample.java b/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/AppRoleExample.java deleted file mode 100644 index f178e34177d..00000000000 --- a/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/AppRoleExample.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (c) 2021 Oracle and/or its affiliates. - * - * Licensed 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 io.helidon.examples.integrations.vault.hcp.reactive; - -import java.time.Duration; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; - -import io.helidon.common.reactive.Single; -import io.helidon.config.Config; -import io.helidon.integrations.common.rest.ApiResponse; -import io.helidon.integrations.vault.Vault; -import io.helidon.integrations.vault.auths.approle.AppRoleAuthRx; -import io.helidon.integrations.vault.auths.approle.AppRoleVaultAuth; -import io.helidon.integrations.vault.auths.approle.CreateAppRole; -import io.helidon.integrations.vault.auths.approle.GenerateSecretId; -import io.helidon.integrations.vault.secrets.kv2.Kv2Secret; -import io.helidon.integrations.vault.secrets.kv2.Kv2SecretsRx; -import io.helidon.integrations.vault.sys.SysRx; - -class AppRoleExample { - private static final String SECRET_PATH = "approle/example/secret"; - private static final String ROLE_NAME = "approle_role"; - private static final String POLICY_NAME = "approle_policy"; - - private final Vault tokenVault; - private final Config config; - private final SysRx sys; - - private Vault appRoleVault; - - AppRoleExample(Vault tokenVault, Config config) { - this.tokenVault = tokenVault; - this.config = config; - - this.sys = tokenVault.sys(SysRx.API); - } - - public Single run() { - /* - The following tasks must be run before we authenticate - */ - return enableAppRoleAuth() - // Now we can login using AppRole - .flatMapSingle(ignored -> workWithSecrets()) - // Now back to token based Vault, as we will clean up - .flatMapSingle(ignored -> disableAppRoleAuth()) - .map(ignored -> "AppRole example finished successfully."); - } - - private Single workWithSecrets() { - Kv2SecretsRx secrets = appRoleVault.secrets(Kv2SecretsRx.ENGINE); - - return secrets.create(SECRET_PATH, Map.of("secret-key", "secretValue", - "secret-user", "username")) - .flatMapSingle(ignored -> secrets.get(SECRET_PATH)) - .peek(secret -> { - if (secret.isPresent()) { - Kv2Secret kv2Secret = secret.get(); - System.out.println("appRole first secret: " + kv2Secret.value("secret-key")); - System.out.println("appRole second secret: " + kv2Secret.value("secret-user")); - } else { - System.out.println("appRole secret not found"); - } - }).flatMapSingle(ignored -> secrets.deleteAll(SECRET_PATH)); - } - - private Single disableAppRoleAuth() { - if (1 == 1) { - return Single.empty(); - } - return sys.deletePolicy(POLICY_NAME) - .flatMapSingle(ignored -> sys.disableAuth(AppRoleAuthRx.AUTH_METHOD.defaultPath())); - } - - private Single enableAppRoleAuth() { - AtomicReference roleId = new AtomicReference<>(); - AtomicReference secretId = new AtomicReference<>(); - - // enable the method - return sys.enableAuth(AppRoleAuthRx.AUTH_METHOD) - // add policy - .flatMapSingle(ignored -> sys.createPolicy(POLICY_NAME, VaultPolicy.POLICY)) - .flatMapSingle(ignored -> tokenVault.auth(AppRoleAuthRx.AUTH_METHOD) - .createAppRole(CreateAppRole.Request.builder() - .roleName(ROLE_NAME) - .addTokenPolicy(POLICY_NAME) - .tokenExplicitMaxTtl(Duration.ofMinutes(1)))) - .flatMapSingle(ignored -> tokenVault.auth(AppRoleAuthRx.AUTH_METHOD) - .readRoleId(ROLE_NAME)) - .peek(it -> it.ifPresent(roleId::set)) - .flatMapSingle(ignored -> tokenVault.auth(AppRoleAuthRx.AUTH_METHOD) - .generateSecretId(GenerateSecretId.Request.builder() - .roleName(ROLE_NAME) - .addMetadata("name", "helidon"))) - .map(GenerateSecretId.Response::secretId) - .peek(secretId::set) - .peek(ignored -> { - System.out.println("roleId: " + roleId.get()); - System.out.println("secretId: " + secretId.get()); - appRoleVault = Vault.builder() - .config(config) - .addVaultAuth(AppRoleVaultAuth.builder() - .appRoleId(roleId.get()) - .secretId(secretId.get()) - .build()) - .build(); - }); - } -} diff --git a/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/CubbyholeService.java b/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/CubbyholeService.java deleted file mode 100644 index f458523f598..00000000000 --- a/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/CubbyholeService.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. - * - * Licensed 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 io.helidon.examples.integrations.vault.hcp.reactive; - -import java.util.Map; - -import io.helidon.common.http.Http; -import io.helidon.integrations.vault.secrets.cubbyhole.CubbyholeSecretsRx; -import io.helidon.integrations.vault.sys.SysRx; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.ServerRequest; -import io.helidon.reactive.webserver.ServerResponse; -import io.helidon.reactive.webserver.Service; - -class CubbyholeService implements Service { - private final SysRx sys; - private final CubbyholeSecretsRx secrets; - - CubbyholeService(SysRx sys, CubbyholeSecretsRx secrets) { - this.sys = sys; - this.secrets = secrets; - } - - @Override - public void update(Routing.Rules rules) { - rules.get("/create", this::createSecrets) - .get("/secrets/{path:.*}", this::getSecret); - } - - private void createSecrets(ServerRequest req, ServerResponse res) { - secrets.create("first/secret", Map.of("key", "secretValue")) - .thenAccept(ignored -> res.send("Created secret on path /first/secret")) - .exceptionally(res::send); - } - - private void getSecret(ServerRequest req, ServerResponse res) { - String path = req.path().param("path"); - - secrets.get(path) - .thenAccept(secret -> { - if (secret.isPresent()) { - // using toString so we do not need to depend on JSON-B - res.send(secret.get().values().toString()); - } else { - res.status(Http.Status.NOT_FOUND_404); - res.send(); - } - }) - .exceptionally(res::send); - } -} diff --git a/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/Kv1Service.java b/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/Kv1Service.java deleted file mode 100644 index 85c41ddc543..00000000000 --- a/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/Kv1Service.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. - * - * Licensed 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 io.helidon.examples.integrations.vault.hcp.reactive; - -import java.util.Map; - -import io.helidon.common.http.Http; -import io.helidon.integrations.vault.secrets.kv1.Kv1SecretsRx; -import io.helidon.integrations.vault.sys.SysRx; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.ServerRequest; -import io.helidon.reactive.webserver.ServerResponse; -import io.helidon.reactive.webserver.Service; - -class Kv1Service implements Service { - private final SysRx sys; - private final Kv1SecretsRx secrets; - - Kv1Service(SysRx sys, Kv1SecretsRx secrets) { - this.sys = sys; - this.secrets = secrets; - } - - @Override - public void update(Routing.Rules rules) { - rules.get("/enable", this::enableEngine) - .get("/create", this::createSecrets) - .get("/secrets/{path:.*}", this::getSecret) - .delete("/secrets/{path:.*}", this::deleteSecret) - .get("/disable", this::disableEngine); - } - - private void disableEngine(ServerRequest req, ServerResponse res) { - sys.disableEngine(Kv1SecretsRx.ENGINE) - .thenAccept(ignored -> res.send("KV1 Secret engine disabled")) - .exceptionally(res::send); - } - - private void enableEngine(ServerRequest req, ServerResponse res) { - sys.enableEngine(Kv1SecretsRx.ENGINE) - .thenAccept(ignored -> res.send("KV1 Secret engine enabled")) - .exceptionally(res::send); - } - - private void createSecrets(ServerRequest req, ServerResponse res) { - secrets.create("first/secret", Map.of("key", "secretValue")) - .thenAccept(ignored -> res.send("Created secret on path /first/secret")) - .exceptionally(res::send); - } - - private void deleteSecret(ServerRequest req, ServerResponse res) { - String path = req.path().param("path"); - - secrets.delete(path) - .thenAccept(ignored -> res.send("Deleted secret on path " + path)); - } - - private void getSecret(ServerRequest req, ServerResponse res) { - String path = req.path().param("path"); - - secrets.get(path) - .thenAccept(secret -> { - if (secret.isPresent()) { - // using toString so we do not need to depend on JSON-B - res.send(secret.get().values().toString()); - } else { - res.status(Http.Status.NOT_FOUND_404); - res.send(); - } - }) - .exceptionally(res::send); - } -} diff --git a/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/Kv2Service.java b/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/Kv2Service.java deleted file mode 100644 index 17bbea1dd99..00000000000 --- a/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/Kv2Service.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. - * - * Licensed 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 io.helidon.examples.integrations.vault.hcp.reactive; - -import java.util.Map; - -import io.helidon.common.http.Http; -import io.helidon.integrations.vault.secrets.kv2.Kv2Secret; -import io.helidon.integrations.vault.secrets.kv2.Kv2SecretsRx; -import io.helidon.integrations.vault.sys.SysRx; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.ServerRequest; -import io.helidon.reactive.webserver.ServerResponse; -import io.helidon.reactive.webserver.Service; - -class Kv2Service implements Service { - private final SysRx sys; - private final Kv2SecretsRx secrets; - - Kv2Service(SysRx sys, Kv2SecretsRx secrets) { - this.sys = sys; - this.secrets = secrets; - } - - @Override - public void update(Routing.Rules rules) { - rules.get("/create", this::createSecrets) - .get("/secrets/{path:.*}", this::getSecret) - .delete("/secrets/{path:.*}", this::deleteSecret); - } - - private void createSecrets(ServerRequest req, ServerResponse res) { - secrets.create("first/secret", Map.of("key", "secretValue")) - .thenAccept(ignored -> res.send("Created secret on path /first/secret")) - .exceptionally(res::send); - } - - private void deleteSecret(ServerRequest req, ServerResponse res) { - String path = req.path().param("path"); - - secrets.deleteAll(path) - .thenAccept(ignored -> res.send("Deleted secret on path " + path)); - } - - private void getSecret(ServerRequest req, ServerResponse res) { - String path = req.path().param("path"); - - secrets.get(path) - .thenAccept(secret -> { - if (secret.isPresent()) { - // using toString so we do not need to depend on JSON-B - Kv2Secret kv2Secret = secret.get(); - res.send("Version " + kv2Secret.metadata().version() + ", secret: " + kv2Secret.values().toString()); - } else { - res.status(Http.Status.NOT_FOUND_404); - res.send(); - } - }) - .exceptionally(res::send); - } -} diff --git a/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/ReactiveVaultMain.java b/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/ReactiveVaultMain.java deleted file mode 100644 index 451514b6ddc..00000000000 --- a/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/ReactiveVaultMain.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. - * - * Licensed 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 io.helidon.examples.integrations.vault.hcp.reactive; - -import java.util.concurrent.TimeUnit; - -import io.helidon.common.reactive.CompletionAwaitable; -import io.helidon.config.Config; -import io.helidon.integrations.vault.Vault; -import io.helidon.integrations.vault.secrets.cubbyhole.CubbyholeSecretsRx; -import io.helidon.integrations.vault.secrets.kv1.Kv1SecretsRx; -import io.helidon.integrations.vault.secrets.kv2.Kv2SecretsRx; -import io.helidon.integrations.vault.secrets.transit.TransitSecretsRx; -import io.helidon.integrations.vault.sys.SysRx; -import io.helidon.logging.common.LogConfig; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.WebServer; - -import static io.helidon.config.ConfigSources.classpath; -import static io.helidon.config.ConfigSources.file; - -/** - * Main class to start the application. - */ -public final class ReactiveVaultMain { - private ReactiveVaultMain() { - } - - /** - * Main method. - * @param args ignored - */ - public static void main(String[] args) { - LogConfig.configureRuntime(); - - Config config = Config.create(); - - // we have two configurations available - // 1. Token based authentication - Vault tokenVault = Vault.builder() - .config(config.get("vault.token")) - .updateWebClient(it -> it.connectTimeout(5, TimeUnit.SECONDS) - .readTimeout(5, TimeUnit.SECONDS)) - .build(); - - // 2. App role based authentication - must be created after we obtain the role id an token - // the tokenVault is using the root token and can be used to enable engines and - // other authentication mechanisms - - CompletionAwaitable appRoleFuture = new AppRoleExample(tokenVault, config.get("vault.approle")) - .run() - .forSingle(System.out::println); - - /* - We do not need to block here for our examples, as the server started below will keep the process running - */ - - SysRx sys = tokenVault.sys(SysRx.API); - // we use await for webserver, as we do not care if we block the main thread - it is not used - // for anything - WebServer webServer = WebServer.builder() - .config(config.get("server")) - .routing(Routing.builder() - .register("/cubbyhole", new CubbyholeService(sys, tokenVault.secrets(CubbyholeSecretsRx.ENGINE))) - .register("/kv1", new Kv1Service(sys, tokenVault.secrets(Kv1SecretsRx.ENGINE))) - .register("/kv2", new Kv2Service(sys, tokenVault.secrets(Kv2SecretsRx.ENGINE))) - .register("/transit", new TransitService(sys, tokenVault.secrets(TransitSecretsRx.ENGINE)))) - .build() - .start() - .await(); - - try { - appRoleFuture.await(); - } catch (Exception e) { - System.err.println("AppRole example failed"); - e.printStackTrace(); - } - - String baseAddress = "http://localhost:" + webServer.port() + "/"; - System.out.println("Server started on " + baseAddress); - System.out.println(); - System.out.println("Key/Value Version 1 Secrets Engine"); - System.out.println("\t" + baseAddress + "kv1/enable"); - System.out.println("\t" + baseAddress + "kv1/create"); - System.out.println("\t" + baseAddress + "kv1/secrets/first/secret"); - System.out.println("\tcurl -i -X DELETE " + baseAddress + "kv1/secrets/first/secret"); - System.out.println("\t" + baseAddress + "kv1/disable"); - System.out.println(); - System.out.println("Key/Value Version 2 Secrets Engine"); - System.out.println("\t" + baseAddress + "kv2/create"); - System.out.println("\t" + baseAddress + "kv2/secrets/first/secret"); - System.out.println("\tcurl -i -X DELETE " + baseAddress + "kv2/secrets/first/secret"); - System.out.println(); - System.out.println("Transit Secrets Engine"); - System.out.println("\t" + baseAddress + "transit/enable"); - System.out.println("\t" + baseAddress + "transit/keys"); - System.out.println("\t" + baseAddress + "transit/encrypt/secret_text"); - System.out.println("\t" + baseAddress + "transit/decrypt/cipher_text"); - System.out.println("\t" + baseAddress + "transit/sign"); - System.out.println("\t" + baseAddress + "transit/verify/sign/signature_text"); - System.out.println("\t" + baseAddress + "transit/hmac"); - System.out.println("\t" + baseAddress + "transit/verify/hmac/hmac_text"); - System.out.println("\tcurl -i -X DELETE " + baseAddress + "transit/keys"); - System.out.println("\t" + baseAddress + "transit/disable"); - } -} diff --git a/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/TransitService.java b/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/TransitService.java deleted file mode 100644 index a0ff2d33650..00000000000 --- a/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/TransitService.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. - * - * Licensed 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 io.helidon.examples.integrations.vault.hcp.reactive; - -import java.util.List; - -import io.helidon.common.Base64Value; -import io.helidon.integrations.vault.secrets.transit.CreateKey; -import io.helidon.integrations.vault.secrets.transit.Decrypt; -import io.helidon.integrations.vault.secrets.transit.DecryptBatch; -import io.helidon.integrations.vault.secrets.transit.DeleteKey; -import io.helidon.integrations.vault.secrets.transit.Encrypt; -import io.helidon.integrations.vault.secrets.transit.EncryptBatch; -import io.helidon.integrations.vault.secrets.transit.Hmac; -import io.helidon.integrations.vault.secrets.transit.Sign; -import io.helidon.integrations.vault.secrets.transit.TransitSecretsRx; -import io.helidon.integrations.vault.secrets.transit.UpdateKeyConfig; -import io.helidon.integrations.vault.secrets.transit.Verify; -import io.helidon.integrations.vault.sys.SysRx; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.ServerRequest; -import io.helidon.reactive.webserver.ServerResponse; -import io.helidon.reactive.webserver.Service; - -class TransitService implements Service { - private static final String ENCRYPTION_KEY = "encryption-key"; - private static final String SIGNATURE_KEY = "signature-key"; - private static final Base64Value SECRET_STRING = Base64Value.create("Hello World"); - private final SysRx sys; - private final TransitSecretsRx secrets; - - TransitService(SysRx sys, TransitSecretsRx secrets) { - this.sys = sys; - this.secrets = secrets; - } - - @Override - public void update(Routing.Rules rules) { - rules.get("/enable", this::enableEngine) - .get("/keys", this::createKeys) - .delete("/keys", this::deleteKeys) - .get("/batch", this::batch) - .get("/encrypt/{text:.*}", this::encryptSecret) - .get("/decrypt/{text:.*}", this::decryptSecret) - .get("/sign", this::sign) - .get("/hmac", this::hmac) - .get("/verify/sign/{text:.*}", this::verify) - .get("/verify/hmac/{text:.*}", this::verifyHmac) - .get("/disable", this::disableEngine); - } - - private void enableEngine(ServerRequest req, ServerResponse res) { - sys.enableEngine(TransitSecretsRx.ENGINE) - .thenAccept(ignored -> res.send("Transit Secret engine enabled")) - .exceptionally(res::send); - } - - private void disableEngine(ServerRequest req, ServerResponse res) { - sys.disableEngine(TransitSecretsRx.ENGINE) - .thenAccept(ignored -> res.send("Transit Secret engine disabled")) - .exceptionally(res::send); - } - - private void createKeys(ServerRequest req, ServerResponse res) { - CreateKey.Request request = CreateKey.Request.builder() - .name(ENCRYPTION_KEY); - - secrets.createKey(request) - .flatMapSingle(ignored -> secrets.createKey(CreateKey.Request.builder() - .name(SIGNATURE_KEY) - .type("rsa-2048"))) - .forSingle(ignored -> res.send("Created keys")) - .exceptionally(res::send); - } - - private void deleteKeys(ServerRequest req, ServerResponse res) { - - secrets.updateKeyConfig(UpdateKeyConfig.Request.builder() - .name(ENCRYPTION_KEY) - .allowDeletion(true)) - .peek(ignored -> System.out.println("Updated key config")) - .flatMapSingle(ignored -> secrets.deleteKey(DeleteKey.Request.create(ENCRYPTION_KEY))) - .forSingle(ignored -> res.send("Deleted key.")) - .exceptionally(res::send); - } - - private void decryptSecret(ServerRequest req, ServerResponse res) { - String encrypted = req.path().param("text"); - - secrets.decrypt(Decrypt.Request.builder() - .encryptionKeyName(ENCRYPTION_KEY) - .cipherText(encrypted)) - .forSingle(response -> res.send(String.valueOf(response.decrypted().toDecodedString()))) - .exceptionally(res::send); - } - - private void encryptSecret(ServerRequest req, ServerResponse res) { - String secret = req.path().param("text"); - - secrets.encrypt(Encrypt.Request.builder() - .encryptionKeyName(ENCRYPTION_KEY) - .data(Base64Value.create(secret))) - .forSingle(response -> res.send(response.encrypted().cipherText())) - .exceptionally(res::send); - } - - private void hmac(ServerRequest req, ServerResponse res) { - secrets.hmac(Hmac.Request.builder() - .hmacKeyName(ENCRYPTION_KEY) - .data(SECRET_STRING)) - .forSingle(response -> res.send(response.hmac())) - .exceptionally(res::send); - } - - private void sign(ServerRequest req, ServerResponse res) { - secrets.sign(Sign.Request.builder() - .signatureKeyName(SIGNATURE_KEY) - .data(SECRET_STRING)) - .forSingle(response -> res.send(response.signature())) - .exceptionally(res::send); - } - - private void verifyHmac(ServerRequest req, ServerResponse res) { - String hmac = req.path().param("text"); - - secrets.verify(Verify.Request.builder() - .digestKeyName(ENCRYPTION_KEY) - .data(SECRET_STRING) - .hmac(hmac)) - .forSingle(response -> res.send("Valid: " + response.isValid())) - .exceptionally(res::send); - } - - private void verify(ServerRequest req, ServerResponse res) { - String signature = req.path().param("text"); - - secrets.verify(Verify.Request.builder() - .digestKeyName(SIGNATURE_KEY) - .data(SECRET_STRING) - .signature(signature)) - .forSingle(response -> res.send("Valid: " + response.isValid())) - .exceptionally(res::send); - } - - private void batch(ServerRequest req, ServerResponse res) { - String[] data = new String[] {"one", "two", "three", "four"}; - EncryptBatch.Request request = EncryptBatch.Request.builder() - .encryptionKeyName(ENCRYPTION_KEY); - DecryptBatch.Request decryptRequest = DecryptBatch.Request.builder() - .encryptionKeyName(ENCRYPTION_KEY); - - for (String dato : data) { - request.addEntry(EncryptBatch.BatchEntry.create(Base64Value.create(dato))); - } - secrets.encrypt(request) - .map(EncryptBatch.Response::batchResult) - .flatMapSingle(batchResult -> { - for (Encrypt.Encrypted encrypted : batchResult) { - System.out.println("Encrypted: " + encrypted.cipherText()); - decryptRequest.addEntry(DecryptBatch.BatchEntry.create(encrypted.cipherText())); - } - return secrets.decrypt(decryptRequest); - }) - .forSingle(response -> { - List base64Values = response.batchResult(); - for (int i = 0; i < data.length; i++) { - String decryptedValue = base64Values.get(i).toDecodedString(); - if (!data[i].equals(decryptedValue)) { - res.send("Data at index " + i + " is invalid. Decrypted " + decryptedValue - + ", expected: " + data[i]); - return; - } - } - res.send("Batch encryption/decryption completed"); - }) - .exceptionally(res::send); - } - -} diff --git a/tests/integration/vault/se/src/test/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultTest.java b/tests/integration/vault/se/src/test/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultTest.java deleted file mode 100644 index 9e6519b7d7d..00000000000 --- a/tests/integration/vault/se/src/test/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultTest.java +++ /dev/null @@ -1,117 +0,0 @@ -package io.helidon.examples.integrations.vault.hcp.reactive; - -import java.time.Duration; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import io.helidon.common.http.Http; -import io.helidon.integrations.vault.Secret; -import io.helidon.integrations.vault.Vault; -import io.helidon.integrations.vault.secrets.cubbyhole.CreateCubbyhole; -import io.helidon.integrations.vault.secrets.cubbyhole.CubbyholeSecretsRx; -import io.helidon.integrations.vault.secrets.cubbyhole.DeleteCubbyhole; -import io.helidon.logging.common.LogConfig; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; -import org.testcontainers.vault.VaultContainer; - -import static io.helidon.common.testing.junit5.OptionalMatcher.optionalPresent; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -@Testcontainers(disabledWithoutDocker = true) -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -class VaultTest { - private static final Duration TIMEOUT = Duration.ofSeconds(5); - private static final DockerImageName MYSQL_IMAGE = DockerImageName.parse("mysql:8"); - private static final DockerImageName HCP_VAULT_IMAGE = DockerImageName.parse("vault:1.11.3"); - - @Container - private static final MySQLContainer mySql = new MySQLContainer(MYSQL_IMAGE) - .withUsername("user") - .withPassword("password") - .withDatabaseName("pokemon"); - - @Container - private static final VaultContainer vaultContainer = new VaultContainer<>(HCP_VAULT_IMAGE) - .withVaultToken("myroot") - .dependsOn(mySql); - - // it seems that test containers run each method on a different test class instance - private static Vault tokenVault; - - @BeforeAll - static void setup() { - LogConfig.configureRuntime(); - } - - @Test - @Order(1) - void validateContainers() { - assertThat("MySQL must be running", mySql.isRunning(), is(true)); - assertThat("Vault must be running", vaultContainer.isRunning(), is(true)); - } - - @Test - @Order(2) - void prepareVaultAndSecrets() { - tokenVault = Vault.builder() - .address("http://localhost:" + vaultContainer.getMappedPort(8200)) - .token("myroot") - .updateWebClient(it -> it.connectTimeout(TIMEOUT) - .readTimeout(TIMEOUT)) - .build(); - // TODO approle - // vaultSys = tokenVault.sys(SysRx.API); - // - // kv1 = tokenVault.secrets(Kv1SecretsRx.ENGINE); - // kv2 = tokenVault.secrets(Kv2SecretsRx.ENGINE); - // database = tokenVault.secrets(DbSecretsRx.ENGINE); - // transit = tokenVault.secrets(TransitSecretsRx.ENGINE); - } - - @Test - @Order(3) - void testCubbyhole() { - CubbyholeSecretsRx cubbyhole = tokenVault.secrets(CubbyholeSecretsRx.ENGINE); - String secretPath = "first/secret"; - - // create secret - CreateCubbyhole.Response createResponse = cubbyhole.create(secretPath, Map.of("key", "secretValue")).await(TIMEOUT); - assertThat("Create secret, got response status: " + createResponse.status(), - createResponse.status().family(), - is(Http.Status.Family.SUCCESSFUL)); - - // get secret - Optional maybeSecret = cubbyhole.get(secretPath) - .await(TIMEOUT); - - assertThat(maybeSecret, optionalPresent()); - Secret secret = maybeSecret.get(); - assertThat(secret.path(), is(secretPath)); - assertThat(secret.values(), is(Map.of("key", "secretValue"))); - - DeleteCubbyhole.Response deleteResponse = cubbyhole.delete(secretPath).await(TIMEOUT); - assertThat("Delete secret, got response status: " + deleteResponse.status(), - deleteResponse.status().family(), - is(Http.Status.Family.SUCCESSFUL)); - - List secretList = cubbyhole.list().await(TIMEOUT); - assertThat(secretList, is(List.of())); - } - - @Test - @Order(3) - void testDatabase() { - int mysqlPort = mySql.getMappedPort(3306); - } -} diff --git a/tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultPolicy.java b/tests/integration/vault/src/test/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultPolicy.java similarity index 100% rename from tests/integration/vault/se/src/main/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultPolicy.java rename to tests/integration/vault/src/test/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultPolicy.java diff --git a/tests/integration/vault/src/test/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultTest.java b/tests/integration/vault/src/test/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultTest.java new file mode 100644 index 00000000000..7566ab24ea9 --- /dev/null +++ b/tests/integration/vault/src/test/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultTest.java @@ -0,0 +1,527 @@ +package io.helidon.examples.integrations.vault.hcp.reactive; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import io.helidon.common.Base64Value; +import io.helidon.common.http.Http; +import io.helidon.integrations.common.rest.ApiResponse; +import io.helidon.integrations.vault.Secret; +import io.helidon.integrations.vault.Vault; +import io.helidon.integrations.vault.auths.approle.AppRoleAuthRx; +import io.helidon.integrations.vault.auths.approle.AppRoleVaultAuth; +import io.helidon.integrations.vault.auths.approle.CreateAppRole; +import io.helidon.integrations.vault.auths.approle.GenerateSecretId; +import io.helidon.integrations.vault.secrets.cubbyhole.CreateCubbyhole; +import io.helidon.integrations.vault.secrets.cubbyhole.CubbyholeSecretsRx; +import io.helidon.integrations.vault.secrets.cubbyhole.DeleteCubbyhole; +import io.helidon.integrations.vault.secrets.database.DbConfigure; +import io.helidon.integrations.vault.secrets.database.DbCreateRole; +import io.helidon.integrations.vault.secrets.database.DbCredentials; +import io.helidon.integrations.vault.secrets.database.DbSecretsRx; +import io.helidon.integrations.vault.secrets.database.MySqlConfigureRequest; +import io.helidon.integrations.vault.secrets.kv1.CreateKv1; +import io.helidon.integrations.vault.secrets.kv1.DeleteKv1; +import io.helidon.integrations.vault.secrets.kv1.Kv1SecretsRx; +import io.helidon.integrations.vault.secrets.kv2.CreateKv2; +import io.helidon.integrations.vault.secrets.kv2.DeleteKv2; +import io.helidon.integrations.vault.secrets.kv2.Kv2Secret; +import io.helidon.integrations.vault.secrets.kv2.Kv2SecretsRx; +import io.helidon.integrations.vault.secrets.transit.CreateKey; +import io.helidon.integrations.vault.secrets.transit.Decrypt; +import io.helidon.integrations.vault.secrets.transit.DecryptBatch; +import io.helidon.integrations.vault.secrets.transit.DeleteKey; +import io.helidon.integrations.vault.secrets.transit.Encrypt; +import io.helidon.integrations.vault.secrets.transit.EncryptBatch; +import io.helidon.integrations.vault.secrets.transit.Hmac; +import io.helidon.integrations.vault.secrets.transit.Sign; +import io.helidon.integrations.vault.secrets.transit.TransitSecretsRx; +import io.helidon.integrations.vault.secrets.transit.UpdateKeyConfig; +import io.helidon.integrations.vault.secrets.transit.Verify; +import io.helidon.integrations.vault.sys.DisableEngine; +import io.helidon.integrations.vault.sys.EnableEngine; +import io.helidon.integrations.vault.sys.SysRx; +import io.helidon.logging.common.LogConfig; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.vault.VaultContainer; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalEmpty; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalPresent; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +@Testcontainers(disabledWithoutDocker = true) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class VaultTest { + private static final String TRANSIT_ENCRYPTION_KEY = "encryption-key"; + private static final String TRANSIT_SIGNATURE_KEY = "signature-key"; + private static final String TRANSIT_HMAC_KEY = "hmac-key"; + private static final String APPROLE_POLICY_NAME = "approle_policy"; + private static final String APPROLE_ROLE_NAME = "approle_role"; + private static final Duration TIMEOUT = Duration.ofSeconds(5); + private static final DockerImageName MYSQL_IMAGE = DockerImageName.parse("mysql:8"); + private static final DockerImageName HCP_VAULT_IMAGE = DockerImageName.parse("vault:1.11.3"); + + private static final Network NETWORK = Network.newNetwork(); + @Container + private static final MySQLContainer MY_SQL_CONTAINER = new MySQLContainer<>(MYSQL_IMAGE) + .withUsername("root") + .withPassword("root") + .withNetworkAliases("mysql") + .withDatabaseName("pokemon") + .withNetwork(NETWORK); + + @Container + private static final VaultContainer VAULT_CONTAINER = new VaultContainer<>(HCP_VAULT_IMAGE) + .withVaultToken("myroot") + .withNetwork(NETWORK) + .dependsOn(MY_SQL_CONTAINER); + + // it seems that test containers run each method on a different test class instance + private static Vault tokenVault; + private static String vaultAddress; + + @BeforeAll + static void setup() { + LogConfig.configureRuntime(); + } + + @Test + @Order(1) + void validateContainers() { + assertThat("MySQL must be running", MY_SQL_CONTAINER.isRunning(), is(true)); + assertThat("Vault must be running", VAULT_CONTAINER.isRunning(), is(true)); + vaultAddress = "http://" + VAULT_CONTAINER.getHost() + ":" + VAULT_CONTAINER.getMappedPort(8200); + } + + @Test + @Order(2) + void prepareVaultAndSecrets() { + tokenVault = Vault.builder() + .address(vaultAddress) + .token("myroot") + .updateWebClient(it -> it.connectTimeout(TIMEOUT) + .readTimeout(TIMEOUT)) + .build(); + } + + @Test + @Order(3) + void testKv1() { + // kv1 is not enabled by default + SysRx sys = tokenVault.sys(SysRx.API); + EnableEngine.Response enableResponse = sys.enableEngine(Kv1SecretsRx.ENGINE) + .await(TIMEOUT); + + assertThat("Enable kv1 engine, got response status: " + enableResponse.status(), + enableResponse.status().family(), + is(Http.Status.Family.SUCCESSFUL)); + + Kv1SecretsRx secrets = tokenVault.secrets(Kv1SecretsRx.ENGINE); + String secretPath = "first/secret"; + + // create secret + CreateKv1.Response createResponse = secrets.create(secretPath, Map.of("key", "secretValue")).await(TIMEOUT); + assertThat("Create secret, got response status: " + createResponse.status(), + createResponse.status().family(), + is(Http.Status.Family.SUCCESSFUL)); + + // get secret + Optional maybeSecret = secrets.get(secretPath) + .await(TIMEOUT); + + assertThat(maybeSecret, optionalPresent()); + Secret secret = maybeSecret.get(); + assertThat(secret.path(), is(secretPath)); + assertThat(secret.values(), is(Map.of("key", "secretValue"))); + + DeleteKv1.Response deleteResponse = secrets.delete(secretPath).await(TIMEOUT); + assertThat("Delete secret, got response status: " + deleteResponse.status(), + deleteResponse.status().family(), + is(Http.Status.Family.SUCCESSFUL)); + + List secretList = secrets.list().await(TIMEOUT); + assertThat(secretList, is(List.of())); + + maybeSecret = secrets.get(secretPath) + .await(TIMEOUT); + + assertThat(maybeSecret, optionalEmpty()); + + // and disable engine + DisableEngine.Response disableResponse = sys.disableEngine(Kv1SecretsRx.ENGINE) + .await(TIMEOUT); + + assertThat("Disable kv1 engine, got response status: " + disableResponse.status(), + disableResponse.status().family(), + is(Http.Status.Family.SUCCESSFUL)); + } + + @Test + @Order(3) + void testKv2() { + Kv2SecretsRx secrets = tokenVault.secrets(Kv2SecretsRx.ENGINE); + String secretPath = "first/secret"; + + // create secret + CreateKv2.Response createResponse = secrets.create(secretPath, Map.of("key", "secretValue")).await(TIMEOUT); + assertThat("Create secret, got response status: " + createResponse.status(), + createResponse.status().family(), + is(Http.Status.Family.SUCCESSFUL)); + + // get secret + Optional maybeSecret = secrets.get(secretPath) + .await(TIMEOUT); + + assertThat(maybeSecret, optionalPresent()); + Kv2Secret secret = maybeSecret.get(); + assertThat(secret.path(), is(secretPath)); + assertThat(secret.values(), is(Map.of("key", "secretValue"))); + assertThat(secret.metadata().version(), is(1)); + + List secretList = secrets.list().await(TIMEOUT); + assertThat(secretList, hasItems("first/")); + + secretList = secrets.list("first").await(TIMEOUT); + assertThat(secretList, hasItems("secret")); + + DeleteKv2.Response deleteResponse = secrets.delete(secretPath, 1).await(TIMEOUT); + assertThat("Delete secret, got response status: " + deleteResponse.status(), + deleteResponse.status().family(), + is(Http.Status.Family.SUCCESSFUL)); + + maybeSecret = secrets.get(secretPath) + .await(TIMEOUT); + + assertThat(maybeSecret, optionalEmpty()); + } + + @Test + @Order(3) + void testCubbyhole() { + CubbyholeSecretsRx secrets = tokenVault.secrets(CubbyholeSecretsRx.ENGINE); + String secretPath = "first/secret"; + + // create secret + CreateCubbyhole.Response createResponse = secrets.create(secretPath, Map.of("key", "secretValue")).await(TIMEOUT); + assertThat("Create secret, got response status: " + createResponse.status(), + createResponse.status().family(), + is(Http.Status.Family.SUCCESSFUL)); + + // get secret + Optional maybeSecret = secrets.get(secretPath) + .await(TIMEOUT); + + assertThat(maybeSecret, optionalPresent()); + Secret secret = maybeSecret.get(); + assertThat(secret.path(), is(secretPath)); + assertThat(secret.values(), is(Map.of("key", "secretValue"))); + + DeleteCubbyhole.Response deleteResponse = secrets.delete(secretPath).await(TIMEOUT); + assertThat("Delete secret, got response status: " + deleteResponse.status(), + deleteResponse.status().family(), + is(Http.Status.Family.SUCCESSFUL)); + + List secretList = secrets.list().await(TIMEOUT); + assertThat(secretList, is(List.of())); + + maybeSecret = secrets.get(secretPath) + .await(TIMEOUT); + + assertThat(maybeSecret, optionalEmpty()); + } + + @Test + @Order(3) + void testDatabase() throws SQLException { + int mysqlPort = MY_SQL_CONTAINER.getMappedPort(3306); + String mysqlHost = MY_SQL_CONTAINER.getHost(); + if (mysqlHost.startsWith("/")) { + mysqlHost = mysqlHost.substring(1); + } + // database is not enabled by default + SysRx sys = tokenVault.sys(SysRx.API); + EnableEngine.Response enableResponse = sys.enableEngine(DbSecretsRx.ENGINE) + .await(TIMEOUT); + assertSuccess("Enable database engine", enableResponse); + DbSecretsRx database = tokenVault.secrets(DbSecretsRx.ENGINE); + + // configure connection + String connectionTemplate = "{{username}}:{{password}}@tcp" + + "(mysql:3306)/"; + DbConfigure.Response dbConfigResponse = database + .configure(MySqlConfigureRequest.builder(connectionTemplate) + .name("mysql") + .username("root") + .password("root") + .maxOpenConnections(5) + .maxConnectionLifetime(Duration.ofMinutes(1)) + .addAllowedRole("readonly")) + .await(TIMEOUT); + assertSuccess("Configure MySQL", dbConfigResponse); + + // add role + DbCreateRole.Response roleResponse = database.createRole(DbCreateRole.Request.builder() + .name("readonly") + .dbName("mysql") + .addCreationStatement( + "CREATE USER '{{name}}'@'%' IDENTIFIED BY " + + "'{{password}}'") + .addCreationStatement( + "GRANT SELECT ON *.* TO '{{name}}'@'%'")) + .await(TIMEOUT); + assertSuccess("Create MySQL role", roleResponse); + + // verify we can get secrets to connect to the database + Optional maybeCreds = database.get("readonly").await(TIMEOUT); + + assertThat(maybeCreds, optionalPresent()); + + DbCredentials mysql = maybeCreds.get(); + + String username = mysql.username(); + String password = mysql.password(); + + String address = "jdbc:mysql://" + mysqlHost + ":" + mysqlPort + "/"; + com.mysql.jdbc.Driver.class.getName(); + try (Connection conn = DriverManager.getConnection(address, username, password)) { + assertThat(conn, notNullValue()); + } + + // remove role + assertSuccess("Delete MySQL role", database.deleteRole("readonly").await(TIMEOUT)); + // remove db + assertSuccess("Delete database", database.delete("mysql").await(TIMEOUT)); + } + + @Test + @Order(3) + void testTransit() { + // kv1 is not enabled by default + SysRx sys = tokenVault.sys(SysRx.API); + EnableEngine.Response enableResponse = sys.enableEngine(TransitSecretsRx.ENGINE) + .await(TIMEOUT); + assertSuccess("Enable transit engine", enableResponse); + + TransitSecretsRx secrets = tokenVault.secrets(TransitSecretsRx.ENGINE); + + /* + Create keys + */ + CreateKey.Response createResponse = secrets.createKey(CreateKey.Request.builder() + .name(TRANSIT_ENCRYPTION_KEY)) + .await(TIMEOUT); + assertSuccess("Create encryption key", createResponse); + + createResponse = secrets.createKey(CreateKey.Request.builder() + .name(TRANSIT_SIGNATURE_KEY) + .type("rsa-2048")) + .await(TIMEOUT); + assertSuccess("Create signature key", createResponse); + createResponse = secrets.createKey(CreateKey.Request.builder() + .name(TRANSIT_HMAC_KEY)) + .await(TIMEOUT); + assertSuccess("Create hmac key", createResponse); + + /* + Test operations + */ + transitBatch(secrets); + transitEncryption(secrets); + transitSignature(secrets); + transitHmac(secrets); + + + /* + Delete keys + */ + assertSuccess("Update key config", secrets.updateKeyConfig(UpdateKeyConfig.Request.builder() + .name(TRANSIT_ENCRYPTION_KEY) + .allowDeletion(true)) + .await(TIMEOUT)); + + assertSuccess("Delete encryption key", secrets.deleteKey(DeleteKey.Request.create(TRANSIT_ENCRYPTION_KEY)) + .await(TIMEOUT)); + + // and disable engine + assertSuccess("Disable transit engine", sys.disableEngine(TransitSecretsRx.ENGINE) + .await(TIMEOUT)); + } + + @Test + @Order(4) + void testAppRole() { + SysRx sys = tokenVault.sys(SysRx.API); + + assertSuccess("Enable App Role auth", sys.enableAuth(AppRoleAuthRx.AUTH_METHOD).await(TIMEOUT)); + assertSuccess("Create App Role policy", sys.createPolicy(APPROLE_POLICY_NAME, VaultPolicy.POLICY).await(TIMEOUT)); + + AppRoleAuthRx auth = tokenVault.auth(AppRoleAuthRx.AUTH_METHOD); + + assertSuccess("Create App Role", auth.createAppRole(CreateAppRole.Request.builder() + .roleName(APPROLE_ROLE_NAME) + .addTokenPolicy(APPROLE_POLICY_NAME) + .tokenExplicitMaxTtl(Duration.ofMinutes(1))) + .await(TIMEOUT)); + + Optional roleIdResponse = auth.readRoleId(APPROLE_ROLE_NAME).await(TIMEOUT); + assertThat(roleIdResponse, optionalPresent()); + String roleId = roleIdResponse.get(); + + GenerateSecretId.Response secretIdResponse = auth.generateSecretId(GenerateSecretId.Request.builder() + .roleName(APPROLE_ROLE_NAME) + .addMetadata("name", "helidon")) + .await(TIMEOUT); + assertSuccess("Generate secret id", secretIdResponse); + + String secretId = secretIdResponse.secretId(); + Vault appRoleVault = Vault.builder() + .address(vaultAddress) + .addVaultAuth(AppRoleVaultAuth.builder() + .appRoleId(roleId) + .secretId(secretId) + .build()) + .build(); + + Kv2SecretsRx secrets = appRoleVault.secrets(Kv2SecretsRx.ENGINE); + String secretPath = "myapprole/secret"; + assertSuccess("Create secrets", secrets.create(secretPath, Map.of("secret-key", "secretValue", + "secret-user", "username")) + .await(TIMEOUT)); + + Optional secretResponse = secrets.get(secretPath).await(TIMEOUT); + assertThat(secretResponse, optionalPresent()); + Kv2Secret secret = secretResponse.get(); + assertThat(secret.value("secret-key"), optionalValue(is("secretValue"))); + assertThat(secret.value("secret-user"), optionalValue(is("username"))); + + assertSuccess("Delete secrets", secrets.deleteAll(secretPath).await(TIMEOUT)); + } + + private void transitHmac(TransitSecretsRx secrets) { + String secret = "Hello World"; + Base64Value data = Base64Value.create(secret); + + Hmac.Response signResponse = secrets.hmac(Hmac.Request.builder() + .hmacKeyName(TRANSIT_HMAC_KEY) + .data(data)) + .await(TIMEOUT); + assertSuccess("HMAC", signResponse); + + String hmac = signResponse.hmac(); + assertThat(hmac, not("")); + assertThat(hmac, not(secret)); + + Verify.Response verifyResponse = secrets.verify(Verify.Request.builder() + .digestKeyName(TRANSIT_HMAC_KEY) + .hmac(hmac) + .data(data)) + .await(TIMEOUT); + assertSuccess("Verify HMAC", verifyResponse); + assertThat("HMAC should be valid", verifyResponse.isValid(), is(true)); + } + + private void transitSignature(TransitSecretsRx secrets) { + String secret = "Hello World"; + Base64Value data = Base64Value.create(secret); + + Sign.Response signResponse = secrets.sign(Sign.Request.builder() + .signatureKeyName(TRANSIT_SIGNATURE_KEY) + .data(data)) + .await(TIMEOUT); + assertSuccess("Sign", signResponse); + + String signature = signResponse.signature(); + assertThat(signature, not("")); + assertThat(signature, not(secret)); + + Verify.Response verifyResponse = secrets.verify(Verify.Request.builder() + .digestKeyName(TRANSIT_SIGNATURE_KEY) + .signature(signature) + .data(data)) + .await(TIMEOUT); + assertSuccess("Verify", verifyResponse); + assertThat("Signature should be valid", verifyResponse.isValid(), is(true)); + } + + private void transitEncryption(TransitSecretsRx secrets) { + String secret = "text"; + Encrypt.Response encryptResponse = secrets.encrypt(Encrypt.Request.builder() + .encryptionKeyName(TRANSIT_ENCRYPTION_KEY) + .data(Base64Value.create(secret))) + .await(TIMEOUT); + assertSuccess("Encrypt", encryptResponse); + + String cipherText = encryptResponse.encrypted().cipherText(); + assertThat(cipherText, not("")); + assertThat(cipherText, not(secret)); + + Decrypt.Response decryptResponse = secrets.decrypt(Decrypt.Request.builder() + .cipherText(cipherText) + .encryptionKeyName(TRANSIT_ENCRYPTION_KEY)) + .await(TIMEOUT); + assertSuccess("Decrypt", decryptResponse); + String decrypted = decryptResponse.decrypted().toDecodedString(); + assertThat(decrypted, is(secret)); + } + + private void transitBatch(TransitSecretsRx secrets) { + // batch + String[] data = {"one", "two", "three", "four"}; + EncryptBatch.Request encryptBatch = EncryptBatch.Request.builder() + .encryptionKeyName(TRANSIT_ENCRYPTION_KEY); + DecryptBatch.Request decryptBatch = DecryptBatch.Request.builder() + .encryptionKeyName(TRANSIT_ENCRYPTION_KEY); + + for (String datum : data) { + encryptBatch.addEntry(EncryptBatch.BatchEntry.create(Base64Value.create(datum))); + } + + EncryptBatch.Response response = secrets.encrypt(encryptBatch) + .await(TIMEOUT); + assertSuccess("Encrypt batch", response); + + List encrypted = response.batchResult(); + Encrypt.Encrypted encryptedOne = encrypted.get(0); + String oneCipherText = encryptedOne.cipherText(); + assertThat(oneCipherText, not("")); + assertThat(oneCipherText, not("one")); + + for (Encrypt.Encrypted encryptedValue : encrypted) { + decryptBatch.addEntry(DecryptBatch.BatchEntry.create(encryptedValue.cipherText())); + } + DecryptBatch.Response decryptResponse = secrets.decrypt(decryptBatch).await(TIMEOUT); + assertSuccess("Decrypt batch", decryptResponse); + + String[] decrypted = decryptResponse.batchResult() + .stream() + .map(Base64Value::toDecodedString) + .toArray(String[]::new); + assertThat(decrypted, is(data)); + } + + private void assertSuccess(String action, ApiResponse response) { + assertThat(action + ", got response status: " + response.status(), + response.status().family(), + is(Http.Status.Family.SUCCESSFUL)); + } +} diff --git a/tests/integration/vault/se/src/main/resources/logging.properties b/tests/integration/vault/src/test/resources/logging.properties similarity index 100% rename from tests/integration/vault/se/src/main/resources/logging.properties rename to tests/integration/vault/src/test/resources/logging.properties From cb14c9d3878b206584438a4e9bb464c006784061 Mon Sep 17 00:00:00 2001 From: Tomas Langer Date: Fri, 30 Sep 2022 16:13:03 +0200 Subject: [PATCH 3/8] Copyright fixes, removed unused config file. --- .../se/src/main/resources/application.yaml | 40 ------------------- .../vault/hcp/reactive/VaultPolicy.java | 2 +- .../vault/hcp/reactive/VaultTest.java | 16 ++++++++ 3 files changed, 17 insertions(+), 41 deletions(-) delete mode 100644 tests/integration/vault/se/src/main/resources/application.yaml diff --git a/tests/integration/vault/se/src/main/resources/application.yaml b/tests/integration/vault/se/src/main/resources/application.yaml deleted file mode 100644 index 5fe7c28a61f..00000000000 --- a/tests/integration/vault/se/src/main/resources/application.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# -# Copyright (c) 2021 Oracle and/or its affiliates. -# -# Licensed 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. -# - -server.port: 8080 - -vault: - properties: - address: "http://localhost:8200" - token: - token: "myroot" - address: "${vault.properties.address}" - auth: - k8s: - enabled: false - app-role: - enabled: false - token: - enabled: true - approle: - address: "${vault.properties.address}" - auth: - k8s: - enabled: false - app-role: - enabled: true - token: - enabled: false \ No newline at end of file diff --git a/tests/integration/vault/src/test/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultPolicy.java b/tests/integration/vault/src/test/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultPolicy.java index 79d9ac84ca2..c8e606cbd73 100644 --- a/tests/integration/vault/src/test/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultPolicy.java +++ b/tests/integration/vault/src/test/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultPolicy.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2022 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/tests/integration/vault/src/test/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultTest.java b/tests/integration/vault/src/test/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultTest.java index 7566ab24ea9..3268ed979f5 100644 --- a/tests/integration/vault/src/test/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultTest.java +++ b/tests/integration/vault/src/test/java/io/helidon/examples/integrations/vault/hcp/reactive/VaultTest.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.examples.integrations.vault.hcp.reactive; import java.sql.Connection; From 70519809fc70c7cb239ae80ddcf89db2da4ce03a Mon Sep 17 00:00:00 2001 From: Tomas Langer Date: Fri, 30 Sep 2022 17:08:58 +0200 Subject: [PATCH 4/8] Removed windows build of examples, as it got stuck for too long (Maven build, no output) --- .github/workflows/validate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 0ead901f3c6..d865ebf0c53 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -84,7 +84,7 @@ jobs: examples: strategy: matrix: - os: [ ubuntu-latest, windows-latest, macos-latest ] + os: [ ubuntu-latest, macos-latest ] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 From 9a39ec1c8845942ce1142191f3ae77b40e773985 Mon Sep 17 00:00:00 2001 From: Tomas Langer Date: Fri, 30 Sep 2022 20:12:06 +0200 Subject: [PATCH 5/8] Packaging tests are now only in etc/scripts, no need to keep test-runtime.sh anymore --- .github/workflows/validate.yml | 6 +-- etc/scripts/github-compile.sh | 31 ++++++++++++ etc/scripts/test-packaging-jar.sh | 9 ---- etc/scripts/test-packaging-jlink.sh | 9 ---- .../native-image/mp-1/test-runtime.sh | 47 ------------------ .../native-image/mp-3/test-runtime.sh | 49 ------------------- 6 files changed, 33 insertions(+), 118 deletions(-) create mode 100755 etc/scripts/github-compile.sh delete mode 100755 tests/integration/native-image/mp-1/test-runtime.sh delete mode 100755 tests/integration/native-image/mp-3/test-runtime.sh diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index d865ebf0c53..5ce48d1cd8e 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -143,11 +143,9 @@ jobs: distribution: ${{ env.JAVA_DISTRO }} java-version: ${{ env.JAVA_VERSION }} cache: maven + - name Build Helidon + run etc/scripts/github-compile.sh - name: JAR packaging run: etc/scripts/test-packaging-jar.sh - name: JLink packaging run: etc/scripts/test-packaging-jlink.sh - - name: MP-1 test - run: tests/integration/native-image/mp-1/test-runtime.sh - - name: MP-3 test - run: tests/integration/native-image/mp-3/test-runtime.sh diff --git a/etc/scripts/github-compile.sh b/etc/scripts/github-compile.sh new file mode 100755 index 00000000000..2c01d8d8277 --- /dev/null +++ b/etc/scripts/github-compile.sh @@ -0,0 +1,31 @@ +#!/bin/bash -e +# +# Copyright (c) 2022 Oracle and/or its affiliates. +# +# Licensed 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. +# + +# Path to this script +[ -h "${0}" ] && readonly SCRIPT_PATH="$(readlink "${0}")" || readonly SCRIPT_PATH="${0}" + +# Load pipeline environment setup and define WS_DIR +. $(dirname -- "${SCRIPT_PATH}")/includes/pipeline-env.sh "${SCRIPT_PATH}" '../..' + +# Setup error handling using default settings (defined in includes/error_handlers.sh) +error_trap_setup + +mvn ${MAVEN_ARGS} -f ${WS_DIR}/pom.xml \ + install -e \ + -Dmaven.test.skip=true \ + -DskipTests \ + -Ppipeline diff --git a/etc/scripts/test-packaging-jar.sh b/etc/scripts/test-packaging-jar.sh index bcb63830e80..9ad4fa9fcb4 100755 --- a/etc/scripts/test-packaging-jar.sh +++ b/etc/scripts/test-packaging-jar.sh @@ -24,15 +24,6 @@ # Setup error handling using default settings (defined in includes/error_handlers.sh) error_trap_setup -mvn ${MAVEN_ARGS} --version - -# Temporary workaround until job stages will share maven repository -mvn ${MAVEN_ARGS} -f ${WS_DIR}/pom.xml \ - install -e \ - -Dmaven.test.skip=true \ - -DskipTests \ - -Ppipeline - # Run native image tests cd ${WS_DIR}/tests/integration/native-image diff --git a/etc/scripts/test-packaging-jlink.sh b/etc/scripts/test-packaging-jlink.sh index 6e719fa279a..9e3be46c9d3 100755 --- a/etc/scripts/test-packaging-jlink.sh +++ b/etc/scripts/test-packaging-jlink.sh @@ -24,15 +24,6 @@ # Setup error handling using default settings (defined in includes/error_handlers.sh) error_trap_setup -mvn ${MAVEN_ARGS} --version - -# Temporary workaround until job stages will share maven repository -mvn ${MAVEN_ARGS} -f ${WS_DIR}/pom.xml \ - install -e \ - -Dmaven.test.skip=true \ - -DskipTests \ - -Ppipeline - # Run native image tests cd ${WS_DIR}/tests/integration/native-image diff --git a/tests/integration/native-image/mp-1/test-runtime.sh b/tests/integration/native-image/mp-1/test-runtime.sh deleted file mode 100755 index cb669f4e1f1..00000000000 --- a/tests/integration/native-image/mp-1/test-runtime.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash -e -# -# Copyright (c) 2020, 2022 Oracle and/or its affiliates. -# -# Licensed 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. -# - -# This file is called from /etc/scripts/build.sh - -trap 'echo "ERROR: Error occurred at ${BASH_SOURCE}:${LINENO} command: ${BASH_COMMAND}"' ERR -set -eo pipefail - -# Path to this script -if [ -h "${0}" ] ; then - readonly SCRIPT_PATH="$(readlink "$0")" -else - readonly SCRIPT_PATH="${0}" -fi - -# Directory this script resides in -readonly MY_DIR=$(cd $(dirname -- "${SCRIPT_PATH}") ; pwd -P) - -# cd the my dir, so we can start the application with correct current directory -cd "${MY_DIR}" - -# build the binary -mvn clean package -DskipTests - -# Attempt to run this example as a java -jar -# This is a self-testing application - -# java -jar target/helidon-tests-native-image-mp-1.jar - -# Attempt to run this example as a java with module path - -java --module-path target/helidon-tests-native-image-mp-1.jar:target/libs \ - -m helidon.tests.nimage.mp/io.helidon.tests.integration.nativeimage.mp1.Mp1Main diff --git a/tests/integration/native-image/mp-3/test-runtime.sh b/tests/integration/native-image/mp-3/test-runtime.sh deleted file mode 100755 index ea7232566e1..00000000000 --- a/tests/integration/native-image/mp-3/test-runtime.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash -e -# -# Copyright (c) 2020, 2022 Oracle and/or its affiliates. -# -# Licensed 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. -# - -# This file is called from /etc/scripts/build.sh - -trap 'echo "ERROR: Error occurred at ${BASH_SOURCE}:${LINENO} command: ${BASH_COMMAND}"' ERR -set -eo pipefail - -# Path to this script -if [ -h "${0}" ] ; then - readonly SCRIPT_PATH="$(readlink "$0")" -else - readonly SCRIPT_PATH="${0}" -fi - -# Directory this script resides in -readonly MY_DIR=$(cd $(dirname -- "${SCRIPT_PATH}") ; pwd -P) - -# cd the my dir, so we can start the application with correct current directory -cd "${MY_DIR}" - -# build the binary -mvn clean package -DskipTests - -# Attempt to run this example as a java -jar -# This is a self-testing application - -java -jar -Dexit.on.started=! target/helidon-tests-native-image-mp-3.jar - -# Attempt to run this example as a java with module path - -java -Dexit.on.started=! \ - --module-path target/helidon-tests-native-image-mp-3.jar:target/libs \ - --add-modules helidon.tests.nimage.quickstartmp \ - -m io.helidon.microprofile.cdi/io.helidon.microprofile.cdi.Main From fd9bab746b830fba1104ca03c5c1e13e0871a481 Mon Sep 17 00:00:00 2001 From: Tomas Langer Date: Fri, 30 Sep 2022 20:26:16 +0200 Subject: [PATCH 6/8] Added timeouts to calls. --- .../helidon/tests/integration/nativeimage/mp1/Mp1Main.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/integration/native-image/mp-1/src/main/java/io/helidon/tests/integration/nativeimage/mp1/Mp1Main.java b/tests/integration/native-image/mp-1/src/main/java/io/helidon/tests/integration/nativeimage/mp1/Mp1Main.java index 76478f937d0..3dd1ef8b359 100644 --- a/tests/integration/native-image/mp-1/src/main/java/io/helidon/tests/integration/nativeimage/mp1/Mp1Main.java +++ b/tests/integration/native-image/mp-1/src/main/java/io/helidon/tests/integration/nativeimage/mp1/Mp1Main.java @@ -30,6 +30,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; @@ -161,7 +162,10 @@ private static void cleanup() { } private static void testBean(int port, String jwtToken) { - Client client = ClientBuilder.newClient(); + Client client = ClientBuilder.newBuilder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .build(); WebTarget target = client.target("http://localhost:" + port); // select a bean From f5ac102d86d99962cb2057d49a3657ad9dcbd1cc Mon Sep 17 00:00:00 2001 From: Tomas Langer Date: Fri, 30 Sep 2022 20:44:04 +0200 Subject: [PATCH 7/8] Fix typos and added a few timeouts. --- .github/workflows/validate.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 5ce48d1cd8e..d933774dd0f 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -67,6 +67,7 @@ jobs: - name: Docs run: etc/scripts/site.sh build: + timeout-minutes: 60 strategy: matrix: os: [ ubuntu-latest ] @@ -82,6 +83,7 @@ jobs: - name: Maven build run: etc/scripts/github-build.sh examples: + timeout-minutes: 35 strategy: matrix: os: [ ubuntu-latest, macos-latest ] @@ -131,6 +133,7 @@ jobs: - name: Test archetypes run: etc/scripts/test-archetypes.sh packaging: + timeout-minutes: 30 strategy: matrix: os: [ ubuntu-latest, macos-latest] @@ -143,8 +146,8 @@ jobs: distribution: ${{ env.JAVA_DISTRO }} java-version: ${{ env.JAVA_VERSION }} cache: maven - - name Build Helidon - run etc/scripts/github-compile.sh + - name: Build Helidon + run: etc/scripts/github-compile.sh - name: JAR packaging run: etc/scripts/test-packaging-jar.sh - name: JLink packaging From e2a4ed982584eed2419115698d5e44883c4f6aa9 Mon Sep 17 00:00:00 2001 From: Tomas Langer Date: Sat, 1 Oct 2022 20:20:04 +0200 Subject: [PATCH 8/8] Moved testcontainers dependency management to integration tests pom Added timeouts to all jobs --- .github/workflows/validate.yml | 8 +++++++- pom.xml | 8 -------- tests/integration/pom.xml | 17 +++++++++++++++++ 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index d933774dd0f..ef155c5abd1 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -17,6 +17,7 @@ concurrency: jobs: copyright: + timeout-minutes: 10 runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -31,6 +32,7 @@ jobs: - name: Copyright run: etc/scripts/copyright.sh checkstyle: + timeout-minutes: 10 runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -43,6 +45,7 @@ jobs: - name: Checkstyle run: etc/scripts/checkstyle.sh spotbugs: + timeout-minutes: 30 runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -55,6 +58,7 @@ jobs: - name: Spotbugs run: etc/scripts/spotbugs.sh docs: + timeout-minutes: 10 runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -83,7 +87,7 @@ jobs: - name: Maven build run: etc/scripts/github-build.sh examples: - timeout-minutes: 35 + timeout-minutes: 30 strategy: matrix: os: [ ubuntu-latest, macos-latest ] @@ -102,6 +106,7 @@ jobs: cd examples mvn -B verify mp-tck: + timeout-minutes: 60 name: "MicroProfile TCKs" strategy: matrix: @@ -118,6 +123,7 @@ jobs: - name: Maven build run: etc/scripts/mp-tck.sh archetypes: + timeout-minutes: 30 strategy: matrix: os: [ ubuntu-latest ] diff --git a/pom.xml b/pom.xml index d10a7943961..a25cb230746 100644 --- a/pom.xml +++ b/pom.xml @@ -75,7 +75,6 @@ 3.0.1 3.1.12 3.0.0-M5 - 1.17.4 7.5 2.12.5 5.0.11 @@ -1173,13 +1172,6 @@ classgraph ${version.lib.classgraph} - - org.testcontainers - testcontainers-bom - ${version.lib.testcontainers} - pom - import - diff --git a/tests/integration/pom.xml b/tests/integration/pom.xml index 3577ae9bc56..b9ff35c1fb9 100644 --- a/tests/integration/pom.xml +++ b/tests/integration/pom.xml @@ -32,6 +32,10 @@ Helidon Integration Tests + + 1.17.4 + + tools zipkin-mp-2.2 @@ -60,6 +64,19 @@ vault + + + + + org.testcontainers + testcontainers-bom + ${version.lib.testcontainers} + pom + import + + + + native-image