diff --git a/packages/aws-cdk-lib/aws-ecr-assets/README.md b/packages/aws-cdk-lib/aws-ecr-assets/README.md index ee69166717734..1bc1a2b468251 100644 --- a/packages/aws-cdk-lib/aws-ecr-assets/README.md +++ b/packages/aws-cdk-lib/aws-ecr-assets/README.md @@ -127,6 +127,17 @@ const asset = new DockerImageAsset(this, 'MyBuildImage', { }) ``` +You can optionally disable the cache: + +```ts +import { DockerImageAsset, Platform } from 'aws-cdk-lib/aws-ecr-assets'; + +const asset = new DockerImageAsset(this, 'MyBuildImage', { + directory: path.join(__dirname, 'my-image'), + cacheDisabled: true, +}) +``` + ## Images from Tarball Images are loaded from a local tarball, uploaded to ECR by the CDK toolkit and/or your app's CI-CD pipeline, and can be diff --git a/packages/aws-cdk-lib/aws-ecr-assets/lib/image-asset.ts b/packages/aws-cdk-lib/aws-ecr-assets/lib/image-asset.ts index c8475ce037873..26507566278ec 100644 --- a/packages/aws-cdk-lib/aws-ecr-assets/lib/image-asset.ts +++ b/packages/aws-cdk-lib/aws-ecr-assets/lib/image-asset.ts @@ -307,6 +307,13 @@ export interface DockerImageAssetOptions extends FingerprintOptions, FileFingerp * @see https://docs.docker.com/build/cache/backends/ */ readonly cacheTo?: DockerCacheOption; + + /** + * Disable the cache and pass `--no-cache` to the `docker build` command. + * + * @default - cache is used + */ + readonly cacheDisabled?: boolean; } /** @@ -409,6 +416,11 @@ export class DockerImageAsset extends Construct implements IAsset { */ private readonly dockerCacheTo?: DockerCacheOption; + /** + * Disable the cache and pass `--no-cache` to the `docker build` command. + */ + private readonly dockerCacheDisabled?: boolean; + /** * Docker target to build to */ @@ -505,6 +517,7 @@ export class DockerImageAsset extends Construct implements IAsset { this.dockerOutputs = props.outputs; this.dockerCacheFrom = props.cacheFrom; this.dockerCacheTo = props.cacheTo; + this.dockerCacheDisabled = props.cacheDisabled; const location = stack.synthesizer.addDockerImageAsset({ directoryName: this.assetPath, @@ -520,6 +533,7 @@ export class DockerImageAsset extends Construct implements IAsset { dockerOutputs: this.dockerOutputs, dockerCacheFrom: this.dockerCacheFrom, dockerCacheTo: this.dockerCacheTo, + dockerCacheDisabled: this.dockerCacheDisabled, }); this.repository = ecr.Repository.fromRepositoryName(this, 'Repository', location.repositoryName); @@ -561,6 +575,7 @@ export class DockerImageAsset extends Construct implements IAsset { resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_DOCKER_OUTPUTS_KEY] = this.dockerOutputs; resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_DOCKER_CACHE_FROM_KEY] = this.dockerCacheFrom; resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_DOCKER_CACHE_TO_KEY] = this.dockerCacheTo; + resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_DOCKER_CACHE_DISABLED_KEY] = this.dockerCacheDisabled; } } diff --git a/packages/aws-cdk-lib/aws-ecr-assets/test/build-image-cache.test.ts b/packages/aws-cdk-lib/aws-ecr-assets/test/build-image-cache.test.ts index 3c4655e0453e6..96a0126bfb3e7 100644 --- a/packages/aws-cdk-lib/aws-ecr-assets/test/build-image-cache.test.ts +++ b/packages/aws-cdk-lib/aws-ecr-assets/test/build-image-cache.test.ts @@ -51,6 +51,26 @@ describe('build cache', () => { }); }); + test('manifest contains cache disabled', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app); + const asset = new DockerImageAsset(stack, 'DockerImage6', { + directory: path.join(__dirname, 'demo-image'), + cacheDisabled: true, + }); + + // WHEN + const asm = app.synth(); + + // THEN + const manifestArtifact = getAssetManifest(asm); + const manifest = readAssetManifest(manifestArtifact); + + expect(Object.keys(manifest.dockerImages ?? {}).length).toBe(1); + expect(manifest.dockerImages?.[asset.assetHash]?.source.cacheDisabled).toBeTruthy(); + }); + test('manifest does not contain options when not specified', () => { // GIVEN const app = new App(); diff --git a/packages/aws-cdk-lib/cloud-assembly-schema/lib/assets/docker-image-asset.ts b/packages/aws-cdk-lib/cloud-assembly-schema/lib/assets/docker-image-asset.ts index 4c1ad6d426a55..70c9761f47c70 100644 --- a/packages/aws-cdk-lib/cloud-assembly-schema/lib/assets/docker-image-asset.ts +++ b/packages/aws-cdk-lib/cloud-assembly-schema/lib/assets/docker-image-asset.ts @@ -122,6 +122,13 @@ export interface DockerImageSource { * @see https://docs.docker.com/build/cache/backends/ */ readonly cacheTo?: DockerCacheOption; + + /** + * Disable the cache and pass `--no-cache` to the `docker build` command. + * + * @default - cache is used + */ + readonly cacheDisabled?: boolean; } /** diff --git a/packages/aws-cdk-lib/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts b/packages/aws-cdk-lib/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts index 9354300baa1ba..35b619ddaa780 100644 --- a/packages/aws-cdk-lib/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts +++ b/packages/aws-cdk-lib/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts @@ -226,6 +226,13 @@ export interface ContainerImageAssetMetadataEntry extends BaseAssetMetadataEntry * @see https://docs.docker.com/build/cache/backends/ */ readonly cacheTo?: ContainerImageAssetCacheOption; + + /** + * Disable the cache and pass `--no-cache` to the `docker build` command. + * + * @default - cache is used + */ + readonly cacheDisabled?: boolean; } /** diff --git a/packages/aws-cdk-lib/cloud-assembly-schema/schema/assets.schema.json b/packages/aws-cdk-lib/cloud-assembly-schema/schema/assets.schema.json index f34adb5ef24f5..194c2ae23b885 100644 --- a/packages/aws-cdk-lib/cloud-assembly-schema/schema/assets.schema.json +++ b/packages/aws-cdk-lib/cloud-assembly-schema/schema/assets.schema.json @@ -191,6 +191,10 @@ "cacheTo": { "description": "Cache to options to pass to the `docker build` command. (Default - no cache to options are passed to the build command)", "$ref": "#/definitions/DockerCacheOption" + }, + "cacheDisabled": { + "description": "Disable the cache and pass `--no-cache` to the `docker build` command. (Default - cache is used)", + "$ref": "#/definitions/DockerCacheDisabled" } } }, diff --git a/packages/aws-cdk-lib/cloud-assembly-schema/schema/cloud-assembly.schema.json b/packages/aws-cdk-lib/cloud-assembly-schema/schema/cloud-assembly.schema.json index bfb369cfd2100..d819113764b8f 100644 --- a/packages/aws-cdk-lib/cloud-assembly-schema/schema/cloud-assembly.schema.json +++ b/packages/aws-cdk-lib/cloud-assembly-schema/schema/cloud-assembly.schema.json @@ -261,6 +261,10 @@ "description": "Cache to options to pass to the `docker build` command. (Default - no cache to options are passed to the build command)", "$ref": "#/definitions/ContainerImageAssetCacheOption" }, + "cacheDisabled": { + "description": "Disable the cache and pass `--no-cache` to the `docker build` command. (Default - cache is used)", + "$ref": "#/definitions/ContainerImageAssetCacheDisabled" + }, "id": { "description": "Logical identifier for the asset", "type": "string" diff --git a/packages/aws-cdk-lib/core/lib/assets.ts b/packages/aws-cdk-lib/core/lib/assets.ts index c9a7d47d98672..d1fc760ad0eba 100644 --- a/packages/aws-cdk-lib/core/lib/assets.ts +++ b/packages/aws-cdk-lib/core/lib/assets.ts @@ -286,6 +286,13 @@ export interface DockerImageAssetSource { * @default - no cache to args are passed */ readonly dockerCacheTo?: DockerCacheOption; + + /** + * Disable the cache and pass `--no-cache` to the `docker build` command. + * + * @default - cache is used + */ + readonly dockerCacheDisabled?: boolean; } /** diff --git a/packages/aws-cdk-lib/core/lib/bundling.ts b/packages/aws-cdk-lib/core/lib/bundling.ts index e4500bf213d3f..b9967ce190842 100644 --- a/packages/aws-cdk-lib/core/lib/bundling.ts +++ b/packages/aws-cdk-lib/core/lib/bundling.ts @@ -358,6 +358,7 @@ export class DockerImage extends BundlingDockerImage { ...(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)] : []), + ...(options.cacheDisabled ? ['--no-cache'] : []), ...flatten(Object.entries(buildArgs).map(([k, v]) => ['--build-arg', `${k}=${v}`])), path, ]; @@ -627,6 +628,13 @@ export interface DockerBuildOptions { * @default - no cache to args are passed */ readonly cacheTo?: DockerCacheOption; + + /** + * Disable the cache and pass `--no-cache` to the `docker build` command. + * + * @default - cache is used + */ + readonly cacheDisabled?: boolean; } function flatten(x: string[][]) { diff --git a/packages/aws-cdk-lib/core/lib/stack-synthesizers/asset-manifest-builder.ts b/packages/aws-cdk-lib/core/lib/stack-synthesizers/asset-manifest-builder.ts index 515573bfca5c1..3cd3924872995 100644 --- a/packages/aws-cdk-lib/core/lib/stack-synthesizers/asset-manifest-builder.ts +++ b/packages/aws-cdk-lib/core/lib/stack-synthesizers/asset-manifest-builder.ts @@ -75,6 +75,7 @@ export class AssetManifestBuilder { dockerOutputs: asset.dockerOutputs, cacheFrom: asset.dockerCacheFrom, cacheTo: asset.dockerCacheTo, + cacheDisabled: asset.dockerCacheDisabled, }, { repositoryName: target.repositoryName, imageTag, diff --git a/packages/aws-cdk-lib/core/lib/stack-synthesizers/legacy.ts b/packages/aws-cdk-lib/core/lib/stack-synthesizers/legacy.ts index 887b32ee5a7c0..29a553486663c 100644 --- a/packages/aws-cdk-lib/core/lib/stack-synthesizers/legacy.ts +++ b/packages/aws-cdk-lib/core/lib/stack-synthesizers/legacy.ts @@ -152,6 +152,7 @@ export class LegacyStackSynthesizer extends StackSynthesizer implements IReusabl outputs: asset.dockerOutputs, cacheFrom: asset.dockerCacheFrom, cacheTo: asset.dockerCacheTo, + cacheDisabled: asset.dockerCacheDisabled, }; this.boundStack.node.addMetadata(cxschema.ArtifactMetadataEntryType.ASSET, metadata); diff --git a/packages/aws-cdk-lib/core/test/bundling.test.ts b/packages/aws-cdk-lib/core/test/bundling.test.ts index a9a2eb2abcf7e..9e074c992efed 100644 --- a/packages/aws-cdk-lib/core/test/bundling.test.ts +++ b/packages/aws-cdk-lib/core/test/bundling.test.ts @@ -88,6 +88,43 @@ describe('bundling', () => { ])).toEqual(true); }); + test('bundling with image from asset with cache disabled', () => { + 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 image = DockerImage.fromBuild('docker-path', { + cacheDisabled: true, + }); + image.run(); + + const tagHash = crypto.createHash('sha256').update(JSON.stringify({ + path: 'docker-path', + cacheDisabled: true, + })).digest('hex'); + const tag = `cdk-${tagHash}`; + + expect(spawnSyncStub.firstCall.calledWith(dockerCmd, [ + 'build', '-t', tag, + '--no-cache', + 'docker-path', + ])).toEqual(true); + + expect(spawnSyncStub.secondCall.calledWith(dockerCmd, [ + 'run', '--rm', + tag, + ])).toEqual(true); + }); + test('bundling with image from asset with platform', () => { const spawnSyncStub = sinon.stub(child_process, 'spawnSync').returns({ status: 0, diff --git a/packages/aws-cdk-lib/cx-api/lib/assets.ts b/packages/aws-cdk-lib/cx-api/lib/assets.ts index ca6581548df63..ad67cfec62f62 100644 --- a/packages/aws-cdk-lib/cx-api/lib/assets.ts +++ b/packages/aws-cdk-lib/cx-api/lib/assets.ts @@ -20,6 +20,7 @@ export const ASSET_RESOURCE_METADATA_IS_BUNDLED_KEY = 'aws:asset:is-bundled'; export const ASSET_RESOURCE_METADATA_DOCKER_OUTPUTS_KEY = 'aws:asset:docker-outputs'; export const ASSET_RESOURCE_METADATA_DOCKER_CACHE_FROM_KEY = 'aws:asset:docker-cache-from'; export const ASSET_RESOURCE_METADATA_DOCKER_CACHE_TO_KEY = 'aws:asset:docker-cache-to'; +export const ASSET_RESOURCE_METADATA_DOCKER_CACHE_DISABLED_KEY = 'aws:asset:docker-cache-disabled'; /** * Separator string that separates the prefix separator from the object key separator. diff --git a/packages/cdk-assets/lib/private/docker.ts b/packages/cdk-assets/lib/private/docker.ts index 12b8ab53a213e..56c223e991504 100644 --- a/packages/cdk-assets/lib/private/docker.ts +++ b/packages/cdk-assets/lib/private/docker.ts @@ -22,6 +22,7 @@ interface BuildOptions { readonly outputs?: string[]; readonly cacheFrom?: DockerCacheOption[]; readonly cacheTo?: DockerCacheOption; + readonly cacheDisabled?: boolean; readonly quiet?: boolean; } @@ -107,6 +108,7 @@ export class Docker { ...options.outputs ? options.outputs.map(output => [`--output=${output}`]) : [], ...options.cacheFrom ? [...options.cacheFrom.map(cacheFrom => ['--cache-from', this.cacheOptionToFlag(cacheFrom)]).flat()] : [], ...options.cacheTo ? ['--cache-to', this.cacheOptionToFlag(options.cacheTo)] : [], + ...options.cacheDisabled ? ['--no-cache'] : [], '.', ]; await this.execute(buildCommand, { diff --git a/packages/cdk-assets/lib/private/handlers/container-images.ts b/packages/cdk-assets/lib/private/handlers/container-images.ts index 381c1c3e635b0..8764b1e9c41b3 100644 --- a/packages/cdk-assets/lib/private/handlers/container-images.ts +++ b/packages/cdk-assets/lib/private/handlers/container-images.ts @@ -197,6 +197,7 @@ class ContainerImageBuilder { outputs: source.dockerOutputs, cacheFrom: source.cacheFrom, cacheTo: source.cacheTo, + cacheDisabled: source.cacheDisabled, quiet: this.options.quiet, }); } diff --git a/packages/cdk-assets/test/docker-images.test.ts b/packages/cdk-assets/test/docker-images.test.ts index 561e37c823916..c396853576c3e 100644 --- a/packages/cdk-assets/test/docker-images.test.ts +++ b/packages/cdk-assets/test/docker-images.test.ts @@ -205,6 +205,25 @@ beforeEach(() => { }, }, }), + '/nocache/cdk.out/assets.json': JSON.stringify({ + version: Manifest.version(), + dockerImages: { + theAsset: { + source: { + directory: 'dockerdir', + cacheDisabled: true, + }, + destinations: { + theDestination: { + region: 'us-north-50', + assumeRoleArn: 'arn:aws:role', + repositoryName: 'repo', + imageTag: 'nopqr', + }, + }, + }, + }, + }), '/platform-arm64/cdk.out/dockerdir/Dockerfile': 'FROM scratch', }); @@ -370,6 +389,30 @@ describe('with a complete manifest', () => { expect(true).toBeTruthy(); // Expect no exception, satisfy linter }); + test('build with cache disabled', async () => { + pub = new AssetPublishing(AssetManifest.fromPath('/nocache/cdk.out'), { aws }); + const defaultNetworkDockerpath = '/nocache/cdk.out/dockerdir'; + aws.mockEcr.describeImages = mockedApiFailure('ImageNotFoundException', 'File does not exist'); + aws.mockEcr.getAuthorizationToken = mockedApiResult({ + authorizationData: [ + { authorizationToken: 'dXNlcjpwYXNz', proxyEndpoint: 'https://proxy.com/' }, + ], + }); + + const expectAllSpawns = mockSpawn( + { commandLine: ['docker', 'login', '--username', 'user', '--password-stdin', 'https://proxy.com/'] }, + { commandLine: ['docker', 'inspect', 'cdkasset-theasset'], exitCode: 1 }, + { commandLine: ['docker', 'build', '--tag', 'cdkasset-theasset', '--no-cache', '.'], cwd: defaultNetworkDockerpath }, + { commandLine: ['docker', 'tag', 'cdkasset-theasset', '12345.amazonaws.com/repo:nopqr'] }, + { commandLine: ['docker', 'push', '12345.amazonaws.com/repo:nopqr'] }, + ); + + await pub.publish(); + + expectAllSpawns(); + expect(true).toBeTruthy(); // Expect no exception, satisfy linter + }); + test('build with multiple cache from option', async () => { pub = new AssetPublishing(AssetManifest.fromPath('/cache-from-multiple/cdk.out'), { aws }); const defaultNetworkDockerpath = '/cache-from-multiple/cdk.out/dockerdir';