diff --git a/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/.dockerignore b/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/.dockerignore new file mode 100644 index 000000000..2c875f2ba --- /dev/null +++ b/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/.dockerignore @@ -0,0 +1,5 @@ +* +!example2 +!example4/nested +!example5/example5.txt +!index.js diff --git a/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/Dockerfile b/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/Dockerfile new file mode 100644 index 000000000..c5747f7e6 --- /dev/null +++ b/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/Dockerfile @@ -0,0 +1,17 @@ +FROM node:10-alpine + +MAINTAINER Cristian Greco + +EXPOSE 8080 + +RUN apk add --no-cache curl dumb-init + +RUN npm init -y \ + && npm install express@4.16.4 + +WORKDIR /opt/app + +COPY . . + +ENTRYPOINT ["/usr/bin/dumb-init", "--"] +CMD ["node", "index.js"] diff --git a/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/example1.txt b/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/example1.txt new file mode 100644 index 000000000..95d09f2b1 --- /dev/null +++ b/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/example1.txt @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/example2/example2.txt b/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/example2/example2.txt new file mode 100644 index 000000000..e69de29bb diff --git a/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/example3/example3.txt b/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/example3/example3.txt new file mode 100644 index 000000000..e69de29bb diff --git a/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/example4/nested/example4.txt b/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/example4/nested/example4.txt new file mode 100644 index 000000000..e69de29bb diff --git a/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/example5/example5.txt b/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/example5/example5.txt new file mode 100644 index 000000000..e69de29bb diff --git a/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/example6/example6.txt b/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/example6/example6.txt new file mode 100644 index 000000000..e69de29bb diff --git a/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/example6/exist2.txt b/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/example6/exist2.txt new file mode 100644 index 000000000..e69de29bb diff --git a/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/example7/example7.txt b/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/example7/example7.txt new file mode 100644 index 000000000..e69de29bb diff --git a/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/example7/exist7.txt b/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/example7/exist7.txt new file mode 100644 index 000000000..e69de29bb diff --git a/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/exist1.txt b/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/exist1.txt new file mode 100644 index 000000000..e69de29bb diff --git a/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/index.js b/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/index.js new file mode 100644 index 000000000..04c9e2030 --- /dev/null +++ b/packages/testcontainers/fixtures/docker/docker-with-dockerignore-nested-exclusions/index.js @@ -0,0 +1,18 @@ +const express = require("express"); + +const app = express(); +const port = 8080; + +app.get("/hello-world", (req, res) => { + res.status(200).send("hello-world"); +}); + +app.get("/env", (req, res) => { + res.status(200).json(process.env); +}); + +app.get("/cmd", (req, res) => { + res.status(200).json(process.argv); +}); + +app.listen(port, () => console.log(`Listening on port ${port}`)); diff --git a/packages/testcontainers/src/container-runtime/clients/image/docker-image-client.ts b/packages/testcontainers/src/container-runtime/clients/image/docker-image-client.ts index 901e0c3be..4c6ab884c 100644 --- a/packages/testcontainers/src/container-runtime/clients/image/docker-image-client.ts +++ b/packages/testcontainers/src/container-runtime/clients/image/docker-image-client.ts @@ -22,17 +22,8 @@ export class DockerImageClient implements ImageClient { async build(context: string, opts: ImageBuildOptions): Promise { try { log.debug(`Building image "${opts.t}" with context "${context}"...`); - const isDockerIgnored = await this.createIsDockerIgnoredFunction(context); - const tarStream = tar.pack(context, { - ignore: (aPath) => { - const relativePath = path.relative(context, aPath); - if (relativePath === opts.dockerfile) { - return false; - } else { - return isDockerIgnored(relativePath); - } - }, - }); + const tarPackOptions = await this.createTarPackOptions(context, opts.dockerfile ?? "Dockerfile"); + const tarStream = tar.pack(context, tarPackOptions); await new Promise((resolve) => { this.dockerode .buildImage(tarStream, opts) @@ -54,18 +45,54 @@ export class DockerImageClient implements ImageClient { } } - private async createIsDockerIgnoredFunction(context: string): Promise<(path: string) => boolean> { + private async createTarPackOptions(context: string, dockerfileName: string): Promise<{ entries?: string[] }> { const dockerIgnoreFilePath = path.join(context, ".dockerignore"); if (!existsSync(dockerIgnoreFilePath)) { - return () => false; + return {}; } const dockerIgnorePatterns = await fs.readFile(dockerIgnoreFilePath, { encoding: "utf-8" }); const instance = dockerIgnore({ ignorecase: false }); instance.add(dockerIgnorePatterns); - const filter = instance.createFilter(); + const allEntries = await this.listContextEntries(context); + const includedEntries = instance.filter(allEntries); + + const dockerfilePath = this.normalizePathForDockerIgnore(path.normalize(dockerfileName)); + if (!includedEntries.includes(dockerfilePath)) { + includedEntries.push(dockerfilePath); + } + + return { entries: includedEntries }; + } + + private async listContextEntries(context: string): Promise { + const entries: string[] = []; + const directoriesToVisit = [""]; + + while (directoriesToVisit.length > 0) { + const directory = directoriesToVisit.pop(); + if (directory === undefined) { + continue; + } + + const absoluteDirectory = directory.length > 0 ? path.join(context, directory) : context; + const directoryEntries = await fs.readdir(absoluteDirectory, { withFileTypes: true }); + + for (const directoryEntry of directoryEntries) { + const relativeEntry = directory.length > 0 ? path.join(directory, directoryEntry.name) : directoryEntry.name; + if (directoryEntry.isDirectory()) { + directoriesToVisit.push(relativeEntry); + } else { + entries.push(this.normalizePathForDockerIgnore(relativeEntry)); + } + } + } + + return entries; + } - return (aPath: string) => !filter(aPath); + private normalizePathForDockerIgnore(aPath: string): string { + return aPath.replace(/\\/gu, "/"); } async inspect(imageName: ImageName): Promise { diff --git a/packages/testcontainers/src/generic-container/generic-container.test.ts b/packages/testcontainers/src/generic-container/generic-container.test.ts index 7b7346015..1dba03e69 100644 --- a/packages/testcontainers/src/generic-container/generic-container.test.ts +++ b/packages/testcontainers/src/generic-container/generic-container.test.ts @@ -483,6 +483,23 @@ describe("GenericContainer", { timeout: 180_000 }, () => { expect(output).not.toContain("Dockerfile"); }); + it("should honour nested .dockerignore exclusion patterns", async () => { + const context = path.resolve(fixtures, "docker-with-dockerignore-nested-exclusions"); + const container = await GenericContainer.fromDockerfile(context).build(); + await using startedContainer = await container.withExposedPorts(8080).start(); + + const { output } = await startedContainer.exec(["find"]); + + expect(output).toContain("index.js"); + expect(output).toContain("example2.txt"); + expect(output).toContain("example4.txt"); + expect(output).toContain("example5.txt"); + expect(output).not.toContain("./example1.txt"); + expect(output).not.toContain("./example3"); + expect(output).not.toContain("./example6"); + expect(output).not.toContain("./example7"); + }); + it("should stop the container", async () => { await using container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14") .withName(`container-${new RandomUuid().nextUuid()}`)