Skip to content

Commit 574c47b

Browse files
authored
Improve PortCheck logging for distroless images (#954)
1 parent ff5af9a commit 574c47b

File tree

4 files changed

+86
-22
lines changed

4 files changed

+86
-22
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM node AS build
2+
WORKDIR /app
3+
RUN npm init -y \
4+
&& npm install [email protected]
5+
COPY index.js .
6+
7+
FROM gcr.io/distroless/nodejs20-debian12 AS app
8+
EXPOSE 8080
9+
COPY --from=build /app /app
10+
WORKDIR /app
11+
CMD ["index.js"]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const http = require("http");
2+
const express = require("express");
3+
4+
const app = express();
5+
6+
app.get("/hello-world", (req, res) => {
7+
res.status(200).send("hello-world");
8+
});
9+
10+
const PORT = 8080;
11+
12+
http
13+
.createServer(app)
14+
.listen(PORT, () => console.log(`Listening on port ${PORT}`));

packages/testcontainers/src/wait-strategies/utils/port-check.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,5 +140,43 @@ describe("PortCheck", () => {
140140
],
141141
]);
142142
});
143+
144+
it("should error log the distroless image once, regardless of logs enabled or not", async () => {
145+
// Make sure logging is disabled explicitly here
146+
mockLogger.enabled.mockImplementation(() => false);
147+
148+
mockContainerExec
149+
.mockReturnValueOnce(Promise.resolve({ output: "ERROR 1", exitCode: 126 }))
150+
.mockReturnValueOnce(Promise.resolve({ output: "ERROR 2", exitCode: 126 }))
151+
.mockReturnValueOnce(Promise.resolve({ output: "ERROR 2", exitCode: 126 }))
152+
.mockReturnValueOnce(Promise.resolve({ output: "ERROR 1", exitCode: 126 }))
153+
.mockReturnValueOnce(Promise.resolve({ output: "ERROR 2", exitCode: 126 }))
154+
.mockReturnValueOnce(Promise.resolve({ output: "ERROR 2", exitCode: 126 }));
155+
156+
await portCheck.isBound(8080);
157+
await portCheck.isBound(8080);
158+
159+
expect(mockLogger.error.mock.calls).toEqual([
160+
[
161+
"The HostPortWaitStrategy will not work on a distroless image, use an alternate wait strategy",
162+
{ containerId: "containerId" },
163+
],
164+
]);
165+
});
166+
167+
it.for([126, 127])("should error log the distroless image when exit code is %i", async (code) => {
168+
mockContainerExec.mockReturnValueOnce(Promise.resolve({ output: "ERROR 1", exitCode: code }));
169+
mockContainerExec.mockReturnValueOnce(Promise.resolve({ output: "ERROR 2", exitCode: code }));
170+
mockContainerExec.mockReturnValueOnce(Promise.resolve({ output: "ERROR 3", exitCode: code }));
171+
172+
await portCheck.isBound(8080);
173+
174+
expect(mockLogger.error.mock.calls).toEqual([
175+
[
176+
"The HostPortWaitStrategy will not work on a distroless image, use an alternate wait strategy",
177+
{ containerId: "containerId" },
178+
],
179+
]);
180+
});
143181
});
144182
});

packages/testcontainers/src/wait-strategies/utils/port-check.ts

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export class HostPortCheck implements PortCheck {
3333

3434
export class InternalPortCheck implements PortCheck {
3535
private isDistroless = false;
36-
private commandOutputs = new Set<string>();
36+
private readonly commandOutputs = new Set<string>();
3737

3838
constructor(
3939
private readonly client: ContainerRuntimeClient,
@@ -53,28 +53,29 @@ export class InternalPortCheck implements PortCheck {
5353
);
5454
const isBound = commandResults.some((result) => result.exitCode === 0);
5555

56+
// https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html
57+
// If a command is not found, the child process created to execute it returns a status of 127.
58+
// If a command is found but is not executable, the return status is 126.
59+
const shellExists = commandResults.some((result) => result.exitCode !== 126 && result.exitCode !== 127);
60+
if (!isBound && !shellExists && !this.isDistroless) {
61+
this.isDistroless = true;
62+
log.error(`The HostPortWaitStrategy will not work on a distroless image, use an alternate wait strategy`, {
63+
containerId: this.container.id,
64+
});
65+
}
66+
5667
if (!isBound && log.enabled()) {
57-
const shellExists = commandResults.some((result) => result.exitCode !== 126);
58-
if (!shellExists) {
59-
if (!this.isDistroless) {
60-
this.isDistroless = true;
61-
log.error(`The HostPortWaitStrategy will not work on a distroless image, use an alternate wait strategy`, {
62-
containerId: this.container.id,
63-
});
64-
}
65-
} else {
66-
commandResults
67-
.map((result) => ({ ...result, output: result.output.trim() }))
68-
.filter((result) => result.exitCode !== 126 && result.output.length > 0)
69-
.forEach((result) => {
70-
if (!this.commandOutputs.has(this.commandOutputsKey(result.output))) {
71-
log.trace(`Port check result exit code ${result.exitCode}: ${result.output}`, {
72-
containerId: this.container.id,
73-
});
74-
this.commandOutputs.add(this.commandOutputsKey(result.output));
75-
}
76-
});
77-
}
68+
commandResults
69+
.map((result) => ({ ...result, output: result.output.trim() }))
70+
.filter((result) => result.exitCode !== 126 && result.output.length > 0)
71+
.forEach((result) => {
72+
if (!this.commandOutputs.has(this.commandOutputsKey(result.output))) {
73+
log.trace(`Port check result exit code ${result.exitCode}: ${result.output}`, {
74+
containerId: this.container.id,
75+
});
76+
this.commandOutputs.add(this.commandOutputsKey(result.output));
77+
}
78+
});
7879
}
7980

8081
return isBound;

0 commit comments

Comments
 (0)