Skip to content
Merged
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
1 change: 1 addition & 0 deletions core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ dependencies {
testImplementation files('testlib/repo/fakejar/fakejar/0/fakejar-0.jar')

testImplementation 'org.assertj:assertj-core:3.25.3'
testImplementation 'io.rest-assured:rest-assured:5.4.0'

jarFileTestCompileOnly "org.projectlombok:lombok:${lombok.version}"
jarFileTestAnnotationProcessor "org.projectlombok:lombok:${lombok.version}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ public class ComposeContainer extends FailureDetectingExternalResource implement

private String project;

private List<String> filesInDirectory = new ArrayList<>();

public ComposeContainer(File... composeFiles) {
this(Arrays.asList(composeFiles));
}
Expand Down Expand Up @@ -134,7 +136,8 @@ public void start() {
this.options,
this.services,
this.scalingPreferences,
this.env
this.env,
this.filesInDirectory
);
this.composeDelegate.startAmbassadorContainer();
this.composeDelegate.waitUntilServiceStarted(this.tailChildContainers);
Expand Down Expand Up @@ -165,7 +168,7 @@ public void stop() {
if (removeImages != null) {
cmd += " --rmi " + removeImages.dockerRemoveImagesType();
}
this.composeDelegate.runWithCompose(this.localCompose, cmd, this.env);
this.composeDelegate.runWithCompose(this.localCompose, cmd, this.env, this.filesInDirectory);
} finally {
this.project = this.composeDelegate.randomProjectId();
}
Expand Down Expand Up @@ -352,6 +355,11 @@ public ComposeContainer withStartupTimeout(Duration startupTimeout) {
return this;
}

public ComposeContainer withCopyFilesInContainer(String... fileCopyInclusions) {
this.filesInDirectory = Arrays.asList(fileCopyInclusions);
return this;
}

public Optional<ContainerState> getContainerByServiceName(String serviceName) {
return this.composeDelegate.getContainerByServiceName(serviceName);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ void createServices(
final Set<String> options,
final List<String> services,
final Map<String, Integer> scalingPreferences,
Map<String, String> env
Map<String, String> env,
List<String> fileCopyInclusions
) {
// services that have been explicitly requested to be started. If empty, all services should be started.
final String serviceNameArgs = Stream
Expand Down Expand Up @@ -160,7 +161,7 @@ void createServices(
}

// Run the docker compose container, which starts up the services
runWithCompose(localCompose, command, env);
runWithCompose(localCompose, command, env, fileCopyInclusions);
}

private String getUpCommand(String options) {
Expand Down Expand Up @@ -237,18 +238,24 @@ private String getServiceNameFromContainer(com.github.dockerjava.api.model.Conta
}

public void runWithCompose(boolean localCompose, String cmd) {
runWithCompose(localCompose, cmd, Collections.emptyMap());
runWithCompose(localCompose, cmd, Collections.emptyMap(), Collections.emptyList());
}

public void runWithCompose(boolean localCompose, String cmd, Map<String, String> env) {
public void runWithCompose(
boolean localCompose,
String cmd,
Map<String, String> env,
List<String> fileCopyInclusions
) {
Preconditions.checkNotNull(composeFiles);
Preconditions.checkArgument(!composeFiles.isEmpty(), "No docker compose file have been provided");

final DockerCompose dockerCompose;
if (localCompose) {
dockerCompose = new LocalDockerCompose(this.executable, composeFiles, project);
} else {
dockerCompose = new ContainerisedDockerCompose(this.defaultImageName, composeFiles, project);
dockerCompose =
new ContainerisedDockerCompose(this.defaultImageName, composeFiles, project, fileCopyInclusions);
}

dockerCompose.withCommand(cmd).withEnv(env).invoke();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ class ContainerisedDockerCompose extends GenericContainer<ContainerisedDockerCom

public static final char UNIX_PATH_SEPARATOR = ':';

public ContainerisedDockerCompose(DockerImageName dockerImageName, List<File> composeFiles, String identifier) {
public ContainerisedDockerCompose(
DockerImageName dockerImageName,
List<File> composeFiles,
String identifier,
List<String> fileCopyInclusions
) {
super(dockerImageName);
addEnv(ENV_PROJECT_NAME, identifier);

Expand All @@ -43,7 +48,22 @@ public ContainerisedDockerCompose(DockerImageName dockerImageName, List<File> co
final String composeFileEnvVariableValue = Joiner.on(UNIX_PATH_SEPARATOR).join(absoluteDockerComposeFiles); // we always need the UNIX path separator
logger().debug("Set env COMPOSE_FILE={}", composeFileEnvVariableValue);
addEnv(ENV_COMPOSE_FILE, composeFileEnvVariableValue);
withCopyFileToContainer(MountableFile.forHostPath(pwd), containerPwd);
if (fileCopyInclusions.isEmpty()) {
logger().info("Copying all files in {} into the container", pwd);
withCopyFileToContainer(MountableFile.forHostPath(pwd), containerPwd);
} else {
// Always copy the compose file itself
logger().info("Copying docker compose file: {}", dockerComposeBaseFile.getAbsolutePath());
withCopyFileToContainer(
MountableFile.forHostPath(dockerComposeBaseFile.getAbsolutePath()),
convertToUnixFilesystemPath(dockerComposeBaseFile.getAbsolutePath())
);
for (String pathToCopy : fileCopyInclusions) {
String hostPath = pwd + "/" + pathToCopy;
logger().info("Copying inclusion file: {}", hostPath);
withCopyFileToContainer(MountableFile.forHostPath(hostPath), convertToUnixFilesystemPath(hostPath));
}
}

// Ensure that compose can access docker. Since the container is assumed to be running on the same machine
// as the docker daemon, just mapping the docker control socket is OK.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ public class DockerComposeContainer<SELF extends DockerComposeContainer<SELF>>

private String project;

private List<String> filesInDirectory = new ArrayList<>();

@Deprecated
public DockerComposeContainer(File composeFile, String identifier) {
this(identifier, composeFile);
Expand Down Expand Up @@ -140,7 +142,8 @@ public void start() {
this.options,
this.services,
this.scalingPreferences,
this.env
this.env,
this.filesInDirectory
);
this.composeDelegate.startAmbassadorContainer();
this.composeDelegate.waitUntilServiceStarted(this.tailChildContainers);
Expand Down Expand Up @@ -172,7 +175,7 @@ public void stop() {
if (removeImages != null) {
cmd += " --rmi " + removeImages.dockerRemoveImagesType();
}
this.composeDelegate.runWithCompose(this.localCompose, cmd, this.env);
this.composeDelegate.runWithCompose(this.localCompose, cmd, this.env, this.filesInDirectory);
} finally {
this.project = this.composeDelegate.randomProjectId();
}
Expand Down Expand Up @@ -355,6 +358,11 @@ public SELF withStartupTimeout(Duration startupTimeout) {
return self();
}

public SELF withCopyFilesInContainer(String... fileCopyInclusions) {
this.filesInDirectory = Arrays.asList(fileCopyInclusions);
return self();
}

public Optional<ContainerState> getContainerByServiceName(String serviceName) {
return this.composeDelegate.getContainerByServiceName(serviceName);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package org.testcontainers.junit;

import io.restassured.RestAssured;
import org.junit.Test;
import org.testcontainers.containers.ComposeContainer;
import org.testcontainers.containers.ContainerLaunchException;

import java.io.File;
import java.io.IOException;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

public class ComposeContainerWithCopyFilesTest {

@Test
public void testShouldCopyAllFilesByDefault() throws IOException {
try (
ComposeContainer environment = new ComposeContainer(
new File("src/test/resources/compose-file-copy-inclusions/compose.yml")
)
.withExposedService("app", 8080)
) {
environment.start();

String response = readStringFromURL(environment);
assertThat(response).isEqualTo("MY_ENV_VARIABLE: override");
}
}

@Test
public void testWithFileCopyInclusionUsingFilePath() throws IOException {
try (
ComposeContainer environment = new ComposeContainer(
new File("src/test/resources/compose-file-copy-inclusions/compose-root-only.yml")
)
.withExposedService("app", 8080)
.withCopyFilesInContainer("Dockerfile", "EnvVariableRestEndpoint.java", ".env")
) {
environment.start();

String response = readStringFromURL(environment);

// The `test/.env` file is not copied, now so we get the original value
assertThat(response).isEqualTo("MY_ENV_VARIABLE: original");
}
}

@Test
public void testWithFileCopyInclusionUsingDirectoryPath() throws IOException {
try (
ComposeContainer environment = new ComposeContainer(
new File("src/test/resources/compose-file-copy-inclusions/compose-test-only.yml")
)
.withExposedService("app", 8080)
.withCopyFilesInContainer("Dockerfile", "EnvVariableRestEndpoint.java", "test")
) {
environment.start();

String response = readStringFromURL(environment);
// The test directory (with its contents) is copied, so we get the override
assertThat(response).isEqualTo("MY_ENV_VARIABLE: override");
}
}

@Test
public void testShouldNotBeAbleToStartIfNeededEnvFileIsNotCopied() {
try (
ComposeContainer environment = new ComposeContainer(
new File("src/test/resources/compose-file-copy-inclusions/compose-test-only.yml")
)
.withExposedService("app", 8080)
.withCopyFilesInContainer("Dockerfile", "EnvVariableRestEndpoint.java")
) {
assertThatExceptionOfType(ContainerLaunchException.class)
.isThrownBy(environment::start)
.withMessageContaining("Container startup failed for image docker");
}
}

private static String readStringFromURL(ComposeContainer container) throws IOException {
Integer servicePort = container.getServicePort("app-1", 8080);
String serviceHost = container.getServiceHost("app-1", 8080);
String requestURL = "http://" + serviceHost + ":" + servicePort + "/env";
return RestAssured.get(requestURL).thenReturn().body().asString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.testcontainers.junit;

import io.restassured.RestAssured;
import org.junit.Test;
import org.testcontainers.containers.DockerComposeContainer;

import java.io.File;
import java.io.IOException;

import static org.assertj.core.api.Assertions.assertThat;

public class DockerComposeContainerWithCopyFilesTest {

@Test
public void testShouldCopyAllFilesByDefault() throws IOException {
try (
DockerComposeContainer environment = new DockerComposeContainer(
new File("src/test/resources/compose-file-copy-inclusions/compose.yml")
)
.withExposedService("app", 8080)
) {
environment.start();

String response = readStringFromURL(environment);
assertThat(response).isEqualTo("MY_ENV_VARIABLE: override");
}
}

@Test
public void testWithFileCopyInclusionUsingFilePath() throws IOException {
try (
DockerComposeContainer environment = new DockerComposeContainer(
new File("src/test/resources/compose-file-copy-inclusions/compose-root-only.yml")
)
.withExposedService("app", 8080)
.withCopyFilesInContainer("Dockerfile", "EnvVariableRestEndpoint.java", ".env")
) {
environment.start();

String response = readStringFromURL(environment);

// The `test/.env` file is not copied, now so we get the original value
assertThat(response).isEqualTo("MY_ENV_VARIABLE: original");
}
}

@Test
public void testWithFileCopyInclusionUsingDirectoryPath() throws IOException {
try (
DockerComposeContainer environment = new DockerComposeContainer(
new File("src/test/resources/compose-file-copy-inclusions/compose-test-only.yml")
)
.withExposedService("app", 8080)
.withCopyFilesInContainer("Dockerfile", "EnvVariableRestEndpoint.java", "test")
) {
environment.start();

String response = readStringFromURL(environment);
// The test directory (with its contents) is copied, so we get the override
assertThat(response).isEqualTo("MY_ENV_VARIABLE: override");
}
}

private static String readStringFromURL(DockerComposeContainer container) throws IOException {
Integer servicePort = container.getServicePort("app_1", 8080);
String serviceHost = container.getServiceHost("app_1", 8080);
String requestURL = "http://" + serviceHost + ":" + servicePort + "/env";
return RestAssured.get(requestURL).thenReturn().body().asString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
MY_ENV_VARIABLE=original
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM jbangdev/jbang-action

WORKDIR /app
COPY EnvVariableRestEndpoint.java .

RUN jbang export portable --force EnvVariableRestEndpoint.java

EXPOSE 8080
CMD ["./EnvVariableRestEndpoint.java"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
///usr/bin/env jbang "$0" "$@" ; exit $?

import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpExchange;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;

public class EnvVariableRestEndpoint {
private static final String ENV_VARIABLE_NAME = "MY_ENV_VARIABLE";
private static final int PORT = 8080;

public static void main(String[] args) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(PORT), 0);
server.createContext("/env", new EnvVariableHandler());
server.setExecutor(null);
server.start();
System.out.println("Server started on port " + PORT);
}

static class EnvVariableHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
if ("GET".equals(exchange.getRequestMethod())) {
String envValue = System.getenv(ENV_VARIABLE_NAME);
String response = envValue != null
? ENV_VARIABLE_NAME + ": " + envValue
: "Environment variable " + ENV_VARIABLE_NAME + " not found";

exchange.sendResponseHeaders(200, response.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
} else {
String response = "Method not allowed";
exchange.sendResponseHeaders(405, response.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
services:
app:
build: .
ports:
- "8080:8080"
env_file:
- '.env'
Loading