Skip to content

Commit 818001e

Browse files
committed
3317 Warn when Docker image was created from scratch and does not contain /bin/sh
1 parent 43c6a97 commit 818001e

File tree

7 files changed

+157
-0
lines changed

7 files changed

+157
-0
lines changed

core/src/main/java/org/testcontainers/containers/wait/internal/InternalCommandPortListeningCheck.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import lombok.RequiredArgsConstructor;
44
import lombok.extern.slf4j.Slf4j;
5+
import org.apache.commons.lang3.Strings;
56
import org.testcontainers.containers.Container.ExecResult;
67
import org.testcontainers.containers.ExecInContainerPattern;
78
import org.testcontainers.containers.wait.strategy.WaitStrategyTarget;
@@ -56,6 +57,9 @@ public Boolean call() {
5657
int exitCode = result.getExitCode();
5758
if (exitCode != 0 && exitCode != 1) {
5859
log.warn("An exception while executing the internal check: {}", result);
60+
if (Strings.CS.contains(result.getStdout(), "/bin/sh: no such file or directory")) {
61+
log.warn("Unable to find '/bin/sh'. Does your Dockerfile extends scratch base Dockerfile?");
62+
}
5963
}
6064
return exitCode == 0;
6165
} catch (Exception e) {

core/src/main/java/org/testcontainers/images/builder/ImageFromDockerfile.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@
2424
import org.testcontainers.utility.DockerLoggerFactory;
2525
import org.testcontainers.utility.ImageNameSubstitutor;
2626
import org.testcontainers.utility.LazyFuture;
27+
import org.testcontainers.utility.MountableFile;
2728
import org.testcontainers.utility.ResourceReaper;
2829

2930
import java.io.IOException;
3031
import java.io.PipedInputStream;
3132
import java.io.PipedOutputStream;
3233
import java.nio.file.Path;
34+
import java.nio.file.Paths;
3335
import java.util.Collections;
3436
import java.util.HashMap;
3537
import java.util.LinkedHashSet;
@@ -280,4 +282,14 @@ public ImageFromDockerfile withBuildImageCmdModifier(Consumer<BuildImageCmd> mod
280282
this.buildImageCmdModifiers.add(modifier);
281283
return this;
282284
}
285+
286+
/**
287+
* Sets the Dockerfile to be used for this image based on file from classpath.
288+
*
289+
* @param classpathResource
290+
*/
291+
public ImageFromDockerfile withDockerfileFromClasspath(String classpathResource) {
292+
withDockerfile(Paths.get(MountableFile.forClasspathResource(classpathResource).getResolvedPath()));
293+
return this;
294+
}
283295
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package org.testcontainers.containers.wait.internal;
2+
3+
import ch.qos.logback.classic.Level;
4+
import ch.qos.logback.classic.Logger;
5+
import ch.qos.logback.classic.spi.ILoggingEvent;
6+
import ch.qos.logback.core.AppenderBase;
7+
import lombok.Cleanup;
8+
import org.apache.commons.lang3.function.FailableRunnable;
9+
import org.assertj.core.api.ListAssert;
10+
import org.junit.jupiter.params.ParameterizedTest;
11+
import org.junit.jupiter.params.provider.Arguments;
12+
import org.junit.jupiter.params.provider.MethodSource;
13+
import org.slf4j.LoggerFactory;
14+
import org.testcontainers.containers.GenericContainer;
15+
import org.testcontainers.images.builder.ImageFromDockerfile;
16+
17+
import java.io.BufferedReader;
18+
import java.io.IOException;
19+
import java.io.InputStreamReader;
20+
import java.net.URL;
21+
import java.net.URLConnection;
22+
import java.util.ArrayList;
23+
import java.util.List;
24+
import java.util.stream.Stream;
25+
26+
import static org.assertj.core.api.Assertions.assertThat;
27+
28+
public class InternalCommandPortListeningCheckWithScratchTest {
29+
30+
public static Stream<Arguments> testDockerfileFromScratchProvider() {
31+
return Stream.of(Arguments.of("scratch", false), Arguments.of("alpine", true));
32+
}
33+
34+
@ParameterizedTest(name = "Dockerfile.{0} -> {1}")
35+
@MethodSource("testDockerfileFromScratchProvider")
36+
public void testDockerfileFromScratch(String dockerfileKind, boolean expectedEmpty) throws Throwable {
37+
List<ILoggingEvent> logEvents = runForLogEvents(() -> {
38+
ImageFromDockerfile image = new ImageFromDockerfile("tc-scratch-wait-hostport-strategy")
39+
// based on https://github.com/jeremyhuiskamp/golang-docker-scratch
40+
.withDockerfileFromClasspath("/scratch-wait-strategy-dockerfile/Dockerfile." + dockerfileKind)
41+
.withFileFromClasspath("go.mod", "/scratch-wait-strategy-dockerfile/go.mod")
42+
.withFileFromClasspath("hello-world.go", "/scratch-wait-strategy-dockerfile/hello-world.go");
43+
try (
44+
GenericContainer<?> container = new GenericContainer<>(image)
45+
.withCommand("/hello-world")
46+
.withExposedPorts(8080)
47+
) {
48+
container.start();
49+
50+
// check if ports are correctly published
51+
String response = responseFromUrl(
52+
new URL("http://" + container.getHost() + ":" + container.getFirstMappedPort() + "/helloworld")
53+
);
54+
assertThat(response).isEqualTo("Hello, World!");
55+
}
56+
});
57+
58+
ListAssert<ILoggingEvent> asserting = assertThat(
59+
logEvents
60+
.stream()
61+
.filter(it -> it.getLevel() == Level.WARN)
62+
.filter(it -> it.getFormattedMessage().contains("/bin/sh: no such file or directory"))
63+
.toList()
64+
);
65+
if (expectedEmpty) {
66+
asserting.isEmpty();
67+
} else {
68+
asserting.isNotEmpty();
69+
}
70+
}
71+
72+
private static List<ILoggingEvent> runForLogEvents(FailableRunnable<?> action) throws Throwable {
73+
Logger logger = (Logger) LoggerFactory.getLogger(InternalCommandPortListeningCheck.class);
74+
TestLogAppender testLogAppender = new TestLogAppender();
75+
logger.addAppender(testLogAppender);
76+
testLogAppender.start();
77+
try {
78+
action.run();
79+
return testLogAppender.events;
80+
} finally {
81+
testLogAppender.stop();
82+
}
83+
}
84+
85+
private static String responseFromUrl(URL baseUrl) throws IOException {
86+
URLConnection urlConnection = baseUrl.openConnection();
87+
@Cleanup
88+
BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
89+
return reader.readLine();
90+
}
91+
92+
private static class TestLogAppender extends AppenderBase<ILoggingEvent> {
93+
94+
private final List<ILoggingEvent> events;
95+
96+
private TestLogAppender() {
97+
this.events = new ArrayList<>();
98+
}
99+
100+
@Override
101+
protected void append(ILoggingEvent eventObject) {
102+
events.add(eventObject);
103+
}
104+
}
105+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
FROM golang:alpine as app-builder
2+
WORKDIR /go/src/app
3+
COPY hello-world.go .
4+
COPY go.mod .
5+
RUN CGO_ENABLED=0 go install -ldflags '-extldflags "-static"'
6+
7+
FROM alpine
8+
COPY --from=app-builder /go/bin/hello-world /hello-world
9+
ENTRYPOINT ["/hello-world"]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
FROM golang:alpine as app-builder
2+
WORKDIR /go/src/app
3+
COPY hello-world.go .
4+
COPY go.mod .
5+
RUN CGO_ENABLED=0 go install -ldflags '-extldflags "-static"'
6+
7+
FROM scratch
8+
COPY --from=app-builder /go/bin/hello-world /hello-world
9+
ENTRYPOINT ["/hello-world"]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/testcontainers/hello-world
2+
3+
go 1.16
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package main;
2+
import (
3+
"fmt"
4+
"log"
5+
"net/http"
6+
)
7+
func main() {
8+
http.HandleFunc("/helloworld", func(w http.ResponseWriter, r *http.Request){
9+
fmt.Fprintf(w, "Hello, World!")
10+
})
11+
fmt.Printf("Server running (port=8080), route: http://localhost:8080/helloworld\n")
12+
if err := http.ListenAndServe(":8080", nil); err != nil {
13+
log.Fatal(err)
14+
}
15+
}

0 commit comments

Comments
 (0)