Skip to content

Commit d5cc465

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

File tree

7 files changed

+158
-0
lines changed

7 files changed

+158
-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: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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.Collectors;
25+
import java.util.stream.Stream;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
29+
public class InternalCommandPortListeningCheckWithScratchTest {
30+
31+
public static Stream<Arguments> testDockerfileFromScratchProvider() {
32+
return Stream.of(Arguments.of("scratch", false), Arguments.of("alpine", true));
33+
}
34+
35+
@ParameterizedTest(name = "Dockerfile.{0} -> {1}")
36+
@MethodSource("testDockerfileFromScratchProvider")
37+
public void testDockerfileFromScratch(String dockerfileKind, boolean expectedEmpty) throws Throwable {
38+
List<ILoggingEvent> logEvents = runForLogEvents(() -> {
39+
ImageFromDockerfile image = new ImageFromDockerfile("tc-scratch-wait-hostport-strategy")
40+
// based on https://github.com/jeremyhuiskamp/golang-docker-scratch
41+
.withDockerfileFromClasspath("/scratch-wait-strategy-dockerfile/Dockerfile." + dockerfileKind)
42+
.withFileFromClasspath("go.mod", "/scratch-wait-strategy-dockerfile/go.mod")
43+
.withFileFromClasspath("hello-world.go", "/scratch-wait-strategy-dockerfile/hello-world.go");
44+
try (
45+
GenericContainer<?> container = new GenericContainer<>(image)
46+
.withCommand("/hello-world")
47+
.withExposedPorts(8080)
48+
) {
49+
container.start();
50+
51+
// check if ports are correctly published
52+
String response = responseFromUrl(
53+
new URL("http://" + container.getHost() + ":" + container.getFirstMappedPort() + "/helloworld")
54+
);
55+
assertThat(response).isEqualTo("Hello, World!");
56+
}
57+
});
58+
59+
ListAssert<ILoggingEvent> asserting = assertThat(
60+
logEvents
61+
.stream()
62+
.filter(it -> it.getLevel() == Level.WARN)
63+
.filter(it -> it.getFormattedMessage().contains("/bin/sh: no such file or directory"))
64+
.toList()
65+
);
66+
if (expectedEmpty) {
67+
asserting.isEmpty();
68+
} else {
69+
asserting.isNotEmpty();
70+
}
71+
}
72+
73+
private static List<ILoggingEvent> runForLogEvents(FailableRunnable<?> action) throws Throwable {
74+
Logger logger = (Logger) LoggerFactory.getLogger(InternalCommandPortListeningCheck.class);
75+
TestLogAppender testLogAppender = new TestLogAppender();
76+
logger.addAppender(testLogAppender);
77+
testLogAppender.start();
78+
try {
79+
action.run();
80+
return testLogAppender.events;
81+
} finally {
82+
testLogAppender.stop();
83+
}
84+
}
85+
86+
private static String responseFromUrl(URL baseUrl) throws IOException {
87+
URLConnection urlConnection = baseUrl.openConnection();
88+
@Cleanup
89+
BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
90+
return reader.readLine();
91+
}
92+
93+
private static class TestLogAppender extends AppenderBase<ILoggingEvent> {
94+
95+
private final List<ILoggingEvent> events;
96+
97+
private TestLogAppender() {
98+
this.events = new ArrayList<>();
99+
}
100+
101+
@Override
102+
protected void append(ILoggingEvent eventObject) {
103+
events.add(eventObject);
104+
}
105+
}
106+
}
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)