Skip to content

Commit

Permalink
Merge pull request #21300 from kdubb/feature/vault_shared_dev_services
Browse files Browse the repository at this point in the history
Implement label-based container discovery for Vault Dev Services
  • Loading branch information
vsevel authored Nov 11, 2021
2 parents 4c45e79 + 2ae5c0e commit 977b5fb
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 56 deletions.
14 changes: 14 additions & 0 deletions docs/src/main/asciidoc/vault.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,20 @@ When running the production version of the application, the Vault connection nee
so if you want to include a production Vault config in your `application.properties` and continue to use Dev Services
we recommend that you use the `%prod.` profile to define your Vault connection settings.

=== Shared server

Most of the time you need to share the server between applications.
Dev Services for Vault implements a _service discovery_ mechanism for your multiple Quarkus applications running in _dev_ mode to share a single server.

NOTE: Dev Services for Vault starts the container with the `quarkus-dev-service-vault` label which is used to identify the container.

If you need multiple (shared) servers, you can configure the `quarkus.vault.devservices.service-name` attribute and indicate the server name.
It looks for a container with the same value, or starts a new one if none can be found.
The default service name is `vault`.

Sharing is enabled by default in dev mode, but disabled in test mode.
You can disable the sharing with `quarkus.vault.devservices.shared=false`.

=== Automatic Secret Engine Provisioning

To help with provisioning the automatically managed Vault instance, you can enable certain secret engines.
Expand Down
4 changes: 4 additions & 0 deletions extensions/vault/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-devservices-common</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@

import java.io.Closeable;
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.function.Supplier;

import org.apache.commons.lang3.RandomStringUtils;
import org.jboss.logging.Logger;
import org.testcontainers.containers.Network;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.vault.VaultContainer;

