Skip to content

Commit

Permalink
feat: Added AbstractRedisEnterpriseContainer to avoid circular reference
Browse files Browse the repository at this point in the history
  • Loading branch information
jruaux committed Feb 29, 2024
1 parent c0d8e99 commit 98656ef
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 108 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package com.redis.testcontainers;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.ContainerLaunchException;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.FrameConsumerResultCallback;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.output.ToStringConsumer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.shaded.org.apache.commons.lang3.ClassUtils;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.TestEnvironment;

import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.ExecCreateCmdResponse;
import com.github.dockerjava.api.command.InspectContainerResponse;
import com.github.dockerjava.api.command.InspectExecResponse;
import com.github.dockerjava.api.exception.DockerException;

public abstract class AbstractRedisEnterpriseContainer<T extends AbstractRedisEnterpriseContainer<T>>
extends GenericContainer<T> implements RedisServer {

public static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("redislabs/redis");
public static final String DEFAULT_TAG = "latest";

private static final Logger log = LoggerFactory.getLogger(AbstractRedisEnterpriseContainer.class);
private static final int WEB_UI_PORT = 8443;
private static final String NODE_EXTERNAL_ADDR = "0.0.0.0";
private static final String RLADMIN = "/opt/redislabs/bin/rladmin";
private static final Long EXIT_CODE_SUCCESS = 0L;

protected AbstractRedisEnterpriseContainer(String dockerImageName) {
this(DockerImageName.parse(dockerImageName));
}

protected AbstractRedisEnterpriseContainer(final DockerImageName dockerImageName) {
super(dockerImageName);
addFixedExposedPort(WEB_UI_PORT, WEB_UI_PORT);
withPrivilegedMode(true);
waitingFor(Wait.forLogMessage(".*success: job_scheduler entered RUNNING state, process has stayed up for.*\\n",
1));
}

@Override
protected void containerIsStarted(InspectContainerResponse containerInfo) {
super.containerIsStarted(containerInfo);
try {
createCluster();
} catch (Exception e) {
throw new ContainerLaunchException("Could not initialize Redis Enterprise", e);
}
}

protected void createCluster() throws Exception {
log.info("Creating cluster");
if (!TestEnvironment.dockerExecutionDriverSupportsExec()) {
// at time of writing, this is the expected result in CircleCI.
throw new UnsupportedOperationException(
"Your docker daemon is running the \"lxc\" driver, which doesn't support \"docker exec\".");
}

InspectContainerResponse containerInfo = getContainerInfo();
if (!isRunning(containerInfo)) {
throw new IllegalStateException("execInContainer can only be used while the Container is running");
}

String containerId = containerInfo.getId();
String containerName = containerInfo.getName();

DockerClient dockerClient = DockerClientFactory.instance().client();

String[] commands = new String[] { RLADMIN, "cluster", "create", "name", "cluster.local", "username",
getAdminUserName(), "password", getAdminPassword(), "external_addr", NODE_EXTERNAL_ADDR };
log.debug("{}: Running \"exec\" command: {}", containerName, commands);
final ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId)
.withAttachStdout(true).withAttachStderr(true).withCmd(commands).withPrivileged(true).exec();

final ToStringConsumer stdoutConsumer = new ToStringConsumer();
final ToStringConsumer stderrConsumer = new ToStringConsumer();

try (FrameConsumerResultCallback callback = new FrameConsumerResultCallback()) {
callback.addConsumer(OutputFrame.OutputType.STDOUT, stdoutConsumer);
callback.addConsumer(OutputFrame.OutputType.STDERR, stderrConsumer);
dockerClient.execStartCmd(execCreateCmdResponse.getId()).exec(callback).awaitCompletion();
InspectExecResponse execResponse = dockerClient.inspectExecCmd(execCreateCmdResponse.getId()).exec();
if (EXIT_CODE_SUCCESS.equals(execResponse.getExitCodeLong())) {
return;
}
if (log.isErrorEnabled()) {
log.error("Could not create cluster: {}", stderrConsumer.toString(StandardCharsets.UTF_8));
}
throw new ContainerLaunchException("Could not create cluster");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ContainerLaunchException("Could not create cluster", e);
} catch (IOException e) {
log.error("Could not close result callback", e);
}
}

