From d152d61a97b3602c2a20d00a2293bcb30c1df5e1 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Fri, 25 Aug 2023 05:12:49 -0700 Subject: [PATCH] fix(core): support cache-from and cache-to flags in DockerImage (#26337) In #24024, we added the ability to specify Docker cache flags during ecr-asset builds. However, it was not added to the `DockerImage` class. This PR adds the ability to specify the `--cache-from` and `--cache-to` flag to `DockerImage` builds. This logic was primarily lifted directly from #24024. Fixup of issues from #25925 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk-lib/core/lib/bundling.ts | 29 +++++++++++- .../aws-cdk-lib/core/test/bundling.test.ts | 45 +++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk-lib/core/lib/bundling.ts b/packages/aws-cdk-lib/core/lib/bundling.ts index f348ce8346de8..e4500bf213d3f 100644 --- a/packages/aws-cdk-lib/core/lib/bundling.ts +++ b/packages/aws-cdk-lib/core/lib/bundling.ts @@ -1,6 +1,7 @@ import { spawnSync } from 'child_process'; import * as crypto from 'crypto'; import { isAbsolute, join } from 'path'; +import { DockerCacheOption } from './assets'; import { FileSystem } from './fs'; import { dockerExec } from './private/asset-staging'; import { quiet, reset } from './private/jsii-deprecated'; @@ -239,7 +240,7 @@ export class BundlingDockerImage { } /** @param image The Docker image */ - protected constructor(public readonly image: string, private readonly _imageHash?: string) {} + protected constructor(public readonly image: string, private readonly _imageHash?: string) { } /** * Provides a stable representation of this image for JSON serialization. @@ -355,6 +356,8 @@ export class DockerImage extends BundlingDockerImage { ...(options.file ? ['-f', join(path, options.file)] : []), ...(options.platform ? ['--platform', options.platform] : []), ...(options.targetStage ? ['--target', options.targetStage] : []), + ...(options.cacheFrom ? [...options.cacheFrom.map(cacheFrom => ['--cache-from', this.cacheOptionToFlag(cacheFrom)]).flat()] : []), + ...(options.cacheTo ? ['--cache-to', this.cacheOptionToFlag(options.cacheTo)] : []), ...flatten(Object.entries(buildArgs).map(([k, v]) => ['--build-arg', `${k}=${v}`])), path, ]; @@ -379,6 +382,14 @@ export class DockerImage extends BundlingDockerImage { return new DockerImage(image); } + private static cacheOptionToFlag(option: DockerCacheOption): string { + let flag = `type=${option.type}`; + if (option.params) { + flag += ',' + Object.entries(option.params).map(([k, v]) => `${k}=${v}`).join(','); + } + return flag; + } + /** The Docker image */ public readonly image: string; @@ -602,13 +613,27 @@ export interface DockerBuildOptions { * @default - Build all stages defined in the Dockerfile */ readonly targetStage?: string; + + /** + * Cache from options to pass to the `docker build` command. + * + * @default - no cache from args are passed + */ + readonly cacheFrom?: DockerCacheOption[]; + + /** + * Cache to options to pass to the `docker build` command. + * + * @default - no cache to args are passed + */ + readonly cacheTo?: DockerCacheOption; } function flatten(x: string[][]) { return Array.prototype.concat([], ...x); } -function isSeLinux() : boolean { +function isSeLinux(): boolean { if (process.platform != 'linux') { return false; } diff --git a/packages/aws-cdk-lib/core/test/bundling.test.ts b/packages/aws-cdk-lib/core/test/bundling.test.ts index 531fec2aca68b..a9a2eb2abcf7e 100644 --- a/packages/aws-cdk-lib/core/test/bundling.test.ts +++ b/packages/aws-cdk-lib/core/test/bundling.test.ts @@ -124,6 +124,51 @@ describe('bundling', () => { ])).toEqual(true); }); + test('bundling with image from asset with cache-to & cache-from', () => { + const spawnSyncStub = sinon.stub(child_process, 'spawnSync').returns({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('stdout'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + + const imageHash = '123456abcdef'; + const fingerprintStub = sinon.stub(FileSystem, 'fingerprint'); + fingerprintStub.callsFake(() => imageHash); + const cacheTo = { type: 'local', params: { dest: 'path/to/local/dir' } }; + const cacheFrom1 = { + type: 's3', params: { region: 'us-west-2', bucket: 'my-bucket', name: 'foo' }, + }; + const cacheFrom2 = { + type: 'gha', params: { url: 'https://example.com', token: 'abc123', scope: 'gh-ref-image2' }, + }; + + const options = { cacheTo, cacheFrom: [cacheFrom1, cacheFrom2] }; + const image = DockerImage.fromBuild('docker-path', options); + image.run(); + + const tagHash = crypto.createHash('sha256').update(JSON.stringify({ + path: 'docker-path', + ...options, + })).digest('hex'); + const tag = `cdk-${tagHash}`; + + expect(spawnSyncStub.firstCall.calledWith(dockerCmd, [ + 'build', '-t', tag, + '--cache-from', 'type=s3,region=us-west-2,bucket=my-bucket,name=foo', + '--cache-from', 'type=gha,url=https://example.com,token=abc123,scope=gh-ref-image2', + '--cache-to', 'type=local,dest=path/to/local/dir', + 'docker-path', + ])).toEqual(true); + + expect(spawnSyncStub.secondCall.calledWith(dockerCmd, [ + 'run', '--rm', + tag, + ])).toEqual(true); + }); + test('bundling with image from asset with target stage', () => { const spawnSyncStub = sinon.stub(child_process, 'spawnSync').returns({ status: 0,