Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ecr-assets): Support cache-from and cache-to flags #24024

Merged
merged 13 commits into from Mar 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/@aws-cdk/aws-ecr-assets/README.md
Expand Up @@ -121,6 +121,18 @@ const asset = new DockerImageAsset(this, 'MyBuildImage', {
})
```

You can optionally pass cache from and cache to options to cache images:

```ts
import { DockerImageAsset, Platform } from '@aws-cdk/aws-ecr-assets';

const asset = new DockerImageAsset(this, 'MyBuildImage', {
directory: path.join(__dirname, 'my-image'),
cacheFrom: [{ type: 'registry', params: { ref: 'ghcr.io/myorg/myimage:cache' }}],
cacheTo: { type: 'registry', params: { ref: 'ghcr.io/myorg/myimage:cache', mode: 'max', compression: 'zstd' }}
})
```

## 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
54 changes: 54 additions & 0 deletions packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts
Expand Up @@ -148,6 +148,28 @@ export interface DockerImageAssetInvalidationOptions {
readonly outputs?: boolean;
}

/**
* Options for configuring the Docker cache backend
*/
export interface DockerCacheOption {
/**
* The type of cache to use.
* Refer to https://docs.docker.com/build/cache/backends/ for full list of backends.
* @default - unspecified
*
* @example 'registry'
*/
readonly type: string;
/**
* Any parameters to pass into the docker cache backend configuration.
* Refer to https://docs.docker.com/build/cache/backends/ for cache backend configuration.
* @default {} No options provided
*
* @example { ref: `12345678.dkr.ecr.us-west-2.amazonaws.com/cache:${branch}`, mode: "max" }
*/
readonly params?: { [key: string]: string };
}

/**
* Options for DockerImageAsset
*/
Expand Down Expand Up @@ -236,6 +258,22 @@ export interface DockerImageAssetOptions extends FingerprintOptions, FileFingerp
* @see https://docs.docker.com/engine/reference/commandline/build/#custom-build-outputs
*/
readonly outputs?: string[];

/**
* Cache from options to pass to the `docker build` command.
*
* @default - no cache from options are passed to the build command
* @see https://docs.docker.com/build/cache/backends/
*/
readonly cacheFrom?: DockerCacheOption[];

/**
* Cache to options to pass to the `docker build` command.
*
* @default - no cache to options are passed to the build command
* @see https://docs.docker.com/build/cache/backends/
*/
readonly cacheTo?: DockerCacheOption;
}

/**
Expand Down Expand Up @@ -316,6 +354,16 @@ export class DockerImageAsset extends Construct implements IAsset {
*/
private readonly dockerOutputs?: string[];

/**
* Cache from options to pass to the `docker build` command.
*/
private readonly dockerCacheFrom?: DockerCacheOption[];

/**
* Cache to options to pass to the `docker build` command.
*/
private readonly dockerCacheTo?: DockerCacheOption;

/**
* Docker target to build to
*/
Expand Down Expand Up @@ -407,6 +455,8 @@ export class DockerImageAsset extends Construct implements IAsset {
this.dockerBuildSecrets = props.buildSecrets;
this.dockerBuildTarget = props.target;
this.dockerOutputs = props.outputs;
this.dockerCacheFrom = props.cacheFrom;
this.dockerCacheTo = props.cacheTo;

const location = stack.synthesizer.addDockerImageAsset({
directoryName: this.assetPath,
Expand All @@ -418,6 +468,8 @@ export class DockerImageAsset extends Construct implements IAsset {
networkMode: props.networkMode?.mode,
platform: props.platform?.platform,
dockerOutputs: this.dockerOutputs,
dockerCacheFrom: this.dockerCacheFrom,
dockerCacheTo: this.dockerCacheTo,
});

this.repository = ecr.Repository.fromRepositoryName(this, 'Repository', location.repositoryName);
Expand Down Expand Up @@ -456,6 +508,8 @@ export class DockerImageAsset extends Construct implements IAsset {
resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_DOCKER_BUILD_TARGET_KEY] = this.dockerBuildTarget;
resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY] = resourceProperty;
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;
}

}
Expand Down
88 changes: 88 additions & 0 deletions packages/@aws-cdk/aws-ecr-assets/test/build-image-cache.test.ts
@@ -0,0 +1,88 @@
import * as fs from 'fs';
import * as path from 'path';
import { AssetManifest } from '@aws-cdk/cloud-assembly-schema';
import { App, Stack } from '@aws-cdk/core';
import { AssetManifestArtifact, CloudArtifact, CloudAssembly } from '@aws-cdk/cx-api';
import { DockerImageAsset } from '../lib';

describe('build cache', () => {
test('manifest contains cache from options ', () => {
// GIVEN
const app = new App();
const stack = new Stack(app);
const asset = new DockerImageAsset(stack, 'DockerImage6', {
directory: path.join(__dirname, 'demo-image'),
cacheFrom: [{ type: 'registry', params: { image: 'foo' } }],
});

// 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.cacheFrom?.length).toBe(1);
expect(manifest.dockerImages?.[asset.assetHash]?.source.cacheFrom?.[0]).toStrictEqual({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MrArnoldPalmer followed the pattern in tar assets, lmk no if test naming and tests lgty!

type: 'registry',
params: { image: 'foo' },
});
});
test('manifest contains cache to options ', () => {
// GIVEN
const app = new App();
const stack = new Stack(app);
const asset = new DockerImageAsset(stack, 'DockerImage6', {
directory: path.join(__dirname, 'demo-image'),
cacheTo: { type: 'inline' },
});

// 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.cacheTo).toStrictEqual({
type: 'inline',
});
});

test('manifest does not contain options when not specified', () => {
// GIVEN
const app = new App();
const stack = new Stack(app);
const asset = new DockerImageAsset(stack, 'DockerImage6', {
directory: path.join(__dirname, 'demo-image'),
});

// 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.cacheFrom).toBeUndefined();
expect(manifest.dockerImages?.[asset.assetHash]?.source.cacheTo).toBeUndefined();
});
});

function isAssetManifest(x: CloudArtifact): x is AssetManifestArtifact {
return x instanceof AssetManifestArtifact;
}

function getAssetManifest(asm: CloudAssembly): AssetManifestArtifact {
const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0];
if (!manifestArtifact) {
throw new Error('no asset manifest in assembly');
}
return manifestArtifact;
}

function readAssetManifest(manifestArtifact: AssetManifestArtifact): AssetManifest {
return JSON.parse(fs.readFileSync(manifestArtifact.file, { encoding: 'utf-8' }));
}
Expand Up @@ -81,6 +81,11 @@
"Value": {
"Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:60dea2e16e94d1977b92fe03fa7085fea446233f1fe499702b69593438baa59f"
}
},
"ImageUri6": {
"Value": {
"Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:0a3355be12051c9984bf2b0b2bba4e6ea535968e5b6e7396449701732fe5ed14"
}
}
},
"Parameters": {
Expand Down
Expand Up @@ -262,8 +262,8 @@
"version": "0.0.0"
}
},
"ImageUri5": {
"id": "ImageUri5",
"ImageUri4": {
"id": "ImageUri4",
"path": "integ-assets-docker/ImageUri5",
"constructInfo": {
"fqn": "@aws-cdk/core.CfnOutput",
Expand Down
7 changes: 7 additions & 0 deletions packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.ts
Expand Up @@ -31,17 +31,24 @@ const asset5 = new assets.DockerImageAsset(stack, 'DockerImage5', {
},
});

const asset6 = new assets.DockerImageAsset(stack, 'DockerImage6', {
directory: path.join(__dirname, 'demo-image'),
cacheTo: { type: 'inline' },
});

const user = new iam.User(stack, 'MyUser');
asset.repository.grantPull(user);
asset2.repository.grantPull(user);
asset3.repository.grantPull(user);
asset4.repository.grantPull(user);
asset5.repository.grantPull(user);
asset6.repository.grantPull(user);

new cdk.CfnOutput(stack, 'ImageUri', { value: asset.imageUri });
new cdk.CfnOutput(stack, 'ImageUri2', { value: asset2.imageUri });
new cdk.CfnOutput(stack, 'ImageUri3', { value: asset3.imageUri });
new cdk.CfnOutput(stack, 'ImageUri4', { value: asset4.imageUri });
new cdk.CfnOutput(stack, 'ImageUri5', { value: asset5.imageUri });
new cdk.CfnOutput(stack, 'ImageUri6', { value: asset6.imageUri });

app.synth();
Expand Up @@ -97,6 +97,22 @@ export interface DockerImageSource {
* @see https://docs.docker.com/engine/reference/commandline/build/#custom-build-outputs
*/
readonly dockerOutputs?: string[];

/**
* Cache from options to pass to the `docker build` command.
*
* @default - no cache from options are passed to the build command
* @see https://docs.docker.com/build/cache/backends/
*/
readonly cacheFrom?: DockerCacheOption[];

/**
* Cache to options to pass to the `docker build` command.
*
* @default - no cache to options are passed to the build command
* @see https://docs.docker.com/build/cache/backends/
*/
readonly cacheTo?: DockerCacheOption;
}

/**
Expand All @@ -113,3 +129,25 @@ export interface DockerImageDestination extends AwsDestination {
*/
readonly imageTag: string;
}

/**
* Options for configuring the Docker cache backend
*/
export interface DockerCacheOption {
/**
* The type of cache to use.
* Refer to https://docs.docker.com/build/cache/backends/ for full list of backends.
* @default - unspecified
*
* @example 'registry'
*/
readonly type: string;
/**
* Any parameters to pass into the docker cache backend configuration.
* Refer to https://docs.docker.com/build/cache/backends/ for cache backend configuration.
* @default {} No options provided
*
* @example { ref: `12345678.dkr.ecr.us-west-2.amazonaws.com/cache:${branch}`, mode: "max" }
*/
readonly params?: { [key: string]: string };
}
Expand Up @@ -71,6 +71,28 @@ export interface Tag {
readonly value: string
}

/**
* Options for configuring the Docker cache backend
*/
export interface ContainerImageAssetCacheOption {
/**
* The type of cache to use.
* Refer to https://docs.docker.com/build/cache/backends/ for full list of backends.
* @default - unspecified
*
* @example 'registry'
*/
readonly type: string;
/**
* Any parameters to pass into the docker cache backend configuration.
* Refer to https://docs.docker.com/build/cache/backends/ for cache backend configuration.
* @default {} No options provided
*
* @example { ref: `12345678.dkr.ecr.us-west-2.amazonaws.com/cache:${branch}`, mode: "max" }
*/
readonly params?: { [key: string]: string };
}

/**
* Metadata Entry spec for container images.
*/
Expand Down Expand Up @@ -160,6 +182,22 @@ export interface ContainerImageAssetMetadataEntry extends BaseAssetMetadataEntry
* @see https://docs.docker.com/engine/reference/commandline/build/#custom-build-outputs
*/
readonly outputs?: string[];

/**
* Cache from options to pass to the `docker build` command.
*
* @default - no cache from options are passed to the build command
* @see https://docs.docker.com/build/cache/backends/
*/
readonly cacheFrom?: ContainerImageAssetCacheOption[];

/**
* Cache to options to pass to the `docker build` command.
*
* @default - no cache to options are passed to the build command
* @see https://docs.docker.com/build/cache/backends/
*/
readonly cacheTo?: ContainerImageAssetCacheOption;
}

/**
Expand Down
31 changes: 31 additions & 0 deletions packages/@aws-cdk/cloud-assembly-schema/schema/assets.schema.json
Expand Up @@ -176,9 +176,40 @@
"items": {
"type": "string"
}
},
"cacheFrom": {
"description": "Cache from options to pass to the `docker build` command. (Default - no cache from options are passed to the build command)",
"type": "array",
"items": {
"$ref": "#/definitions/DockerCacheOption"
}
},
"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"
}
}
},
"DockerCacheOption": {
"description": "Options for configuring the Docker cache backend",
"type": "object",
"properties": {
"type": {
"description": "The type of cache to use.\nRefer to https://docs.docker.com/build/cache/backends/ for full list of backends. (Default - unspecified)",
"type": "string"
},
"params": {
"description": "Any parameters to pass into the docker cache backend configuration.\nRefer to https://docs.docker.com/build/cache/backends/ for cache backend configuration. (Default {} No options provided)",
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"required": [
"type"
]
},
"DockerImageDestination": {
"description": "Where to publish docker images",
"type": "object",
Expand Down