diff --git a/config-vault/pom.xml b/config-vault/pom.xml index 7418b30f..97e88c90 100644 --- a/config-vault/pom.xml +++ b/config-vault/pom.xml @@ -10,6 +10,12 @@ config-vault + + 2.5 + 5.0.0 + 1.12.1 + + io.scalecube @@ -19,12 +25,24 @@ com.bettercloud vault-java-driver - 3.1.0 + ${vault-java-driver.version} org.testcontainers vault - 1.6.0 + ${org.testcontainers.vault.version} + test + + + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j.version} + test + + + org.apache.logging.log4j + log4j-core + ${log4j.version} test diff --git a/config-vault/src/main/java/io/scalecube/config/vault/KubernetesVaultTokenSupplier.java b/config-vault/src/main/java/io/scalecube/config/vault/KubernetesVaultTokenSupplier.java new file mode 100644 index 00000000..cc7ee49f --- /dev/null +++ b/config-vault/src/main/java/io/scalecube/config/vault/KubernetesVaultTokenSupplier.java @@ -0,0 +1,30 @@ +package io.scalecube.config.vault; + +import com.bettercloud.vault.EnvironmentLoader; +import com.bettercloud.vault.Vault; +import com.bettercloud.vault.VaultConfig; +import io.scalecube.config.utils.ThrowableUtil; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Objects; +import java.util.stream.Collectors; + +public class KubernetesVaultTokenSupplier implements VaultTokenSupplier { + + private static final String VAULT_ROLE = "VAULT_ROLE"; + private static final String SERVICE_ACCOUNT_TOKEN_PATH = + "/var/run/secrets/kubernetes.io/serviceaccount/token"; + + @Override + public String getToken(EnvironmentLoader environmentLoader, VaultConfig config) { + String role = Objects.requireNonNull(environmentLoader.loadVariable(VAULT_ROLE), "vault role"); + try { + String jwt = Files.lines(Paths.get(SERVICE_ACCOUNT_TOKEN_PATH)).collect(Collectors.joining()); + return Objects.requireNonNull( + new Vault(config).auth().loginByKubernetes(role, jwt).getAuthClientToken(), + "vault token"); + } catch (Exception e) { + throw ThrowableUtil.propagate(e); + } + } +} diff --git a/config-vault/src/main/java/io/scalecube/config/vault/VaultConfigSource.java b/config-vault/src/main/java/io/scalecube/config/vault/VaultConfigSource.java index 81189bd1..61144809 100644 --- a/config-vault/src/main/java/io/scalecube/config/vault/VaultConfigSource.java +++ b/config-vault/src/main/java/io/scalecube/config/vault/VaultConfigSource.java @@ -1,9 +1,6 @@ package io.scalecube.config.vault; -import static java.util.Objects.requireNonNull; - import com.bettercloud.vault.EnvironmentLoader; -import com.bettercloud.vault.SslConfig; import com.bettercloud.vault.Vault; import com.bettercloud.vault.VaultConfig; import com.bettercloud.vault.VaultException; @@ -15,10 +12,12 @@ import io.scalecube.config.utils.ThrowableUtil; import java.time.Duration; import java.util.Map; +import java.util.Objects; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.function.Function; +import java.util.function.UnaryOperator; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,47 +31,69 @@ public class VaultConfigSource implements ConfigSource { private static final Logger LOGGER = LoggerFactory.getLogger(VaultConfigSource.class); + private static final ThreadFactory THREAD_FACTORY = + r -> { + Thread thread = new Thread(r); + thread.setDaemon(true); + thread.setName(VaultConfigSource.class.getSimpleName().toLowerCase() + "-token-renewer"); + return thread; + }; + + private static final String VAULT_SECRETS_PATH = "VAULT_SECRETS_PATH"; + private static final String VAULT_RENEW_PERIOD = "VAULT_RENEW_PERIOD"; + private final Vault vault; private final String secretsPath; - private final Duration renewEvery; /** * Create a new {@link VaultConfigSource} with the given {@link Builder}. * * @param builder configuration to create vault access with. */ - private VaultConfigSource(Builder builder) { - this.secretsPath = builder.secretsPath(); - this.renewEvery = builder.renewEvery; - vault = new Vault(builder.config); + private VaultConfigSource(Builder builder) throws VaultException { + EnvironmentLoader environmentLoader = + builder.environmentLoader != null ? builder.environmentLoader : new EnvironmentLoader(); + + secretsPath = + Objects.requireNonNull( + builder.secretsPath != null + ? builder.secretsPath + : environmentLoader.loadVariable(VAULT_SECRETS_PATH), + "Missing secretsPath"); + + VaultConfig vaultConfig = + builder.config.apply(new VaultConfig()).environmentLoader(environmentLoader).build(); + String token = builder.tokenSupplier.getToken(environmentLoader, vaultConfig); + vault = new Vault(vaultConfig.token(token)); + + Duration renewEvery = + builder.renewEvery != null + ? builder.renewEvery + : duration(environmentLoader.loadVariable(VAULT_RENEW_PERIOD)); if (renewEvery != null) { - long initialDelay = renewEvery.toMillis(); - long period = renewEvery.toMillis(); - TimeUnit unit = TimeUnit.MILLISECONDS; - ThreadFactory factory = - r -> { - Thread thread = new Thread(r); - thread.setDaemon(true); - thread.setName(VaultConfigSource.class.getSimpleName() + " token renewer"); - return thread; - }; - Executors.newScheduledThreadPool(1, factory) - .scheduleAtFixedRate( - () -> { - try { - vault.auth().renewSelf(); - LOGGER.info("renew token success"); - } catch (VaultException vaultException) { - LOGGER.error("failed to renew token", vaultException); - } - }, - initialDelay, - period, - unit); + scheduleVaultTokenRenew(renewEvery); } } + private void scheduleVaultTokenRenew(Duration renewEvery) { + long initialDelay = renewEvery.toMillis(); + long period = renewEvery.toMillis(); + Executors.newSingleThreadScheduledExecutor(THREAD_FACTORY) + .scheduleAtFixedRate( + () -> { + try { + this.vault.auth().renewSelf(); + LOGGER.info("renew token success"); + } catch (VaultException vaultException) { + LOGGER.error("failed to renew token", vaultException); + } + }, + initialDelay, + period, + TimeUnit.MILLISECONDS); + } + private void checkVaultStatus() throws VaultException { if (vault.seal().sealStatus().getSealed()) { throw new VaultException("Vault is sealed"); @@ -83,6 +104,10 @@ private void checkVaultStatus() throws VaultException { } } + private Duration duration(String duration) { + return duration != null ? Duration.parse(duration) : null; + } + @Override public Map loadConfig() { try { @@ -119,42 +144,38 @@ public static Builder builder() { * @param environmentLoader an {@link EnvironmentLoader} */ static Builder builder(EnvironmentLoader environmentLoader) { - return builder( - environmentLoader.loadVariable("VAULT_ADDR"), - environmentLoader.loadVariable("VAULT_TOKEN"), - environmentLoader.loadVariable("VAULT_SECRETS_PATH")); - } - - public static Builder builder(String address, String token, String secretsPath) { - return new Builder(address, token, secretsPath); + return new Builder(environmentLoader); } public static final class Builder { - final VaultConfig config = new VaultConfig(); - private final String secretsPath; - private Duration renewEvery = null; + private Function config = Function.identity(); + private VaultTokenSupplier tokenSupplier = new VaultTokenSupplier() {}; + private EnvironmentLoader environmentLoader; + private String secretsPath; + private Duration renewEvery; - Builder(String address, String token, String secretsPath) { - config - .address(requireNonNull(address, "Missing address")) - .token(requireNonNull(token, "Missing token")) - .sslConfig(new SslConfig()); - this.secretsPath = requireNonNull(secretsPath, "Missing secretsPath"); + private Builder(EnvironmentLoader environmentLoader) { + this.environmentLoader = environmentLoader; } - public Builder connectTimeout(int connectTimeout) { - config.openTimeout(connectTimeout); + public Builder renewEvery(Duration duration) { + renewEvery = duration; return this; } - public Builder readTimeout(int readTimeout) { - config.readTimeout(readTimeout); + public Builder secretsPath(String secretsPath) { + this.secretsPath = secretsPath; return this; } - public Builder renewEvery(Duration duration) { - renewEvery = duration; + public Builder config(UnaryOperator config) { + this.config = this.config.andThen(config); + return this; + } + + public Builder tokenSupplier(VaultTokenSupplier supplier) { + this.tokenSupplier = supplier; return this; } @@ -165,17 +186,11 @@ public Builder renewEvery(Duration duration) { */ public VaultConfigSource build() { try { - this.config.build(); return new VaultConfigSource(this); - } catch (VaultException propogateException) { - LOGGER.error( - "Unable to build " + VaultConfigSource.class.getSimpleName(), propogateException); - throw ThrowableUtil.propagate(propogateException); + } catch (VaultException e) { + LOGGER.error("Unable to build " + VaultConfigSource.class.getSimpleName(), e); + throw ThrowableUtil.propagate(e); } } - - public String secretsPath() { - return secretsPath; - } } } diff --git a/config-vault/src/main/java/io/scalecube/config/vault/VaultTokenSupplier.java b/config-vault/src/main/java/io/scalecube/config/vault/VaultTokenSupplier.java new file mode 100644 index 00000000..ef3f8ac0 --- /dev/null +++ b/config-vault/src/main/java/io/scalecube/config/vault/VaultTokenSupplier.java @@ -0,0 +1,12 @@ +package io.scalecube.config.vault; + +import com.bettercloud.vault.EnvironmentLoader; +import com.bettercloud.vault.VaultConfig; +import java.util.Objects; + +public interface VaultTokenSupplier { + + default String getToken(EnvironmentLoader environmentLoader, VaultConfig config) { + return Objects.requireNonNull(config.getToken(), "vault token"); + } +} diff --git a/config-vault/src/test/java/io/scalecube/config/vault/VaultConfigSourceTest.java b/config-vault/src/test/java/io/scalecube/config/vault/VaultConfigSourceTest.java index b1e822be..aedcdb9d 100644 --- a/config-vault/src/test/java/io/scalecube/config/vault/VaultConfigSourceTest.java +++ b/config-vault/src/test/java/io/scalecube/config/vault/VaultConfigSourceTest.java @@ -1,13 +1,5 @@ package io.scalecube.config.vault; -import static io.scalecube.config.vault.VaultContainerExtension.VAULT_IMAGE_NAME; -import static io.scalecube.config.vault.VaultContainerExtension.VAULT_PORT; -import static io.scalecube.config.vault.VaultContainerExtension.VAULT_SECRETS_PATH; -import static io.scalecube.config.vault.VaultContainerExtension.VAULT_SECRETS_PATH1; -import static io.scalecube.config.vault.VaultContainerExtension.VAULT_SECRETS_PATH2; -import static io.scalecube.config.vault.VaultContainerExtension.VAULT_SECRETS_PATH3; -import static io.scalecube.config.vault.VaultContainerExtension.VAULT_SERVER_STARTED; -import static io.scalecube.config.vault.VaultContainerExtension.VAULT_TOKEN; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; @@ -22,81 +14,55 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue; import com.bettercloud.vault.EnvironmentLoader; -import com.bettercloud.vault.SslConfig; import com.bettercloud.vault.Vault; -import com.bettercloud.vault.VaultConfig; import com.bettercloud.vault.VaultException; import io.scalecube.config.ConfigProperty; import io.scalecube.config.ConfigRegistry; import io.scalecube.config.ConfigRegistrySettings; import io.scalecube.config.ConfigSourceNotAvailableException; import io.scalecube.config.StringConfigProperty; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import org.testcontainers.containers.Container.ExecResult; -import org.testcontainers.containers.output.OutputFrame; -import org.testcontainers.vault.VaultContainer; class VaultConfigSourceTest { - private static final Pattern unsealKeyPattern = Pattern.compile("Unseal Key: ([a-z/0-9=A-Z]*)\n"); + /** the environment variable name for vault secret path */ + private static final String VAULT_SECRETS_PATH = "VAULT_SECRETS_PATH"; - private EnvironmentLoader loader1, loader2, loader3; - private Map environmentVariables = new HashMap<>(); + // these 3 are actual values we would like to test with + private static final String VAULT_SECRETS_PATH1 = "secret/application/tenant1"; + private static final String VAULT_SECRETS_PATH2 = "secret/application/tenant2"; + private static final String VAULT_SECRETS_PATH3 = "secret/application2/tenant3"; @RegisterExtension static final VaultContainerExtension vaultContainerExtension = new VaultContainerExtension(); - private Consumer waitingForUnsealKey(AtomicReference unsealKey) { - return onFrame -> { - Matcher matcher = unsealKeyPattern.matcher(onFrame.getUtf8String()); - if (matcher.find()) { - unsealKey.set(matcher.group(1)); - } - }; + @BeforeAll + static void beforeAll() { + VaultInstance vaultInstance = vaultContainerExtension.vaultInstance(); + vaultInstance.putSecrets( + VAULT_SECRETS_PATH1, "top_secret=password1", "db_password=dbpassword1"); + vaultInstance.putSecrets( + VAULT_SECRETS_PATH2, "top_secret=password2", "db_password=dbpassword2"); + vaultInstance.putSecrets(VAULT_SECRETS_PATH3, "secret=password", "password=dbpassword"); } - @BeforeEach - void setUp() { - environmentVariables.put("VAULT_TOKEN", VAULT_TOKEN); - environmentVariables.put( - "VAULT_ADDR", - "http://" + vaultContainerExtension.container().getContainerIpAddress() + ':' + VAULT_PORT); - - Map tenant1 = new HashMap<>(environmentVariables); - tenant1.put(VAULT_SECRETS_PATH, VAULT_SECRETS_PATH1); - this.loader1 = new MockEnvironmentLoader(tenant1); - - Map tenant2 = new HashMap<>(environmentVariables); - tenant2.put(VAULT_SECRETS_PATH, VAULT_SECRETS_PATH2); - this.loader2 = new MockEnvironmentLoader(tenant2); - - Map tenant3 = new HashMap<>(environmentVariables); - tenant3.put(VAULT_SECRETS_PATH, VAULT_SECRETS_PATH3); - this.loader3 = new MockEnvironmentLoader(tenant3); - } - - private class MockEnvironmentLoader extends EnvironmentLoader { - private final Map delegate; - - MockEnvironmentLoader(Map delegate) { - this.delegate = delegate; - } - - @Override - public String loadVariable(String name) { - return delegate.get(name); - } - } + private EnvironmentLoader baseLoader = + new MockEnvironmentLoader() + .put("VAULT_TOKEN", vaultContainerExtension.vaultInstance().rootToken()) + .put("VAULT_ADDR", vaultContainerExtension.vaultInstance().address()); + private EnvironmentLoader loader1 = + new MockEnvironmentLoader(baseLoader).put(VAULT_SECRETS_PATH, VAULT_SECRETS_PATH1); + private EnvironmentLoader loader2 = + new MockEnvironmentLoader(baseLoader).put(VAULT_SECRETS_PATH, VAULT_SECRETS_PATH2); + private EnvironmentLoader loader3 = + new MockEnvironmentLoader(baseLoader).put(VAULT_SECRETS_PATH, VAULT_SECRETS_PATH3); @Test void testFirstTenant() { @@ -133,74 +99,72 @@ void testMissingProperty() { @Test void testMissingTenant() { - EnvironmentLoader loader4; - Map tenant4 = new HashMap<>(environmentVariables); - - tenant4.put(VAULT_SECRETS_PATH, "secrets/unknown/path"); - loader4 = new MockEnvironmentLoader(tenant4); - + EnvironmentLoader loader4 = + new MockEnvironmentLoader(baseLoader).put(VAULT_SECRETS_PATH, "secrets/unknown/path"); VaultConfigSource vaultConfigSource = VaultConfigSource.builder(loader4).build(); - assertThrows(ConfigSourceNotAvailableException.class, vaultConfigSource::loadConfig); + assumeTrue(vaultConfigSource.loadConfig().isEmpty()); + + // root token + // assertThrows(ConfigSourceNotAvailableException.class, vaultConfigSource::loadConfig); } @Test void testInvalidAddress() { - Map invalidAddress = new HashMap<>(); - invalidAddress.put("VAULT_ADDR", "http://invalid.host.local:8200"); - invalidAddress.put("VAULT_TOKEN", VAULT_TOKEN); - invalidAddress.put(VAULT_SECRETS_PATH, VAULT_SECRETS_PATH1); - VaultConfigSource vaultConfigSource = - VaultConfigSource.builder(new MockEnvironmentLoader(invalidAddress)).build(); + VaultConfigSource.builder( + new MockEnvironmentLoader() + .put("VAULT_ADDR", "http://invalid.host.local:8200") + .put("VAULT_TOKEN", vaultContainerExtension.vaultInstance().rootToken()) + .put(VAULT_SECRETS_PATH, VAULT_SECRETS_PATH1)) + .build(); assertThrows(ConfigSourceNotAvailableException.class, vaultConfigSource::loadConfig); } @Test void testInvalidToken() { - Map invalidToken = new HashMap<>(environmentVariables); - invalidToken.put("VAULT_TOKEN", "zzzzzz"); - invalidToken.put(VAULT_SECRETS_PATH, "secrets/unknown/path"); - VaultConfigSource vaultConfigSource = - VaultConfigSource.builder(new MockEnvironmentLoader(invalidToken)).build(); + VaultConfigSource.builder( + new MockEnvironmentLoader(baseLoader) + .put("VAULT_TOKEN", "zzzzzz") + .put(VAULT_SECRETS_PATH, "secrets/unknown/path")) + .build(); - assertThrows(ConfigSourceNotAvailableException.class, vaultConfigSource::loadConfig); + assumeTrue(vaultConfigSource.loadConfig().isEmpty()); + + // root token + // assertThrows(ConfigSourceNotAvailableException.class, vaultConfigSource::loadConfig); } @Test - void shouldWorkWhenRegistryIsReloadedAndVaultIsRunning() { - try (VaultContainer vaultContainer2 = new VaultContainer<>(VAULT_IMAGE_NAME)) { - vaultContainer2 - .withVaultToken(VAULT_TOKEN) - .withVaultPort(8202) - .withSecretInVault(VAULT_SECRETS_PATH1, "top_secret=password1", "db_password=dbpassword1") - .waitingFor(VAULT_SERVER_STARTED) - .start(); - String address = "http://" + vaultContainer2.getContainerIpAddress() + ':' + 8202; - ConfigRegistrySettings settings = - ConfigRegistrySettings.builder() - .addLastSource( - "vault", - VaultConfigSource.builder(address, VAULT_TOKEN, VAULT_SECRETS_PATH1).build()) - .reloadIntervalSec(1) - .build(); - ConfigRegistry configRegistry = ConfigRegistry.create(settings); - StringConfigProperty configProperty = configRegistry.stringProperty("top_secret"); - - assertThat(configProperty.value().get(), containsString("password1")); - try { - ExecResult execResult = - vaultContainer2.execInContainer( - "/bin/sh", "-c", "vault write " + VAULT_SECRETS_PATH1 + " top_secret=new_password"); - assumeTrue(execResult.getStdout().contains("Success")); - TimeUnit.SECONDS.sleep(2); - } catch (Exception ignoredException) { - fail("oops"); - } - assertThat(configProperty.value().get(), containsString("new_password")); - } + void shouldWorkWhenRegistryIsReloadedAndVaultIsRunning() throws InterruptedException { + VaultInstance vaultInstance = vaultContainerExtension.startNewVaultInstance(); + vaultInstance.putSecrets( + VAULT_SECRETS_PATH1, "top_secret=password1", "db_password=dbpassword1"); + String address = vaultInstance.address(); + String rootToken = vaultInstance.rootToken(); + + ConfigRegistrySettings settings = + ConfigRegistrySettings.builder() + .addLastSource( + "vault", + VaultConfigSource.builder() + .config(vaultConfig -> vaultConfig.address(address).token(rootToken)) + .secretsPath(VAULT_SECRETS_PATH1) + .build()) + .reloadIntervalSec(1) + .build(); + ConfigRegistry configRegistry = ConfigRegistry.create(settings); + StringConfigProperty configProperty = configRegistry.stringProperty("top_secret"); + + assertThat(configProperty.value().get(), containsString("password1")); + + vaultInstance.putSecrets(VAULT_SECRETS_PATH1, " top_secret=new_password"); + + TimeUnit.SECONDS.sleep(2); + + assertThat(configProperty.value().get(), containsString("new_password")); } @Test @@ -208,60 +172,50 @@ void shouldWorkWhenRegistryIsReloadedAndVaultIsDown() { String PASSWORD_PROPERTY_NAME = "password"; String PASSWORD_PROPERTY_VALUE = "123456"; String secret = PASSWORD_PROPERTY_NAME + "=" + PASSWORD_PROPERTY_VALUE; - try (VaultContainer vaultContainer2 = new VaultContainer<>(VAULT_IMAGE_NAME)) { - vaultContainer2 - .withVaultToken(VAULT_TOKEN) - .withVaultPort(8203) - .withEnv("VAULT_DEV_ROOT_TOKEN_ID", (String) VAULT_TOKEN) - .withSecretInVault(VAULT_SECRETS_PATH1, secret) - .waitingFor(VAULT_SERVER_STARTED) - .start(); - - String address = "http://" + vaultContainer2.getContainerIpAddress() + ':' + 8203; - - ConfigRegistrySettings settings = - ConfigRegistrySettings.builder() - .addLastSource( - "vault", - VaultConfigSource.builder(address, VAULT_TOKEN, VAULT_SECRETS_PATH1).build()) - .reloadIntervalSec(1) - .build(); - ConfigRegistry configRegistry = ConfigRegistry.create(settings); - StringConfigProperty configProperty = configRegistry.stringProperty(PASSWORD_PROPERTY_NAME); - configProperty.addValidator(Objects::nonNull); - - vaultContainer2.stop(); - assertFalse(vaultContainer2.isRunning()); - - try { - TimeUnit.SECONDS.sleep(2); - } catch (InterruptedException ignoredException) { - } - - assertThat(configProperty.value().get(), containsString(PASSWORD_PROPERTY_VALUE)); + + VaultInstance vaultInstance = vaultContainerExtension.startNewVaultInstance(); + vaultInstance.putSecrets(VAULT_SECRETS_PATH1, secret); + String address = vaultInstance.address(); + String rootToken = vaultInstance.rootToken(); + + ConfigRegistrySettings settings = + ConfigRegistrySettings.builder() + .addLastSource( + "vault", + VaultConfigSource.builder() + .config(vaultConfig -> vaultConfig.address(address).token(rootToken)) + .secretsPath(VAULT_SECRETS_PATH1) + .build()) + .reloadIntervalSec(1) + .build(); + ConfigRegistry configRegistry = ConfigRegistry.create(settings); + StringConfigProperty configProperty = configRegistry.stringProperty(PASSWORD_PROPERTY_NAME); + configProperty.addValidator(Objects::nonNull); + + vaultInstance.close(); + assertFalse(vaultInstance.container().isRunning()); + + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + fail(e); } + + assertThat(configProperty.value().get(), containsString(PASSWORD_PROPERTY_VALUE)); } @Test void testSealed() throws Throwable { - try (VaultContainer vaultContainerSealed = new VaultContainer<>()) { - vaultContainerSealed - .withVaultToken(VAULT_TOKEN) - .withVaultPort(8204) - .waitingFor(VAULT_SERVER_STARTED) - .start(); - - String address = "http://" + vaultContainerSealed.getContainerIpAddress() + ':' + 8204; - Vault vault = - new Vault( - new VaultConfig().address(address).token(VAULT_TOKEN).sslConfig(new SslConfig())); + VaultInstance vaultInstance = vaultContainerExtension.startNewVaultInstance(); + Vault vault = vaultInstance.vault(); + try { vault.seal().seal(); assumeTrue(vault.seal().sealStatus().getSealed(), "vault seal status"); Map clientEnv = new HashMap<>(); clientEnv.put("VAULT_TOKEN", "ROOT"); - clientEnv.put("VAULT_ADDR", address); + clientEnv.put("VAULT_ADDR", vaultInstance.address()); clientEnv.put(VAULT_SECRETS_PATH, VAULT_SECRETS_PATH1); VaultConfigSource.builder(new MockEnvironmentLoader(clientEnv)).build().loadConfig(); @@ -275,60 +229,83 @@ void testSealed() throws Throwable { @Test void shouldWorkWhenRegistryIsReloadedAndVaultIsUnSealed() throws InterruptedException { - AtomicReference unsealKey = new AtomicReference<>(); - try (VaultContainer sealedVaultContainer = new VaultContainer<>(VAULT_IMAGE_NAME)) { - sealedVaultContainer - .withVaultToken(VAULT_TOKEN) - .withVaultPort(8205) - .withSecretInVault(VAULT_SECRETS_PATH1, "top_secret=password1", "db_password=dbpassword1") - .withLogConsumer(waitingForUnsealKey(unsealKey)) - .waitingFor(VAULT_SERVER_STARTED) - .start(); - - assumeTrue(unsealKey.get() != null, "unable to get unseal key"); - - String address = "http://" + sealedVaultContainer.getContainerIpAddress() + ':' + 8205; - - ConfigRegistrySettings settings = - ConfigRegistrySettings.builder() - .addLastSource( - "vault", - VaultConfigSource.builder(address, VAULT_TOKEN, VAULT_SECRETS_PATH1).build()) - .reloadIntervalSec(1) - .build(); - - ConfigRegistry configRegistry = ConfigRegistry.create(settings); - StringConfigProperty configProperty = configRegistry.stringProperty("top_secret"); - - assertThat( - "initial value of top_secret", configProperty.value().get(), containsString("password1")); - - Vault vault = - new Vault( - new VaultConfig().address(address).token(VAULT_TOKEN).sslConfig(new SslConfig())); - Map newValues = new HashMap<>(); - newValues.put(configProperty.name(), "new_password"); - - try { - vault.logical().write(VAULT_SECRETS_PATH1, newValues); - vault.seal().seal(); - assumeTrue(vault.seal().sealStatus().getSealed(), "vault seal status"); - } catch (VaultException vaultException) { - fail(vaultException.getMessage()); - } - TimeUnit.SECONDS.sleep(2); - assumeFalse( - configProperty.value().isPresent() - && configProperty.value().get().contains("new_password"), - "new value was unexpectedly set"); - try { - vault.seal().unseal(unsealKey.get()); - assumeFalse(vault.seal().sealStatus().getSealed(), "vault seal status"); - } catch (VaultException vaultException) { - fail(vaultException.getMessage()); - } - TimeUnit.SECONDS.sleep(2); - assertThat(configProperty.value().get(), containsString("new_password")); + VaultInstance sealedVaultInstance = vaultContainerExtension.startNewVaultInstance(); + sealedVaultInstance.putSecrets( + VAULT_SECRETS_PATH1, "top_secret=password1", "db_password=dbpassword1"); + String address = sealedVaultInstance.address(); + String unsealKey = sealedVaultInstance.unsealKey(); + String rootToken = sealedVaultInstance.rootToken(); + + ConfigRegistrySettings settings = + ConfigRegistrySettings.builder() + .addLastSource( + "vault", + VaultConfigSource.builder() + .config(vaultConfig -> vaultConfig.address(address).token(rootToken)) + .secretsPath(VAULT_SECRETS_PATH1) + .build()) + .reloadIntervalSec(1) + .build(); + + ConfigRegistry configRegistry = ConfigRegistry.create(settings); + StringConfigProperty configProperty = configRegistry.stringProperty("top_secret"); + + assertThat( + "initial value of top_secret", configProperty.value().get(), containsString("password1")); + + Vault vault = sealedVaultInstance.vault(); + Map newValues = new HashMap<>(); + newValues.put(configProperty.name(), "new_password"); + + try { + vault.logical().write(VAULT_SECRETS_PATH1, newValues); + vault.seal().seal(); + assumeTrue(vault.seal().sealStatus().getSealed(), "vault seal status"); + } catch (VaultException vaultException) { + fail(vaultException.getMessage()); + } + TimeUnit.SECONDS.sleep(2); + assumeFalse( + configProperty.value().isPresent() && configProperty.value().get().contains("new_password"), + "new value was unexpectedly set"); + try { + vault.seal().unseal(unsealKey); + assumeFalse(vault.seal().sealStatus().getSealed(), "vault seal status"); + } catch (VaultException vaultException) { + fail(vaultException.getMessage()); + } + TimeUnit.SECONDS.sleep(2); + assertThat(configProperty.value().get(), containsString("new_password")); + } + + private static class MockEnvironmentLoader extends EnvironmentLoader { + private final Map env; + + MockEnvironmentLoader() { + this(Collections.emptyMap()); + } + + MockEnvironmentLoader(EnvironmentLoader loader) { + this(((MockEnvironmentLoader) loader).env); + } + + MockEnvironmentLoader(Map base) { + this.env = new HashMap<>(base); + } + + MockEnvironmentLoader put(String key, String value) { + env.put(key, value); + return this; + } + + @Override + public String loadVariable(String name) { + return env.get(name); + } + + @Override + public String toString() { + return "MockEnvironmentLoader{" + "env=" + env + '}'; } } } diff --git a/config-vault/src/test/java/io/scalecube/config/vault/VaultContainerExtension.java b/config-vault/src/test/java/io/scalecube/config/vault/VaultContainerExtension.java index 958fd399..e7aafa3b 100644 --- a/config-vault/src/test/java/io/scalecube/config/vault/VaultContainerExtension.java +++ b/config-vault/src/test/java/io/scalecube/config/vault/VaultContainerExtension.java @@ -1,55 +1,50 @@ package io.scalecube.config.vault; +import java.util.ArrayList; +import java.util.List; +import java.util.function.UnaryOperator; import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.ExtensionContext; -import org.testcontainers.containers.wait.LogMessageWaitStrategy; -import org.testcontainers.containers.wait.WaitStrategy; import org.testcontainers.vault.VaultContainer; -public class VaultContainerExtension implements AfterAllCallback, BeforeAllCallback { +public class VaultContainerExtension + implements AfterAllCallback, BeforeAllCallback, AfterEachCallback { - static final String VAULT_IMAGE_NAME = "vault:0.9.6"; - static final int VAULT_PORT = 8200; - static final String VAULT_TOKEN = "my-root-token"; - /** the environment variable name for vault secret path */ - static final String VAULT_SECRETS_PATH = "VAULT_SECRETS_PATH"; + private VaultInstance vaultInstance; + private List vaultInstances = new ArrayList<>(); - // these 3 are actual values we would like to test with - static final String VAULT_SECRETS_PATH1 = "secret/application/tenant1"; - static final String VAULT_SECRETS_PATH2 = "secret/application/tenant2"; - static final String VAULT_SECRETS_PATH3 = "secret/application2/tenant3"; - - static final WaitStrategy VAULT_SERVER_STARTED = - new LogMessageWaitStrategy() - .withRegEx("==> Vault server started! Log data will stream in below:\n") - .withTimes(1); - - private VaultContainer vaultContainer; + @Override + public void beforeAll(ExtensionContext context) { + vaultInstance = startNewVaultInstance(); + vaultInstances.clear(); + } @Override public void afterAll(ExtensionContext context) { - if (vaultContainer.isRunning()) { - vaultContainer.stop(); + if (vaultInstance != null && vaultInstance.container().isRunning()) { + vaultInstance.container().stop(); } } @Override - public void beforeAll(ExtensionContext context) { - vaultContainer = - new VaultContainer<>() - .waitingFor(VAULT_SERVER_STARTED) - .withVaultToken(VAULT_TOKEN) - .withVaultPort(VAULT_PORT) - .withSecretInVault( - VAULT_SECRETS_PATH1, "top_secret=password1", "db_password=dbpassword1") - .withSecretInVault( - VAULT_SECRETS_PATH2, "top_secret=password2", "db_password=dbpassword2") - .withSecretInVault(VAULT_SECRETS_PATH3, "secret=password", "password=dbpassword"); - vaultContainer.start(); + public void afterEach(ExtensionContext context) { + vaultInstances.forEach(VaultInstance::close); + vaultInstances.clear(); + } + + VaultInstance vaultInstance() { + return vaultInstance; + } + + VaultInstance startNewVaultInstance() { + return startNewVaultInstance(UnaryOperator.identity()); } - VaultContainer container() { - return vaultContainer; + VaultInstance startNewVaultInstance(UnaryOperator function) { + VaultInstance vaultInstance = VaultInstance.start(function); + vaultInstances.add(vaultInstance); + return vaultInstance; } } diff --git a/config-vault/src/test/java/io/scalecube/config/vault/VaultInstance.java b/config-vault/src/test/java/io/scalecube/config/vault/VaultInstance.java new file mode 100644 index 00000000..e14e3974 --- /dev/null +++ b/config-vault/src/test/java/io/scalecube/config/vault/VaultInstance.java @@ -0,0 +1,157 @@ +package io.scalecube.config.vault; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.bettercloud.vault.SslConfig; +import com.bettercloud.vault.Vault; +import com.bettercloud.vault.VaultConfig; +import io.scalecube.config.utils.ThrowableUtil; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; +import org.testcontainers.DockerClientFactory; +import org.testcontainers.containers.Container.ExecResult; +import org.testcontainers.containers.ContainerLaunchException; +import org.testcontainers.containers.output.OutputFrame; +import org.testcontainers.containers.output.WaitingConsumer; +import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; +import org.testcontainers.utility.LogUtils; +import org.testcontainers.vault.VaultContainer; + +public class VaultInstance implements AutoCloseable { + + private static final String VAULT_IMAGE_NAME = "vault:1.2.3"; + private static final int VAULT_PORT = 8200; + private static final AtomicInteger PORT_COUNTER = new AtomicInteger(VAULT_PORT); + private static final String UNSEAL_KEY_LOG = "Unseal Key: "; + private static final String ROOT_TOKEN_LOG = "Root Token: "; + + private final VaultContainer container; + private final String unsealKey; + private final String rootToken; + + private VaultInstance(VaultContainer container, String unsealKey, String rootToken) { + this.container = container; + this.unsealKey = Objects.requireNonNull(unsealKey, "unseal key"); + this.rootToken = Objects.requireNonNull(rootToken, "root token"); + } + + static VaultInstance start(UnaryOperator function) { + VaultInstanceWaitStrategy waitStrategy = new VaultInstanceWaitStrategy(); + VaultContainer container = + function.apply( + new VaultContainer<>(VAULT_IMAGE_NAME) + .withVaultToken(UUID.randomUUID().toString()) + .withVaultPort(PORT_COUNTER.incrementAndGet()) + .waitingFor(waitStrategy)); + container.start(); + return new VaultInstance(container, waitStrategy.unsealKey, waitStrategy.rootToken); + } + + public VaultContainer container() { + return container; + } + + public Vault vault() { + return invoke( + () -> { + String vaultToken = container.getEnvMap().get("VAULT_TOKEN").toString(); + VaultConfig config = + new VaultConfig() + .address(address()) + .token(vaultToken) + .openTimeout(5) + .readTimeout(30) + .sslConfig(new SslConfig().build()) + .build(); + return new Vault(config).withRetries(5, 1000); + }); + } + + public void putSecrets(String path, String firstSecret, String... remainingSecrets) { + StringBuilder command = + new StringBuilder() + .append("vault kv put ") + .append(path) + .append(" ") + .append(firstSecret) + .append(" "); + for (String secret : remainingSecrets) { + command.append(secret).append(" "); + } + ExecResult execResult = + invoke(() -> container.execInContainer("/bin/sh", "-c", command.toString())); + assertEquals(0, execResult.getExitCode(), execResult.toString()); + } + + public String address() { + return String.format( + "http://%s:%d", container.getContainerIpAddress(), container.getMappedPort(VAULT_PORT)); + } + + public String rootToken() { + return rootToken; + } + + public String unsealKey() { + return unsealKey; + } + + public void close() { + container.close(); + } + + private T invoke(Callable action) { + try { + return action.call(); + } catch (Exception e) { + throw ThrowableUtil.propagate(e); + } + } + + private static class VaultInstanceWaitStrategy extends AbstractWaitStrategy { + + private static final String VAULT_STARTED_LOG_MESSAGE = + "==> Vault server started! Log data will stream in below:"; + + private boolean isVaultStarted; + private String unsealKey; + private String rootToken; + + @Override + protected void waitUntilReady() { + WaitingConsumer waitingConsumer = new WaitingConsumer(); + LogUtils.followOutput( + DockerClientFactory.instance().client(), + waitStrategyTarget.getContainerId(), + waitingConsumer); + + Predicate waitPredicate = + outputFrame -> { + String log = outputFrame.getUtf8String(); + if (log.contains(UNSEAL_KEY_LOG)) { + unsealKey = log.substring(UNSEAL_KEY_LOG.length()).replaceAll("\\r?\\n", ""); + } + if (log.contains(ROOT_TOKEN_LOG)) { + rootToken = log.substring(ROOT_TOKEN_LOG.length()).replaceAll("\\r?\\n", ""); + } + if (log.contains(VAULT_STARTED_LOG_MESSAGE)) { + isVaultStarted = true; + } + return isVaultStarted && unsealKey != null && rootToken != null; + }; + + try { + waitingConsumer.waitUntil(waitPredicate, startupTimeout.getSeconds(), TimeUnit.SECONDS, 1); + } catch (TimeoutException e) { + throw new ContainerLaunchException( + "Timed out waiting for log output matching '" + VAULT_STARTED_LOG_MESSAGE + "'"); + } + } + } +} diff --git a/config-vault/src/test/resources/log4j2-test.xml b/config-vault/src/test/resources/log4j2-test.xml new file mode 100644 index 00000000..1f95c986 --- /dev/null +++ b/config-vault/src/test/resources/log4j2-test.xml @@ -0,0 +1,18 @@ + + + + + + + %level{length=1} %date{MMdd-HHmm:ss,SSS} %logger{1.} %message [%thread]%n + + + + + + + + + + +