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
+
+
+
+
+
+
+
+
+
+
+