Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Dockerode circular dependency when finding host through default gateway #394

Merged
merged 12 commits into from Sep 10, 2022
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
@@ -1,7 +1,7 @@
{
"name": "testcontainers",
"author": "Cristian Greco",
"version": "8.13.1",
"version": "8.13.1-beta.5",
"main": "dist/index",
"types": "dist/index",
"keywords": [
Expand Down
9 changes: 6 additions & 3 deletions src/docker/docker-client.ts
Expand Up @@ -28,7 +28,7 @@ const getDockerClient = async (): Promise<DockerClient> => {
if (await isDockerDaemonReachable(dockerode)) {
const host = await resolveHost(dockerode, uri);
log.info(`Using Docker client strategy: ${strategy.getName()}, Docker host: ${host}`);
logSystemDiagnostics();
logSystemDiagnostics(dockerode);
return { host, dockerode };
} else {
log.warn(`Docker client strategy ${strategy.getName()} is not reachable`);
Expand Down Expand Up @@ -158,12 +158,15 @@ const resolveHost = async (dockerode: Dockerode, uri: string): Promise<string> =
};

const findGateway = async (dockerode: Dockerode): Promise<string | undefined> => {
log.debug(`Checking gateway for Docker host`);
const inspectResult: NetworkInspectInfo = await dockerode.getNetwork("bridge").inspect();
return inspectResult?.IPAM?.Config?.find((config) => config.Gateway !== undefined)?.Gateway;
};

const findDefaultGateway = async (dockerode: Dockerode): Promise<string | undefined> =>
runInContainer(dockerode, "alpine:3.14", ["sh", "-c", "ip route|awk '/default/ { print $3 }'"]);
const findDefaultGateway = async (dockerode: Dockerode): Promise<string | undefined> => {
log.debug(`Checking default gateway for Docker host`);
return runInContainer(dockerode, "alpine:3.14", ["sh", "-c", "ip route|awk '/default/ { print $3 }'"]);
};

const isInContainer = () => existsSync("/.dockerenv");

Expand Down
4 changes: 2 additions & 2 deletions src/docker/functions/container/attach-container.ts
Expand Up @@ -3,10 +3,10 @@ import Dockerode from "dockerode";
import { demuxStream } from "../demux-stream";
import { log } from "../../../logger";

export const attachContainer = async (container: Dockerode.Container): Promise<Readable> => {
export const attachContainer = async (dockerode: Dockerode, container: Dockerode.Container): Promise<Readable> => {
try {
const stream = (await container.attach({ stream: true, stdout: true, stderr: true })) as NodeJS.ReadableStream;
return demuxStream(stream as Readable);
return demuxStream(dockerode, stream as Readable);
} catch (err) {
log.error(`Failed to attach to container ${container.id}: ${err}`);
throw err;
Expand Down
3 changes: 2 additions & 1 deletion src/docker/functions/container/container-logs.ts
Expand Up @@ -3,6 +3,7 @@ import { log } from "../../../logger";
import Dockerode from "dockerode";
import { demuxStream } from "../demux-stream";
import { Readable } from "stream";
import { dockerClient } from "../../docker-client";

export const containerLogs = async (container: Dockerode.Container): Promise<Readable> => {
try {
Expand All @@ -15,7 +16,7 @@ export const containerLogs = async (container: Dockerode.Container): Promise<Rea

stream.socket.unref();

return demuxStream(stream);
return demuxStream((await dockerClient()).dockerode, stream);
} catch (err) {
log.error(`Failed to get container logs: ${err}`);
throw err;
Expand Down
5 changes: 2 additions & 3 deletions src/docker/functions/demux-stream.ts
@@ -1,11 +1,10 @@
import { PassThrough, Readable } from "stream";
import { log } from "../../logger";
import { dockerClient } from "../docker-client";
import Dockerode from "dockerode";

export const demuxStream = async (stream: Readable): Promise<Readable> => {
export const demuxStream = async (dockerode: Dockerode, stream: Readable): Promise<Readable> => {
try {
const demuxedStream = new PassThrough({ autoDestroy: true, encoding: "utf-8" });
const { dockerode } = await dockerClient();
dockerode.modem.demuxStream(stream, demuxedStream, demuxedStream);
stream.on("end", () => demuxedStream.end());
demuxedStream.on("close", () => {
Expand Down
5 changes: 2 additions & 3 deletions src/docker/functions/get-info.ts
@@ -1,4 +1,4 @@
import { dockerClient } from "../docker-client";
import Dockerode from "dockerode";

type DockerInfo = {
serverVersion: number;
Expand All @@ -9,8 +9,7 @@ type DockerInfo = {
memory: number;
};

export const getDockerInfo = async (): Promise<DockerInfo> => {
const { dockerode } = await dockerClient();
export const getDockerInfo = async (dockerode: Dockerode): Promise<DockerInfo> => {
const info = await dockerode.info();

return {
Expand Down
7 changes: 5 additions & 2 deletions src/docker/functions/image/image-exists.ts
@@ -1,6 +1,9 @@
import { DockerImageName } from "../../../docker-image-name";
import { listImages } from "./list-images";
import { log } from "../../../logger";
import Dockerode from "dockerode";

export const imageExists = async (imageName: DockerImageName): Promise<boolean> => {
return (await listImages()).some((image) => image.equals(imageName));
export const imageExists = async (dockerode: Dockerode, imageName: DockerImageName): Promise<boolean> => {
log.debug(`Checking if image exists: ${imageName}`);
return (await listImages(dockerode)).some((image) => image.equals(imageName));
};
5 changes: 1 addition & 4 deletions src/docker/functions/image/list-images.ts
@@ -1,13 +1,10 @@
import { DockerImageName } from "../../../docker-image-name";
import { dockerClient } from "../../docker-client";
import Dockerode from "dockerode";
import { log } from "../../../logger";

export const listImages = async (): Promise<DockerImageName[]> => {
export const listImages = async (dockerode: Dockerode): Promise<DockerImageName[]> => {
try {
const { dockerode } = await dockerClient();
const images = await dockerode.listImages();

return images.reduce((dockerImageNames: DockerImageName[], image) => {
if (isDanglingImage(image)) {
return dockerImageNames;
Expand Down
6 changes: 3 additions & 3 deletions src/docker/functions/image/pull-image.ts
Expand Up @@ -4,22 +4,22 @@ import { PullStreamParser } from "../../pull-stream-parser";
import { dockerClient } from "../../docker-client";
import { AuthConfig } from "../../types";
import { imageExists } from "./image-exists";
import Dockerode from "dockerode";

export type PullImageOptions = {
imageName: DockerImageName;
force: boolean;
authConfig?: AuthConfig;
};

export const pullImage = async (options: PullImageOptions): Promise<void> => {
export const pullImage = async (dockerode: Dockerode, options: PullImageOptions): Promise<void> => {
try {
if ((await imageExists(options.imageName)) && !options.force) {
if ((await imageExists(dockerode, options.imageName)) && !options.force) {
log.debug(`Not pulling image as it already exists: ${options.imageName}`);
return;
}

log.info(`Pulling image: ${options.imageName}`);
const { dockerode } = await dockerClient();
const stream = await dockerode.pull(options.imageName.toString(), { authconfig: options.authConfig });

await new PullStreamParser(options.imageName, log).consume(stream);
Expand Down
4 changes: 2 additions & 2 deletions src/docker/functions/run-in-container.ts
Expand Up @@ -15,12 +15,12 @@ export const runInContainer = async (
try {
const imageName = DockerImageName.fromString(image);

await pullImage({ imageName, force: false });
await pullImage(dockerode, { imageName, force: false });

log.debug(`Creating container: ${image} with command: ${command.join(" ")}`);
const container = await dockerode.createContainer({ Image: image, Cmd: command, HostConfig: { AutoRemove: true } });
log.debug(`Attaching to container: ${container.id}`);
const stream = await attachContainer(container);
const stream = await attachContainer(dockerode, container);

const promise = new Promise<string>((resolve) => {
const interval = setInterval(async () => {
Expand Down
3 changes: 2 additions & 1 deletion src/generic-container/generic-container-builder.ts
Expand Up @@ -10,6 +10,7 @@ import { buildImage } from "../docker/functions/image/build-image";
import { imageExists } from "../docker/functions/image/image-exists";
import { getAuthConfig } from "../registry-auth-locator";
import { GenericContainer } from "./generic-container";
import { dockerClient } from "../docker/docker-client";

export class GenericContainerBuilder {
private buildArgs: BuildArgs = {};
Expand Down Expand Up @@ -51,7 +52,7 @@ export class GenericContainerBuilder {
});
const container = new GenericContainer(imageName.toString());

if (!(await imageExists(imageName))) {
if (!(await imageExists((await dockerClient()).dockerode, imageName))) {
throw new Error("Failed to build image");
}

Expand Down
2 changes: 1 addition & 1 deletion src/generic-container/generic-container.ts
Expand Up @@ -81,7 +81,7 @@ export class GenericContainer implements TestContainer {
protected preStart?(): Promise<void>;

public async start(): Promise<StartedTestContainer> {
await pullImage({
await pullImage((await dockerClient()).dockerode, {
imageName: this.imageName,
force: this.pullPolicy.shouldPull(),
authConfig: await getAuthConfig(this.imageName.registry),
Expand Down
7 changes: 5 additions & 2 deletions src/log-system-diagnostics.ts
@@ -1,11 +1,14 @@
import { log } from "./logger";
import { version as dockerComposeVersion } from "./docker-compose/docker-compose";
import { getDockerInfo } from "./docker/functions/get-info";
import Dockerode from "dockerode";

export const logSystemDiagnostics = async (dockerode: Dockerode): Promise<void> => {
log.debug("Fetching system diagnostics");

export const logSystemDiagnostics = async (): Promise<void> => {
const info = {
node: getNodeInfo(),
docker: await getDockerInfo(),
docker: await getDockerInfo(dockerode),
dockerCompose: await getDockerComposeInfo(),
};

Expand Down
6 changes: 4 additions & 2 deletions src/reaper.test.ts
Expand Up @@ -7,6 +7,7 @@ import waitForExpect from "wait-for-expect";
import { listImages } from "./docker/functions/image/list-images";
import { DockerImageName } from "./docker-image-name";
import { DockerComposeEnvironment } from "./docker-compose-environment/docker-compose-environment";
import { dockerClient } from "./docker/docker-client";

const fixtures = path.resolve(__dirname, "..", "fixtures");

Expand Down Expand Up @@ -67,9 +68,10 @@ describe("Reaper", () => {
const reaperContainerId = await getReaperContainerId();
await stopReaper();

expect(await listImages()).toContainEqual(DockerImageName.fromString(imageId));
const { dockerode } = await dockerClient();
expect(await listImages(dockerode)).toContainEqual(DockerImageName.fromString(imageId));
await waitForExpect(async () => {
expect(await listImages()).not.toContainEqual(DockerImageName.fromString(imageId));
expect(await listImages(dockerode)).not.toContainEqual(DockerImageName.fromString(imageId));
expect(await getContainerIds()).not.toContain(reaperContainerId);
}, 30_000);
});
Expand Down