Expand All @@ -25,6 +22,7 @@
import io.quarkus.deployment.console.StartupLogCompressor;
import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig;
import io.quarkus.deployment.logging.LoggingSetupBuildItem;
import io.quarkus.devservices.common.ContainerLocator;
import io.quarkus.runtime.configuration.ConfigUtils;
import io.quarkus.vault.runtime.VaultVersions;
import io.quarkus.vault.runtime.config.DevServicesConfig;
Expand All @@ -33,12 +31,15 @@
public class DevServicesVaultProcessor {
private static final Logger log = Logger.getLogger(DevServicesVaultProcessor.class);
private static final String VAULT_IMAGE = "vault:" + VaultVersions.VAULT_TEST_VERSION;
private static final String DEV_SERVICE_LABEL = "quarkus-dev-service-vault";
private static final String DEV_SERVICE_TOKEN = "root";
private static final int VAULT_EXPOSED_PORT = 8200;
private static final String CONFIG_PREFIX = "quarkus.vault.";
private static final String URL_CONFIG_KEY = CONFIG_PREFIX + "url";
private static final String AUTH_CONFIG_PREFIX = CONFIG_PREFIX + "authentication.";
private static final String CLIENT_TOKEN_CONFIG_KEY = AUTH_CONFIG_PREFIX + "client-token";
private static volatile List<Closeable> closeables;
private static final ContainerLocator vaultContainerLocator = new ContainerLocator(DEV_SERVICE_LABEL, VAULT_EXPOSED_PORT);
private static volatile Closeable closeable;
private static volatile DevServicesConfig capturedDevServicesConfiguration;
private static volatile boolean first = true;
private final IsDockerWorking isDockerWorking = new IsDockerWorking(true);
Expand All @@ -55,72 +56,72 @@ public void startVaultContainers(BuildProducer<DevServicesConfigResultBuildItem>

// figure out if we need to shut down and restart any existing Vault container
// if not and the Vault container have already started we just return
if (closeables != null) {
if (closeable != null) {
boolean restartRequired = !currentDevServicesConfiguration.equals(capturedDevServicesConfiguration);
if (!restartRequired) {
return;
}
for (Closeable closeable : closeables) {
try {
closeable.close();
} catch (Throwable e) {
log.error("Failed to stop Vault container", e);
}
try {
closeable.close();
} catch (Throwable e) {
log.error("Failed to stop Vault container", e);
}
closeables = null;
closeable = null;
capturedDevServicesConfiguration = null;
}

capturedDevServicesConfiguration = currentDevServicesConfiguration;

StartResult startResult;

StartupLogCompressor compressor = new StartupLogCompressor(
(launchMode.isTest() ? "(test) " : "") + "Vault Dev Services Starting:", consoleInstalledBuildItem,
loggingSetupBuildItem);
try {
startResult = startContainer(currentDevServicesConfiguration, devServicesConfig.timeout);
VaultInstance vaultInstance = startContainer(currentDevServicesConfiguration, launchMode,
devServicesConfig.timeout);
if (vaultInstance != null) {
closeable = vaultInstance.getCloseable();

devConfig.produce(new DevServicesConfigResultBuildItem(URL_CONFIG_KEY, vaultInstance.url));
devConfig.produce(new DevServicesConfigResultBuildItem(CLIENT_TOKEN_CONFIG_KEY, vaultInstance.clientToken));

if (vaultInstance.isOwner()) {
log.info("Dev Services for Vault started.");
log.infof("Other Quarkus applications in dev mode will find the "
+ "instance automatically. For Quarkus applications in production mode, you can connect to"
+ " this by starting your application with -D%s=%s -D%s=%s",
URL_CONFIG_KEY, vaultInstance.url, CLIENT_TOKEN_CONFIG_KEY, vaultInstance.clientToken);
}
}
compressor.close();
} catch (Throwable t) {
compressor.closeAndDumpCaptured();
throw new RuntimeException(t);
}
if (startResult == null) {
return;
}
Map<String, String> connectionProperties = new HashMap<>();
connectionProperties.put(URL_CONFIG_KEY, startResult.url);
connectionProperties.put(CLIENT_TOKEN_CONFIG_KEY, startResult.clientToken);

closeables = Collections.singletonList(startResult.closeable);

if (first) {
first = false;
Runnable closeTask = new Runnable() {
@Override
public void run() {
if (closeables != null) {
for (Closeable closeable : closeables) {
try {
closeable.close();
} catch (Throwable t) {
log.error("Failed to stop Vault container", t);
}
if (closeable != null) {
try {
closeable.close();
} catch (Throwable t) {
log.error("Failed to stop Vault container", t);
}
closeable = null;
log.info("Dev Services for Vault shut down.");
}
first = true;
closeables = null;
capturedDevServicesConfiguration = null;
}
};
closeBuildItem.addCloseTask(closeTask, true);
}
for (Map.Entry<String, String> entry : connectionProperties.entrySet()) {
devConfig.produce(new DevServicesConfigResultBuildItem(entry.getKey(), entry.getValue()));
}
}

private StartResult startContainer(DevServicesConfig devServicesConfig, Optional<Duration> timeout) {
private VaultInstance startContainer(DevServicesConfig devServicesConfig, LaunchModeBuildItem launchMode,
Optional<Duration> timeout) {
if (!devServicesConfig.enabled) {
// explicitly disabled
log.debug("Not starting devservices for Vault as it has been disabled in the config");
Expand All @@ -138,12 +139,13 @@ private StartResult startContainer(DevServicesConfig devServicesConfig, Optional
return null;
}

String token = RandomStringUtils.randomAlphanumeric(10);

DockerImageName dockerImageName = DockerImageName.parse(devServicesConfig.imageName.orElse(VAULT_IMAGE))
.asCompatibleSubstituteFor(VAULT_IMAGE);
FixedPortVaultContainer vaultContainer = new FixedPortVaultContainer(dockerImageName, devServicesConfig.port)
.withVaultToken(token);
ConfiguredVaultContainer vaultContainer = new ConfiguredVaultContainer(dockerImageName, devServicesConfig.port,
devServicesConfig.serviceName)
.withVaultToken(DEV_SERVICE_TOKEN);

vaultContainer.withNetwork(Network.SHARED);

if (devServicesConfig.transitEnabled) {
vaultContainer.withInitCommand("secrets enable transit");
Expand All @@ -155,37 +157,59 @@ private StartResult startContainer(DevServicesConfig devServicesConfig, Optional

devServicesConfig.initCommands.ifPresent(initCommands -> initCommands.forEach(vaultContainer::withInitCommand));

timeout.ifPresent(vaultContainer::withStartupTimeout);
vaultContainer.start();

String url = "http://" + vaultContainer.getHost() + ":" + vaultContainer.getPort();
return new StartResult(url, token,
new Closeable() {
@Override
public void close() {
vaultContainer.close();
}
});
final Supplier<VaultInstance> defaultVaultInstanceSupplier = () -> {
// Starting Vault
timeout.ifPresent(vaultContainer::withStartupTimeout);
vaultContainer.start();

return new VaultInstance(
vaultContainer.getHost(),
vaultContainer.getPort(),
DEV_SERVICE_TOKEN,
vaultContainer::close);
};

return vaultContainerLocator
.locateContainer(devServicesConfig.serviceName, devServicesConfig.shared, launchMode.getLaunchMode())
.map(containerAddress -> new VaultInstance(containerAddress.getHost(), containerAddress.getPort(),
DEV_SERVICE_TOKEN, null))
.orElseGet(defaultVaultInstanceSupplier);
}

private static class StartResult {
private static class VaultInstance {
private final String url;
private final String clientToken;
private final Closeable closeable;

public StartResult(String url, String clientToken, Closeable closeable) {
public VaultInstance(String host, int port, String clientToken, Closeable closeable) {
this("http://" + host + ":" + port, clientToken, closeable);
}

public VaultInstance(String url, String clientToken, Closeable closeable) {
this.url = url;
this.clientToken = clientToken;
this.closeable = closeable;
}

public boolean isOwner() {
return closeable != null;
}

public Closeable getCloseable() {
return closeable;
}
}

private static class FixedPortVaultContainer extends VaultContainer<FixedPortVaultContainer> {
private static class ConfiguredVaultContainer extends VaultContainer<ConfiguredVaultContainer> {
OptionalInt fixedExposedPort;

public FixedPortVaultContainer(DockerImageName dockerImageName, OptionalInt fixedExposedPort) {
public ConfiguredVaultContainer(DockerImageName dockerImageName, OptionalInt fixedExposedPort, String serviceName) {
super(dockerImageName);
this.fixedExposedPort = fixedExposedPort;
withNetwork(Network.SHARED);
if (serviceName != null) { // Only adds the label in dev mode.
withLabel(DEV_SERVICE_LABEL, serviceName);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,33 @@ public class DevServicesConfig {
@ConfigItem
public Optional<String> imageName;

/**
* Indicates if the Vault instance managed by Quarkus Dev Services is shared.
* When shared, Quarkus looks for running containers using label-based service discovery.
* If a matching container is found, it is used, and so a second one is not started.
* Otherwise, Dev Services for Vault starts a new container.
* <p>
* The discovery uses the {@code quarkus-dev-service-vault} label.
* The value is configured using the {@code service-name} property.
* <p>
* Container sharing is only used in dev mode.
*/
@ConfigItem(defaultValue = "true")
public boolean shared;

/**
* The value of the {@code quarkus-dev-service-vault} label attached to the started container.
* This property is used when {@code shared} is set to {@code true}.
* In this case, before starting a container, Dev Services for Vault looks for a container with the
* {@code quarkus-dev-service-vault} label
* set to the configured value. If found, it will use this container instead of starting a new one. Otherwise it
* starts a new container with the {@code quarkus-dev-service-vault} label set to the specified value.
* <p>
* This property is used when you need multiple shared Vault instances.
*/
@ConfigItem(defaultValue = "vault")
public String serviceName;

/**
* Optional fixed port the dev service will listen to.
* <p>
Expand Down

0 comments on commit 977b5fb

Please sign in to comment.