Skip to content

Commit 4e73beb

Browse files
committed
3317 Warn when Docker image was created from scratch and does not contain /bin/sh
1 parent 1a61543 commit 4e73beb

File tree

7 files changed

+159
-0
lines changed

7 files changed

+159
-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.StringUtils;
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 (StringUtils.contains(result.getStdout(), "exec: \"/bin/sh\": stat /bin/sh: no such file or directory: unknown")) {
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.Map;
@@ -248,4 +250,14 @@ public ImageFromDockerfile withDockerfile(Path dockerfile) {
248250
this.dockerfile = Optional.of(dockerfile);
249251
return this;
250252
}
253+
254+
/**
255+
* Sets the Dockerfile to be used for this image based on file from classpath.
256+
*
257+
* @param classpathResource
258+
*/
259+
public ImageFromDockerfile withDockerfileFromClasspath(String classpathResource) {
260+
withDockerfile(Paths.get(MountableFile.forClasspathResource(classpathResource).getResolvedPath()));
261+
return this;
262+
}
251263
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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.Test;
11+
import org.junit.runner.RunWith;
12+
import org.junit.runners.Parameterized;
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+
26+
import static org.assertj.core.api.Assertions.assertThat;
27+
28+
@RunWith(Parameterized.class)
29+
public class InternalCommandPortListeningCheckWithScratchTest {
30+
@Parameterized.Parameters(name = "dockerfile.{0} -> {1})")
31+
public static Object[][] params() {
32+
return new Object[][]{
33+
new Object[]{"scratch", false},
34+
new Object[]{"alpine", true},
35+
};
36+
}
37+
38+
@Parameterized.Parameter(0)
39+
public String dockerfileKind;
40+
41+
@Parameterized.Parameter(1)
42+
public boolean expectedEmpty;
43+
44+
@Test
45+
public void testDockerfileFromScratch() throws Throwable {
46+
List<ILoggingEvent> logEvents = runForLogEvents(() -> {
47+
ImageFromDockerfile image = new ImageFromDockerfile("tc-scratch-wait-hostport-strategy")
48+
.withDockerfileFromClasspath("/scratch-wait-strategy-dockerfile/Dockerfile." + dockerfileKind)
49+
.withFileFromClasspath("go.mod", "/scratch-wait-strategy-dockerfile/go.mod")
50+
.withFileFromClasspath("hello-world.go", "/scratch-wait-strategy-dockerfile/hello-world.go");
51+
try (GenericContainer<?> container = new GenericContainer<>(image)
52+
.withCommand("/hello-world")
53+
.withExposedPorts(8080)
54+
) {
55+
container.start();
56+
57+
// check if ports are correctly published
58+
String response = responseFromUrl(new URL("http://" + container.getHost() + ":" + container.getFirstMappedPort() + "/helloworld"));
59+
assertThat(response).isEqualTo("Hello, World!");
60+
}
61+
});
62+
63+
ListAssert<ILoggingEvent> asserting = assertThat(logEvents.stream()
64+
.filter(it -> it.getLevel() == Level.WARN)
65+
.filter(it -> "Unable to find '/bin/sh'. Does your Dockerfile extends scratch base Dockerfile?".equals(it.getMessage()))
66+
.collect(Collectors.toList())
67+
);
68+
if (expectedEmpty) {
69+
asserting.isEmpty();
70+
} else {
71+
asserting.isNotEmpty();
72+
}
73+
}
74+
75+
private static List<ILoggingEvent> runForLogEvents(FailableRunnable<?> action) throws Throwable {
76+
Logger logger = (Logger) LoggerFactory.getLogger(InternalCommandPortListeningCheck.class);
77+
TestLogAppender testLogAppender = new TestLogAppender();
78+
logger.addAppender(testLogAppender);
79+
testLogAppender.start();
80+
try {
81+
action.run();
82+
return testLogAppender.events;
83+
} finally {
84+
testLogAppender.stop();
85+
}
86+
}
87+
88+
private static String responseFromUrl(URL baseUrl) throws IOException {
89+
URLConnection urlConnection = baseUrl.openConnection();
90+
@Cleanup
91+
BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
92+
return reader.readLine();
93+
}
94+
95+
private static class TestLogAppender extends AppenderBase<ILoggingEvent> {
96+
private final List<ILoggingEvent> events;
97+
98+
private TestLogAppender() {
99+
this.events = new ArrayList<>();
100+
}
101+
102+
@Override
103+
protected void append(ILoggingEvent eventObject) {
104+
events.add(eventObject);
105+
}
106+
}
107+
}
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)