![JAR search and dependency download from the Maven repository](/logo.png)
com.wl4g.infra.common.tests.integration.manager.GenericITContainerManager Maven / Gradle / Ivy
/**
* Copyright (C) 2023 ~ 2035 the original authors WL4G (James Wong).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wl4g.infra.common.tests.integration.manager;
import com.github.dockerjava.api.command.InspectContainerResponse;
import com.wl4g.infra.common.lang.Assert2;
import com.wl4g.infra.common.tests.integration.testcontainers.RocketMQContainer;
import org.apache.commons.lang3.RandomUtils;
import org.junit.jupiter.api.Assertions;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.images.builder.Transferable;
import org.testcontainers.utility.DockerImageName;
import javax.annotation.Nullable;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.function.Consumer;
import java.util.function.Supplier;
import static com.wl4g.infra.common.collection.CollectionUtils2.safeList;
import static com.wl4g.infra.common.collection.CollectionUtils2.safeMap;
import static java.lang.String.format;
import static java.lang.String.valueOf;
import static java.util.Arrays.asList;
import static java.util.Collections.*;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.testcontainers.shaded.org.apache.commons.lang3.StringUtils.isBlank;
/**
* The {@link GenericITContainerManager}
*
* @author James Wong
* @since v3.1
**/
//@Testcontainers
@SuppressWarnings({"rawtypes", "unused"})
public abstract class GenericITContainerManager extends AbstractITContainerManager {
public GenericITContainerManager(@NotNull Class> testClass) {
super(testClass);
}
// --------------------- Getting Run Containers Configuration -----------------------
public String getKafkaConnectString(String clusterName) {
return getServersConnectString("PLAINTEXT://", clusterName, 0);
}
public String getRocketMQConnectString(String clusterName) {
// return ((RocketMQContainer) getRequiredContainer(clusterName)).getNamesrvAddr();
return getServersConnectString("", clusterName, 0);
}
// --------------------- ZOOKEEPER build container ---------------------
public ITGenericContainer buildBitnamiZookeeper34xContainer(@NotNull Supplier startedLatchSupplier,
@Min(1024) int serverPort,
@Nullable Map env) {
return buildBitnamiZookeeperContainer(startedLatchSupplier, "registry.cn-shenzhen.aliyuncs.com/wl4g-k8s/bitnami_zookeeper",
"3.4.13", serverPort, "(.*)binding to port (.*)", null, env, null, null);
}
public ITGenericContainer buildBitnamiZookeeper35xContainer(@NotNull Supplier startedLatchSupplier,
@Min(1024) int serverPort,
@Nullable Map env) {
return buildBitnamiZookeeperContainer(startedLatchSupplier, "registry.cn-shenzhen.aliyuncs.com/wl4g-k8s/bitnami_zookeeper",
"3.5.9", serverPort, "(.*)binding to port (.*)", null, env, null, null);
}
public ITGenericContainer buildBitnamiZookeeper36xContainer(@NotNull Supplier startedLatchSupplier,
@Min(1024) int serverPort,
@Nullable Map env) {
return buildBitnamiZookeeperContainer(startedLatchSupplier,
"registry.cn-shenzhen.aliyuncs.com/wl4g-k8s/bitnami_zookeeper",
"3.6.2", serverPort, "(.*)binding to port (.*)", null, env, null, null);
}
public ITGenericContainer buildBitnamiZookeeperContainer(@NotNull Supplier startedLatchSupplier,
@NotBlank String imageRepo,
@NotBlank String imageTag,
@Min(1024) int serverPort,
@NotBlank String startedLogRegex,
@Nullable Duration startupTimeout,
@Nullable Map env,
@Nullable Consumer createdListener,
@Nullable Consumer startedListener) {
Assertions.assertTrue(serverPort > 1024, "serverPort must be greater than 1024");
final Map mergeEnv = new HashMap<>(safeMap(env));
//mergeEnv.putIfAbsent("ZOO_ENABLE_AUTH", "true");
mergeEnv.putIfAbsent("ALLOW_ANONYMOUS_LOGIN", "yes");
mergeEnv.putIfAbsent("ZOO_PORT_NUMBER", String.valueOf(serverPort));
return buildGenericContainer(startedLatchSupplier, imageRepo, imageTag, asList(serverPort, serverPort),
"zookeeper", startedLogRegex, null, startupTimeout, null,
mergeEnv, createdListener, startedListener);
}
// --------------------- KAFKA build container ---------------------------
public ITGenericContainer buildBitnamiKafka22xContainer(@NotNull Supplier startedLatchSupplier,
@Min(1024) int serverPort,
@Min(1024) int jmxPort,
@Nullable Map env,
@NotBlank String zookeeperServers) {
Assertions.assertTrue(serverPort > 1024, "serverPort must be greater than 1024");
Assertions.assertTrue(jmxPort > 1024, "jmxPort must be greater than 1024");
Assertions.assertTrue(isNotBlank(zookeeperServers), "zookeeperServers must not be blank");
final String serverListen = getServersConnectString("PLAINTEXT://", serverPort);
final Map mergeEnv = new HashMap<>(safeMap(env));
mergeEnv.putIfAbsent("ALLOW_PLAINTEXT_LISTENER", "yes");
mergeEnv.putIfAbsent("JMX_PORT", valueOf(jmxPort));
mergeEnv.putIfAbsent("KAFKA_ADVERTISED_LISTENERS", serverListen);
mergeEnv.putIfAbsent("KAFKA_LISTENERS", String.format("PLAINTEXT://0.0.0.0:%s", serverPort));
mergeEnv.putIfAbsent("KAFKA_ZOOKEEPER_CONNECT", zookeeperServers);
// Merge of override mergeEnv.
if (nonNull(env)) {
mergeEnv.putAll(env);
}
return buildBitnamiKafkaContainer(startedLatchSupplier, "registry.cn-shenzhen.aliyuncs.com/wl4g-k8s/bitnami_kafka",
"2.2.0", asList(serverPort, serverPort, jmxPort, jmxPort), "(.*)started \\(kafka\\.server\\.KafkaServer\\)(.*)",
null, null, mergeEnv, null, null, null);
}
public ITGenericContainer buildBitnamiKafka35xPureSimpleServerSslContainer(@javax.validation.constraints.NotNull Supplier startedLatch,
@NotBlank String caCertPem,
@NotBlank String serverCertPem,
@NotBlank String serverCertKey,
@Min(1024) int serverPort,
@Min(1024) int jmxPort,
@Nullable Map env) {
final Map mergeEnv = new HashMap<>(safeMap(env));
// SSL configs. Notice: in simple server mode, client auth must be none.
// see:https://github.com/bitnami/containers/blob/main/bitnami/kafka/README.md#configuration
mergeEnv.putIfAbsent("KAFKA_TLS_CLIENT_AUTH", "none"); // required|requested|none
return buildBitnamiKafka35xPureMutualSslContainer(startedLatch, caCertPem, serverCertPem, serverCertKey,
serverPort, jmxPort, mergeEnv);
}
public ITGenericContainer buildBitnamiKafka35xPureMutualSslContainer(@javax.validation.constraints.NotNull Supplier startedLatch,
@NotBlank String caCertPem,
@NotBlank String serverCertPem,
@NotBlank String serverCertKey,
@Min(1024) int serverPort,
@Min(1024) int jmxPort,
@Nullable Map env) {
final Map mergeEnv = new HashMap<>(safeMap(env));
// Listeners configs.
// see:https://github.com/bitnami/containers/blob/main/bitnami/kafka/README.md#security
// see:https://github.com/bitnami/containers/blob/main/bitnami/kafka/README.md#apache-kafka-kraft-mode-configuration
// see:https://help.aliyun.com/document_detail/68325.html#section-0a2-3hs-9bb
mergeEnv.putIfAbsent("KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP", "CONTROLLER:PLAINTEXT,SSL:SSL");
mergeEnv.putIfAbsent("KAFKA_CFG_INTER_BROKER_LISTENER_NAME", "SSL");
return buildBitnamiKafka35xMutualSslContainer(startedLatch, "SSL://", "SSL",
caCertPem, serverCertPem, serverCertKey, serverPort, jmxPort, mergeEnv);
}
public ITGenericContainer buildBitnamiKafka35xSaslScramSha256MutualSslContainer(@NotNull Supplier startedLatch,
@NotBlank String caCertPem,
@NotBlank String serverCertPem,
@NotBlank String serverCertKey,
@Min(1024) int serverPort,
@Min(1024) int jmxPort,
@Nullable Map env) {
final Map mergeEnv = new HashMap<>(safeMap(env));
mergeEnv.putIfAbsent("KAFKA_SASL_MECHANISM", "SCRAM-SHA-256");
mergeEnv.putIfAbsent("KAFKA_CFG_SASL_MECHANISM_CONTROLLER_PROTOCOL", "SCRAM-SHA-256");
mergeEnv.putIfAbsent("KAFKA_CFG_SASL_MECHANISM_INTER_BROKER_PROTOCOL", "SCRAM-SHA-256");
return buildBitnamiKafka35xSaslPlainMutualSslContainer(startedLatch, caCertPem, serverCertPem, serverCertKey,
serverPort, jmxPort, mergeEnv);
}
@SuppressWarnings("all")
public ITGenericContainer buildBitnamiKafka35xSaslPlainMutualSslContainer(@NotNull Supplier startedLatch,
@NotBlank String caCertPem,
@NotBlank String serverCertPem,
@NotBlank String serverCertKey,
@Min(1024) int serverPort,
@Min(1024) int jmxPort,
@Nullable Map env) {
final Map mergeEnv = new HashMap<>(safeMap(env));
// SASL configs.
mergeEnv.putIfAbsent("KAFKA_SASL_MECHANISM", "PLAIN");
mergeEnv.putIfAbsent("KAFKA_CFG_SASL_MECHANISM_CONTROLLER_PROTOCOL", "PLAIN");
mergeEnv.putIfAbsent("KAFKA_CFG_SASL_MECHANISM_INTER_BROKER_PROTOCOL", "PLAIN");
// Listeners configs.
// see:https://github.com/bitnami/containers/blob/main/bitnami/kafka/README.md#security
// see:https://help.aliyun.com/document_detail/68325.html#section-0a2-3hs-9bb
mergeEnv.putIfAbsent("KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP", "CONTROLLER:SASL_PLAINTEXT,SASL_SSL:SASL_SSL");
mergeEnv.putIfAbsent("KAFKA_CFG_INTER_BROKER_LISTENER_NAME", "SASL_SSL");
return buildBitnamiKafka35xMutualSslContainer(startedLatch, "SASL_SSL://", "SASL_SSL",
caCertPem, serverCertPem, serverCertKey, serverPort, jmxPort, mergeEnv);
}
// see:https://github.com/bitnami/containers/blob/main/bitnami/kafka/README.md#security
// see:https://help.aliyun.com/document_detail/68325.html#section-0a2-3hs-9bb
@SuppressWarnings("all")
protected ITGenericContainer buildBitnamiKafka35xMutualSslContainer(@NotNull Supplier startedLatch,
@Nullable String listenProtocol,
@Nullable String listenName,
@NotBlank String caCertPem,
@NotBlank String serverCertPem,
@NotBlank String serverCertKey,
@Min(1024) int serverPort,
@Min(1024) int jmxPort,
@Nullable Map env) {
Assertions.assertTrue(serverPort > 1024, "serverPort must be greater than 1024");
Assertions.assertTrue(jmxPort > 1024, "jmxPort must be greater than 1024");
Assertions.assertTrue(isNotBlank(caCertPem), "caCertPem must not be blank");
Assertions.assertTrue(isNotBlank(serverCertPem), "serverCertPem must not be blank");
Assertions.assertTrue(isNotBlank(serverCertKey), "serverCertKey must not be blank");
final Map mergeEnv = new HashMap<>(safeMap(env));
mergeEnv.putIfAbsent("ALLOW_PLAINTEXT_LISTENER", "no");
// SSL configs.
mergeEnv.putIfAbsent("KAFKA_TLS_TYPE", "PEM");
// see:https://github.com/bitnami/containers/blob/main/bitnami/kafka/README.md#configuration
mergeEnv.putIfAbsent("KAFKA_TLS_CLIENT_AUTH", "required"); // required|requested|none
final List commandParts = new ArrayList<>();
commandParts.add("sh");
commandParts.add("-c");
commandParts.add("mkdir -p /bitnami/kafka/config/certs && /opt/bitnami/scripts/kafka/run.sh");
final Map configs = new HashMap<>();
configs.put("/bitnami/kafka/config/certs/kafka.truststore.pem", caCertPem);
configs.put("/bitnami/kafka/config/certs/kafka.keystore.pem", serverCertPem);
configs.put("/bitnami/kafka/config/certs/kafka.keystore.key", serverCertKey);
return buildBitnamiKafka35xContainer(startedLatch, listenProtocol, listenName,
commandParts, serverPort, jmxPort, configs, mergeEnv);
}
public ITGenericContainer buildBitnamiKafka35xContainer(@NotNull Supplier startedLatch,
@Min(1024) int serverPort,
@Min(1024) int jmxPort,
@Nullable Map env) {
return buildBitnamiKafka35xContainer(startedLatch, null, null, null,
serverPort, jmxPort, null, env);
}
public ITGenericContainer buildBitnamiKafka35xContainer(@NotNull Supplier startedLatch,
@Nullable String listenProtocol,
@Nullable String listenName,
@Nullable List commandParts,
@Min(1024) int serverPort,
@Min(1024) int jmxPort,
@Nullable Map configs,
@Nullable Map env) {
Assertions.assertTrue(serverPort > 1024, "serverPort must be greater than 1024");
Assertions.assertTrue(jmxPort > 1024, "jmxPort must be greater than 1024");
listenProtocol = isBlank(listenProtocol) ? "PLAINTEXT://" : listenProtocol;
listenName = isBlank(listenName) ? listenProtocol.substring(0, listenProtocol.indexOf("://")) : listenName;
// Generate controller port with retry.
int controllerPort;
do {
controllerPort = RandomUtils.nextInt(55535, 65535);
} while (controllerPort == serverPort);
final String serverListen = getServersConnectString(listenProtocol, serverPort);
final Map mergeEnv = new HashMap<>(safeMap(env));
mergeEnv.putIfAbsent("ALLOW_PLAINTEXT_LISTENER", "yes");
mergeEnv.putIfAbsent("JMX_PORT", valueOf(jmxPort));
// see:https://github.com/bitnami/containers/blob/main/bitnami/kafka/3.5/debian-11/docker-compose.yml
// KRaft settings
mergeEnv.putIfAbsent("KAFKA_CFG_NODE_ID", "0");
mergeEnv.putIfAbsent("KAFKA_CFG_PROCESS_ROLES", "controller,broker");
mergeEnv.putIfAbsent("KAFKA_CFG_CONTROLLER_QUORUM_VOTERS", format("0@localhost:%s", controllerPort));
//mergeEnv.putIfAbsent("KAFKA_CFG_CONTROLLER_QUORUM_VOTERS", format("0@%s:%s", localHostIp, controllerPort));
// Listeners
mergeEnv.putIfAbsent("KAFKA_CFG_LISTENERS", format("%s:%s,CONTROLLER://:%s", listenProtocol, serverPort, controllerPort));
//mergeEnv.putIfAbsent("KAFKA_CFG_LISTENERS", serverListen + "," + format("CONTROLLER://%s:%s", localHostIp, controllerPort));
mergeEnv.putIfAbsent("KAFKA_CFG_ADVERTISED_LISTENERS", serverListen);
// CONTROLLER:PLAINTEXT,CONTROLLER:SASL_PLAINTEXT,PLAINTEXT:PLAINTEXT,SASL_SSL:SASL_SSL
mergeEnv.putIfAbsent("KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP", "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT");
mergeEnv.putIfAbsent("KAFKA_CFG_CONTROLLER_LISTENER_NAMES", "CONTROLLER");
mergeEnv.putIfAbsent("KAFKA_CFG_INTER_BROKER_LISTENER_NAME", listenName);
// Other
// see:https://github.com/bitnami/containers/blob/main/bitnami/kafka/3.5/debian-11/rootfs/opt/bitnami/scripts/libkafka.sh#L937
mergeEnv.putIfAbsent("KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE", "true");
return buildBitnamiKafkaContainer(startedLatch, "registry.cn-shenzhen.aliyuncs.com/wl4g-k8s/bitnami_kafka",
"3.5", asList(serverPort, serverPort, jmxPort, jmxPort), "(.*)Kafka Server started (.*)",
null, configs, mergeEnv, null, null, null);
}
/**
* Manual tests for examples:
*
*
* export imageName='registry.cn-shenzhen.aliyuncs.com/wl4g-k8s/bitnami_kafka:3.5'
* export kafkaPort='19092'
* export kafkaTopic='my-test'
* export localHostIp=$(ip a | grep -E '^[a-zA-Z0-9]+: (em|eno|enp|ens|eth|wlp|en)+[0-9]' -A2 | grep inet | awk -F ' ' '{print $2}' | cut -f 1 -d / | tail -n 1)
* export localHostIp=$([ -z "${localHostIp}" ] && echo $([ $(command -v multipass) ] && multipass info docker | grep -i IPv4 | awk '{print $2}') || echo ${localHostIp})
*
* docker run --rm --name kafka_test --network host --entrypoint /bin/bash ${imageName} -c \
* "echo 'key1:value1' | /opt/bitnami/kafka/bin/kafka-topics.sh --bootstrap-server ${localHostIp}:${kafkaPort} --create --topic ${kafkaTopic}"
*
* docker run --rm --name kafka_test --network host --entrypoint /bin/bash ${imageName} -c \
* "echo 'key1:value1' | /opt/bitnami/kafka/bin/kafka-topics.sh --bootstrap-server ${localHostIp}:${kafkaPort} --list"
*
* docker run --rm --name kafka_test --network host --entrypoint /bin/bash ${imageName} -c \
* "echo 'key1:value1-of-${kafkaPort}' | /opt/bitnami/kafka/bin/kafka-console-producer.sh --bootstrap-server ${localHostIp}:${kafkaPort} \
* --topic ${kafkaTopic} --property parse.key=true --property key.separator=:"
*
* docker run --rm --name kafka_test --network host --entrypoint /bin/bash ${imageName} -c \
* "/opt/bitnami/kafka/bin/kafka-console-consumer.sh --bootstrap-server ${localHostIp}:${kafkaPort} --topic ${kafkaTopic} --from-beginning"
*
*/
@SuppressWarnings("all")
public ITGenericContainer buildBitnamiKafkaContainer(@NotNull Supplier startedLatchSupplier,
@NotBlank String imageRepo,
@NotBlank String imageTag,
@NotEmpty List portBindings,
@NotBlank String startedLogRegex,
@Nullable Duration startupTimeout,
@Nullable Map configs,
@NotNull Map env,
@Nullable List commandParts,
@Nullable Consumer createdListener,
@Nullable Consumer startedListener) {
//final GenericContainer> kafka01 = new KafkaContainer(DockerImageName.parse("bitnami/kafka:2.8.1")
// .asCompatibleSubstituteFor("confluentinc/cp-kafka"))
// .withEmbeddedZookeeper();
// see:https://java.testcontainers.org/modules/kafka/#example
//new KafkaContainer().withEmbeddedZookeeper();
//startupTimeout = isNull(startupTimeout) ? Duration.ofSeconds(IT_START_MW_CONTAINERS_TIMEOUT) : startupTimeout;
//kafkaContainer
// //.withNetworkMode("host")
// .withEnv("ALLOW_PLAINTEXT_LISTENER", "yes")
// // see:https://github.com/bitnami/containers/blob/main/bitnami/kafka/3.5/debian-11/rootfs/opt/bitnami/scripts/libkafka.sh#L937
// .withEnv(env)
// .withReuse(false)
// .withExposedPorts(serverPort)
// .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("testcontainers.kafka")))
// //.withCreateContainerCmdModifier((Consumer) cmd ->
// // cmd.withHostConfig(new HostConfig()
// // .withPortBindings(new PortBinding(Ports.Binding.bindPort(containerPort),
// // new ExposedPort(serverPort)))
// // ))
// //.withCommand("run", "-e", "{\"kafka_advertised_hostname\":" + kafkaAdvertisedHostname + "}")
// .waitingFor(Wait.forLogMessage("(.*)Kafka Server started (.*)", 1)
// .withStartupTimeout(startupTimeout));
//kafkaContainer.setPortBindings(singletonList(serverPort + ":" + containerPort));
return buildGenericContainer(startedLatchSupplier, imageRepo, imageTag, portBindings,
"kafka", startedLogRegex, null, startupTimeout, configs,
env, commandParts, createdListener, startedListener);
}
// --------------------- RocketMQ build container ------------------------
public ITGenericContainer buildApacheRocketMQ49xContainer(@NotNull Supplier startedLatchSupplier,
@Min(1024) int serverPort) {
return buildApacheRocketMQContainer(startedLatchSupplier,
"registry.cn-shenzhen.aliyuncs.com/wl4g-k8s/apache_rocketmq",
"4.9.7", serverPort, null, null);
}
public ITGenericContainer buildApacheRocketMQContainer(@NotNull Supplier startedLatchSupplier,
@NotBlank String imageRepo,
@NotBlank String imageTag,
@Min(1024) int serverPort,
@Nullable Consumer createdListener,
@Nullable Consumer startedListener) {
Assertions.assertTrue(serverPort > 1024, "serverPort must be greater than 1024");
final DockerImageName image = DockerImageName.parse(imageRepo.concat(":").concat(imageTag))
.asCompatibleSubstituteFor("apache/rocketmq");
final RocketMQContainer container = new RocketMQContainer(image) {
@Override
protected void containerIsCreated(String containerId) {
log.info("RocketMQ container is created: " + containerId);
super.containerIsCreated(containerId);
if (nonNull(createdListener)) {
createdListener.accept(containerId);
}
}
@Override
protected void containerIsStarted(InspectContainerResponse containerInfo) {
log.info("RocketMQ container is started: " + containerInfo.getId());
startedLatchSupplier.get().countDown();
super.containerIsStarted(containerInfo);
if (nonNull(startedListener)) {
startedListener.accept(containerInfo);
}
}
};
container.setPortBindings(singletonList(String.format("%s:%s", serverPort, serverPort)));
return new ITGenericContainer(singletonList(serverPort + ":" + serverPort), container);
}
// --------------------- Redis build Containers ----------------------
public String getRedisConnectString(String serverName) {
return getServersConnectString("redis://", serverName, 0);
}
public ITGenericContainer buildBitnamiRedis6xContainer(Supplier startedLatchSupplier,
@Min(1024) int serverPort,
@Nullable String redisMasterPassword,
@Nullable Map env) {
return buildBitnamiRedisContainer(startedLatchSupplier, "registry.cn-shenzhen.aliyuncs.com/wl4g-k8s/bitnami_redis", "6.2.14",
serverPort, redisMasterPassword, env, null, null);
}
public ITGenericContainer buildBitnamiRedis7xContainer(Supplier startedLatchSupplier,
@Min(1024) int serverPort,
@Nullable String redisMasterPassword,
@Nullable Map env) {
return buildBitnamiRedisContainer(startedLatchSupplier, "registry.cn-shenzhen.aliyuncs.com/wl4g-k8s/bitnami_redis", "7.0.14",
serverPort, redisMasterPassword, env, null, null);
}
@SuppressWarnings({"rawtypes", "unchecked", "unused", "all"})
public ITGenericContainer buildBitnamiRedisContainer(@NotNull Supplier startedLatchSupplier,
@NotBlank String imageName,
@NotBlank String imageTag,
@Min(1024) int serverPort,
@Nullable String redisMasterPassword,
@Nullable Map env,
@Nullable Consumer createdListener,
@Nullable Consumer startedListener) {
final Map mergeEnv = new HashMap<>();
//mergeEnv.put("REDIS_DISABLE_COMMANDS", "FLUSHDB,FLUSHALL");
mergeEnv.put("REDIS_PORT_NUMBER", serverPort + "");
if (isNotBlank(redisMasterPassword)) {
mergeEnv.put("REDIS_PASSWORD", redisMasterPassword);
} else {
mergeEnv.put("ALLOW_EMPTY_PASSWORD", "yes");
}
return buildGenericContainer(startedLatchSupplier, imageName,
imageTag, asList(serverPort, serverPort), "redis", "(.*)Ready to accept connections(.*)",
null, null, null, mergeEnv, createdListener, startedListener);
}
// --------------------- Kafka UI build Containers ----------------------
public ITGenericContainer buildProvectuslabsKafkaUI07xContainer(@NotNull Supplier startedLatchSupplier,
@NotNull List kafkaClusters,
@NotEmpty List kafkaMetricsPorts) {
return buildProvectuslabsKafkaUIContainer(startedLatchSupplier, "v0.7.1", 58888,
null, null, kafkaClusters, kafkaMetricsPorts, null, null);
}
@SuppressWarnings("all")
public ITGenericContainer buildProvectuslabsKafkaUIContainer(@NotNull Supplier startedLatchSupplier,
@NotBlank String imageTag,
@Min(1024) int serverPort,
@NotNull Map env,
@Nullable String auditLogTopic,
@NotNull List kafkaClusters,
@NotEmpty List kafkaMetricsPorts,
@Nullable Consumer createdListener,
@Nullable Consumer startedListener) {
Assertions.assertNotNull(startedLatchSupplier, "startedLatchSupplier must not be null");
Assertions.assertTrue(serverPort > 1024, "serverPort must be greater than 1024");
Assertions.assertTrue(nonNull(kafkaClusters) && !kafkaClusters.isEmpty(), "kafkaClusters must not be empty");
long kafkaClusterCount = kafkaClusters.stream().filter(d -> !isBlank(d)).count();
long kafkaMetricsPortsCount = safeList(kafkaMetricsPorts).stream().filter(Objects::nonNull).count();
Assertions.assertEquals(kafkaMetricsPortsCount, kafkaClusterCount, format("kafkaMetricsPorts size(%s) must be equal to kafkaClusters size(%s)",
kafkaMetricsPortsCount, kafkaClusterCount));
final Map mergeEnv = new HashMap<>(safeMap(env));
mergeEnv.putIfAbsent("JAVA_OPTS", "-Djava.net.preferIPv4Stack=true -Xmx1G");
mergeEnv.putIfAbsent("SERVER_PORT", valueOf(serverPort));
for (int i = 0; i < kafkaClusters.size(); i++) {
final String kafkaServers = kafkaClusters.get(i);
mergeEnv.putIfAbsent("KAFKA_CLUSTERS_" + i + "_NAME", "it-cluster-" + i);
mergeEnv.putIfAbsent("KAFKA_CLUSTERS_" + i + "_BOOTSTRAPSERVERS", kafkaServers);
mergeEnv.putIfAbsent("KAFKA_CLUSTERS_" + i + "_METRICS_PORT", valueOf(kafkaMetricsPorts.get(i)));
mergeEnv.putIfAbsent("KAFKA_CLUSTERS_" + i + "_AUDIT_TOPIC_AUDIT_ENABLED", "true");
mergeEnv.putIfAbsent("KAFKA_CLUSTERS_" + i + "_AUDIT_TOPIC", isBlank(auditLogTopic) ? "__kui-audit-log" : auditLogTopic);
mergeEnv.putIfAbsent("KAFKA_CLUSTERS_" + i + "_AUDIT_TOPIC_PROPERTIES_RETENTION_MS", "43200000");
mergeEnv.putIfAbsent("KAFKA_CLUSTERS_" + i + "_AUDIT_TOPICS_PARTITIONS", "3");
mergeEnv.putIfAbsent("KAFKA_CLUSTERS_" + i + "_AUDIT_LEVEL", "ALTER_ONLY");
}
return buildGenericContainer(startedLatchSupplier, "registry.cn-shenzhen.aliyuncs.com/wl4g-k8s/provectuslabs_kafka-ui",
imageTag, asList(serverPort, serverPort), "kafka-ui", "(.*)Started KafkaUiApplication (.*)",
null, null, emptyMap(), mergeEnv, createdListener, startedListener);
}
// --------------------- Prometheus build Containers --------------------
public ITGenericContainer buildBitnamiPrometheus24xContainer(Supplier startedLatchSupplier,
@Min(1024) int serverPort,
@Min(1) int scrapeIntervalSeconds,
List scrapeUrls,
@Nullable Map env) {
return buildBitnamiPrometheusContainer(startedLatchSupplier, "registry.cn-shenzhen.aliyuncs.com/wl4g-k8s/bitnami_prometheus",
"2.47.2", serverPort, scrapeIntervalSeconds, scrapeUrls, env, null, null);
}
@SuppressWarnings({"rawtypes", "unchecked", "unused", "all"})
public ITGenericContainer buildBitnamiPrometheusContainer(@NotNull Supplier startedLatchSupplier,
@NotBlank String imageName,
@NotBlank String imageTag,
@Min(1024) int serverPort,
@Min(1) int scrapeIntervalSeconds,
@NotEmpty List scrapeUrls,
@Nullable Map env,
@Nullable Consumer createdListener,
@Nullable Consumer startedListener) {
Assert2.isTrue(scrapeIntervalSeconds > 0, "scrapeIntervalSeconds must be greater than 0");
Assert2.notEmptyOf(scrapeUrls, "scrapeUrls");
// @formatter:off
String prometheusConfig =
"global:\n" +
" scrape_interval: %ss\n" +
" evaluation_interval: %ss\n" +
"scrape_configs:\n" +
" - job_name: it-application-job\n" +
" static_configs:\n";
prometheusConfig = String.format(prometheusConfig, scrapeIntervalSeconds, scrapeIntervalSeconds);
for (String scrapeUrl : scrapeUrls) {
prometheusConfig +=
" - targets: ['%s']\n" +
" labels:\n" +
" instance: '%s'";
prometheusConfig = String.format(prometheusConfig, scrapeUrl, scrapeUrl);
}
// @formatter:on
return buildGenericContainer(startedLatchSupplier, "registry.cn-shenzhen.aliyuncs.com/wl4g-k8s/bitnami_prometheus",
imageTag, asList(serverPort, 9090), "prometheus", "(.*)Server is ready to receive web requests(.*)",
null, null, singletonMap("/opt/bitnami/prometheus/conf/prometheus.yml", prometheusConfig),
env, createdListener, startedListener);
}
// --------------------- Grafana build Containers -----------------------
public ITGenericContainer buildBitnamiGrafana101xContainer(Supplier startedLatchSupplier,
int mappedPort,
@Nullable Map env) {
return buildBitnamiGrafanaContainer(startedLatchSupplier, mappedPort, "10.1.5", env, null, null);
}
@SuppressWarnings({"rawtypes", "unchecked", "unused", "all"})
public ITGenericContainer buildBitnamiGrafanaContainer(Supplier startedLatchSupplier,
@Min(1024) int mappedPort,
@NotBlank String imageTag,
@Nullable Map env,
@Nullable Consumer createdListener,
@Nullable Consumer startedListener) {
return buildGenericContainer(startedLatchSupplier, "registry.cn-shenzhen.aliyuncs.com/wl4g-k8s/bitnami_grafana",
imageTag, asList(mappedPort, 3000), "grafana", "(.*)HTTP Server Listen(.*)",
null, null, null, env, createdListener, startedListener);
}
// --------------------- Generic build container ---------------------
public ITGenericContainer buildGenericContainer(@NotNull Supplier startedLatchSupplier,
@NotBlank String imageRepo,
@NotBlank String imageTag,
@NotEmpty List portBindings,
@NotBlank String loggerName,
@NotBlank String startedLogRegex,
@Nullable String networkMode,
@Nullable Duration startupTimeout,
@Nullable Map configs,
@Nullable Map env,
@Nullable Consumer createdListener,
@Nullable Consumer startedListener) {
return buildGenericContainer(startedLatchSupplier, imageRepo, imageTag, portBindings, loggerName, startedLogRegex, networkMode,
startupTimeout, configs, env, null, createdListener, startedListener);
}
public ITGenericContainer buildGenericContainer(@javax.validation.constraints.NotNull Supplier startedLatchSupplier,
@NotBlank String imageRepo,
@NotBlank String imageTag,
@NotEmpty List portBindings,
@NotBlank String loggerName,
@NotBlank String startedLogRegex,
@Nullable String networkMode,
@Nullable Duration startupTimeout,
@Nullable Map configs,
@Nullable Map env,
@Nullable List commandParts,
@Nullable Consumer createdListener,
@Nullable Consumer startedListener) {
Assertions.assertNotNull(startedLatchSupplier, "startedLatchSupplier must not be null");
Assertions.assertTrue(isNotBlank(imageRepo), "imageRepo must be like e.g: 'bitnami/zookeeper' or 'registry.cn-shenzhen.aliyuncs.com/wl4g-k8s/bitnami_zookeeper'");
Assertions.assertTrue(isNotBlank(imageTag), "imageTag must be like 3.4.13");
Assertions.assertTrue(nonNull(portBindings) && portBindings.size() % 2 == 0, "portBindings must be in pairs, e.g: [58080,8080,59090,9090]");
Assertions.assertTrue(isNotBlank(loggerName), "loggerName must be empty");
Assertions.assertTrue(isNotBlank(startedLogRegex), "loggerName must be empty");
startupTimeout = isNull(startupTimeout) ? Duration.ofSeconds(IT_START_CONTAINERS_TIMEOUT) : startupTimeout;
final List portBindings0 = new ArrayList<>();
for (int i = 0; i < portBindings.size(); i += 2) {
Integer mappedPort = portBindings.get(i);
Integer containerPort = portBindings.get(i + 1);
Assertions.assertTrue(nonNull(mappedPort) && mappedPort > 1024, "mappedPort must be greater than 1024");
Assertions.assertTrue(nonNull(containerPort) && containerPort > 1024, "containerPort must be greater than 1024");
portBindings0.add(mappedPort + ":" + containerPort);
}
@SuppressWarnings("rawtypes") final GenericContainer> container = new GenericContainer(imageRepo.concat(":").concat(imageTag)) {
@SuppressWarnings("all")
@Override
protected void containerIsCreated(String containerId) {
log.info("{}:{} container is created: {}", imageRepo, imageTag, containerId);
//final String originalCmd = StringUtils.join(containerInfo.getConfig().getCmd());
//withCommand("sh", "-c", config + "; " + originalCmd);
safeMap(configs).forEach((configPath, configContent) ->
copyFileToContainer(Transferable.of(configContent, 0777), configPath));
if (nonNull(createdListener)) {
createdListener.accept(containerId);
}
}
@Override
protected void containerIsStarted(InspectContainerResponse containerInfo) {
log.info("{}:{} container is started: {}", imageRepo, imageTag, containerInfo.toString());
startedLatchSupplier.get().countDown();
if (nonNull(startedListener)) {
startedListener.accept(containerInfo);
}
}
};
container.withEnv(env)
.withReuse(false)
//.withExposedPorts(portBindings) // no need
.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("testcontainers.".concat(loggerName))))
.waitingFor(Wait.forLogMessage(startedLogRegex, 1).withStartupTimeout(startupTimeout));
container.setPortBindings(portBindings0);
if (!isBlank(networkMode)) {
container.withNetworkMode(networkMode);
}
container.setStartupAttempts(3);
if (nonNull(commandParts) && !commandParts.isEmpty()) {
container.setCommand(commandParts.toArray(new String[0]));
}
return new ITGenericContainer(portBindings0, container);
}
}