From 8c16764018dfe0b6f040ee299d3c0588f5b4d91b Mon Sep 17 00:00:00 2001 From: Patryk Najda Date: Tue, 22 Jun 2021 16:57:19 +0200 Subject: [PATCH] Implement label-based container lookup for Redis Dev Service --- .../client/deployment/DevServicesConfig.java | 33 +++++++++- .../deployment/DevServicesProcessor.java | 62 +++++++++++++++++-- 2 files changed, 88 insertions(+), 7 deletions(-) diff --git a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/DevServicesConfig.java b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/DevServicesConfig.java index c10d481a5f84e..0a6fcb9c87206 100644 --- a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/DevServicesConfig.java +++ b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/DevServicesConfig.java @@ -34,6 +34,33 @@ public class DevServicesConfig { @ConfigItem public OptionalInt port; + /** + * Indicates if the Redis server 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 Redis starts a new container. + *

+ * The discovery uses the {@code quarkus-dev-service-redis} 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-redis} 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 Redis looks for a container with the + * {@code quarkus-dev-service-redis} 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-redis} label set to the specified value. + *

+ * This property is used when you need multiple shared Redis servers. + */ + @ConfigItem(defaultValue = "redis") + public String serviceName; + @Override public boolean equals(Object o) { if (this == o) @@ -43,11 +70,13 @@ public boolean equals(Object o) { DevServicesConfig that = (DevServicesConfig) o; return enabled == that.enabled && Objects.equals(imageName, that.imageName) && - Objects.equals(port, that.port); + Objects.equals(port, that.port) && + Objects.equals(shared, that.shared) && + Objects.equals(serviceName, that.serviceName); } @Override public int hashCode() { - return Objects.hash(enabled, imageName, port); + return Objects.hash(enabled, imageName, port, shared, serviceName); } } diff --git a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/DevServicesProcessor.java b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/DevServicesProcessor.java index 3a2840336aa57..27755729cbdc6 100644 --- a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/DevServicesProcessor.java +++ b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/DevServicesProcessor.java @@ -1,6 +1,7 @@ package io.quarkus.redis.client.deployment; import static io.quarkus.redis.client.runtime.RedisClientUtil.isDefault; +import static io.quarkus.runtime.LaunchMode.DEVELOPMENT; import java.io.Closeable; import java.util.ArrayList; @@ -10,7 +11,10 @@ import java.util.Map.Entry; import java.util.OptionalInt; +import com.github.dockerjava.api.model.Container; +import com.github.dockerjava.api.model.ContainerPort; import org.jboss.logging.Logger; +import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerImageName; @@ -35,6 +39,13 @@ public class DevServicesProcessor { private static final String REDIS_6_ALPINE = "redis:6-alpine"; private static final int REDIS_EXPOSED_PORT = 6379; private static final String REDIS_SCHEME = "redis://"; + + /** + * Label to add to shared Dev Service for Redis running in containers. + * This allows other applications to discover the running service and use it instead of starting a new instance. + */ + private static final String DEV_SERVICE_LABEL = "quarkus-dev-service-redis"; + private static final String QUARKUS = "quarkus."; private static final String DOT = "."; private static volatile List closeables; @@ -76,7 +87,7 @@ public void startRedisContainers(LaunchModeBuildItem launchMode, List currentCloseables = new ArrayList<>(); for (Entry entry : currentDevServicesConfiguration.entrySet()) { String connectionName = entry.getKey(); - StartResult startResult = startContainer(connectionName, entry.getValue().devservices); + StartResult startResult = startContainer(connectionName, entry.getValue().devservices, launchMode.getLaunchMode()); if (startResult == null) { continue; } @@ -120,7 +131,29 @@ public void run() { } } - private StartResult startContainer(String connectionName, DevServicesConfig devServicesConfig) { + private static Container lookup(String expectedLabelValue) { + List containers = DockerClientFactory.lazyClient().listContainersCmd().exec(); + for (Container container : containers) { + String s = container.getLabels().get(DEV_SERVICE_LABEL); + if (expectedLabelValue.equalsIgnoreCase(s)) { + return container; + } + } + return null; + } + + private static ContainerPort getMappedPort(Container container, int port) { + for (ContainerPort p : container.getPorts()) { + Integer mapped = p.getPrivatePort(); + Integer publicPort = p.getPublicPort(); + if (mapped != null && mapped == port && publicPort != null) { + return p; + } + } + return null; + } + + private StartResult startContainer(String connectionName, DevServicesConfig devServicesConfig, LaunchMode launchMode) { if (!devServicesConfig.enabled) { // explicitly disabled log.debug("Not starting devservices for " + (isDefault(connectionName) ? "default redis client" : connectionName) @@ -139,7 +172,23 @@ private StartResult startContainer(String connectionName, DevServicesConfig devS DockerImageName dockerImageName = DockerImageName.parse(devServicesConfig.imageName.orElse(REDIS_6_ALPINE)) .asCompatibleSubstituteFor(REDIS_6_ALPINE); - FixedPortRedisContainer redisContainer = new FixedPortRedisContainer(dockerImageName, devServicesConfig.port); + + if (devServicesConfig.shared && launchMode == DEVELOPMENT) { + Container container = lookup(devServicesConfig.serviceName); + if (container != null) { + ContainerPort port = getMappedPort(container, REDIS_EXPOSED_PORT); + if (port != null) { + String url = port.getIp() + ":" + port.getPublicPort(); + log.infof("Dev Services for Redis container found: %s (%s). " + + "Connecting to: %s.", + container.getId(), + container.getImage(), url); + return new StartResult(url, null); + } + } + } + + FixedPortRedisContainer redisContainer = new FixedPortRedisContainer(dockerImageName, devServicesConfig.port, launchMode == DEVELOPMENT ? devServicesConfig.serviceName : null); redisContainer.start(); String redisHost = REDIS_SCHEME + redisContainer.getHost() + ":" + redisContainer.getPort(); return new StartResult(redisHost, @@ -169,12 +218,15 @@ public StartResult(String url, Closeable closeable) { } } - private static class FixedPortRedisContainer extends GenericContainer { + private static class FixedPortRedisContainer extends GenericContainer { OptionalInt fixedExposedPort; - public FixedPortRedisContainer(DockerImageName dockerImageName, OptionalInt fixedExposedPort) { + public FixedPortRedisContainer(DockerImageName dockerImageName, OptionalInt fixedExposedPort, String serviceName) { super(dockerImageName); this.fixedExposedPort = fixedExposedPort; + if (serviceName != null) { + withLabel(DEV_SERVICE_LABEL, serviceName); + } } @Override