From 6c31c4d98a5a0eca7975f520ea1a8b1ba27f4e0b Mon Sep 17 00:00:00 2001 From: Bulgakov Alexander Date: Wed, 17 Apr 2024 01:06:28 +0300 Subject: [PATCH] refactor: extracts Kubernetes related classes --- README.md | 9 - .../DefaultPodNameGenerator.java | 9 +- .../m4gshm/testcontainers/GenericPod.java | 20 +- .../testcontainers/JdbcDatabasePod.java | 14 +- ...dEngineUtils.java => KubernetesUtils.java} | 37 ++- .../m4gshm/testcontainers/MongoDBPod.java | 10 +- .../testcontainers/PodBuilderFactory.java | 109 ++++++++ .../m4gshm/testcontainers/PodEngine.java | 239 +++++------------- .../testcontainers/PodNameGenerator.java | 4 +- 9 files changed, 218 insertions(+), 233 deletions(-) rename src/main/java/io/github/m4gshm/testcontainers/{PodEngineUtils.java => KubernetesUtils.java} (64%) create mode 100644 src/main/java/io/github/m4gshm/testcontainers/PodBuilderFactory.java diff --git a/README.md b/README.md index 131f8a1..3fcd705 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,6 @@ To run locally, it is highly recommended to use ``` java package example; -import io.fabric8.kubernetes.client.Config; -import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.github.m4gshm.testcontainers.PostgresqlPod; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -42,19 +40,12 @@ import org.testcontainers.containers.JdbcDatabaseContainer; import java.sql.SQLException; -import static io.fabric8.kubernetes.client.Config.autoConfigure; import static java.sql.DriverManager.getConnection; public class JdbcTest { protected static JdbcDatabaseContainer postgres = new PostgresqlPod(); - private static Config getConfig() { - var config = autoConfigure(null); - config.setDefaultNamespace(true); - return config; - } - @BeforeAll static void beforeAll() { postgres.start(); diff --git a/src/main/java/io/github/m4gshm/testcontainers/DefaultPodNameGenerator.java b/src/main/java/io/github/m4gshm/testcontainers/DefaultPodNameGenerator.java index a0968a6..9cec464 100644 --- a/src/main/java/io/github/m4gshm/testcontainers/DefaultPodNameGenerator.java +++ b/src/main/java/io/github/m4gshm/testcontainers/DefaultPodNameGenerator.java @@ -1,7 +1,7 @@ package io.github.m4gshm.testcontainers; +import lombok.NonNull; import lombok.RequiredArgsConstructor; -import org.testcontainers.containers.Container; import java.security.SecureRandom; @@ -24,17 +24,16 @@ public static DefaultPodNameGenerator newDefaultPodNameGenerator(boolean useImag return new DefaultPodNameGenerator(new SecureRandom(), "testcontainer-", useImageNamePrefix); } - private static String imageNamePrefix(Container container) { - var dockerImageName = container.getDockerImageName(); + private static String imageNamePrefix(@NonNull String dockerImageName) { var parts = dockerImageName.split(":"); var prefix = parts.length > 0 ? parts[0] : ""; return !prefix.isEmpty() ? prefix + "-" : ""; } @Override - public String generatePodName(Container container) { + public String generatePodName(@NonNull String dockerImageName) { var maxNameLength = 63; - var prefix = this.prefix + imageNamePrefix(container); + var prefix = this.prefix + imageNamePrefix(dockerImageName); var reminds = maxNameLength - prefix.length(); var bytes = new byte[reminds / 2]; random.nextBytes(bytes); diff --git a/src/main/java/io/github/m4gshm/testcontainers/GenericPod.java b/src/main/java/io/github/m4gshm/testcontainers/GenericPod.java index 677a0fa..a6582dc 100644 --- a/src/main/java/io/github/m4gshm/testcontainers/GenericPod.java +++ b/src/main/java/io/github/m4gshm/testcontainers/GenericPod.java @@ -6,41 +6,31 @@ import lombok.extern.slf4j.Slf4j; import org.testcontainers.containers.GenericContainer; -import static java.util.Objects.requireNonNull; - /** * General purpose pod engine implementation. + * * @param - implementation type. */ @Slf4j public class GenericPod> extends GenericContainer implements PodAware { - private PodEngine podEngine; + @Delegate + private final PodEngine podEngine; public GenericPod(@NonNull String dockerImageName) { super(dockerImageName); - setDockerImageName(dockerImageName); - var podEngine = this.podEngine; - requireNonNull(podEngine, "podEngine is null"); + podEngine = new PodEngine<>((SELF) this, dockerImageName); waitStrategy = new PodPortWaitStrategy(); } - @Delegate @Override public PodEngine getPod() { - initPodEngine(); return podEngine; } - private void initPodEngine() { - if (podEngine == null) { - podEngine = new PodEngine<>((SELF) this); - } - } - @Override protected void doStart() { - configure(); + podEngine.start(); } @Override diff --git a/src/main/java/io/github/m4gshm/testcontainers/JdbcDatabasePod.java b/src/main/java/io/github/m4gshm/testcontainers/JdbcDatabasePod.java index 5a32817..60cead7 100644 --- a/src/main/java/io/github/m4gshm/testcontainers/JdbcDatabasePod.java +++ b/src/main/java/io/github/m4gshm/testcontainers/JdbcDatabasePod.java @@ -7,8 +7,6 @@ import lombok.extern.slf4j.Slf4j; import org.testcontainers.containers.JdbcDatabaseContainer; -import static java.util.Objects.requireNonNull; - /** * Base relation database pod engine that provides JDBC connection config. @@ -37,25 +35,17 @@ public abstract class JdbcDatabasePod> extend public JdbcDatabasePod(@NonNull String dockerImageName) { super(dockerImageName); - setDockerImageName(dockerImageName); - requireNonNull(this.podEngine, "podEngine is null"); + podEngine = new PodEngine<>((SELF) this, dockerImageName); } @Delegate(excludes = Excludes.class) public PodEngine getPod() { - initPodEngine(); return podEngine; } - private void initPodEngine() { - if (podEngine == null) { - podEngine = new PodEngine<>((SELF) this); - } - } - @Override public void start() { - getPod().start(); + podEngine.start(); } @Override diff --git a/src/main/java/io/github/m4gshm/testcontainers/PodEngineUtils.java b/src/main/java/io/github/m4gshm/testcontainers/KubernetesUtils.java similarity index 64% rename from src/main/java/io/github/m4gshm/testcontainers/PodEngineUtils.java rename to src/main/java/io/github/m4gshm/testcontainers/KubernetesUtils.java index dbe39e2..0bb8046 100644 --- a/src/main/java/io/github/m4gshm/testcontainers/PodEngineUtils.java +++ b/src/main/java/io/github/m4gshm/testcontainers/KubernetesUtils.java @@ -4,23 +4,30 @@ import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodStatus; import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.LocalPortForward; import io.fabric8.kubernetes.client.dsl.ExecWatch; import io.fabric8.kubernetes.client.dsl.PodResource; import io.fabric8.kubernetes.client.dsl.internal.ExecWebSocketListener; import io.fabric8.kubernetes.client.http.WebSocket; import lombok.SneakyThrows; import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; +import java.net.InetAddress; +import java.util.Collection; +import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import static java.lang.Boolean.TRUE; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.stream.Collectors.toMap; +@Slf4j @UtilityClass -public class PodEngineUtils { +public class KubernetesUtils { public static final String RUNNING = "Running"; public static final String PENDING = "Pending"; public static final String UNKNOWN = "Unknown"; @@ -89,4 +96,32 @@ public static boolean isRunning(Pod pod) { }).findFirst().orElse(null); } + public static Map startPortForward( + PodResource pod, InetAddress inetAddress, Collection ports) { + var podName = pod.get().getMetadata().getName(); + return ports.stream().collect(toMap(port -> port, port -> { + var localPortForward = inetAddress != null + ? pod.portForward(port, inetAddress, 0) + : pod.portForward(port); + var localAddress = localPortForward.getLocalAddress(); + var localPort = localPortForward.getLocalPort(); + if (localPortForward.errorOccurred()) { + var clientThrowables = localPortForward.getClientThrowables(); + if (!clientThrowables.isEmpty()) { + var throwable = clientThrowables.iterator().next(); + throw new StartPodException("port forward client error", podName, throwable); + } + var serverThrowables = localPortForward.getServerThrowables(); + if (!serverThrowables.isEmpty()) { + var throwable = serverThrowables.iterator().next(); + throw new StartPodException("port forward server error", podName, throwable); + } + } else { + log.info("port forward local {}:{} to remote {}:{}, ", localAddress.getHostAddress(), localPort, + podName, port); + } + return localPortForward; + })); + } + } diff --git a/src/main/java/io/github/m4gshm/testcontainers/MongoDBPod.java b/src/main/java/io/github/m4gshm/testcontainers/MongoDBPod.java index 49d08c8..d01aeeb 100644 --- a/src/main/java/io/github/m4gshm/testcontainers/MongoDBPod.java +++ b/src/main/java/io/github/m4gshm/testcontainers/MongoDBPod.java @@ -21,21 +21,13 @@ public MongoDBPod() { public MongoDBPod(@NonNull final String dockerImageName) { super(dockerImageName); - setDockerImageName(dockerImageName); - requireNonNull(this.podEngine, "podEngine is null"); + podEngine = new PodEngine<>(this, dockerImageName); } @Delegate @Override public PodEngine getPod() { - initPodEngine(); return podEngine; } - private void initPodEngine() { - if (podEngine == null) { - podEngine = new PodEngine<>(this); - } - } - } diff --git a/src/main/java/io/github/m4gshm/testcontainers/PodBuilderFactory.java b/src/main/java/io/github/m4gshm/testcontainers/PodBuilderFactory.java new file mode 100644 index 0000000..686ac46 --- /dev/null +++ b/src/main/java/io/github/m4gshm/testcontainers/PodBuilderFactory.java @@ -0,0 +1,109 @@ +package io.github.m4gshm.testcontainers; + +import io.fabric8.kubernetes.api.model.ContainerBuilder; +import io.fabric8.kubernetes.api.model.ContainerPort; +import io.fabric8.kubernetes.api.model.ContainerPortBuilder; +import io.fabric8.kubernetes.api.model.EnvVar; +import io.fabric8.kubernetes.api.model.LocalObjectReferenceBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.PodBuilder; +import io.fabric8.kubernetes.api.model.PodSecurityContextBuilder; +import io.fabric8.kubernetes.api.model.PodSpecBuilder; +import io.fabric8.kubernetes.api.model.SecurityContextBuilder; +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; + +@Getter +@Setter +public class PodBuilderFactory { + private final Map labels = new HashMap<>(); + private final Map hostPorts = new HashMap<>(); + + private String dockerImageName; + private Boolean runAsNonRoot; + private Long runAsUser; + private Long runAsGroup; + private Long fsGroup; + private boolean privilegedMode; + private boolean allowPrivilegeEscalation; + private String imagePullPolicy = "Always"; + private String imagePullSecretName; + private UnaryOperator podBuilderCustomizer; + private UnaryOperator containerBuilderCustomizer; + private String podContainerName = "main"; + private List entryPoint; + private String portProtocol = "TCP"; + private List vars; + private String[] args; + private List exposedPorts = new ArrayList<>(); + private boolean hostPortEnabled = false; + + public PodBuilder newPodBuilder() { + var containerBuilder = new ContainerBuilder() + .withImagePullPolicy(getImagePullPolicy()) + .withImage(getDockerImageName()) + .withSecurityContext(new SecurityContextBuilder() + .withRunAsNonRoot(getRunAsNonRoot()) + .withRunAsUser(getRunAsUser()) + .withRunAsGroup(getRunAsGroup()) + .withPrivileged(isPrivilegedMode()) + .withAllowPrivilegeEscalation(isAllowPrivilegeEscalation()) + .build()) + .withName(getPodContainerName()) + .withArgs(getArgs()) + .withCommand(getEntryPoint()) + .withPorts(getContainerPorts()) + .withEnv(getVars()); + if (getContainerBuilderCustomizer() != null) { + containerBuilder = getContainerBuilderCustomizer().apply(containerBuilder); + } + var podBuilder = new PodBuilder() + .withMetadata(new ObjectMetaBuilder() + .addToLabels(getLabels()) + .addToLabels(Map.of( + PodEngine.ORG_TESTCONTAINERS_TYPE, PodEngine.KUBECONTAINERS + )) + .build()) + .withSpec(new PodSpecBuilder() + .withSecurityContext(new PodSecurityContextBuilder() + .withRunAsNonRoot(getRunAsNonRoot()) + .withRunAsUser(getRunAsUser()) + .withFsGroup(getFsGroup()) + .build()) + .withImagePullSecrets(new LocalObjectReferenceBuilder() + .withName(getImagePullSecretName()) + .build()) + .withContainers(containerBuilder.build()) + .build()); + + if (getPodBuilderCustomizer() != null) { + podBuilder = getPodBuilderCustomizer().apply(podBuilder); + } + return podBuilder; + } + + @NotNull + protected List getContainerPorts() { + return exposedPorts.stream().map(port -> { + var portBuilder = new ContainerPortBuilder() + .withContainerPort(port) + .withProtocol(getPortProtocol()); + if (isHostPortEnabled()) { + portBuilder.withHostPort(getHostPorts().get(port)); + } + return portBuilder.build(); + }).toList(); + } + + public void addHostPort(Integer port, Integer hostPort) { + getHostPorts().put(port, hostPort); + } + +} diff --git a/src/main/java/io/github/m4gshm/testcontainers/PodEngine.java b/src/main/java/io/github/m4gshm/testcontainers/PodEngine.java index 99247e3..35b8888 100644 --- a/src/main/java/io/github/m4gshm/testcontainers/PodEngine.java +++ b/src/main/java/io/github/m4gshm/testcontainers/PodEngine.java @@ -6,17 +6,10 @@ import com.github.dockerjava.api.command.InspectContainerResponse; import io.fabric8.kubernetes.api.model.ContainerBuilder; import io.fabric8.kubernetes.api.model.ContainerPort; -import io.fabric8.kubernetes.api.model.ContainerPortBuilder; -import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.EnvVarBuilder; import io.fabric8.kubernetes.api.model.ListOptionsBuilder; -import io.fabric8.kubernetes.api.model.LocalObjectReferenceBuilder; -import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodBuilder; -import io.fabric8.kubernetes.api.model.PodSecurityContextBuilder; -import io.fabric8.kubernetes.api.model.PodSpecBuilder; -import io.fabric8.kubernetes.api.model.SecurityContextBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.fabric8.kubernetes.client.LocalPortForward; @@ -66,16 +59,16 @@ import static io.github.m4gshm.testcontainers.DefaultPodNameGenerator.newDefaultPodNameGenerator; import static io.github.m4gshm.testcontainers.PodEngine.Reuse.GLOBAL; import static io.github.m4gshm.testcontainers.PodEngine.Reuse.SESSION; -import static io.github.m4gshm.testcontainers.PodEngineUtils.PENDING; -import static io.github.m4gshm.testcontainers.PodEngineUtils.RUNNING; -import static io.github.m4gshm.testcontainers.PodEngineUtils.UNKNOWN; -import static io.github.m4gshm.testcontainers.PodEngineUtils.createPod; -import static io.github.m4gshm.testcontainers.PodEngineUtils.escapeQuotes; -import static io.github.m4gshm.testcontainers.PodEngineUtils.getError; -import static io.github.m4gshm.testcontainers.PodEngineUtils.getFirstNotReadyContainer; -import static io.github.m4gshm.testcontainers.PodEngineUtils.resource; -import static io.github.m4gshm.testcontainers.PodEngineUtils.shellQuote; -import static io.github.m4gshm.testcontainers.PodEngineUtils.waitEmptyQueue; +import static io.github.m4gshm.testcontainers.KubernetesUtils.PENDING; +import static io.github.m4gshm.testcontainers.KubernetesUtils.RUNNING; +import static io.github.m4gshm.testcontainers.KubernetesUtils.UNKNOWN; +import static io.github.m4gshm.testcontainers.KubernetesUtils.createPod; +import static io.github.m4gshm.testcontainers.KubernetesUtils.escapeQuotes; +import static io.github.m4gshm.testcontainers.KubernetesUtils.getError; +import static io.github.m4gshm.testcontainers.KubernetesUtils.getFirstNotReadyContainer; +import static io.github.m4gshm.testcontainers.KubernetesUtils.resource; +import static io.github.m4gshm.testcontainers.KubernetesUtils.shellQuote; +import static io.github.m4gshm.testcontainers.KubernetesUtils.waitEmptyQueue; import static java.lang.Boolean.getBoolean; import static java.lang.String.format; import static java.lang.reflect.Proxy.newProxyInstance; @@ -85,7 +78,6 @@ import static java.util.Objects.requireNonNull; import static java.util.Optional.ofNullable; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.stream.Collectors.toMap; import static lombok.AccessLevel.PROTECTED; import static org.apache.commons.compress.archivers.tar.TarArchiveOutputStream.BIGNUMBER_POSIX; import static org.apache.commons.compress.archivers.tar.TarArchiveOutputStream.LONGFILE_POSIX; @@ -105,8 +97,7 @@ public class PodEngine> { private static final String ORG_TESTCONTAINERS_SESSION_LIMITED = "org.testcontainers.sessionLimited"; private final T container; private final Map copyToTransferableContainerPathMap = new HashMap<>(); - private final Map labels = new HashMap<>(); - private final Map hostPorts = new HashMap<>(); + private final PodBuilderFactory podBuilderFactory = new PodBuilderFactory(); @Getter @Setter protected PodNameGenerator podNameGenerator; @@ -115,35 +106,10 @@ public class PodEngine> { protected KubernetesClientBuilder kubernetesClientBuilder; protected KubernetesClient kubernetesClient; @Getter - @Setter - protected String dockerImageName; - @Getter - protected Boolean runAsNonRoot; - @Getter - protected Long runAsUser; - @Getter - protected Long runAsGroup; - @Getter - protected Long fsGroup; - protected boolean privilegedMode; - protected boolean allowPrivilegeEscalation; - protected String imagePullPolicy = "Always"; - @Getter protected Duration startupTimeout = ofSeconds(60); - @Getter - protected String portProtocol = "TCP"; private PodResource pod; private boolean localPortForwardEnabled = true; private Map localPortForwards = Map.of(); - private boolean hostPortEnabled = false; - @Getter - private String imagePullSecretName; - @Getter - private UnaryOperator podBuilderCustomizer; - @Getter - private UnaryOperator containerBuilderCustomizer; - @Getter - private String podContainerName = "main"; @Getter private String podName; private boolean deletePodOnStop = false; @@ -151,9 +117,6 @@ public class PodEngine> { private boolean started; private InetAddress localPortForwardHost; @Getter - @Setter(PROTECTED) - private List entryPoint; - @Getter private Reuse reuse = SESSION; private boolean reused; @@ -167,9 +130,9 @@ public PodEngine(@NonNull T container, String dockerImageName) { public PodEngine(@NonNull T container, String dockerImageName, @NonNull PodNameGenerator podNameGenerator) { + podBuilderFactory.setDockerImageName(dockerImageName); this.container = container; this.podNameGenerator = podNameGenerator; - this.dockerImageName = dockerImageName; if (container instanceof GenericContainer) { var rawWaitStrategy = invokeContainerMethod("getWaitStrategy"); @@ -217,34 +180,42 @@ public String toString() { } public String toStringFields() { - return ", dockerImageName='" + dockerImageName + '\'' + - ", runAsNonRoot=" + runAsNonRoot + - ", runAsUser=" + runAsUser + - ", fsGroup=" + fsGroup + - ", privilegedMode=" + privilegedMode + - ", imagePullPolicy='" + imagePullPolicy + '\'' + + return ", dockerImageName='" + podBuilderFactory.getDockerImageName() + '\'' + + ", runAsNonRoot=" + podBuilderFactory.getRunAsNonRoot() + + ", runAsUser=" + podBuilderFactory.getRunAsUser() + + ", fsGroup=" + podBuilderFactory.getFsGroup() + + ", privilegedMode=" + podBuilderFactory.isPrivilegedMode() + + ", imagePullPolicy='" + podBuilderFactory.getImagePullPolicy() + '\'' + ", startupTimeout=" + startupTimeout + - ", portProtocol='" + portProtocol + '\'' + + ", portProtocol='" + podBuilderFactory.getPortProtocol() + '\'' + ", localPortForwards=" + localPortForwards + - ", imagePullSecretName='" + imagePullSecretName + '\'' + - ", podBuilderCustomizer=" + podBuilderCustomizer + - ", podContainerName='" + podContainerName + '\'' + + ", imagePullSecretName='" + podBuilderFactory.getImagePullSecretName() + '\'' + + ", podBuilderCustomizer=" + podBuilderFactory.getPodBuilderCustomizer() + + ", podContainerName='" + podBuilderFactory.getPodContainerName() + '\'' + ", podName='" + podName + '\'' + ", deletePodOnStop=" + deletePodOnStop + ", started=" + started; } + public String getDockerImageName() { + return podBuilderFactory.getDockerImageName(); + } + + public void setDockerImageName(String dockerImageName) { + podBuilderFactory.setDockerImageName(dockerImageName); + } + public DockerClient getDockerClient() { throw new UnsupportedOperationException("getDockerClient"); } public T withImagePullPolicy(ImagePullPolicy imagePullPolicy) { if (imagePullPolicy == null) { - this.imagePullPolicy = "Never"; + podBuilderFactory.setImagePullPolicy("Never"); } else if (imagePullPolicy.getClass().equals(PullPolicy.alwaysPull().getClass())) { - this.imagePullPolicy = "Always"; + podBuilderFactory.setImagePullPolicy("Always"); } else { - this.imagePullPolicy = "IfNotPresent"; + podBuilderFactory.setImagePullPolicy("IfNotPresent"); } return container; } @@ -265,47 +236,47 @@ public T withJsonMapper(JsonMapper jsonMapper) { } public T withRunAsNonRoot(Boolean runAsNonRoot) { - this.runAsNonRoot = runAsNonRoot; + podBuilderFactory.setRunAsNonRoot(runAsNonRoot); return container; } public T withRunAsUser(Long runAsUser) { - this.runAsUser = runAsUser; + podBuilderFactory.setRunAsUser(runAsUser); return container; } public T withRunAsGroup(Long runAsGroup) { - this.runAsGroup = runAsGroup; + podBuilderFactory.setRunAsGroup(runAsGroup); return container; } public T withFsGroup(Long fsGroup) { - this.fsGroup = fsGroup; + podBuilderFactory.setFsGroup(fsGroup); return container; } public T withPodBuilderCustomizer(UnaryOperator podBuilderCustomizer) { - this.podBuilderCustomizer = podBuilderCustomizer; + podBuilderFactory.setPodBuilderCustomizer(podBuilderCustomizer); return container; } public T withContainerBuilderCustomizer(UnaryOperator podContainerBuilderCustomizer) { - this.containerBuilderCustomizer = podContainerBuilderCustomizer; + podBuilderFactory.setContainerBuilderCustomizer(podContainerBuilderCustomizer); return container; } public T withPodContainerName(String podContainerName) { - this.podContainerName = podContainerName; + podBuilderFactory.setPodContainerName(podContainerName); return container; } public T withImagePullSecretName(String imagePullSecretName) { - this.imagePullSecretName = imagePullSecretName; + podBuilderFactory.setImagePullSecretName(imagePullSecretName); return container; } public T withPortProtocol(String portProtocol) { - this.portProtocol = portProtocol; + podBuilderFactory.setPortProtocol(portProtocol); return container; } @@ -342,12 +313,12 @@ public T waitingFor(@NonNull WaitStrategy waitStrategy) { } public T withPrivilegedMode(boolean privilegedMode) { - this.privilegedMode = privilegedMode; + podBuilderFactory.setPrivilegedMode(privilegedMode); return container; } public T withAllowPrivilegeEscalation(boolean allowPrivilegeEscalation) { - this.allowPrivilegeEscalation = allowPrivilegeEscalation; + podBuilderFactory.setAllowPrivilegeEscalation(allowPrivilegeEscalation); return container; } @@ -391,9 +362,9 @@ private CreateContainerCmd newCreateContainerCmd() { case "withEntrypoint" -> { var firstArg = args[0]; if (firstArg instanceof String[] strings) { - this.entryPoint = List.of(strings); + podBuilderFactory.setEntryPoint(List.of(strings)); } else if (firstArg instanceof List list) { - this.entryPoint = list.stream().map(String::valueOf).toList(); + podBuilderFactory.setEntryPoint(list.stream().map(String::valueOf).toList()); } yield null; } @@ -403,7 +374,7 @@ private CreateContainerCmd newCreateContainerCmd() { } public boolean isRunning() { - return getPod().map(PodEngineUtils::isRunning).orElse(false); + return getPod().map(KubernetesUtils::isRunning).orElse(false); } public boolean isHealthy() { @@ -439,7 +410,7 @@ protected ContainerPort getContainerPort(Integer port) { } protected io.fabric8.kubernetes.api.model.Container getContainer() { - var containerName = podContainerName; + var containerName = podBuilderFactory.getPodContainerName(); return getPod().flatMap(pod -> pod.getSpec().getContainers().stream().filter(c -> containerName.equals(c.getName())) .findFirst()).orElseThrow(() -> new IllegalStateException("container '" + containerName + "' not found")); } @@ -472,9 +443,13 @@ public String getPodIP() { public void start() { configure(); - var podName = podNameGenerator.generatePodName(this.container); + var podName = podNameGenerator.generatePodName(getDockerImageName()); this.podName = podName; - var podBuilder = newPodBuilder(podName); + + podBuilderFactory.setArgs(container.getCommandParts()); + podBuilderFactory.setVars(container.getEnvMap().entrySet().stream().map(e -> + new EnvVarBuilder().withName(e.getKey()).withValue(e.getValue()).build()).toList()); + var podBuilder = podBuilderFactory.newPodBuilder(); var hash = hash(podBuilder.build()); var session = Session.instance(); @@ -501,7 +476,7 @@ public void start() { var labels = p.getMetadata().getLabels(); var deleteOnStop = getBoolean(labels.get(ORG_TESTCONTAINERS_DELETE_ON_STOP)); var sessionLimited = getBoolean(labels.get(ORG_TESTCONTAINERS_SESSION_LIMITED)); - return PodEngineUtils.isRunning(p) + return KubernetesUtils.isRunning(p) && hash.equals(labels.get(ORG_TESTCONTAINERS_HASH)) && !(deleteOnStop || sessionLimited) && getFirstNotReadyContainer(p.getStatus()) == null; @@ -562,76 +537,6 @@ public void start() { containerIsStarted(containerInfo, false); } - protected PodBuilder newPodBuilder(String podName) { - var containerBuilder = new ContainerBuilder() - .withImagePullPolicy(imagePullPolicy) - .withImage(getDockerImageName()) - .withSecurityContext(new SecurityContextBuilder() - .withRunAsNonRoot(runAsNonRoot) - .withRunAsUser(runAsUser) - .withRunAsGroup(runAsGroup) - .withPrivileged(privilegedMode) - .withAllowPrivilegeEscalation(allowPrivilegeEscalation) - .build()) - .withName(podContainerName) - .withArgs(getArgs()) - .withCommand(entryPoint) - .withPorts(getContainerPorts()) - .withEnv(getVars()); - if (containerBuilderCustomizer != null) { - containerBuilder = containerBuilderCustomizer.apply(containerBuilder); - } - - var podBuilder = new PodBuilder() - .withMetadata(new ObjectMetaBuilder() -// .withName(podName) - .addToLabels(labels) - .addToLabels(Map.of( - ORG_TESTCONTAINERS_TYPE, KUBECONTAINERS//, -// ORG_TESTCONTAINERS_NAME, podName - )) - .build()) - .withSpec(new PodSpecBuilder() - .withSecurityContext(new PodSecurityContextBuilder() - .withRunAsNonRoot(runAsNonRoot) - .withRunAsUser(runAsUser) - .withFsGroup(fsGroup) - .build()) - .withImagePullSecrets(new LocalObjectReferenceBuilder() - .withName(imagePullSecretName) - .build()) - .withContainers(containerBuilder.build()) - .build()); - - if (podBuilderCustomizer != null) { - podBuilder = podBuilderCustomizer.apply(podBuilder); - } - return podBuilder; - } - - @NotNull - protected List getContainerPorts() { - return getExposedPorts().stream().map(port -> { - var portBuilder = new ContainerPortBuilder() - .withContainerPort(port) - .withProtocol(portProtocol); - if (hostPortEnabled) { - portBuilder.withHostPort(hostPorts.get(port)); - } - return portBuilder.build(); - }).toList(); - } - - @NotNull - private List getVars() { - return container.getEnvMap().entrySet().stream().map(e -> - new EnvVarBuilder().withName(e.getKey()).withValue(e.getValue()).build()).toList(); - } - - private String[] getArgs() { - return container.getCommandParts(); - } - protected KubernetesClient kubernetesClient() { var kubernetesClient = this.kubernetesClient; if (kubernetesClient == null) { @@ -839,7 +744,7 @@ public T withLabel(String key, String value) { if (key.startsWith("org.testcontainers")) { throw new IllegalArgumentException("The org.testcontainers namespace is reserved for interal use"); } - labels.put(key, value); + podBuilderFactory.getLabels().put(key, value); return container; } @@ -963,35 +868,10 @@ private void setContainerField(String name, Object value) { } protected void startPortForward() { - var inetAddress = localPortForwardHost; - var exposedPorts = getExposedPorts(); - localPortForwards = exposedPorts.stream().collect(toMap(port -> port, port -> { - var pod = getPodResource(); - var localPortForward = inetAddress != null - ? pod.portForward(port, inetAddress, 0) - : pod.portForward(port); - var localAddress = localPortForward.getLocalAddress(); - var localPort = localPortForward.getLocalPort(); - if (localPortForward.errorOccurred()) { - var clientThrowables = localPortForward.getClientThrowables(); - if (!clientThrowables.isEmpty()) { - var throwable = clientThrowables.iterator().next(); - throw new StartPodException("port forward client error", podName, throwable); - } - var serverThrowables = localPortForward.getServerThrowables(); - if (!serverThrowables.isEmpty()) { - var throwable = serverThrowables.iterator().next(); - throw new StartPodException("port forward server error", podName, throwable); - } - } else { - log.info("port forward local {}:{} to remote {}.{}:{}, ", localAddress.getHostAddress(), localPort, - podName, podContainerName, port); - } - return localPortForward; - })); + localPortForwards = KubernetesUtils.startPortForward(getPodResource(), localPortForwardHost, getExposedPorts()); } - private List getExposedPorts() { + protected List getExposedPorts() { return container.getExposedPorts(); } @@ -1014,7 +894,6 @@ protected Optional getPod() { return ofNullable(getPodResource().get()); } - private void assertPodRunning(String funcName) { if (getContainerId() == null) { throw new IllegalStateException(funcName + " can only be used with running pod"); @@ -1022,7 +901,7 @@ private void assertPodRunning(String funcName) { } public void addHostPort(Integer port, Integer hostPort) { - this.hostPorts.put(port, hostPort); + podBuilderFactory.addHostPort(port, hostPort); } public enum Reuse { diff --git a/src/main/java/io/github/m4gshm/testcontainers/PodNameGenerator.java b/src/main/java/io/github/m4gshm/testcontainers/PodNameGenerator.java index de70021..b6d8bdb 100644 --- a/src/main/java/io/github/m4gshm/testcontainers/PodNameGenerator.java +++ b/src/main/java/io/github/m4gshm/testcontainers/PodNameGenerator.java @@ -1,10 +1,10 @@ package io.github.m4gshm.testcontainers; -import org.testcontainers.containers.Container; +import lombok.NonNull; /** * A generator of unique pod name interface. */ public interface PodNameGenerator { - String generatePodName(Container container); + String generatePodName(@NonNull String dockerImageName); }