Skip to content

Commit

Permalink
refactor: extracts Kubernetes related classes
Browse files Browse the repository at this point in the history
  • Loading branch information
m4gshm committed Apr 17, 2024
1 parent b8059d7 commit 8398b21
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 127 deletions.
131 changes: 9 additions & 122 deletions src/main/java/io/github/m4gshm/testcontainers/AbstractPod.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,13 @@
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
import io.fabric8.kubernetes.client.LocalPortForward;
import io.fabric8.kubernetes.client.RequestConfig;
import io.fabric8.kubernetes.client.dsl.PodResource;
import io.fabric8.kubernetes.client.dsl.internal.OperationSupport;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.jetbrains.annotations.NotNull;
import org.testcontainers.containers.Container;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.images.builder.Transferable;
import org.testcontainers.shaded.com.google.common.hash.Hashing;
Expand All @@ -30,9 +26,7 @@
import java.io.InputStream;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.nio.charset.Charset;
import java.time.Duration;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -46,10 +40,10 @@
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.uploadStdIn;
import static io.github.m4gshm.testcontainers.KubernetesUtils.waitEmptyQueue;
import static io.github.m4gshm.testcontainers.PodContainerUtils.config;
import static java.lang.Boolean.getBoolean;
Expand Down Expand Up @@ -223,7 +217,7 @@ public String getDockerImageName() {

protected abstract void containerIsStarted(InspectContainerResponse containerInfo, boolean reused);

protected abstract @NotNull List<EnvVar> getEnvVars();
protected abstract List<EnvVar> getEnvVars();

protected abstract String[] getCommandParts();

Expand Down Expand Up @@ -266,16 +260,19 @@ public void copyFileToContainer(Transferable transferable, String containerPath)

@SneakyThrows
public void uploadTmpTar(byte[] payload) {
var podName = this.podName;
var podResource = pod;

var tmpDir = "/tmp";
var tarName = tmpDir + "/" + this.podName + ".tar";
var tarName = tmpDir + "/" + podName + ".tar";
log.debug("tar uploading {}", tarName);

var escapedTarPath = escapeQuotes(tarName);
try (var exec = pod.terminateOnError().exec("touch", escapedTarPath)) {
try (var exec = podResource.terminateOnError().exec("touch", escapedTarPath)) {
waitEmptyQueue(exec);
}

uploadStdIn(payload, escapedTarPath);
uploadStdIn(pod, getRequestTimeout(), escapedTarPath, payload);
// uploadBase64(payload, escapedTarPath);

var unpackDir = "/";
Expand All @@ -284,7 +281,7 @@ public void uploadTmpTar(byte[] payload) {

var out = new ByteArrayOutputStream();
var err = new ByteArrayOutputStream();
try (var exec = pod.redirectingInput().writingOutput(out).writingError(err).exec("sh", "-c", extractTarCmd)) {
try (var exec = podResource.redirectingInput().writingOutput(out).writingError(err).exec("sh", "-c", extractTarCmd)) {
waitEmptyQueue(exec);
var exitedCode = exec.exitCode();
var exitCode = exitedCode.get(getRequestTimeout(), MILLISECONDS);
Expand All @@ -301,40 +298,6 @@ public void uploadTmpTar(byte[] payload) {
}
}

@SneakyThrows
private void uploadStdIn(byte[] payload, String escapedTarPath) {
try (var exec = pod.redirectingInput().terminateOnError().exec("cp", "/dev/stdin", escapedTarPath)) {
var input = exec.getInput();
input.write(payload);
input.flush();
waitEmptyQueue(exec);
checkSize(escapedTarPath, payload.length);
}
}

@SneakyThrows
protected void checkSize(String filePath, long expected) {
var size = getSize(filePath);
if (size != expected) {
Thread.sleep(100);
size = getSize(filePath);
}
if (size != expected) {
throw new UploadFileException("Unexpected file size " + size + ", expected " + expected + ", file '" + filePath + "'");
}
}

@SneakyThrows
private long getSize(String filePath) {
var byteCount = new ByteArrayOutputStream();
try (var exec = pod.writingOutput(byteCount).terminateOnError().exec("sh", "-c", "wc -c < " + filePath)) {
var exitCode = exec.exitCode().get(getRequestTimeout(), MILLISECONDS);
var remoteSizeRaw = byteCount.toString(UTF_8).trim();
waitEmptyQueue(exec);
return Integer.parseInt(remoteSizeRaw);
}
}

protected void waitUntilPodStarted() {
var startTime = System.currentTimeMillis();
var pod = getPodResource().get();
Expand Down Expand Up @@ -382,7 +345,6 @@ protected void waitUntilPodStarted() {
throw new StartPodException("unexpected pod status", pod.getMetadata().getName(), status.getPhase());
}


var firstNotReadyContainer = getFirstNotReadyContainer(status);

if (firstNotReadyContainer != null) {
Expand Down Expand Up @@ -441,7 +403,6 @@ protected int getRequestTimeout() {
return kubernetesClient().getConfiguration().getRequestTimeout();
}


public void addHostPort(Integer port, Integer hostPort) {
podBuilderFactory.addHostPort(port, hostPort);
}
Expand Down Expand Up @@ -501,80 +462,6 @@ public String getPodIP() {
return getPod().map(pod -> pod.getStatus().getHostIP()).orElse(null);
}


@SneakyThrows
public <T> T copyFileFromContainer(String containerPath, ThrowingFunction<InputStream, T> function) {
assertPodRunning("copyFileFromContainer");
try (var inputStream = pod.file(containerPath).read()) {
return function.apply(inputStream);
}
}

@SneakyThrows
private void uploadBase64(byte[] payload, String escapedTarPath) {
var encoded = Base64.getEncoder().encodeToString(payload);
try (var uploadWatch = pod.terminateOnError().exec("sh", "-c", "echo " + encoded + "| base64 -d >" + "'" + escapedTarPath + "'")) {
var code = uploadWatch.exitCode().get(getRequestConfig().getRequestTimeout(), MILLISECONDS);
if (code != 0) {
throw new UploadFileException("Unexpected exit code " + code + ", file '" + escapedTarPath + "'");
}
checkSize(escapedTarPath, payload.length);
}
}

private RequestConfig getRequestConfig() {
return ((OperationSupport) pod).getRequestConfig();
}

@SneakyThrows
public boolean removeFile(String tarName) {
try (var exec = waitEmptyQueue(pod.redirectingError().exec("rm", tarName))) {
var exitCode = exec.exitCode().get(getRequestTimeout(), MILLISECONDS);
var deleted = exitCode != 0;
if (deleted) {
log.warn("deleting of temporary file {} finished with unexpected code {}, errOut: {}",
tarName, exitCode, getError(exec));
}
return deleted;
}
}

@SneakyThrows
public Container.ExecResult execInContainer(Charset outputCharset, String... command) {
var hasShellCall = command.length > 1 && command[0].equals("sh") && command[1].equals("-c");
if (!hasShellCall) {
var newCmd = new String[command.length + 2];
newCmd[0] = "sh";
newCmd[1] = "-c";
System.arraycopy(command, 0, newCmd, 2, command.length);
command = newCmd;
}

try (var execWatch = pod.redirectingOutput().redirectingError().exec(command)) {
var exited = execWatch.exitCode();
var exitCode = exited.get(getRequestTimeout(), MILLISECONDS);
;
String errOut;
try {
errOut = new String(execWatch.getError().readAllBytes(), outputCharset);
} catch (IOException e) {
errOut = "";
log.info("err output read error", e);
}
String output;
try {
output = new String(execWatch.getOutput().readAllBytes(), outputCharset);
} catch (IOException e) {
output = "";
log.info("output read error", e);
}
var constructor = Container.ExecResult.class.getDeclaredConstructor(int.class, String.class, String.class);
constructor.setAccessible(true);

return constructor.newInstance(exitCode, output, errOut);
}
}

@Override
public String toString() {
return getClass().getSimpleName() + "{" + toStringFields() + '}';
Expand Down
102 changes: 102 additions & 0 deletions src/main/java/io/github/m4gshm/testcontainers/KubernetesUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.nio.charset.Charset;
import java.util.Base64;
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.concurrent.TimeUnit.MILLISECONDS;
import static java.util.stream.Collectors.toMap;

@Slf4j
Expand Down Expand Up @@ -124,4 +128,102 @@ public static Map<Integer, LocalPortForward> startPortForward(
}));
}


@SneakyThrows
public boolean removeFile(PodResource podResource, int requestTimeout, String tarName) {
try (var exec = waitEmptyQueue(podResource.redirectingError().exec("rm", tarName))) {
var exitCode = exec.exitCode().get(requestTimeout, MILLISECONDS);
var deleted = exitCode != 0;
if (deleted) {
log.warn("deleting of temporary file {} finished with unexpected code {}, errOut: {}",
tarName, exitCode, getError(exec));
}
return deleted;
}
}

@SneakyThrows
public static ExecResult exec(PodResource podResource, int requestTimeout, Charset outputCharset, String... command) {
var hasShellCall = command.length > 1 && command[0].equals("sh") && command[1].equals("-c");
if (!hasShellCall) {
var newCmd = new String[command.length + 2];
newCmd[0] = "sh";
newCmd[1] = "-c";
System.arraycopy(command, 0, newCmd, 2, command.length);
command = newCmd;
}

try (var execWatch = podResource.redirectingOutput().redirectingError().exec(command)) {
var exited = execWatch.exitCode();
var exitCode = exited.get(requestTimeout, MILLISECONDS);
;
String errOut;
try {
errOut = new String(execWatch.getError().readAllBytes(), outputCharset);
} catch (IOException e) {
errOut = "";
log.info("err output read error", e);
}
String output;
try {
output = new String(execWatch.getOutput().readAllBytes(), outputCharset);
} catch (IOException e) {
output = "";
log.info("output read error", e);
}

return new ExecResult(exitCode, output, errOut);
}
}


@SneakyThrows
public static void uploadBase64(PodResource podResource, int requestTimeout, String escapedTarPath, byte[] payload) {
var encoded = Base64.getEncoder().encodeToString(payload);
try (var uploadWatch = podResource.terminateOnError().exec("sh", "-c", "echo " + encoded + "| base64 -d >" + "'" + escapedTarPath + "'")) {
var code = uploadWatch.exitCode().get(requestTimeout, MILLISECONDS);
if (code != 0) {
throw new UploadFileException("Unexpected exit code " + code + ", file '" + escapedTarPath + "'");
}
checkSize(podResource, requestTimeout, escapedTarPath, payload.length);
}
}

@SneakyThrows
public static void uploadStdIn(PodResource podResource, int requestTimeout, String escapedTarPath, byte[] payload) {
try (var exec = podResource.redirectingInput().terminateOnError().exec("cp", "/dev/stdin", escapedTarPath)) {
var input = exec.getInput();
input.write(payload);
input.flush();
waitEmptyQueue(exec);
checkSize(podResource, requestTimeout, escapedTarPath, payload.length);
}
}

@SneakyThrows
public static long getFileSize(PodResource podResource, int requestTimeout, String filePath) {
var byteCount = new ByteArrayOutputStream();
try (var exec = podResource.writingOutput(byteCount).terminateOnError().exec("sh", "-c", "wc -c < " + filePath)) {
var exitCode = exec.exitCode().get(requestTimeout, MILLISECONDS);
var remoteSizeRaw = byteCount.toString(UTF_8).trim();
waitEmptyQueue(exec);
return Integer.parseInt(remoteSizeRaw);
}
}

@SneakyThrows
public static void checkSize(PodResource podResource, int requestTimeout, String filePath, long expected) {
var size = getFileSize(podResource, requestTimeout, filePath);
if (size != expected) {
Thread.sleep(100);
size = getFileSize(podResource, requestTimeout, filePath);
}
if (size != expected) {
throw new UploadFileException("Unexpected file size " + size + ", expected " + expected + ", file '" + filePath + "'");
}
}

public record ExecResult(int exitCode, String output, String error) {
}

}
2 changes: 0 additions & 2 deletions src/main/java/io/github/m4gshm/testcontainers/MongoDBPod.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
import lombok.extern.slf4j.Slf4j;
import org.testcontainers.containers.MongoDBContainer;

import static java.util.Objects.requireNonNull;

/**
* Kubernetes based extension of the {@link org.testcontainers.containers.MongoDBContainer}.
*/
Expand Down
Loading

0 comments on commit 8398b21

Please sign in to comment.