diff --git a/docs/src/main/asciidoc/vault.adoc b/docs/src/main/asciidoc/vault.adoc index c326f2e6cb72e..f11c4078587e2 100644 --- a/docs/src/main/asciidoc/vault.adoc +++ b/docs/src/main/asciidoc/vault.adoc @@ -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. diff --git a/extensions/vault/deployment/pom.xml b/extensions/vault/deployment/pom.xml index 925b065b78d96..7dc5eaf790a69 100644 --- a/extensions/vault/deployment/pom.xml +++ b/extensions/vault/deployment/pom.xml @@ -68,6 +68,10 @@ quarkus-junit5 test + + io.quarkus + quarkus-devservices-common + diff --git a/extensions/vault/deployment/src/main/java/io/quarkus/vault/deployment/DevServicesVaultProcessor.java b/extensions/vault/deployment/src/main/java/io/quarkus/vault/deployment/DevServicesVaultProcessor.java index 773ab2c891051..257c9109712a2 100644 --- a/extensions/vault/deployment/src/main/java/io/quarkus/vault/deployment/DevServicesVaultProcessor.java +++ b/extensions/vault/deployment/src/main/java/io/quarkus/vault/deployment/DevServicesVaultProcessor.java @@ -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; @@ -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; @@ -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 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); @@ -55,72 +56,72 @@ public void startVaultContainers(BuildProducer // 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 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 entry : connectionProperties.entrySet()) { - devConfig.produce(new DevServicesConfigResultBuildItem(entry.getKey(), entry.getValue())); - } } - private StartResult startContainer(DevServicesConfig devServicesConfig, Optional timeout) { + private VaultInstance startContainer(DevServicesConfig devServicesConfig, LaunchModeBuildItem launchMode, + Optional timeout) { if (!devServicesConfig.enabled) { // explicitly disabled log.debug("Not starting devservices for Vault as it has been disabled in the config"); @@ -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"); @@ -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 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 { + private static class ConfiguredVaultContainer extends VaultContainer { 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 diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/config/DevServicesConfig.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/config/DevServicesConfig.java index 3d171985f29c7..7f40adab2a2ae 100644 --- a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/config/DevServicesConfig.java +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/config/DevServicesConfig.java @@ -27,6 +27,33 @@ public class DevServicesConfig { @ConfigItem public Optional 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. + *

+ * The discovery uses the {@code quarkus-dev-service-vault} label. + * The value is configured using the {@code service-name} property. + *

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

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