Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Common label based container locator #3

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,11 @@
<artifactId>quarkus-devservices-mssql</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-devservices-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elasticsearch-rest-client-common</artifactId>
Expand Down
14 changes: 14 additions & 0 deletions docs/src/main/asciidoc/redis.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,20 @@ docker run --ulimit memlock=-1:-1 -it --rm=true --memory-swappiness=0 --name red
If you use <<dev-services,DevServices>>, launching the container manually is not necessary!
====

== Shared server

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

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

If you need multiple (shared) servers, you can configure the `quarkus.redis.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 `redis`.

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

== Configuring Redis properties

Once we have the Redis server running, we need to configure the Redis connection properties.
Expand Down
35 changes: 35 additions & 0 deletions extensions/devservices/common/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>quarkus-devservices-parent</artifactId>
<groupId>io.quarkus</groupId>
<version>999-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>quarkus-devservices-common</artifactId>
<name>Quarkus - DevServices - Common</name>

<dependencies>
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-api</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.quarkus.devservices.common;

public class ContainerAddress {
private final String ipAddress;
private final int port;

public ContainerAddress(String ipAddress, Integer port) {
this.ipAddress = ipAddress;
this.port = port;
}

public String getIpAddress() {
return ipAddress;
}

public int getPort() {
return port;
}

public String getUrl() {
return String.format("%s:%d", ipAddress, port);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.quarkus.devservices.common;

import java.util.List;

import org.jboss.logging.Logger;
import org.testcontainers.DockerClientFactory;

import com.github.dockerjava.api.model.Container;
import com.github.dockerjava.api.model.ContainerPort;

public class ContainerLocator {

private static final Logger log = Logger.getLogger(ContainerLocator.class);

private final String devServiceLabel;
private final int port;

public ContainerLocator(String devServiceLabel, int port) {
this.devServiceLabel = devServiceLabel;
this.port = port;
}

private Container lookup(String expectedLabelValue) {
List<Container> containers = DockerClientFactory.lazyClient().listContainersCmd().exec();
for (Container container : containers) {
String s = container.getLabels().get(devServiceLabel);
if (expectedLabelValue.equalsIgnoreCase(s)) {
return container;
}
}
return null;
}

private 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;
}

public ContainerAddress locateContainer(String serviceName) {
Container container = lookup(serviceName);
if (container != null) {
ContainerPort containerPort = getMappedPort(container, port);
if (containerPort != null) {
final ContainerAddress containerAddress = new ContainerAddress(containerPort.getIp(),
containerPort.getPublicPort());
log.infof("Dev Services container locator found: %s (%s). "
+ "Connecting to: %s.",
container.getId(),
container.getImage(), containerAddress.getUrl());
return containerAddress;
}
}
return null;
}
}
1 change: 1 addition & 0 deletions extensions/devservices/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<module>derby</module>
<module>mssql</module>
<module>db2</module>
<module>common</module>
</modules>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* The discovery uses the {@code quarkus-dev-service-redis} 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-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.
* <p>
* 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)
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand All @@ -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<Closeable> closeables;
Expand Down Expand Up @@ -76,7 +87,7 @@ public void startRedisContainers(LaunchModeBuildItem launchMode,
List<Closeable> currentCloseables = new ArrayList<>();
for (Entry<String, DevServiceConfiguration> 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;
}
Expand Down Expand Up @@ -120,7 +131,29 @@ public void run() {
}
}

private StartResult startContainer(String connectionName, DevServicesConfig devServicesConfig) {
private static Container lookup(String expectedLabelValue) {
List<Container> 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)
Expand All @@ -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,
Expand Down Expand Up @@ -169,12 +218,15 @@ public StartResult(String url, Closeable closeable) {
}
}

private static class FixedPortRedisContainer extends GenericContainer {
private static class FixedPortRedisContainer extends GenericContainer<FixedPortRedisContainer> {
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
Expand Down