diff --git a/docs/features/images.md b/docs/features/images.md index f5cd6d434..38a2f9c79 100644 --- a/docs/features/images.md +++ b/docs/features/images.md @@ -91,6 +91,15 @@ const container = await GenericContainer .build(); ``` +### Using BuildKit + +```javascript +const container = await GenericContainer + .fromDockerfile("/path/to/build-context") + .withBuildKit() + .build(); +``` + ## Image name substitution Testcontainers supports automatic substitution of Docker image names. diff --git a/packages/testcontainers/fixtures/docker/docker-with-buildkit-features/Dockerfile b/packages/testcontainers/fixtures/docker/docker-with-buildkit-features/Dockerfile new file mode 100644 index 000000000..8e6177e7a --- /dev/null +++ b/packages/testcontainers/fixtures/docker/docker-with-buildkit-features/Dockerfile @@ -0,0 +1,21 @@ +FROM node:10-alpine + +MAINTAINER Cristian Greco + +EXPOSE 8080 + +RUN --mount=type=cache,target=/var/cache/apk apk add --no-cache curl dumb-init libcap openssl + +RUN openssl req -x509 -nodes -days 36500 \ + -subj "/C=CA/ST=QC/O=Company Inc/CN=localhost" \ + -newkey rsa:2048 -keyout /etc/ssl/private/cert.key \ + -out /etc/ssl/certs/cert.crt \ + && chmod 666 /etc/ssl/private/cert.key + +RUN npm init -y \ + && npm install express@4.16.4 + +COPY index.js . + +ENTRYPOINT ["/usr/bin/dumb-init", "--"] +CMD ["node", "index.js"] diff --git a/packages/testcontainers/fixtures/docker/docker-with-buildkit-features/index.js b/packages/testcontainers/fixtures/docker/docker-with-buildkit-features/index.js new file mode 100644 index 000000000..e8dbbebd6 --- /dev/null +++ b/packages/testcontainers/fixtures/docker/docker-with-buildkit-features/index.js @@ -0,0 +1,62 @@ +const fs = require("fs"); +const http = require("http"); +const https = require("https"); +const express = require("express"); + +const app = express(); + +app.get("/hello-world", (req, res) => { + res.status(200).send("hello-world"); +}); + +app.get("/hello-world-delay", (req, res) => { + setTimeout(() => { + res.status(200).send("hello-world"); + }, 3000); +}); + +app.post("/hello-world-post", (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.get("/auth", (req, res) => { + const auth = req.headers.authorization; + const [, base64Encoded] = auth.split(" "); + const credentials = Buffer.from(base64Encoded, "base64").toString("ascii"); + const [username, password] = credentials.split(":"); + if (username === "user" && password === "pass") { + res.status(200).end(); + } else { + res.status(401).end(); + } +}); + +app.get("/header-or-400/:headerName", (req, res) => { + if (req.headers[req.params["headerName"]] !== undefined) { + res.status(200).end(); + } else { + res.status(400).end(); + } +}); + +const PORT = 8080; +const TLS_PORT = 8443; + +http.createServer(app).listen(PORT, () => console.log(`Listening on port ${PORT}`)); +https + .createServer( + { + key: fs.readFileSync("/etc/ssl/private/cert.key", "utf8"), + cert: fs.readFileSync("/etc/ssl/certs/cert.crt", "utf8"), + }, + app + ) + .listen(TLS_PORT, () => console.log(`Listening on secure port ${TLS_PORT}`)); diff --git a/packages/testcontainers/fixtures/docker/docker-with-buildkit-features/test.txt b/packages/testcontainers/fixtures/docker/docker-with-buildkit-features/test.txt new file mode 100644 index 000000000..95d09f2b1 --- /dev/null +++ b/packages/testcontainers/fixtures/docker/docker-with-buildkit-features/test.txt @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/packages/testcontainers/src/generic-container/generic-container-builder.ts b/packages/testcontainers/src/generic-container/generic-container-builder.ts index 06a23d56b..8ddbb6c26 100644 --- a/packages/testcontainers/src/generic-container/generic-container-builder.ts +++ b/packages/testcontainers/src/generic-container/generic-container-builder.ts @@ -7,6 +7,7 @@ import { getAuthConfig, getContainerRuntimeClient, ImageName } from "../containe import { getReaper } from "../reaper/reaper"; import { getDockerfileImages } from "../utils/dockerfile-parser"; import { createLabels, LABEL_TESTCONTAINERS_SESSION_ID } from "../utils/labels"; +import type { ImageBuildOptions } from "dockerode"; export type BuildOptions = { deleteOnExit: boolean; @@ -17,6 +18,7 @@ export class GenericContainerBuilder { private pullPolicy: ImagePullPolicy = PullPolicy.defaultPolicy(); private cache = true; private target?: string; + private useBuildKit = false; constructor( private readonly context: string, @@ -44,6 +46,11 @@ export class GenericContainerBuilder { return this; } + public withBuildKit(useBuildKit = true): this { + this.useBuildKit = useBuildKit; + return this; + } + public async build( image = `localhost/${this.uuid.nextUuid()}:${this.uuid.nextUuid()}`, options: BuildOptions = { deleteOnExit: true } @@ -66,12 +73,13 @@ export class GenericContainerBuilder { t: imageName.string, dockerfile: this.dockerfileName, buildargs: this.buildArgs, - pull: this.pullPolicy ? "true" : undefined, + pull: this.pullPolicy.shouldPull() ? "true" : undefined, nocache: !this.cache, registryconfig: registryConfig, labels, target: this.target, - }); + version: this.useBuildKit ? "2" : undefined, + } as ImageBuildOptions); const container = new GenericContainer(imageName.string); if (!(await client.image.exists(imageName))) { diff --git a/packages/testcontainers/src/generic-container/generic-container-dockerfile.test.ts b/packages/testcontainers/src/generic-container/generic-container-dockerfile.test.ts index 7f24163ea..a68421701 100644 --- a/packages/testcontainers/src/generic-container/generic-container-dockerfile.test.ts +++ b/packages/testcontainers/src/generic-container/generic-container-dockerfile.test.ts @@ -102,4 +102,14 @@ describe("GenericContainer Dockerfile", () => { await startedContainer.stop(); }); + + it("should work with buildKit", async () => { + const context = path.resolve(fixtures, "docker-with-buildkit-features"); + const container = await GenericContainer.fromDockerfile(context).withBuildKit().build(); + const startedContainer = await container.withExposedPorts(8080).start(); + + await checkContainerIsHealthy(startedContainer); + + await startedContainer.stop(); + }); });