From 1b25a4b44e992f076d0bcf2d805880fdbebca34a Mon Sep 17 00:00:00 2001 From: Eli Polonsky Date: Mon, 6 Jan 2020 10:58:51 +0200 Subject: [PATCH] feat(ecr-assets): custom docker files (#5652) * support custom docker files * revert tsconfig changes * doc strings modification according to conventions --- .../aws-ecr-assets/lib/image-asset.ts | 15 ++++++- .../Dockerfile.Custom | 5 +++ .../demo-image-custom-docker-file/index.py | 33 ++++++++++++++ .../aws-ecr-assets/test/test.image-asset.ts | 33 +++++++++++++- .../aws-ecs/lib/images/asset-image.ts | 9 ++++ packages/@aws-cdk/core/lib/assets.ts | 7 +++ packages/@aws-cdk/core/lib/stack.ts | 3 +- packages/@aws-cdk/cx-api/lib/assets.ts | 8 ++++ packages/aws-cdk/lib/docker.ts | 4 ++ packages/aws-cdk/test/test.docker.ts | 43 +++++++++++++++++++ 10 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecr-assets/test/demo-image-custom-docker-file/Dockerfile.Custom create mode 100644 packages/@aws-cdk/aws-ecr-assets/test/demo-image-custom-docker-file/index.py diff --git a/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts b/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts index 77c532d41881d..1b733aafbab96 100644 --- a/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts +++ b/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts @@ -39,6 +39,13 @@ export interface DockerImageAssetProps extends assets.CopyOptions { * @default - no target */ readonly target?: string; + + /** + * Path to the Dockerfile (relative to the directory). + * + * @default 'Dockerfile' + */ + readonly file?: string; } /** @@ -71,8 +78,11 @@ export class DockerImageAsset extends Construct implements assets.IAsset { if (!fs.existsSync(dir)) { throw new Error(`Cannot find image directory at ${dir}`); } - if (!fs.existsSync(path.join(dir, 'Dockerfile'))) { - throw new Error(`No 'Dockerfile' found in ${dir}`); + + // validate the docker file exists + const file = path.join(dir, props.file || 'Dockerfile'); + if (!fs.existsSync(file)) { + throw new Error(`Cannot find file at ${file}`); } let exclude: string[] = props.exclude || []; @@ -96,6 +106,7 @@ export class DockerImageAsset extends Construct implements assets.IAsset { directoryName: staging.stagedPath, dockerBuildArgs: props.buildArgs, dockerBuildTarget: props.target, + dockerFile: file, repositoryName: props.repositoryName || `cdk/${this.node.uniqueId.replace(/[:/]/g, '-').toLowerCase()}`, sourceHash: staging.sourceHash }); diff --git a/packages/@aws-cdk/aws-ecr-assets/test/demo-image-custom-docker-file/Dockerfile.Custom b/packages/@aws-cdk/aws-ecr-assets/test/demo-image-custom-docker-file/Dockerfile.Custom new file mode 100644 index 0000000000000..123b5670febc8 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr-assets/test/demo-image-custom-docker-file/Dockerfile.Custom @@ -0,0 +1,5 @@ +FROM python:3.6 +EXPOSE 8000 +WORKDIR /src +ADD . /src +CMD python3 index.py diff --git a/packages/@aws-cdk/aws-ecr-assets/test/demo-image-custom-docker-file/index.py b/packages/@aws-cdk/aws-ecr-assets/test/demo-image-custom-docker-file/index.py new file mode 100644 index 0000000000000..2ccedfce3ab76 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr-assets/test/demo-image-custom-docker-file/index.py @@ -0,0 +1,33 @@ +#!/usr/bin/python +import sys +import textwrap +import http.server +import socketserver + +PORT = 8000 + + +class Handler(http.server.SimpleHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header('Content-Type', 'text/html') + self.end_headers() + self.wfile.write(textwrap.dedent('''\ + + It works + +

Hello from the integ test container

+

This container got built and started as part of the integ test.

+ + + ''').encode('utf-8')) + + +def main(): + httpd = http.server.HTTPServer(("", PORT), Handler) + print("serving at port", PORT) + httpd.serve_forever() + + +if __name__ == '__main__': + main() diff --git a/packages/@aws-cdk/aws-ecr-assets/test/test.image-asset.ts b/packages/@aws-cdk/aws-ecr-assets/test/test.image-asset.ts index 57e6598485876..1d109532c433c 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/test.image-asset.ts +++ b/packages/@aws-cdk/aws-ecr-assets/test/test.image-asset.ts @@ -87,6 +87,23 @@ export = { test.done(); }, + 'with file'(test: Test) { + // GIVEN + const stack = new Stack(); + + const directoryPath = path.join(__dirname, 'demo-image-custom-docker-file'); + // WHEN + new DockerImageAsset(stack, 'Image', { + directory: directoryPath, + file: 'Dockerfile.Custom' + }); + + // THEN + const assetMetadata = stack.node.metadata.find(({ type }) => type === ASSET_METADATA); + test.deepEqual(assetMetadata && assetMetadata.data.file, path.join(directoryPath, 'Dockerfile.Custom')); + test.done(); + }, + 'asset.repository.grantPull can be used to grant a principal permissions to use the image'(test: Test) { // GIVEN const stack = new Stack(); @@ -211,7 +228,21 @@ export = { new DockerImageAsset(stack, 'Asset', { directory: __dirname }); - }, /No 'Dockerfile' found in/); + }, /Cannot find file at/); + test.done(); + }, + + 'fails if the file does not exist'(test: Test) { + // GIVEN + const stack = new Stack(); + + // THEN + test.throws(() => { + new DockerImageAsset(stack, 'Asset', { + directory: __dirname, + file: 'doesnt-exist' + }); + }, /Cannot find file at/); test.done(); }, diff --git a/packages/@aws-cdk/aws-ecs/lib/images/asset-image.ts b/packages/@aws-cdk/aws-ecs/lib/images/asset-image.ts index b4af251572837..541a92b77a1b1 100644 --- a/packages/@aws-cdk/aws-ecs/lib/images/asset-image.ts +++ b/packages/@aws-cdk/aws-ecs/lib/images/asset-image.ts @@ -20,6 +20,14 @@ export interface AssetImageProps { * @default none */ readonly target?: string; + + /** + * Path to the Dockerfile (relative to the directory). + * + * @default 'Dockerfile' + */ + readonly file?: string; + } /** @@ -40,6 +48,7 @@ export class AssetImage extends ContainerImage { directory: this.directory, buildArgs: this.props.buildArgs, target: this.props.target, + file: this.props.file, }); asset.repository.grantPull(containerDefinition.taskDefinition.obtainExecutionRole()); diff --git a/packages/@aws-cdk/core/lib/assets.ts b/packages/@aws-cdk/core/lib/assets.ts index 55d11ce02571a..ed2e59cd94d0f 100644 --- a/packages/@aws-cdk/core/lib/assets.ts +++ b/packages/@aws-cdk/core/lib/assets.ts @@ -57,6 +57,13 @@ export interface DockerImageAssetSource { */ readonly dockerBuildTarget?: string; + /** + * Path to the Dockerfile (relative to the directory). + * + * @default - no file + */ + readonly dockerFile?: string; + /** * ECR repository name * diff --git a/packages/@aws-cdk/core/lib/stack.ts b/packages/@aws-cdk/core/lib/stack.ts index 15fa0304fdf5d..1683063020163 100644 --- a/packages/@aws-cdk/core/lib/stack.ts +++ b/packages/@aws-cdk/core/lib/stack.ts @@ -563,7 +563,8 @@ export class Stack extends Construct implements ITaggable { imageNameParameter: params.imageNameParameter.logicalId, repositoryName: asset.repositoryName, buildArgs: asset.dockerBuildArgs, - target: asset.dockerBuildTarget + target: asset.dockerBuildTarget, + file: asset.dockerFile, }; this.node.addMetadata(cxapi.ASSET_METADATA, metadata); diff --git a/packages/@aws-cdk/cx-api/lib/assets.ts b/packages/@aws-cdk/cx-api/lib/assets.ts index f144a8ff03dce..b19730973daf7 100644 --- a/packages/@aws-cdk/cx-api/lib/assets.ts +++ b/packages/@aws-cdk/cx-api/lib/assets.ts @@ -106,6 +106,14 @@ export interface ContainerImageAssetMetadataEntry extends BaseAssetMetadataEntry * @default no build target */ readonly target?: string; + + /** + * Path to the Dockerfile (relative to the directory). + * + * @default - no file is passed + */ + readonly file?: string; + } export type AssetMetadataEntry = FileAssetMetadataEntry | ContainerImageAssetMetadataEntry; diff --git a/packages/aws-cdk/lib/docker.ts b/packages/aws-cdk/lib/docker.ts index ca50706114fb4..5d229460642b3 100644 --- a/packages/aws-cdk/lib/docker.ts +++ b/packages/aws-cdk/lib/docker.ts @@ -74,6 +74,10 @@ export async function prepareContainerAsset(assemblyDir: string, baseCommand.push('--target', asset.target); } + if (asset.file) { + baseCommand.push('--file', asset.file); + } + const command = ci ? [...baseCommand, '--cache-from', latest] // This does not fail if latest is not available : baseCommand; diff --git a/packages/aws-cdk/test/test.docker.ts b/packages/aws-cdk/test/test.docker.ts index 6c57f65aa541f..265b8efcadf04 100644 --- a/packages/aws-cdk/test/test.docker.ts +++ b/packages/aws-cdk/test/test.docker.ts @@ -264,6 +264,49 @@ export = { test.done(); }, + async 'passes the correct file to docker build'(test: Test) { + // GIVEN + const toolkit = new ToolkitInfo({ + sdk: new MockSDK(), + bucketName: 'BUCKET_NAME', + bucketEndpoint: 'BUCKET_ENDPOINT', + environment: { name: 'env', account: '1234', region: 'abc' } + }); + + const prepareEcrRepositoryStub = sinon.stub(toolkit, 'prepareEcrRepository').resolves({ + repositoryUri: 'uri', + repositoryName: 'name' + }); + + const shellStub = sinon.stub(os, 'shell').rejects('STOPTEST'); + + // WHEN + const asset: cxapi.ContainerImageAssetMetadataEntry = { + id: 'assetId', + imageNameParameter: 'MyParameter', + packaging: 'container-image', + path: '/foo', + sourceHash: '1234567890abcdef', + repositoryName: 'some-name', + file: 'some-file' + }; + + try { + await prepareContainerAsset('.', asset, toolkit, false); + } catch (e) { + if (!/STOPTEST/.test(e.toString())) { throw e; } + } + + // THEN + const command = ['docker', 'build', '--tag', `uri:latest`, '/foo', '--file', 'some-file']; + + test.ok(shellStub.calledWith(command)); + + prepareEcrRepositoryStub.restore(); + shellStub.restore(); + test.done(); + }, + async 'relative path'(test: Test) { // GIVEN const toolkit = new ToolkitInfo({