diff --git a/lib/docker-client.ts b/lib/docker-client.ts index fd0b3d0..887854f 100644 --- a/lib/docker-client.ts +++ b/lib/docker-client.ts @@ -1121,6 +1121,7 @@ export class DockerClient { * @param options.target Target build stage * @param options.outputs BuildKit output configuration in the format of a stringified JSON array of objects. Each object must have two top-level properties: `Type` and `Attrs`. The `Type` property must be set to \'moby\'. The `Attrs` property is a map of attributes for the BuildKit output configuration. See https://docs.docker.com/build/exporters/oci-docker/ for more information. Example: ``` [{\"Type\":\"moby\",\"Attrs\":{\"type\":\"image\",\"force-compression\":\"true\",\"compression\":\"zstd\"}}] ``` * @param options.version Version of the builder backend to use. - `1` is the first generation classic (deprecated) builder in the Docker daemon (default) - `2` is [BuildKit](https://github.com/moby/buildkit) + * @param options.secrets BuildKit secrets to pass to the build. A record mapping secret IDs to their values. Secrets are exposed in the build at `/run/secrets/<id>` when using `RUN --mount=type=secret,id=<id>` in the Dockerfile. Requires BuildKit (version: `2`). For more information, see https://docs.docker.com/build/building/secrets/ */ public imageBuild( buildContext: ReadableStream, @@ -1151,6 +1152,7 @@ export class DockerClient { target?: string; outputs?: string; version?: '1' | '2'; + secrets?: Record; }, ): JSONMessages { const headers: Record = {}; @@ -1162,6 +1164,19 @@ export class DockerClient { ); } + // Prepare secrets parameter for BuildKit + let secretsParam: string | undefined; + if (options?.secrets) { + // Convert secrets to BuildKit format: array of secret specs + const secretSpecs = Object.entries(options.secrets).map( + ([id, value]) => ({ + ID: id, + Source: value, + }), + ); + secretsParam = JSON.stringify(secretSpecs); + } + const request = this.api.post( '/build', { @@ -1190,6 +1205,7 @@ export class DockerClient { target: options?.target, outputs: options?.outputs, version: options?.version || '2', + secret: secretsParam, }, buildContext, headers, diff --git a/test/build.test.ts b/test/build.test.ts index 4ab5739..af0aa4b 100644 --- a/test/build.test.ts +++ b/test/build.test.ts @@ -62,3 +62,76 @@ COPY test.txt /test.txt } }, ); + +test( + 'imageBuild: build image with BuildKit secrets', + { timeout: 60000 }, + async () => { + const client = await DockerClient.fromDockerConfig(); + const testImageName = 'test-build-secrets-image'; + const testTag = 'latest'; + const testSecret = 'my-test-secret-value-12345'; + + try { + const pack = createTarPack(); + pack.entry( + { name: 'Dockerfile' }, + `FROM alpine:latest +# Use a secret during build without including it in the final image +RUN --mount=type=secret,id=test_secret \\ + if [ -f /run/secrets/test_secret ]; then \\ + echo "Secret found and mounted successfully"; \\ + cat /run/secrets/test_secret > /tmp/secret_check; \\ + else \\ + echo "ERROR: Secret not found at /run/secrets/test_secret"; \\ + exit 1; \\ + fi +# Verify secret was available but not in final image +RUN test ! -f /run/secrets/test_secret && echo "Secret not in final layer (good!)" +`, + ); + pack.finalize(); + + console.log(' Building image with BuildKit secrets...'); + const builtImage = await client + .imageBuild( + Readable.toWeb(pack, { + strategy: { highWaterMark: 16384 }, + }), + { + tag: `${testImageName}:${testTag}`, + rm: true, + forcerm: true, + version: '2', // BuildKit required for secrets + secrets: { + test_secret: testSecret, + }, + }, + ) + .wait(); + + console.log(` Inspecting built image ${builtImage}`); + const imageInspect = await client.imageInspect(builtImage || ''); + console.log(' Image with secrets built successfully!'); + + assert.notStrictEqual( + imageInspect.RepoTags?.includes(`${testImageName}:${testTag}`), + false, + ); + console.log(` Image size: ${imageInspect.Size} bytes`); + } finally { + // Clean up: delete the test image + console.log(' Cleaning up test image...'); + try { + await client.imageDelete(`${testImageName}:${testTag}`, { + force: true, + }); + console.log(' Test image deleted successfully'); + } catch (cleanupError) { + console.log( + ` Warning: Failed to delete test image: ${(cleanupError as any)?.message}`, + ); + } + } + }, +);