diff --git a/core/src/main/java/org/testcontainers/containers/wait/internal/InternalCommandPortListeningCheck.java b/core/src/main/java/org/testcontainers/containers/wait/internal/InternalCommandPortListeningCheck.java index aeedbea1e3f..67af4a1c06c 100644 --- a/core/src/main/java/org/testcontainers/containers/wait/internal/InternalCommandPortListeningCheck.java +++ b/core/src/main/java/org/testcontainers/containers/wait/internal/InternalCommandPortListeningCheck.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.Strings; import org.testcontainers.containers.Container.ExecResult; import org.testcontainers.containers.ExecInContainerPattern; import org.testcontainers.containers.wait.strategy.WaitStrategyTarget; @@ -56,6 +57,9 @@ public Boolean call() { int exitCode = result.getExitCode(); if (exitCode != 0 && exitCode != 1) { log.warn("An exception while executing the internal check: {}", result); + if (Strings.CS.contains(result.getStdout(), "/bin/sh: no such file or directory")) { + log.warn("Unable to find '/bin/sh'. Does your Dockerfile extends scratch base Dockerfile?"); + } } return exitCode == 0; } catch (Exception e) { diff --git a/core/src/main/java/org/testcontainers/images/builder/ImageFromDockerfile.java b/core/src/main/java/org/testcontainers/images/builder/ImageFromDockerfile.java index ced612bfefa..8a09e8879fa 100644 --- a/core/src/main/java/org/testcontainers/images/builder/ImageFromDockerfile.java +++ b/core/src/main/java/org/testcontainers/images/builder/ImageFromDockerfile.java @@ -24,12 +24,14 @@ import org.testcontainers.utility.DockerLoggerFactory; import org.testcontainers.utility.ImageNameSubstitutor; import org.testcontainers.utility.LazyFuture; +import org.testcontainers.utility.MountableFile; import org.testcontainers.utility.ResourceReaper; import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; @@ -280,4 +282,14 @@ public ImageFromDockerfile withBuildImageCmdModifier(Consumer mod this.buildImageCmdModifiers.add(modifier); return this; } + + /** + * Sets the Dockerfile to be used for this image based on file from classpath. + * + * @param classpathResource + */ + public ImageFromDockerfile withDockerfileFromClasspath(String classpathResource) { + withDockerfile(Paths.get(MountableFile.forClasspathResource(classpathResource).getResolvedPath())); + return this; + } } diff --git a/core/src/test/java/org/testcontainers/containers/wait/internal/InternalCommandPortListeningCheckWithScratchTest.java b/core/src/test/java/org/testcontainers/containers/wait/internal/InternalCommandPortListeningCheckWithScratchTest.java new file mode 100644 index 00000000000..c26e5d9c31d --- /dev/null +++ b/core/src/test/java/org/testcontainers/containers/wait/internal/InternalCommandPortListeningCheckWithScratchTest.java @@ -0,0 +1,105 @@ +package org.testcontainers.containers.wait.internal; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import lombok.Cleanup; +import org.apache.commons.lang3.function.FailableRunnable; +import org.assertj.core.api.ListAssert; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.images.builder.ImageFromDockerfile; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +public class InternalCommandPortListeningCheckWithScratchTest { + + public static Stream testDockerfileFromScratchProvider() { + return Stream.of(Arguments.of("scratch", false), Arguments.of("alpine", true)); + } + + @ParameterizedTest(name = "Dockerfile.{0} -> {1}") + @MethodSource("testDockerfileFromScratchProvider") + public void testDockerfileFromScratch(String dockerfileKind, boolean expectedEmpty) throws Throwable { + List logEvents = runForLogEvents(() -> { + ImageFromDockerfile image = new ImageFromDockerfile("tc-scratch-wait-hostport-strategy") + // based on https://github.com/jeremyhuiskamp/golang-docker-scratch + .withDockerfileFromClasspath("/scratch-wait-strategy-dockerfile/Dockerfile." + dockerfileKind) + .withFileFromClasspath("go.mod", "/scratch-wait-strategy-dockerfile/go.mod") + .withFileFromClasspath("hello-world.go", "/scratch-wait-strategy-dockerfile/hello-world.go"); + try ( + GenericContainer container = new GenericContainer<>(image) + .withCommand("/hello-world") + .withExposedPorts(8080) + ) { + container.start(); + + // check if ports are correctly published + String response = responseFromUrl( + new URL("http://" + container.getHost() + ":" + container.getFirstMappedPort() + "/helloworld") + ); + assertThat(response).isEqualTo("Hello, World!"); + } + }); + + ListAssert asserting = assertThat( + logEvents + .stream() + .filter(it -> it.getLevel() == Level.WARN) + .filter(it -> it.getFormattedMessage().contains("/bin/sh: no such file or directory")) + .toList() + ); + if (expectedEmpty) { + asserting.isEmpty(); + } else { + asserting.isNotEmpty(); + } + } + + private static List runForLogEvents(FailableRunnable action) throws Throwable { + Logger logger = (Logger) LoggerFactory.getLogger(InternalCommandPortListeningCheck.class); + TestLogAppender testLogAppender = new TestLogAppender(); + logger.addAppender(testLogAppender); + testLogAppender.start(); + try { + action.run(); + return testLogAppender.events; + } finally { + testLogAppender.stop(); + } + } + + private static String responseFromUrl(URL baseUrl) throws IOException { + URLConnection urlConnection = baseUrl.openConnection(); + @Cleanup + BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); + return reader.readLine(); + } + + private static class TestLogAppender extends AppenderBase { + + private final List events; + + private TestLogAppender() { + this.events = new ArrayList<>(); + } + + @Override + protected void append(ILoggingEvent eventObject) { + events.add(eventObject); + } + } +} diff --git a/core/src/test/resources/scratch-wait-strategy-dockerfile/Dockerfile.alpine b/core/src/test/resources/scratch-wait-strategy-dockerfile/Dockerfile.alpine new file mode 100644 index 00000000000..366ac3262b1 --- /dev/null +++ b/core/src/test/resources/scratch-wait-strategy-dockerfile/Dockerfile.alpine @@ -0,0 +1,9 @@ +FROM golang:alpine as app-builder +WORKDIR /go/src/app +COPY hello-world.go . +COPY go.mod . +RUN CGO_ENABLED=0 go install -ldflags '-extldflags "-static"' + +FROM alpine +COPY --from=app-builder /go/bin/hello-world /hello-world +ENTRYPOINT ["/hello-world"] diff --git a/core/src/test/resources/scratch-wait-strategy-dockerfile/Dockerfile.scratch b/core/src/test/resources/scratch-wait-strategy-dockerfile/Dockerfile.scratch new file mode 100644 index 00000000000..781f0007eae --- /dev/null +++ b/core/src/test/resources/scratch-wait-strategy-dockerfile/Dockerfile.scratch @@ -0,0 +1,9 @@ +FROM golang:alpine as app-builder +WORKDIR /go/src/app +COPY hello-world.go . +COPY go.mod . +RUN CGO_ENABLED=0 go install -ldflags '-extldflags "-static"' + +FROM scratch +COPY --from=app-builder /go/bin/hello-world /hello-world +ENTRYPOINT ["/hello-world"] diff --git a/core/src/test/resources/scratch-wait-strategy-dockerfile/go.mod b/core/src/test/resources/scratch-wait-strategy-dockerfile/go.mod new file mode 100644 index 00000000000..63c29bac7ef --- /dev/null +++ b/core/src/test/resources/scratch-wait-strategy-dockerfile/go.mod @@ -0,0 +1,3 @@ +module github.com/testcontainers/hello-world + +go 1.16 diff --git a/core/src/test/resources/scratch-wait-strategy-dockerfile/hello-world.go b/core/src/test/resources/scratch-wait-strategy-dockerfile/hello-world.go new file mode 100644 index 00000000000..d6e3c4b2f29 --- /dev/null +++ b/core/src/test/resources/scratch-wait-strategy-dockerfile/hello-world.go @@ -0,0 +1,15 @@ +package main; +import ( + "fmt" + "log" + "net/http" +) +func main() { + http.HandleFunc("/helloworld", func(w http.ResponseWriter, r *http.Request){ + fmt.Fprintf(w, "Hello, World!") + }) + fmt.Printf("Server running (port=8080), route: http://localhost:8080/helloworld\n") + if err := http.ListenAndServe(":8080", nil); err != nil { + log.Fatal(err) + } +}