@Override
public String toString() {
return ClassUtils.getShortClassName(getClass());
}

protected abstract String getAdminPassword();

protected abstract String getAdminUserName();

private boolean isRunning(InspectContainerResponse containerInfo) {
try {
return containerInfo != null && Boolean.TRUE.equals(containerInfo.getState().getRunning());
} catch (DockerException e) {
return false;
}
}

@Override
public String getRedisHost() {
return getHost();
}

}
Original file line number Diff line number Diff line change
@@ -1,47 +1,26 @@
package com.redis.testcontainers;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.ContainerLaunchException;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.FrameConsumerResultCallback;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.output.ToStringConsumer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.shaded.org.apache.commons.lang3.ClassUtils;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.TestEnvironment;

import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.ExecCreateCmdResponse;
import com.github.dockerjava.api.command.InspectContainerResponse;
import com.github.dockerjava.api.command.InspectExecResponse;
import com.github.dockerjava.api.exception.DockerException;
import com.redis.enterprise.Admin;
import com.redis.enterprise.Database;
import com.redis.enterprise.RedisModule;

public class RedisEnterpriseContainer extends GenericContainer<RedisEnterpriseContainer> implements RedisServer {
public class RedisEnterpriseContainer extends AbstractRedisEnterpriseContainer<RedisEnterpriseContainer> {

public static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("redislabs/redis");
public static final String DEFAULT_TAG = "latest";
public static final int DEFAULT_DATABASE_SHARD_COUNT = 2;
public static final int DEFAULT_DATABASE_PORT = 12000;
public static final String DEFAULT_DATABASE_NAME = "testcontainers";
public static final RedisModule[] DEFAULT_DATABASE_MODULES = { RedisModule.JSON, RedisModule.SEARCH,
RedisModule.TIMESERIES, RedisModule.BLOOM };

private static final Logger log = LoggerFactory.getLogger(RedisEnterpriseContainer.class);
private static final String NODE_EXTERNAL_ADDR = "0.0.0.0";
private static final String RLADMIN = "/opt/redislabs/bin/rladmin";
private static final Long EXIT_CODE_SUCCESS = 0L;
private static final int WEB_UI_PORT = 8443;

private final Admin admin = new Admin();
private Database database = defaultDatabase();

public static Database defaultDatabase() {
Expand All @@ -50,17 +29,11 @@ public static Database defaultDatabase() {
}

public RedisEnterpriseContainer(String dockerImageName) {
this(DockerImageName.parse(dockerImageName));
super(dockerImageName);
}

public RedisEnterpriseContainer(final DockerImageName dockerImageName) {
super(dockerImageName);
addFixedExposedPort(Admin.DEFAULT_PORT, Admin.DEFAULT_PORT);
addFixedExposedPort(WEB_UI_PORT, WEB_UI_PORT);
addFixedExposedPort(database.getPort(), database.getPort());
withPrivilegedMode(true);
waitingFor(Wait.forLogMessage(".*success: job_scheduler entered RUNNING state, process has stayed up for.*\\n",
1));
}

public Database getDatabase() {
Expand All @@ -75,94 +48,36 @@ public RedisEnterpriseContainer withDatabase(Database database) {
return this;
}

private Admin admin() {
Admin admin = new Admin();
admin.withHost(getRedisHost());
return admin;
}

@Override
protected void containerIsStarted(InspectContainerResponse containerInfo) {
super.containerIsStarted(containerInfo);
try (Admin admin = admin()) {
log.info("Waiting for cluster bootstrap");
admin.waitForBoostrap();
createCluster();
Database response = admin.createDatabase(database);
log.info("Created database {} with UID {}", response.getName(), response.getUid());
} catch (Exception e) {
throw new ContainerLaunchException("Could not initialize Redis Enterprise container", e);
}
}

private void createCluster() {
log.info("Creating cluster");
if (!TestEnvironment.dockerExecutionDriverSupportsExec()) {
// at time of writing, this is the expected result in CircleCI.
throw new UnsupportedOperationException(
"Your docker daemon is running the \"lxc\" driver, which doesn't support \"docker exec\".");
}

InspectContainerResponse containerInfo = getContainerInfo();
if (!isRunning(containerInfo)) {
throw new IllegalStateException("execInContainer can only be used while the Container is running");
}

String containerId = containerInfo.getId();
String containerName = containerInfo.getName();

DockerClient dockerClient = DockerClientFactory.instance().client();

String[] commands = new String[] { RLADMIN, "cluster", "create", "name", "cluster.local", "username",
Admin.DEFAULT_USER_NAME, "password", Admin.DEFAULT_PASSWORD, "external_addr", NODE_EXTERNAL_ADDR };
log.debug("{}: Running \"exec\" command: {}", containerName, commands);
final ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId)
.withAttachStdout(true).withAttachStderr(true).withCmd(commands).withPrivileged(true).exec();

final ToStringConsumer stdoutConsumer = new ToStringConsumer();
final ToStringConsumer stderrConsumer = new ToStringConsumer();

try (FrameConsumerResultCallback callback = new FrameConsumerResultCallback()) {
callback.addConsumer(OutputFrame.OutputType.STDOUT, stdoutConsumer);
callback.addConsumer(OutputFrame.OutputType.STDERR, stderrConsumer);
dockerClient.execStartCmd(execCreateCmdResponse.getId()).exec(callback).awaitCompletion();
InspectExecResponse execResponse = dockerClient.inspectExecCmd(execCreateCmdResponse.getId()).exec();
if (EXIT_CODE_SUCCESS.equals(execResponse.getExitCodeLong())) {
return;
}
if (log.isErrorEnabled()) {
log.error("Could not create cluster: {}", stderrConsumer.toString(StandardCharsets.UTF_8));
}
throw new ContainerLaunchException("Could not create cluster");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ContainerLaunchException("Could not create cluster", e);
} catch (IOException e) {
log.error("Could not close result callback", e);
}
protected String getAdminUserName() {
return admin.getUserName();
}

@Override
public boolean isRedisCluster() {
return database.isOssCluster();
protected String getAdminPassword() {
return admin.getPassword();
}

@Override
public String toString() {
return ClassUtils.getShortClassName(RedisEnterpriseContainer.class);
protected void doStart() {
admin.withHost(getHost());
addFixedExposedPort(admin.getPort(), admin.getPort());
addFixedExposedPort(database.getPort(), database.getPort());
super.doStart();
}

private boolean isRunning(InspectContainerResponse containerInfo) {
try {
return containerInfo != null && Boolean.TRUE.equals(containerInfo.getState().getRunning());
} catch (DockerException e) {
return false;
}
@Override
protected void createCluster() throws Exception {
log.info("Waiting for cluster bootstrap");
admin.waitForBoostrap();
super.createCluster();
Database response = admin.createDatabase(database);
log.info("Created database {} with UID {}", response.getName(), response.getUid());
}

@Override
public String getRedisHost() {
return getHost();
public boolean isRedisCluster() {
return database.isOssCluster();
}

@Override
Expand Down
2 changes: 1 addition & 1 deletion core/testcontainers-redis/testcontainers-redis.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
dependencies {
api 'org.testcontainers:testcontainers'
api group: 'com.redis', name: 'redis-enterprise-admin', version: rsAdminVersion
api group: 'com.redis', name: 'redis-enterprise-admin', version: adminVersion
testImplementation 'org.slf4j:slf4j-simple'
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testImplementation 'org.junit.jupiter:junit-jupiter-params'
Expand Down
6 changes: 5 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
group = com.redis
sourceCompatibility = 17
targetCompatibility = 17
testSourceCompatibility = 17
testTargetCompatibility = 17
reproducibleBuild = true

adminVersion = 0.5.3
dependencyPluginVersion = 1.1.4
kordampBuildVersion = 3.4.0
kordampPluginVersion = 0.54.0
springBootVersion = 3.2.3

rsAdminVersion = 0.5.1
jacocoVersion = 0.8.11
lettucemodVersion = 3.7.3

Expand Down
10 changes: 10 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,14 @@ projects {
id 'io.spring.dependency-management'
}
}
}

enforce {
rule(enforcer.rules.RequireJavaVersion) { r ->
r.version.set('17')
}
rule(enforcer.rules.BanDuplicateClasses) { r ->
// search only on compile and runtime classpaths
r.configurations.addAll(['compileClasspath', 'runtimeClasspath'])
}
}

0 comments on commit 98656ef

Please sign in to comment.