Skip to content

Commit

Permalink
feat(docker): add --no-cache option for docker build
Browse files Browse the repository at this point in the history
  • Loading branch information
msambol committed Dec 3, 2023
1 parent 03a5ecd commit 8275853
Show file tree
Hide file tree
Showing 16 changed files with 169 additions and 0 deletions.
11 changes: 11 additions & 0 deletions packages/aws-cdk-lib/aws-ecr-assets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions packages/aws-cdk-lib/aws-ecr-assets/lib/image-asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -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;
}

}
Expand Down
20 changes: 20 additions & 0 deletions packages/aws-cdk-lib/aws-ecr-assets/test/build-image-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 7 additions & 0 deletions packages/aws-cdk-lib/core/lib/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/aws-cdk-lib/core/lib/bundling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
];
Expand Down Expand Up @@ -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[][]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export class AssetManifestBuilder {
dockerOutputs: asset.dockerOutputs,
cacheFrom: asset.dockerCacheFrom,
cacheTo: asset.dockerCacheTo,
cacheDisabled: asset.dockerCacheDisabled,
}, {
repositoryName: target.repositoryName,
imageTag,
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk-lib/core/lib/stack-synthesizers/legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
37 changes: 37 additions & 0 deletions packages/aws-cdk-lib/core/test/bundling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk-lib/cx-api/lib/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions packages/cdk-assets/lib/private/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface BuildOptions {
readonly outputs?: string[];
readonly cacheFrom?: DockerCacheOption[];
readonly cacheTo?: DockerCacheOption;
readonly cacheDisabled?: boolean;
readonly quiet?: boolean;
}

Expand Down Expand Up @@ -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, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ class ContainerImageBuilder {
outputs: source.dockerOutputs,
cacheFrom: source.cacheFrom,
cacheTo: source.cacheTo,
cacheDisabled: source.cacheDisabled,
quiet: this.options.quiet,
});
}
Expand Down
43 changes: 43 additions & 0 deletions packages/cdk-assets/test/docker-images.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});

Expand Down Expand Up @@ -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';
Expand Down

0 comments on commit 8275853

Please sign in to comment.