From 9ab90066ee3a539ee9b20d8c43f780202ab41892 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 22 Nov 2018 14:16:46 +0200 Subject: [PATCH] feat(aws-codebuild): allow using docker image assets as build images This change enables using Docker assets as CodeBuild images. In order to enable that, a few changes were required in how Docker assets are modeled: Extracted a low-level `DockerImageAsset` class into a new module called @aws-cdk/assets-docker. This module provides the basic interaction with the toolkit and allows higher level APIs such as ECR or CodeBuild to manage permissions on the asset repository. Since CodeBuild needs permissions at the resource policy level (since images are not pulled via an assumed role), the adopted repository custom resource was extended to support specifying an arbitrary policy document for the repository. This is made available via an overload of `addToResourcePolicy` implemented by `AdoptedRepository`. In order to fix #1232 and simplify, the protocol between the toolkit and the asset class have been changed to only pass in the _repository name_ and the _tag_ (together). Previously the ARN of the repository was used, but it is impossible to parse the repository name from the ARN using CFN's split/select. It is possible to produce the ARN from a name, and the repository is ensured to be "local" to the account/region. Modernized ECR import/export and converted `RepositoryRef` to `IRepository`, and modified `ImportedRepository` to only parse the repository name from ARN in case the ARN is concrete (not a token). Otherwise, the name is also required. Renamed `grantUseImage` to `grantPull` and added `grantPullPush`. Added `repositoryUriForTag(tag)`. Fixes #1219 BREAKING CHANGE: `ecr.RepositoryRef` has been replaced by `ecr.IRepository`, which means that `RepositoryRef.import` is now `Repository.import`. Futhermore, the CDK Toolkit must also be upgraded since the docker asset protocol was modified. `IRepository.grantUseImage` was renamed to `IRepository.grantPull`. --- docs/src/assets.rst | 19 +- packages/@aws-cdk/assets-docker/.gitignore | 17 + packages/@aws-cdk/assets-docker/.npmignore | 16 + packages/@aws-cdk/assets-docker/LICENSE | 201 +++++++++ packages/@aws-cdk/assets-docker/NOTICE | 2 + packages/@aws-cdk/assets-docker/README.md | 38 ++ .../lib/adopt-repository/.gitignore | 1 + .../lib}/adopt-repository/handler.js | 35 +- .../@aws-cdk/assets-docker/lib/image-asset.ts | 151 +++++++ packages/@aws-cdk/assets-docker/lib/index.ts | 1 + .../@aws-cdk/assets-docker/package-lock.json | 5 + packages/@aws-cdk/assets-docker/package.json | 76 ++++ .../assets-docker/test/demo-image/Dockerfile | 5 + .../assets-docker/test/demo-image/index.py | 33 ++ .../assets-docker/test/test.image-asset.ts | 404 ++++++++++++++++++ packages/@aws-cdk/aws-codebuild/README.md | 81 +++- .../@aws-cdk/aws-codebuild/lib/project.ts | 104 ++++- packages/@aws-cdk/aws-codebuild/package.json | 5 +- .../aws-codebuild/test/demo-image/Dockerfile | 5 + .../aws-codebuild/test/demo-image/index.py | 33 ++ .../aws-codebuild/test/integ.defaults.lit.ts | 23 + .../test/integ.docker-asset.expected.json | 302 +++++++++++++ .../test/integ.docker-asset.lit.ts | 33 ++ .../test/integ.ecr.lit.expected.json | 184 ++++++++ .../aws-codebuild/test/integ.ecr.lit.ts | 33 ++ .../@aws-cdk/aws-ecr/lib/repository-ref.ts | 159 ++++++- packages/@aws-cdk/aws-ecr/lib/repository.ts | 4 +- .../@aws-cdk/aws-ecr/test/test.repository.ts | 79 +++- .../@aws-cdk/aws-ecs/lib/container-image.ts | 4 +- .../aws-ecs/lib/images/asset-image.ts | 113 +---- packages/@aws-cdk/aws-ecs/lib/images/ecr.ts | 8 +- packages/@aws-cdk/aws-ecs/package.json | 3 +- .../fargate/integ.asset-image.expected.json | 251 +++++++++-- .../@aws-cdk/aws-ecs/test/test.asset-image.ts | 148 ------- packages/@aws-cdk/cx-api/lib/cxapi.ts | 9 +- packages/aws-cdk/lib/api/toolkit-info.ts | 8 +- packages/aws-cdk/lib/docker.ts | 3 +- 37 files changed, 2245 insertions(+), 351 deletions(-) create mode 100644 packages/@aws-cdk/assets-docker/.gitignore create mode 100644 packages/@aws-cdk/assets-docker/.npmignore create mode 100644 packages/@aws-cdk/assets-docker/LICENSE create mode 100644 packages/@aws-cdk/assets-docker/NOTICE create mode 100644 packages/@aws-cdk/assets-docker/README.md create mode 100644 packages/@aws-cdk/assets-docker/lib/adopt-repository/.gitignore rename packages/@aws-cdk/{aws-ecs/lib/images => assets-docker/lib}/adopt-repository/handler.js (81%) create mode 100644 packages/@aws-cdk/assets-docker/lib/image-asset.ts create mode 100644 packages/@aws-cdk/assets-docker/lib/index.ts create mode 100644 packages/@aws-cdk/assets-docker/package-lock.json create mode 100644 packages/@aws-cdk/assets-docker/package.json create mode 100644 packages/@aws-cdk/assets-docker/test/demo-image/Dockerfile create mode 100644 packages/@aws-cdk/assets-docker/test/demo-image/index.py create mode 100644 packages/@aws-cdk/assets-docker/test/test.image-asset.ts create mode 100644 packages/@aws-cdk/aws-codebuild/test/demo-image/Dockerfile create mode 100644 packages/@aws-cdk/aws-codebuild/test/demo-image/index.py create mode 100644 packages/@aws-cdk/aws-codebuild/test/integ.defaults.lit.ts create mode 100644 packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.expected.json create mode 100644 packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.lit.ts create mode 100644 packages/@aws-cdk/aws-codebuild/test/integ.ecr.lit.expected.json create mode 100644 packages/@aws-cdk/aws-codebuild/test/integ.ecr.lit.ts delete mode 100644 packages/@aws-cdk/aws-ecs/test/test.asset-image.ts diff --git a/docs/src/assets.rst b/docs/src/assets.rst index d2087e2ba7df9..4bcc0fb31f9ee 100644 --- a/docs/src/assets.rst +++ b/docs/src/assets.rst @@ -9,20 +9,21 @@ limitations under the License. .. _assets: - + ###### Assets ###### -Assets are local files or directories which can be bundled into CDK constructs -and apps. A common example is a directory which contains the handler code for an -AWS Lambda function, but assets can represent any artifact that is needed for -the app’s operation. +Assets are local files, directories or docker images which can be bundled into +CDK constructs and apps. A common example is a directory which contains the +handler code for an AWS Lambda function, but assets can represent any artifact +that is needed for the app’s operation. When deploying an AWS CDK app that includes constructs with assets, the toolkit -will first upload all the assets to S3, and only then deploy the stacks. The S3 -locations of the uploaded assets will be passed in as CloudFormation Parameters +will first prepare and publish them to S3 or ECR, and only then deploy the stacks. +The locations of the published assets will be passed in as CloudFormation Parameters to the relevant stacks. -For more details, see the :py:doc:`Assets ` library documentation. - +See :py:doc:`Assets ` for documentation about file assets +and :py:doc:`Docker Assets ` for documentation about +Docker image assets. diff --git a/packages/@aws-cdk/assets-docker/.gitignore b/packages/@aws-cdk/assets-docker/.gitignore new file mode 100644 index 0000000000000..2a7ab813ef1bf --- /dev/null +++ b/packages/@aws-cdk/assets-docker/.gitignore @@ -0,0 +1,17 @@ +*.js +*.js.map +*.d.ts +node_modules +dist +tsconfig.json +tslint.json + +.LAST_BUILD +.nyc_output +coverage + +.jsii + +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/packages/@aws-cdk/assets-docker/.npmignore b/packages/@aws-cdk/assets-docker/.npmignore new file mode 100644 index 0000000000000..b757d55c46996 --- /dev/null +++ b/packages/@aws-cdk/assets-docker/.npmignore @@ -0,0 +1,16 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk \ No newline at end of file diff --git a/packages/@aws-cdk/assets-docker/LICENSE b/packages/@aws-cdk/assets-docker/LICENSE new file mode 100644 index 0000000000000..1739faaebb745 --- /dev/null +++ b/packages/@aws-cdk/assets-docker/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/assets-docker/NOTICE b/packages/@aws-cdk/assets-docker/NOTICE new file mode 100644 index 0000000000000..95fd48569c743 --- /dev/null +++ b/packages/@aws-cdk/assets-docker/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/assets-docker/README.md b/packages/@aws-cdk/assets-docker/README.md new file mode 100644 index 0000000000000..39b5f4f39dfbb --- /dev/null +++ b/packages/@aws-cdk/assets-docker/README.md @@ -0,0 +1,38 @@ +# AWS CDK Docker Image Assets + +This module allows bundling Docker images as assets. + +Images are built from a local Docker context directory (with a `Dockerfile`), +uploaded to ECR by the CDK toolkit and/or your app's CI-CD pipeline, and can be +naturally referenced in your CDK app. + +```typescript +import { DockerImageAsset } from '@aws-cdk/assets-docker'; + +const asset = new DockerImageAsset(this, 'MyBuildImage', { + directory: path.join(__dirname, 'my-image') +}); +``` + +The directory `my-image` must include a `Dockerfile`. + +This will instruct the toolkit to build a Docker image from `my-image`, push it +to an AWS ECR repository and wire the name of the repository as CloudFormation +parameters to your stack. + +Use `asset.imageUri` can be used to reference the image (it includes both the +ECR image URL and tag. + +### Pull Permissions + +Depending on the consumer of your image asset, you will need to make sure +the principal has permissions to pull the image. + +In most cases, you should use the `asset.repository.grantPull(principal)` +method. This will modify the IAM policy of the principal to allow it to +pull images from this repository. + +If the pulling principal is not in the same account or is an AWS service that +doesn't assume a role in your account (e.g. AWS CodeBuild), pull permissions +must be granted on the __resource policy__ (and not on the principal's policy). +To do that, you can use `asset.repository.addToResourcePolicy(statement)`. diff --git a/packages/@aws-cdk/assets-docker/lib/adopt-repository/.gitignore b/packages/@aws-cdk/assets-docker/lib/adopt-repository/.gitignore new file mode 100644 index 0000000000000..d4aa116a26c73 --- /dev/null +++ b/packages/@aws-cdk/assets-docker/lib/adopt-repository/.gitignore @@ -0,0 +1 @@ +!*.js diff --git a/packages/@aws-cdk/aws-ecs/lib/images/adopt-repository/handler.js b/packages/@aws-cdk/assets-docker/lib/adopt-repository/handler.js similarity index 81% rename from packages/@aws-cdk/aws-ecs/lib/images/adopt-repository/handler.js rename to packages/@aws-cdk/assets-docker/lib/adopt-repository/handler.js index 640b5d4e538f3..de35baa327ac2 100644 --- a/packages/@aws-cdk/aws-ecs/lib/images/adopt-repository/handler.js +++ b/packages/@aws-cdk/assets-docker/lib/adopt-repository/handler.js @@ -13,8 +13,8 @@ exports.handler = async function(event, context, _callback, respond) { Principal: "*" }; - function repoName(props) { - return props.RepositoryArn.split('/').slice(1).join('/'); + function repoName(arn) { + return arn.split('/').slice(1).join('/'); } // The repository must already exist @@ -30,7 +30,7 @@ exports.handler = async function(event, context, _callback, respond) { } } - const repo = repoName(event.ResourceProperties); + const repo = repoName(event.ResourceProperties.RepositoryArn); const adopter = await getAdopter(repo); if (event.RequestType === 'Delete') { if (adopter.Sid !== markerStatement.Sid) { @@ -51,16 +51,33 @@ exports.handler = async function(event, context, _callback, respond) { throw new Error(`This repository is already owned by another stack: ${adopter.Sid}`); } console.log('Adopting', repo); - await ecr.setRepositoryPolicy({ repositoryName: repo, policyText: JSON.stringify({ + + const policy = event.ResourceProperties.PolicyDocument || { Version: '2008-10-17', - Statement: [markerStatement] - }) }).promise(); + Statement: [ ] + }; + + if (!policy.Version) { + policy.Version = '2008-10-17'; + } + + if (!policy.Statement) { + policy.Statement = [ ]; + } + + if (!Array.isArray(policy.Statement)) { + policy.Statement = [ policy.Statement ]; + } + + policy.Statement.push(markerStatement); + + console.log('policy document:', JSON.stringify(policy, undefined, 2)); + + await ecr.setRepositoryPolicy({ repositoryName: repo, policyText: JSON.stringify(policy) }).promise(); } const arn = event.ResourceProperties.RepositoryArn.split(':'); - await respond("SUCCESS", "OK", repo, { - RepositoryUri: `${arn[4]}.dkr.ecr.${arn[3]}.amazonaws.com/${repoName(event.ResourceProperties)}` - }); + await respond("SUCCESS", "OK", repo, {}); } catch (e) { console.log(e); await respond("FAILED", e.message, context.logStreamName, {}); diff --git a/packages/@aws-cdk/assets-docker/lib/image-asset.ts b/packages/@aws-cdk/assets-docker/lib/image-asset.ts new file mode 100644 index 0000000000000..cb803e6571a3e --- /dev/null +++ b/packages/@aws-cdk/assets-docker/lib/image-asset.ts @@ -0,0 +1,151 @@ +import cfn = require('@aws-cdk/aws-cloudformation'); +import ecr = require('@aws-cdk/aws-ecr'); +import iam = require('@aws-cdk/aws-iam'); +import lambda = require('@aws-cdk/aws-lambda'); +import cdk = require('@aws-cdk/cdk'); +import cxapi = require('@aws-cdk/cx-api'); +import fs = require('fs'); +import path = require('path'); + +export interface DockerImageAssetProps { + /** + * The directory where the Dockerfile is stored + */ + directory: string; +} + +/** + * An asset that represents a Docker image. + * + * The image will be created in build time and uploaded to an ECR repository. + */ +export class DockerImageAsset extends cdk.Construct { + /** + * The full URI of the image (including a tag). Use this reference to pull + * the asset. + */ + public imageUri: string; + + /** + * Repository where the image is stored + */ + public repository: ecr.IRepository; + + /** + * Directory where the source files are stored + */ + private readonly directory: string; + + constructor(parent: cdk.Construct, id: string, props: DockerImageAssetProps) { + super(parent, id); + + // resolve full path + this.directory = path.resolve(props.directory); + if (!fs.existsSync(this.directory)) { + throw new Error(`Cannot find image directory at ${this.directory}`); + } + if (!fs.existsSync(path.join(this.directory, 'Dockerfile'))) { + throw new Error(`No 'Dockerfile' found in ${this.directory}`); + } + + const imageNameParameter = new cdk.Parameter(this, 'ImageName', { + type: 'String', + description: `ECR repository name and tag asset "${this.path}"`, + }); + + const asset: cxapi.ContainerImageAssetMetadataEntry = { + packaging: 'container-image', + path: this.directory, + id: this.uniqueId, + imageNameParameter: imageNameParameter.logicalId + }; + + this.addMetadata(cxapi.ASSET_METADATA, asset); + + const components = new cdk.FnSplit(':', imageNameParameter.value); + const repositoryName = new cdk.FnSelect(0, components).toString(); + const imageTag = new cdk.FnSelect(1, components).toString(); + + this.repository = ecr.Repository.import(this, 'RepositoryObject', { + repositoryArn: ecr.Repository.arnForLocalRepository(repositoryName), + repositoryName, + }); + + // Require that repository adoption happens first, so we route the + // input ARN into the Custom Resource and then get the URI which we use to + // refer to the image FROM the Custom Resource. + // + // If adoption fails (because the repository might be twice-adopted), we + // haven't already started using the image. + this.repository = new AdoptRepository(this, 'AdoptRepository', { + repository: this.repository, + }); + + this.imageUri = this.repository.repositoryUriForTag(imageTag); + } +} + +interface AdoptRepositoryProps { + /** + * The imported ECR repository + */ + repository: ecr.IRepository; +} + +/** + * Custom Resource which will adopt the repository used for the locally built + * image into the stack. + * + * Since the repository is not created by the stack (but by the CDK toolkit), + * adopting will make the repository "owned" by the stack. It will be cleaned + * up when the stack gets deleted, to avoid leaving orphaned repositories on + * stack cleanup. + */ +class AdoptRepository extends ecr.RepositoryBase { + private readonly policyDocument = new iam.PolicyDocument(); + private readonly importedRepository: ecr.IRepository; + + constructor(parent: cdk.Construct, id: string, props: AdoptRepositoryProps) { + super(parent, id); + + const fn = new lambda.SingletonFunction(this, 'Function', { + runtime: lambda.Runtime.NodeJS810, + lambdaPurpose: 'AdoptEcrRepository', + handler: 'handler.handler', + code: lambda.Code.asset(path.join(__dirname, 'adopt-repository')), + uuid: 'dbc60def-c595-44bc-aa5c-28c95d68f62c', + timeout: 300 + }); + + fn.addToRolePolicy(new iam.PolicyStatement() + .addActions('ecr:GetRepositoryPolicy', 'ecr:SetRepositoryPolicy', +'ecr:DeleteRepository', 'ecr:ListImages', 'ecr:BatchDeleteImage') + .addResource(props.repository.repositoryArn)); + + new cfn.CustomResource(this, 'Resource', { + resourceType: 'Custom::CDKECRRepositoryAdoption', + lambdaProvider: fn, + properties: { + RepositoryArn: props.repository.repositoryArn, + PolicyDocument: this.policyDocument + } + }); + + this.importedRepository = props.repository; + } + + public get repositoryName() { + return this.importedRepository.repositoryName; + } + + public get repositoryArn() { + return this.importedRepository.repositoryArn; + } + + /** + * Adds a statement to the repository resource policy + */ + public addToResourcePolicy(statement: iam.PolicyStatement) { + this.policyDocument.addStatement(statement); + } +} diff --git a/packages/@aws-cdk/assets-docker/lib/index.ts b/packages/@aws-cdk/assets-docker/lib/index.ts new file mode 100644 index 0000000000000..579fee533587d --- /dev/null +++ b/packages/@aws-cdk/assets-docker/lib/index.ts @@ -0,0 +1 @@ +export * from './image-asset'; diff --git a/packages/@aws-cdk/assets-docker/package-lock.json b/packages/@aws-cdk/assets-docker/package-lock.json new file mode 100644 index 0000000000000..a4c1960928be4 --- /dev/null +++ b/packages/@aws-cdk/assets-docker/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "@aws-cdk/assets", + "version": "0.9.0", + "lockfileVersion": 1 +} diff --git a/packages/@aws-cdk/assets-docker/package.json b/packages/@aws-cdk/assets-docker/package.json new file mode 100644 index 0000000000000..9eb23dd24bfc9 --- /dev/null +++ b/packages/@aws-cdk/assets-docker/package.json @@ -0,0 +1,76 @@ +{ + "name": "@aws-cdk/assets-docker", + "version": "0.18.0", + "description": "Docker image assets", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awscdk.assets.docker", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "cdk-assets-docker" + } + }, + "dotnet": { + "namespace": "Amazon.CDK.Assets.Docker", + "packageId": "Amazon.CDK.Assets.Docker", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk" + }, + "sphinx": {} + } + }, + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-cdk.git" + }, + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package" + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "assets", + "docker" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assert": "^0.18.0", + "aws-cdk": "^0.18.0", + "cdk-build-tools": "^0.18.0", + "cdk-integ-tools": "^0.18.0", + "pkglint": "^0.18.0", + "@types/proxyquire": "^1.3.28", + "proxyquire": "^2.1.0" + }, + "dependencies": { + "@aws-cdk/aws-lambda": "^0.18.0", + "@aws-cdk/aws-cloudformation": "^0.18.0", + "@aws-cdk/aws-ecr": "^0.18.0", + "@aws-cdk/aws-iam": "^0.18.0", + "@aws-cdk/aws-s3": "^0.18.0", + "@aws-cdk/cdk": "^0.18.0", + "@aws-cdk/cx-api": "^0.18.0" + }, + "homepage": "https://github.com/awslabs/aws-cdk", + "peerDependencies": { + "@aws-cdk/aws-iam": "^0.18.0", + "@aws-cdk/aws-s3": "^0.18.0", + "@aws-cdk/cdk": "^0.18.0" + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/assets-docker/test/demo-image/Dockerfile b/packages/@aws-cdk/assets-docker/test/demo-image/Dockerfile new file mode 100644 index 0000000000000..123b5670febc8 --- /dev/null +++ b/packages/@aws-cdk/assets-docker/test/demo-image/Dockerfile @@ -0,0 +1,5 @@ +FROM python:3.6 +EXPOSE 8000 +WORKDIR /src +ADD . /src +CMD python3 index.py diff --git a/packages/@aws-cdk/assets-docker/test/demo-image/index.py b/packages/@aws-cdk/assets-docker/test/demo-image/index.py new file mode 100644 index 0000000000000..2ccedfce3ab76 --- /dev/null +++ b/packages/@aws-cdk/assets-docker/test/demo-image/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/assets-docker/test/test.image-asset.ts b/packages/@aws-cdk/assets-docker/test/test.image-asset.ts new file mode 100644 index 0000000000000..0ead553c21830 --- /dev/null +++ b/packages/@aws-cdk/assets-docker/test/test.image-asset.ts @@ -0,0 +1,404 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import iam = require('@aws-cdk/aws-iam'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import path = require('path'); +import proxyquire = require('proxyquire'); +import { DockerImageAsset } from '../lib'; + +// tslint:disable:object-literal-key-quotes + +let ecrMock: any; + +export = { + 'test instantiating Asset Image'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new DockerImageAsset(stack, 'Image', { + directory: path.join(__dirname, 'demo-image'), + }); + + // THEN + const template = stack.toCloudFormation(); + + test.deepEqual(template.Parameters.ImageImageName5E684353, { + Type: 'String', + Description: 'ECR repository name and tag asset "Image"' + }); + + test.done(); + }, + + 'asset.repository.grantPull can be used to grant a principal permissions to use the image'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const user = new iam.User(stack, 'MyUser'); + const asset = new DockerImageAsset(stack, 'Image', { + directory: path.join(__dirname, 'demo-image') + }); + + // WHEN + asset.repository.grantPull(user); + + // THEN + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + "Statement": [ + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { "Ref": "AWS::Partition" }, + ":ecr:", + { "Ref": "AWS::Region" }, + ":", + { "Ref": "AWS::AccountId" }, + ":repository/", + { "Fn::Select": [ 0, { "Fn::Split": [ ":", { "Ref": "ImageImageName5E684353" } ] } ] } + ] + ] + } + }, + { + "Action": [ + "ecr:GetAuthorizationToken", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyUserDefaultPolicy7B897426", + "Users": [ + { + "Ref": "MyUserDC45028B" + } + ] + })); + + test.done(); + }, + + 'asset.repository.addToResourcePolicy can be used to modify the ECR resource policy via the adoption custom resource'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const asset = new DockerImageAsset(stack, 'Image', { + directory: path.join(__dirname, 'demo-image') + }); + + // WHEN + asset.repository.addToResourcePolicy(new iam.PolicyStatement() + .addAction('BOOM') + .addPrincipal(new iam.ServicePrincipal('DAMN'))); + + // THEN + expect(stack).to(haveResource('Custom::CDKECRRepositoryAdoption', { + "RepositoryArn": { + "Fn::Join": [ + "", + [ + "arn:", + { "Ref": "AWS::Partition" }, + ":ecr:", + { "Ref": "AWS::Region" }, + ":", + { "Ref": "AWS::AccountId" }, + ":repository/", + { + "Fn::Select": [ 0, { "Fn::Split": [ ":", { "Ref": "ImageImageName5E684353" } ] } ] + } + ] + ] + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "BOOM", + "Effect": "Allow", + "Principal": { + "Service": "DAMN" + } + } + ], + "Version": "2012-10-17" + } + })); + + test.done(); + }, + + async 'exercise handler create with policy'(test: Test) { + const handler = proxyquire(path.resolve(__dirname, '..', 'lib', 'adopt-repository', 'handler'), { + 'aws-sdk': { + '@noCallThru': true, + "ECR": ECRWithEmptyPolicy, + } + }); + + let output; + async function response(responseStatus: string, reason: string, physId: string, data: any) { + output = { responseStatus, reason, physId, data }; + } + + await handler.handler({ + StackId: 'StackId', + ResourceProperties: { + RepositoryArn: 'RepositoryArn', + PolicyDocument: { + Version: '2008-10-01', + My: 'Document' + } + }, + RequestType: 'Create', + ResponseURL: 'https://localhost/test' + }, { + logStreamName: 'xyz', + }, undefined, response); + + test.deepEqual(JSON.parse(ecrMock.lastSetRepositoryPolicyRequest.policyText), { + My: "Document", + Version: '2008-10-01', + Statement: [ + { Sid: "StackId", Effect: "Deny", Action: "OwnedBy:CDKStack", Principal: "*" } + ] + }); + + test.deepEqual(output, { + responseStatus: 'SUCCESS', + reason: 'OK', + physId: '', + data: { } + }); + + test.done(); + }, + + async 'exercise handler create with policy with object statement'(test: Test) { + const handler = proxyquire(path.resolve(__dirname, '..', 'lib', 'adopt-repository', 'handler'), { + 'aws-sdk': { + '@noCallThru': true, + "ECR": ECRWithEmptyPolicy, + } + }); + + let output; + async function response(responseStatus: string, reason: string, physId: string, data: any) { + output = { responseStatus, reason, physId, data }; + } + + await handler.handler({ + StackId: 'StackId', + ResourceProperties: { + RepositoryArn: 'RepositoryArn', + PolicyDocument: { + Statement: { Action: 'boom' } + } + }, + RequestType: 'Create', + ResponseURL: 'https://localhost/test' + }, { + logStreamName: 'xyz', + }, undefined, response); + + test.deepEqual(JSON.parse(ecrMock.lastSetRepositoryPolicyRequest.policyText), { + Version: '2008-10-17', + Statement: [ + { Action: 'boom' }, + { Sid: "StackId", Effect: "Deny", Action: "OwnedBy:CDKStack", Principal: "*" } + ] + }); + + test.deepEqual(output, { + responseStatus: 'SUCCESS', + reason: 'OK', + physId: '', + data: { } + }); + + test.done(); + }, + + async 'exercise handler create with policy with array statement'(test: Test) { + const handler = proxyquire(path.resolve(__dirname, '..', 'lib', 'adopt-repository', 'handler'), { + 'aws-sdk': { + '@noCallThru': true, + "ECR": ECRWithEmptyPolicy, + } + }); + + let output; + async function response(responseStatus: string, reason: string, physId: string, data: any) { + output = { responseStatus, reason, physId, data }; + } + + await handler.handler({ + StackId: 'StackId', + ResourceProperties: { + RepositoryArn: 'RepositoryArn', + PolicyDocument: { + Statement: [ { Action: 'boom' }, { Resource: "foo" } ] + } + }, + RequestType: 'Create', + ResponseURL: 'https://localhost/test' + }, { + logStreamName: 'xyz', + }, undefined, response); + + test.deepEqual(JSON.parse(ecrMock.lastSetRepositoryPolicyRequest.policyText), { + Version: '2008-10-17', + Statement: [ + { Action: "boom" }, + { Resource: "foo" }, + { Sid: "StackId", Effect: "Deny", Action: "OwnedBy:CDKStack", Principal: "*" } + ] + }); + + test.deepEqual(output, { + responseStatus: 'SUCCESS', + reason: 'OK', + physId: '', + data: { } + }); + + test.done(); + }, + + async 'exercise handler create'(test: Test) { + const handler = proxyquire(path.resolve(__dirname, '..', 'lib', 'adopt-repository', 'handler'), { + 'aws-sdk': { + '@noCallThru': true, + "ECR": ECRWithEmptyPolicy, + } + }); + + let output; + async function response(responseStatus: string, reason: string, physId: string, data: any) { + output = { responseStatus, reason, physId, data }; + } + + await handler.handler({ + StackId: 'StackId', + ResourceProperties: { + RepositoryArn: 'RepositoryArn', + }, + RequestType: 'Create', + ResponseURL: 'https://localhost/test' + }, { + logStreamName: 'xyz', + }, undefined, response); + + test.deepEqual(output, { + responseStatus: 'SUCCESS', + reason: 'OK', + physId: '', + data: { } + }); + + test.done(); + }, + + async 'exercise handler delete'(test: Test) { + const handler = proxyquire(path.resolve(__dirname, '..', 'lib', 'adopt-repository', 'handler'), { + 'aws-sdk': { '@noCallThru': true, "ECR": ECRWithOwningPolicy } + }); + + let output; + async function response(responseStatus: string, reason: string, physId: string, data: any) { + output = { responseStatus, reason, physId, data }; + } + + await handler.handler({ + StackId: 'StackId', + ResourceProperties: { + RepositoryArn: 'RepositoryArn', + }, + RequestType: 'Delete', + ResponseURL: 'https://localhost/test' + }, { + logStreamName: 'xyz', + }, undefined, response); + + test.deepEqual(output, { + responseStatus: 'SUCCESS', + reason: 'OK', + physId: '', + data: { } + }); + + test.done(); + }, +}; + +function ECRWithEmptyPolicy() { + ecrMock = new ECR({ asdf: 'asdf' }); + return ecrMock; +} + +function ECRWithOwningPolicy() { + return new ECR({ + Statement: [ + { + Sid: 'StackId', + Effect: "Deny", + Action: "OwnedBy:CDKStack", + Principal: "*" + } + ] + }); +} + +class ECR { + public lastSetRepositoryPolicyRequest: any; + + public constructor(private policy: any) { + } + + public getRepositoryPolicy() { + const self = this; + return { async promise() { return { + policyText: JSON.stringify(self.policy) + }; } }; + } + + public setRepositoryPolicy(req: any) { + this.lastSetRepositoryPolicyRequest = req; + + return { + async promise() { + return; + } + }; + } + + public listImages() { + return { async promise() { + return { imageIds: [] }; + } }; + } + + public batchDeleteImage() { + return { async promise() { + return {}; + } }; + } + + public deleteRepository() { + return { async promise() { + return {}; + } }; + } +} diff --git a/packages/@aws-cdk/aws-codebuild/README.md b/packages/@aws-cdk/aws-codebuild/README.md index e70e30ad97881..2f0fe59874999 100644 --- a/packages/@aws-cdk/aws-codebuild/README.md +++ b/packages/@aws-cdk/aws-codebuild/README.md @@ -1,6 +1,85 @@ ## AWS CodeBuild Construct Library -Define a project. This will also create an IAM Role and IAM Policy for CodeBuild to use. +AWS CodeBuild is a fully managed continuous integration service that compiles +source code, runs tests, and produces software packages that are ready to +deploy. With CodeBuild, you don’t need to provision, manage, and scale your own +build servers. CodeBuild scales continuously and processes multiple builds +concurrently, so your builds are not left waiting in a queue. You can get +started quickly by using prepackaged build environments, or you can create +custom build environments that use your own build tools. With CodeBuild, you are +charged by the minute for the compute resources you use. + +### Installation + +Install the module: + +```console +$ npm i @aws-cdk/aws-codebuild +``` + +Import it into your code: + +```ts +import codebuild = require('@aws-cdk/aws-codebuild'); +``` + +The `codebuild.Project` construct represents a build project resource. See the +reference documentation for a comprehensive list of initialization properties, +methods and attributes. + +### Source + +Build projects are usually associated with a _source_, which is specified via +the `source` property which accepts a class that extends the `BuildSource` +abstract base class. The supported sources are: + +* `NoSource` (default, requires `buildSpec`) +* `CodeCommitSource` +* `S3BucketSource` +* `CodePipelineSource` +* `GitHubSource` +* `GitHubEnterpriseSource` +* `BitBucketSource` + +Here's an AWS CodeBuild project with no source which simply prints `Hello, +CodeBuild!`: + +[Minimal Example](./test/integ.defaults.lit.ts) + +### Environment + +By default, projects will use a small instance with an Ubuntu 14.04 image. You can +use the `environment` property to customize the build environment: + +* `buildImage` defines the Docker image used. See [Images](#images) below for details + on how to define build images. +* `computeType` defines the instance type used for the build. +* `privileged` can be set to `true` to allow privileged access. +* `environmentVariables` can be set at this level (and also at the project level). + +### Images + +The AWS CodeBuild library supports both Linux and Windows images via the `LinuxBuildImage` +and `WindowsBuildImage` classes, respectively. + +You can either specify one of the predefined Windows/Linux images by using +one of the constants such as `WindowsBuildImage.WIN_SERVER_CORE_2016_BASE` or +`LinuxBuildImage.UBUNTU_14_04_RUBY_2_5_1`. + +Alternatively, you can specify a custom image using one of the static methods +on `XxxBuildImage`: + +* Use `.fromDockerHub(image)` to reference an image publicly available in Docker Hub. +* Use `.fromEcrRepository(repo[, tag])` to reference an image available in an ECR repository. +* Use `.fromAsset(this, id, { directory: dir })` to use an image created from a local asset. + +The following example shows how to define an image from a Docker asset: + +[Docker asset example](./test/integ.docker.asset.lit.ts) + +The following example shows how to define an image from an ECR repository: + +[ECR example](./test/integ.ecr.lit.ts) ### Using CodeBuild with other AWS services diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index 134a336a783ae..a81dd239125b4 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -1,6 +1,8 @@ import assets = require('@aws-cdk/assets'); +import { DockerImageAsset, DockerImageAssetProps } from '@aws-cdk/assets-docker'; import cloudwatch = require('@aws-cdk/aws-cloudwatch'); import codepipeline = require('@aws-cdk/aws-codepipeline-api'); +import ecr = require('@aws-cdk/aws-ecr'); import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); @@ -801,6 +803,13 @@ export interface IBuildImage { * If you need to use with an image that isn't in the named constants, * you can always instantiate it directly. * + * You can also specify a custom image using one of the static methods: + * + * - LinuxBuildImage.fromDockerHub(image) + * - LinuxBuildImage.fromEcrRepository(repo[, tag]) + * - LinuxBuildImage.fromAsset(parent, id, props) + * + * * @see https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-available.html */ export class LinuxBuildImage implements IBuildImage { @@ -828,10 +837,48 @@ export class LinuxBuildImage implements IBuildImage { public static readonly UBUNTU_14_04_DOTNET_CORE_2_0 = new LinuxBuildImage('aws/codebuild/dot-net:core-2.0'); public static readonly UBUNTU_14_04_DOTNET_CORE_2_1 = new LinuxBuildImage('aws/codebuild/dot-net:core-2.1'); + /** + * @returns a Linux build image from a Docker Hub image. + */ + public static fromDockerHub(name: string): LinuxBuildImage { + return new LinuxBuildImage(name); + } + + /** + * @returns A Linux build image from an ECR repository. + * + * NOTE: if the repository is external (i.e. imported), then we won't be able to add + * a resource policy statement for it so CodeBuild can pull the image. + * + * @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-ecr.html + * + * @param repository The ECR repository + * @param tag Image tag (default "latest") + */ + public static fromEcrRepository(repository: ecr.IRepository, tag: string = 'latest'): LinuxBuildImage { + const image = new LinuxBuildImage(repository.repositoryUriForTag(tag)); + repository.addToResourcePolicy(resourcePolicyStatementForRepository()); + return image; + } + + /** + * Uses an Docker image asset as a Linux build image. + */ + public static fromAsset(parent: cdk.Construct, id: string, props: DockerImageAssetProps): LinuxBuildImage { + const asset = new DockerImageAsset(parent, id, props); + const image = new LinuxBuildImage(asset.imageUri); + + // allow this codebuild to pull this image (CodeBuild doesn't use a role, so + // we can't use `asset.grantUseImage()`. + asset.repository.addToResourcePolicy(resourcePolicyStatementForRepository()); + + return image; + } + public readonly type = 'LINUX_CONTAINER'; public readonly defaultComputeType = ComputeType.Small; - public constructor(public readonly imageId: string) { + private constructor(public readonly imageId: string) { } public validate(_: BuildEnvironment): string[] { @@ -869,19 +916,63 @@ export class LinuxBuildImage implements IBuildImage { /** * A CodeBuild image running Windows. + * * This class has a bunch of public constants that represent the most popular images. * If you need to use with an image that isn't in the named constants, * you can always instantiate it directly. * + * You can also specify a custom image using one of the static methods: + * + * - WindowsBuildImage.fromDockerHub(image) + * - WindowsBuildImage.fromEcrRepository(repo[, tag]) + * - WindowsBuildImage.fromAsset(parent, id, props) + * * @see https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-available.html */ export class WindowsBuildImage implements IBuildImage { public static readonly WIN_SERVER_CORE_2016_BASE = new WindowsBuildImage('aws/codebuild/windows-base:1.0'); + /** + * @returns a Windows build image from a Docker Hub image. + */ + public static fromDockerHub(name: string): WindowsBuildImage { + return new WindowsBuildImage(name); + } + + /** + * @returns A Linux build image from an ECR repository. + * + * NOTE: if the repository is external (i.e. imported), then we won't be able to add + * a resource policy statement for it so CodeBuild can pull the image. + * + * @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-ecr.html + * + * @param repository The ECR repository + * @param tag Image tag (default "latest") + */ + public static fromEcrRepository(repository: ecr.IRepository, tag: string = 'latest'): WindowsBuildImage { + const image = new WindowsBuildImage(repository.repositoryUriForTag(tag)); + repository.addToResourcePolicy(resourcePolicyStatementForRepository()); + return image; + } + + /** + * Uses an Docker image asset as a Windows build image. + */ + public static fromAsset(parent: cdk.Construct, id: string, props: DockerImageAssetProps): WindowsBuildImage { + const asset = new DockerImageAsset(parent, id, props); + const image = new WindowsBuildImage(asset.imageUri); + + // allow this codebuild to pull this image (CodeBuild doesn't use a role, so + // we can't use `asset.grantUseImage()`. + asset.repository.addToResourcePolicy(resourcePolicyStatementForRepository()); + + return image; + } public readonly type = 'WINDOWS_CONTAINER'; public readonly defaultComputeType = ComputeType.Medium; - public constructor(public readonly imageId: string) { + private constructor(public readonly imageId: string) { } public validate(buildEnvironment: BuildEnvironment): string[] { @@ -970,3 +1061,12 @@ function extendBuildSpec(buildSpec: any, extend: any) { phase.commands.push(...extend.phases[phaseName].commands); } } + +function resourcePolicyStatementForRepository() { + return new iam.PolicyStatement() + .describe('CodeBuild') + .addServicePrincipal('codebuild.amazonaws.com') + .addAction('ecr:GetDownloadUrlForLayer') + .addAction('ecr:BatchGetImage') + .addAction('ecr:BatchCheckLayerAvailability'); +} diff --git a/packages/@aws-cdk/aws-codebuild/package.json b/packages/@aws-cdk/aws-codebuild/package.json index eeb3fc945ea57..ea0c9dd3c46c2 100644 --- a/packages/@aws-cdk/aws-codebuild/package.json +++ b/packages/@aws-cdk/aws-codebuild/package.json @@ -66,9 +66,11 @@ }, "dependencies": { "@aws-cdk/assets": "^0.18.1", + "@aws-cdk/assets-docker": "^0.18.1", "@aws-cdk/aws-cloudwatch": "^0.18.1", "@aws-cdk/aws-codecommit": "^0.18.1", "@aws-cdk/aws-codepipeline-api": "^0.18.1", + "@aws-cdk/aws-ecr": "^0.18.1", "@aws-cdk/aws-events": "^0.18.1", "@aws-cdk/aws-iam": "^0.18.1", "@aws-cdk/aws-kms": "^0.18.1", @@ -80,6 +82,7 @@ "@aws-cdk/assets": "^0.18.1", "@aws-cdk/aws-cloudwatch": "^0.18.1", "@aws-cdk/aws-codecommit": "^0.18.1", + "@aws-cdk/assets-docker": "^0.18.1", "@aws-cdk/aws-codepipeline-api": "^0.18.1", "@aws-cdk/aws-events": "^0.18.1", "@aws-cdk/aws-iam": "^0.18.1", @@ -87,4 +90,4 @@ "@aws-cdk/aws-s3": "^0.18.1", "@aws-cdk/cdk": "^0.18.1" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-codebuild/test/demo-image/Dockerfile b/packages/@aws-cdk/aws-codebuild/test/demo-image/Dockerfile new file mode 100644 index 0000000000000..123b5670febc8 --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/demo-image/Dockerfile @@ -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-codebuild/test/demo-image/index.py b/packages/@aws-cdk/aws-codebuild/test/demo-image/index.py new file mode 100644 index 0000000000000..2ccedfce3ab76 --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/demo-image/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-codebuild/test/integ.defaults.lit.ts b/packages/@aws-cdk/aws-codebuild/test/integ.defaults.lit.ts new file mode 100644 index 0000000000000..e8d44409e9813 --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/integ.defaults.lit.ts @@ -0,0 +1,23 @@ +import cdk = require('@aws-cdk/cdk'); +import codebuild = require('../lib'); + +class TestStack extends cdk.Stack { + constructor(parent: cdk.App, id: string) { + super(parent, id); + + /// !show + new codebuild.Project(this, 'MyProject', { + buildSpec: { + version: '0.2', + phases: { build: { commands: [ 'echo "Hello, CodeBuild!"' ] } } + } + }); + /// !hide + } +} + +const app = new cdk.App(); + +new TestStack(app, 'codebuild-default-project'); + +app.run(); diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.expected.json new file mode 100644 index 0000000000000..4d7e14ed9c248 --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.expected.json @@ -0,0 +1,302 @@ +{ + "Parameters": { + "MyImageRepository10239498": { + "Type": "String", + "Description": "Repository ARN for asset \"test-codebuild-docker-asset/MyImage\"" + }, + "MyImageTagFB60FD10": { + "Type": "String", + "Description": "Tag for asset \"test-codebuild-docker-asset/MyImage\"" + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3Bucket92AB06B6": { + "Type": "String", + "Description": "S3 bucket for asset \"test-codebuild-docker-asset/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276": { + "Type": "String", + "Description": "S3 key for asset version \"test-codebuild-docker-asset/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" + } + }, + "Resources": { + "MyImageAdoptRepository6CA902F6": { + "Type": "AWS::CloudFormation::CustomResource", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c52BE89E9", + "Arn" + ] + }, + "RepositoryArn": { + "Ref": "MyImageRepository10239498" + } + } + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecr:GetRepositoryPolicy", + "ecr:SetRepositoryPolicy", + "ecr:DeleteRepository", + "ecr:ListImages", + "ecr:BatchDeleteImage" + ], + "Effect": "Allow", + "Resource": { + "Ref": "MyImageRepository10239498" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C", + "Roles": [ + { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17" + } + ] + } + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c52BE89E9": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3Bucket92AB06B6" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "handler.handler", + "Role": { + "Fn::GetAtt": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17", + "Arn" + ] + }, + "Runtime": "nodejs8.10", + "Timeout": 300 + }, + "DependsOn": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17", + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C" + ] + }, + "MyProjectRole9BBE5233": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyProjectRoleDefaultPolicyB19B7C29": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "MyProject39F7B0AE" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "MyProject39F7B0AE" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage" + ], + "Effect": "Allow", + "Resource": { + "Ref": "MyImageRepository10239498" + } + }, + { + "Action": [ + "ecr:GetAuthorizationToken", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyProjectRoleDefaultPolicyB19B7C29", + "Roles": [ + { + "Ref": "MyProjectRole9BBE5233" + } + ] + } + }, + "MyProject39F7B0AE": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "NO_ARTIFACTS" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyImageAdoptRepository6CA902F6", + "RepositoryUri" + ] + }, + ":", + { + "Ref": "MyImageTagFB60FD10" + } + ] + ] + }, + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "MyProjectRole9BBE5233", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"2.0\",\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"ls\"\n ]\n }\n }\n}", + "Type": "NO_SOURCE" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.lit.ts b/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.lit.ts new file mode 100644 index 0000000000000..9d7a01b103c7a --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.lit.ts @@ -0,0 +1,33 @@ +import cdk = require('@aws-cdk/cdk'); +import path = require('path'); +import codebuild = require('../lib'); + +class TestStack extends cdk.Stack { + constructor(parent: cdk.App, id: string) { + super(parent, id); + + new codebuild.Project(this, 'MyProject', { + buildSpec: { + version: "0.2", + phases: { + build: { + commands: [ 'ls' ] + } + } + }, + /// !show + environment: { + buildImage: codebuild.LinuxBuildImage.fromAsset(this, 'MyImage', { + directory: path.join(__dirname, 'demo-image') + }) + } + /// !hide + }); + } +} + +const app = new cdk.App(); + +new TestStack(app, 'test-codebuild-docker-asset'); + +app.run(); diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.ecr.lit.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.ecr.lit.expected.json new file mode 100644 index 0000000000000..af6d99e97bab1 --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/integ.ecr.lit.expected.json @@ -0,0 +1,184 @@ +{ + "Resources": { + "MyRepoF4F48043": { + "Type": "AWS::ECR::Repository", + "Properties": { + "RepositoryPolicyText": { + "Statement": [ + { + "Action": [ + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "ecr:BatchCheckLayerAvailability" + ], + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + }, + "Sid": "CodeBuild" + } + ], + "Version": "2012-10-17" + } + } + }, + "MyProjectRole9BBE5233": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyProjectRoleDefaultPolicyB19B7C29": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "MyProject39F7B0AE" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "MyProject39F7B0AE" + }, + ":*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyProjectRoleDefaultPolicyB19B7C29", + "Roles": [ + { + "Ref": "MyProjectRole9BBE5233" + } + ] + } + }, + "MyProject39F7B0AE": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "NO_ARTIFACTS" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "MyRepoF4F48043", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "MyRepoF4F48043", + "Arn" + ] + } + ] + } + ] + }, + ".amazonaws.com/", + { + "Ref": "MyRepoF4F48043" + }, + ":v1.0" + ] + ] + }, + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "MyProjectRole9BBE5233", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"ls\"\n ]\n }\n }\n}", + "Type": "NO_SOURCE" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.ecr.lit.ts b/packages/@aws-cdk/aws-codebuild/test/integ.ecr.lit.ts new file mode 100644 index 0000000000000..d90695607a54e --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/integ.ecr.lit.ts @@ -0,0 +1,33 @@ +import ecr = require('@aws-cdk/aws-ecr'); +import cdk = require('@aws-cdk/cdk'); +import codebuild = require('../lib'); + +class TestStack extends cdk.Stack { + constructor(parent: cdk.App, id: string) { + super(parent, id); + + const ecrRepository = new ecr.Repository(this, 'MyRepo'); + + new codebuild.Project(this, 'MyProject', { + buildSpec: { + version: "0.2", + phases: { + build: { + commands: [ 'ls' ] + } + } + }, + /// !show + environment: { + buildImage: codebuild.LinuxBuildImage.fromEcrRepository(ecrRepository, "v1.0") + } + /// !hide + }); + } +} + +const app = new cdk.App(); + +new TestStack(app, 'test-codebuild-docker-asset'); + +app.run(); diff --git a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts index 2e41764d145c0..60da5c5ecfc5a 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts @@ -2,16 +2,100 @@ import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); /** - * An ECR repository + * Represents an ECR repository. */ -export abstract class RepositoryRef extends cdk.Construct { +export interface IRepository { + /** + * The name of the repository + */ + readonly repositoryName: string; + + /** + * The ARN of the repository + */ + readonly repositoryArn: string; + + /** + * The URI of this repository (represents the latest image): + * + * ACCOUNT.dkr.ecr.REGION.amazonaws.com/REPOSITORY + * + */ + readonly repositoryUri: string; + + /** + * Returns the URI of the repository for a certain tag. Can be used in `docker push/pull`. + * + * ACCOUNT.dkr.ecr.REGION.amazonaws.com/REPOSITORY[:TAG] + * + * @param tag Image tag to use (tools usually default to "latest" if omitted) + */ + repositoryUriForTag(tag?: string): string; + + /** + * Add a policy statement to the repository's resource policy + */ + addToResourcePolicy(statement: iam.PolicyStatement): void; + + /** + * Grant the given principal identity permissions to perform the actions on this repository + */ + grant(identity?: iam.IPrincipal, ...actions: string[]): void; + + /** + * Grant the given identity permissions to pull images in this repository. + */ + grantPull(identity?: iam.IPrincipal): void; + + /** + * Grant the given identity permissions to pull and push images to this repository. + */ + grantPullPush(identity?: iam.IPrincipal): void; +} + +export interface ImportRepositoryProps { + /** + * The ARN of the repository to import. + * + * If you only have a repository name and the repository is in the same account/region + * as the current stack, you can use the static method `Repository.arnForLocalRepository(name)` + * to format an ARN from a name. + */ + repositoryArn: string; + + /** + * The full name of the repository to import. + * + * This is only needed if the repository ARN is not a concrete string, in which + * case it is impossible to safely parse the ARN and extract full repository + * names from it if it includes multiple components (e.g. `foo/bar/myrepo`). + */ + repositoryName?: string; +} + +/** + * Base class for ECR repository. Reused between imported repositories and owned repositories. + */ +export abstract class RepositoryBase extends cdk.Construct implements IRepository { /** * Import a repository */ - public static import(parent: cdk.Construct, id: string, props: RepositoryRefProps): RepositoryRef { + public static import(parent: cdk.Construct, id: string, props: ImportRepositoryProps): IRepository { return new ImportedRepository(parent, id, props); } + /** + * Returns an ECR ARN for a repository that resides in the same account/region + * as the current stack. + */ + public static arnForLocalRepository(repositoryName: string): string { + return cdk.ArnUtils.fromComponents({ + service: 'ecr', + resource: 'repository', + resourceName: repositoryName + }); + } + /** * The name of the repository */ @@ -28,21 +112,36 @@ export abstract class RepositoryRef extends cdk.Construct { public abstract addToResourcePolicy(statement: iam.PolicyStatement): void; /** - * Export this repository from the stack + * The URI of this repository (represents the latest image): + * + * ACCOUNT.dkr.ecr.REGION.amazonaws.com/REPOSITORY + * */ - public export(): RepositoryRefProps { - return { - repositoryArn: new cdk.Output(this, 'RepositoryArn', { value: this.repositoryArn }).makeImportValue().toString(), - }; + public get repositoryUri() { + return this.repositoryUriForTag(); } /** - * The URI of the repository, for use in Docker/image references + * Returns the URL of the repository. Can be used in `docker push/pull`. + * + * ACCOUNT.dkr.ecr.REGION.amazonaws.com/REPOSITORY[:TAG] + * + * @param tag Optional image tag */ - public get repositoryUri(): string { - // Calculate this from the ARN + public repositoryUriForTag(tag?: string): string { + const tagSuffix = tag ? `:${tag}` : ''; const parts = cdk.ArnUtils.parse(this.repositoryArn); - return `${parts.account}.dkr.ecr.${parts.region}.amazonaws.com/${parts.resourceName}`; + return `${parts.account}.dkr.ecr.${parts.region}.amazonaws.com/${this.repositoryName}${tagSuffix}`; + } + + /** + * Export this repository from the stack + */ + public export(): ImportRepositoryProps { + return { + repositoryArn: new cdk.Output(this, 'RepositoryArn', { value: this.repositoryArn }).makeImportValue().toString(), + repositoryName: new cdk.Output(this, 'RepositoryName', { value: this.repositoryName }).makeImportValue().toString() + }; } /** @@ -60,7 +159,7 @@ export abstract class RepositoryRef extends cdk.Construct { /** * Grant the given identity permissions to use the images in this repository */ - public grantUseImage(identity?: iam.IPrincipal) { + public grantPull(identity?: iam.IPrincipal) { this.grant(identity, "ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage"); if (identity) { @@ -69,26 +168,46 @@ export abstract class RepositoryRef extends cdk.Construct { .addAllResources()); } } -} -export interface RepositoryRefProps { - repositoryArn: string; + /** + * Grant the given identity permissions to pull and push images to this repository. + */ + public grantPullPush(identity?: iam.IPrincipal) { + this.grantPull(identity); + this.grant(identity, + "ecr:PutImage", + "ecr:InitiateLayerUpload", + "ecr:UploadLayerPart", + "ecr:CompleteLayerUpload"); + } } /** * An already existing repository */ -class ImportedRepository extends RepositoryRef { +class ImportedRepository extends RepositoryBase { public readonly repositoryName: string; public readonly repositoryArn: string; - constructor(parent: cdk.Construct, id: string, props: RepositoryRefProps) { + constructor(parent: cdk.Construct, id: string, props: ImportRepositoryProps) { super(parent, id); this.repositoryArn = props.repositoryArn; - this.repositoryName = cdk.ArnUtils.parse(props.repositoryArn).resourceName!; + + // if repositoryArn is a token, the repository name is also required. this is because + // repository names can include "/" (e.g. foo/bar/myrepo) and it is impossible to + // parse the name from an ARN using CloudFormation's split/select. + if (cdk.unresolved(props.repositoryArn)) { + if (!props.repositoryName) { + throw new Error('repositoryArn is a late-bound value, and therefore repositoryName is required'); + } + + this.repositoryName = props.repositoryName; + } else { + this.repositoryName = props.repositoryArn.split('/').slice(1).join('/'); + } } public addToResourcePolicy(_statement: iam.PolicyStatement) { // FIXME: Add annotation about policy we dropped on the floor } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecr/lib/repository.ts b/packages/@aws-cdk/aws-ecr/lib/repository.ts index 142fa6f278dfa..f77e9232ac539 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository.ts @@ -2,7 +2,7 @@ import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { cloudformation } from './ecr.generated'; import { CountType, LifecycleRule, TagStatus } from './lifecycle'; -import { RepositoryRef } from "./repository-ref"; +import { RepositoryBase } from "./repository-ref"; export interface RepositoryProps { /** @@ -41,7 +41,7 @@ export interface RepositoryProps { /** * Define an ECR repository */ -export class Repository extends RepositoryRef { +export class Repository extends RepositoryBase { public readonly repositoryName: string; public readonly repositoryArn: string; private readonly lifecycleRules = new Array(); diff --git a/packages/@aws-cdk/aws-ecr/test/test.repository.ts b/packages/@aws-cdk/aws-ecr/test/test.repository.ts index 39201bb8dbed3..b26b74c719b1f 100644 --- a/packages/@aws-cdk/aws-ecr/test/test.repository.ts +++ b/packages/@aws-cdk/aws-ecr/test/test.repository.ts @@ -140,7 +140,7 @@ export = { '.dkr.ecr.', { 'Fn::Select': [ 3, arnSplit ] }, '.amazonaws.com/', - { 'Fn::Select': [ 1, { 'Fn::Split': [ '/', { 'Fn::Select': [ 5, arnSplit ] } ] } ] } + { Ref: 'Repo02AC86CF' } ]]}); test.done(); @@ -154,13 +154,88 @@ export = { const stack2 = new cdk.Stack(); // WHEN - const repo2 = ecr.RepositoryRef.import(stack2, 'Repo', repo1.export()); + const repo2 = ecr.Repository.import(stack2, 'Repo', repo1.export()); // THEN test.deepEqual(cdk.resolve(repo2.repositoryArn), { 'Fn::ImportValue': 'RepoRepositoryArn7F2901C9' }); + test.deepEqual(cdk.resolve(repo2.repositoryName), { + 'Fn::ImportValue': 'RepoRepositoryName58A7E467' + }); + + test.done(); + }, + + 'import with concrete arn'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const repo2 = ecr.Repository.import(stack, 'Repo', { + repositoryArn: 'arn:aws:ecr:us-east-1:585695036304:repository/foo/bar/foo/fooo' + }); + + // THEN + test.deepEqual(cdk.resolve(repo2.repositoryArn), 'arn:aws:ecr:us-east-1:585695036304:repository/foo/bar/foo/fooo'); + test.deepEqual(cdk.resolve(repo2.repositoryName), 'foo/bar/foo/fooo'); + + test.done(); + }, + + 'fails if importing with token arn and no name'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN/THEN + test.throws(() => ecr.Repository.import(stack, 'Repo', { + repositoryArn: new cdk.FnGetAtt('Boom', 'Boom').toString() + }), /repositoryArn is a late-bound value, and therefore repositoryName is required/); + + test.done(); + }, + + 'import with token arn and repository name (see awslabs/aws-cdk#1232)'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const repo = ecr.Repository.import(stack, 'Repo', { + repositoryArn: new cdk.FnGetAtt('Boom', 'Arn').toString(), + repositoryName: new cdk.FnGetAtt('Boom', 'Name').toString() + }); + + // THEN + test.deepEqual(cdk.resolve(repo.repositoryArn), { 'Fn::GetAtt': [ 'Boom', 'Arn' ] }); + test.deepEqual(cdk.resolve(repo.repositoryName), { 'Fn::GetAtt': [ 'Boom', 'Name' ] }); + test.done(); + }, + + 'arnForLocalRepository can be used to render an ARN for a local repository'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const repoName = new cdk.FnGetAtt('Boom', 'Name').toString(); + + // WHEN + const repo = ecr.Repository.import(stack, 'Repo', { + repositoryArn: ecr.Repository.arnForLocalRepository(repoName), + repositoryName: repoName + }); + + // THEN + test.deepEqual(cdk.resolve(repo.repositoryName), { 'Fn::GetAtt': [ 'Boom', 'Name' ] }); + test.deepEqual(cdk.resolve(repo.repositoryArn), { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':ecr:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':repository/', + { 'Fn::GetAtt': [ 'Boom', 'Name' ] } ] ] + }); test.done(); }, diff --git a/packages/@aws-cdk/aws-ecs/lib/container-image.ts b/packages/@aws-cdk/aws-ecs/lib/container-image.ts index a441d3285318e..f1a9bb8583eb2 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-image.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-image.ts @@ -35,7 +35,7 @@ export class ContainerImage { /** * Reference an image in an ECR repository */ - public static fromEcrRepository(repository: ecr.RepositoryRef, tag: string = 'latest') { + public static fromEcrRepository(repository: ecr.IRepository, tag: string = 'latest') { return new EcrImage(repository, tag); } @@ -45,4 +45,4 @@ export class ContainerImage { public static fromAsset(parent: cdk.Construct, id: string, props: AssetImageProps) { return new AssetImage(parent, id, props); } -} \ No newline at end of file +} 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 ac0c0c63cbe7b..eb281218798d4 100644 --- a/packages/@aws-cdk/aws-ecs/lib/images/asset-image.ts +++ b/packages/@aws-cdk/aws-ecs/lib/images/asset-image.ts @@ -1,11 +1,5 @@ -import cfn = require('@aws-cdk/aws-cloudformation'); -import ecr = require('@aws-cdk/aws-ecr'); -import iam = require('@aws-cdk/aws-iam'); -import lambda = require('@aws-cdk/aws-lambda'); +import { DockerImageAsset } from '@aws-cdk/assets-docker'; import cdk = require('@aws-cdk/cdk'); -import cxapi = require('@aws-cdk/cx-api'); -import fs = require('fs'); -import path = require('path'); import { ContainerDefinition } from '../container-definition'; import { IContainerImage } from '../container-image'; @@ -19,111 +13,16 @@ export interface AssetImageProps { /** * An image that will be built at synthesis time */ -export class AssetImage extends cdk.Construct implements IContainerImage { - /** - * Full name of this image - */ - public readonly imageName: string; - - /** - * Directory where the source files are stored - */ - private readonly directory: string; - - /** - * Repository where the image is stored - */ - private repository: ecr.RepositoryRef; - +export class AssetImage extends DockerImageAsset implements IContainerImage { constructor(parent: cdk.Construct, id: string, props: AssetImageProps) { - super(parent, id); - - // resolve full path - this.directory = path.resolve(props.directory); - if (!fs.existsSync(this.directory)) { - throw new Error(`Cannot find image directory at ${this.directory}`); - } - if (!fs.existsSync(path.join(this.directory, 'Dockerfile'))) { - throw new Error(`No 'Dockerfile' found in ${this.directory}`); - } - - const repositoryParameter = new cdk.Parameter(this, 'Repository', { - type: 'String', - description: `Repository ARN for asset "${this.path}"`, - }); - - const tagParameter = new cdk.Parameter(this, 'Tag', { - type: 'String', - description: `Tag for asset "${this.path}"`, - }); - - const asset: cxapi.ContainerImageAssetMetadataEntry = { - packaging: 'container-image', - path: this.directory, - id: this.uniqueId, - repositoryParameter: repositoryParameter.logicalId, - tagParameter: tagParameter.logicalId - }; - - this.addMetadata(cxapi.ASSET_METADATA, asset); - - this.repository = ecr.Repository.import(this, 'RepositoryObject', { - repositoryArn: repositoryParameter.value.toString(), - }); - - // Require that repository adoption happens first, so we route the - // input ARN into the Custom Resource and then get the URI which we use to - // refer to the image FROM the Custom Resource. - // - // If adoption fails (because the repository might be twice-adopted), we - // haven't already started using the image. - const adopted = new AdoptRepository(this, 'AdoptRepository', { repositoryArn: this.repository.repositoryArn }); - this.imageName = `${adopted.repositoryUri}:${tagParameter.value}`; + super(parent, id, { directory: props.directory }); } public bind(containerDefinition: ContainerDefinition): void { - this.repository.grantUseImage(containerDefinition.taskDefinition.obtainExecutionRole()); + this.repository.grantPull(containerDefinition.taskDefinition.obtainExecutionRole()); } -} - -interface AdoptRepositoryProps { - repositoryArn: string; -} - -/** - * Custom Resource which will adopt the repository used for the locally built image into the stack. - * - * Since the repository is not created by the stack (but by the CDK toolkit), - * adopting will make the repository "owned" by the stack. It will be cleaned - * up when the stack gets deleted, to avoid leaving orphaned repositories on stack - * cleanup. - */ -class AdoptRepository extends cdk.Construct { - public readonly repositoryUri: string; - - constructor(parent: cdk.Construct, id: string, props: AdoptRepositoryProps) { - super(parent, id); - - const fn = new lambda.SingletonFunction(this, 'Function', { - runtime: lambda.Runtime.NodeJS810, - lambdaPurpose: 'AdoptEcrRepository', - handler: 'handler.handler', - code: lambda.Code.asset(path.join(__dirname, 'adopt-repository')), - uuid: 'dbc60def-c595-44bc-aa5c-28c95d68f62c', - timeout: 300 - }); - - fn.addToRolePolicy(new iam.PolicyStatement() - .addActions('ecr:GetRepositoryPolicy', 'ecr:SetRepositoryPolicy', 'ecr:DeleteRepository', 'ecr:ListImages', 'ecr:BatchDeleteImage') - .addResource(props.repositoryArn)); - - const resource = new cfn.CustomResource(this, 'Resource', { - lambdaProvider: fn, - properties: { - RepositoryArn: props.repositoryArn, - } - }); - this.repositoryUri = resource.getAtt('RepositoryUri').toString(); + public get imageName() { + return this.imageUri; } } diff --git a/packages/@aws-cdk/aws-ecs/lib/images/ecr.ts b/packages/@aws-cdk/aws-ecs/lib/images/ecr.ts index f0c7803789120..424f23fe04d0a 100644 --- a/packages/@aws-cdk/aws-ecs/lib/images/ecr.ts +++ b/packages/@aws-cdk/aws-ecs/lib/images/ecr.ts @@ -7,14 +7,14 @@ import { IContainerImage } from '../container-image'; */ export class EcrImage implements IContainerImage { public readonly imageName: string; - private readonly repository: ecr.RepositoryRef; + private readonly repository: ecr.IRepository; - constructor(repository: ecr.RepositoryRef, tag: string) { - this.imageName = `${repository.repositoryUri}:${tag}`; + constructor(repository: ecr.IRepository, tag: string) { + this.imageName = repository.repositoryUriForTag(tag); this.repository = repository; } public bind(containerDefinition: ContainerDefinition): void { - this.repository.grantUseImage(containerDefinition.taskDefinition.obtainExecutionRole()); + this.repository.grantPull(containerDefinition.taskDefinition.obtainExecutionRole()); } } diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index 65c86d1e88da2..3fa7c113575cd 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -70,6 +70,7 @@ "@aws-cdk/aws-ecr": "^0.18.1", "@aws-cdk/aws-elasticloadbalancing": "^0.18.1", "@aws-cdk/aws-elasticloadbalancingv2": "^0.18.1", + "@aws-cdk/assets-docker": "^0.18.1", "@aws-cdk/aws-iam": "^0.18.1", "@aws-cdk/aws-lambda": "^0.18.1", "@aws-cdk/aws-logs": "^0.18.1", @@ -93,4 +94,4 @@ "@aws-cdk/aws-route53": "^0.18.1", "@aws-cdk/cdk": "^0.18.1" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json b/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json index 09893127090a0..eaaaec08ece2b 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json @@ -67,9 +67,6 @@ }, "VpcPublicSubnet1DefaultRoute3DA9E72A": { "Type": "AWS::EC2::Route", - "DependsOn": [ - "VpcVPCGWBF912B6E" - ], "Properties": { "RouteTableId": { "Ref": "VpcPublicSubnet1RouteTable6C95E38E" @@ -78,7 +75,10 @@ "GatewayId": { "Ref": "VpcIGWD7BA715C" } - } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] }, "VpcPublicSubnet1EIPD7E02669": { "Type": "AWS::EC2::EIP", @@ -158,9 +158,6 @@ }, "VpcPublicSubnet2DefaultRoute97F91067": { "Type": "AWS::EC2::Route", - "DependsOn": [ - "VpcVPCGWBF912B6E" - ], "Properties": { "RouteTableId": { "Ref": "VpcPublicSubnet2RouteTable94F7E489" @@ -169,7 +166,10 @@ "GatewayId": { "Ref": "VpcIGWD7BA715C" } - } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] }, "VpcPublicSubnet2EIP3C605A87": { "Type": "AWS::EC2::EIP", @@ -347,7 +347,7 @@ "Type": "AWS::ECS::Cluster" }, "ImageAdoptRepositoryE1E84E35": { - "Type": "AWS::CloudFormation::CustomResource", + "Type": "Custom::CDKECRRepositoryAdoption", "Properties": { "ServiceToken": { "Fn::GetAtt": [ @@ -356,7 +356,37 @@ ] }, "RepositoryArn": { - "Ref": "ImageRepositoryC2BE7AD4" + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "ImageImageName5E684353" + } + ] + } + ] + } + ] + ] } } }, @@ -406,7 +436,37 @@ ], "Effect": "Allow", "Resource": { - "Ref": "ImageRepositoryC2BE7AD4" + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "ImageImageName5E684353" + } + ] + } + ] + } + ] + ] } } ], @@ -504,14 +564,119 @@ "", [ { - "Fn::GetAtt": [ - "ImageAdoptRepositoryE1E84E35", - "RepositoryUri" + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "ImageImageName5E684353" + } + ] + } + ] + } + ] + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "ImageImageName5E684353" + } + ] + } + ] + } + ] + ] + } + ] + } + ] + }, + ".amazonaws.com/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "ImageImageName5E684353" + } + ] + } ] }, ":", { - "Ref": "ImageTagE17D8A6B" + "Fn::Select": [ + 1, + { + "Fn::Split": [ + ":", + { + "Ref": "ImageImageName5E684353" + } + ] + } + ] } ] ] @@ -525,8 +690,8 @@ "Devices": [], "Tmpfs": [] }, - "LogConfiguration":{ - "LogDriver":"awslogs", + "LogConfiguration": { + "LogDriver": "awslogs", "Options": { "awslogs-group": { "Ref": "FargateServiceLoggingLogGroup9B16742A" @@ -601,7 +766,37 @@ ], "Effect": "Allow", "Resource": { - "Ref": "ImageRepositoryC2BE7AD4" + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "ImageImageName5E684353" + } + ] + } + ] + } + ] + ] } }, { @@ -625,11 +820,11 @@ } }, "FargateServiceLoggingLogGroup9B16742A": { - "Type": "AWS::Logs::LogGroup", - "Properties": { - "RetentionInDays": 365 - }, - "DeletionPolicy": "Retain" + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 365 + }, + "DeletionPolicy": "Retain" }, "FargateServiceECC8084D": { "Type": "AWS::ECS::Service", @@ -817,13 +1012,9 @@ } }, "Parameters": { - "ImageRepositoryC2BE7AD4": { - "Type": "String", - "Description": "Repository ARN for asset \"aws-ecs-integ/Image\"" - }, - "ImageTagE17D8A6B": { + "ImageImageName5E684353": { "Type": "String", - "Description": "Tag for asset \"aws-ecs-integ/Image\"" + "Description": "ECR repository name and tag asset \"aws-ecs-integ/Image\"" }, "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3Bucket92AB06B6": { "Type": "String", @@ -858,4 +1049,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts b/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts deleted file mode 100644 index 7e386ddcae252..0000000000000 --- a/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { expect, MatchStyle } from '@aws-cdk/assert'; -import cdk = require('@aws-cdk/cdk'); -import { Test } from 'nodeunit'; -import path = require('path'); -import proxyquire = require('proxyquire'); -import ecs = require('../lib'); - -export = { - 'test instantiating Asset Image'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - new ecs.AssetImage(stack, 'Image', { - directory: path.join(__dirname, 'demo-image'), - }); - - // THEN - expect(stack).toMatch({ - ImageRepositoryC2BE7AD4: { - Type: "String", - Description: "Repository ARN for asset \"Image\"" - }, - ImageTagE17D8A6B: { - Type: "String", - Description: "Tag for asset \"Image\"" - }, - }, MatchStyle.SUPERSET); - - test.done(); - }, - - async 'exercise handler create'(test: Test) { - const handler = proxyquire(path.resolve(__dirname, '..', 'lib', 'images', 'adopt-repository', 'handler'), { - 'aws-sdk': { - '@noCallThru': true, - "ECR": ECRWithEmptyPolicy, - } - }); - - let output; - async function response(responseStatus: string, reason: string, physId: string, data: any) { - output = { responseStatus, reason, physId, data }; - } - - await handler.handler({ - StackId: 'StackId', - ResourceProperties: { - RepositoryArn: 'RepositoryArn', - }, - RequestType: 'Create', - ResponseURL: 'https://localhost/test' - }, { - logStreamName: 'xyz', - }, undefined, response); - - test.deepEqual(output, { - responseStatus: 'SUCCESS', - reason: 'OK', - physId: '', - data: { RepositoryUri: 'undefined.dkr.ecr.undefined.amazonaws.com/' } - }); - - test.done(); - }, - - async 'exercise handler delete'(test: Test) { - const handler = proxyquire(path.resolve(__dirname, '..', 'lib', 'images', 'adopt-repository', 'handler'), { - 'aws-sdk': { '@noCallThru': true, "ECR": ECRWithOwningPolicy } - }); - - let output; - async function response(responseStatus: string, reason: string, physId: string, data: any) { - output = { responseStatus, reason, physId, data }; - } - - await handler.handler({ - StackId: 'StackId', - ResourceProperties: { - RepositoryArn: 'RepositoryArn', - }, - RequestType: 'Delete', - ResponseURL: 'https://localhost/test' - }, { - logStreamName: 'xyz', - }, undefined, response); - - test.deepEqual(output, { - responseStatus: 'SUCCESS', - reason: 'OK', - physId: '', - data: { RepositoryUri: 'undefined.dkr.ecr.undefined.amazonaws.com/' } - }); - - test.done(); - }, -}; - -function ECRWithEmptyPolicy() { - return new ECR({ asdf: 'asdf' }); -} - -function ECRWithOwningPolicy() { - return new ECR({ - Statement: [ - { - Sid: 'StackId', - Effect: "Deny", - Action: "OwnedBy:CDKStack", - Principal: "*" - } - ] - }); -} - -class ECR { - public constructor(private policy: any) { - } - - public getRepositoryPolicy() { - const self = this; - return { async promise() { return { - policyText: JSON.stringify(self.policy) - }; } }; - } - - public setRepositoryPolicy() { - return { async promise() { return; } }; - } - - public listImages() { - return { async promise() { - return { imageIds: [] }; - } }; - } - - public batchDeleteImage() { - return { async promise() { - return {}; - } }; - } - - public deleteRepository() { - return { async promise() { - return {}; - } }; - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/lib/cxapi.ts b/packages/@aws-cdk/cx-api/lib/cxapi.ts index 32fc928ef7407..61a0bd5481958 100644 --- a/packages/@aws-cdk/cx-api/lib/cxapi.ts +++ b/packages/@aws-cdk/cx-api/lib/cxapi.ts @@ -149,14 +149,9 @@ export interface ContainerImageAssetMetadataEntry { id: string; /** - * Name of the parameter that takes the repository name + * ECR Repository name and tag (separated by ":") where this asset is stored. */ - repositoryParameter: string; - - /** - * Name of the parameter that takes the tag - */ - tagParameter: string; + imageNameParameter: string; } export type AssetMetadataEntry = FileAssetMetadataEntry | ContainerImageAssetMetadataEntry; diff --git a/packages/aws-cdk/lib/api/toolkit-info.ts b/packages/aws-cdk/lib/api/toolkit-info.ts index 762fd1b1ff07a..275c71b16ebd3 100644 --- a/packages/aws-cdk/lib/api/toolkit-info.ts +++ b/packages/aws-cdk/lib/api/toolkit-info.ts @@ -123,7 +123,7 @@ export class ToolkitInfo { return { alreadyExists: true, repositoryUri: repository.repositoryUri!, - repositoryArn: repository.repositoryArn!, + repositoryName }; } catch (e) { if (e.code !== 'ImageNotFoundException') { throw e; } @@ -152,7 +152,7 @@ export class ToolkitInfo { return { alreadyExists: false, repositoryUri: repository.repositoryUri!, - repositoryArn: repository.repositoryArn!, + repositoryName, username, password, endpoint: authData[0].proxyEndpoint!, @@ -164,13 +164,13 @@ export type EcrRepositoryInfo = CompleteEcrRepositoryInfo | UploadableEcrReposit export interface CompleteEcrRepositoryInfo { repositoryUri: string; - repositoryArn: string; + repositoryName: string; alreadyExists: true; } export interface UploadableEcrRepositoryInfo { repositoryUri: string; - repositoryArn: string; + repositoryName: string; alreadyExists: false; username: string; password: string; diff --git a/packages/aws-cdk/lib/docker.ts b/packages/aws-cdk/lib/docker.ts index e916ff0480c29..18f8a7bb0019c 100644 --- a/packages/aws-cdk/lib/docker.ts +++ b/packages/aws-cdk/lib/docker.ts @@ -59,8 +59,7 @@ export async function prepareContainerAsset(asset: ContainerImageAssetMetadataEn } return [ - { ParameterKey: asset.repositoryParameter, ParameterValue: ecr.repositoryArn }, - { ParameterKey: asset.tagParameter, ParameterValue: tag }, + { ParameterKey: asset.imageNameParameter, ParameterValue: `${ecr.repositoryName}:${tag}` }, ]; } catch (e) { if (e.code === 'ENOENT') {