From 9ab90066ee3a539ee9b20d8c43f780202ab41892 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 22 Nov 2018 14:16:46 +0200 Subject: [PATCH 01/14] 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') { From 8f3a62a8fc4b201c9d922a365b7a3aa91cd88adb Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 22 Nov 2018 14:26:03 +0200 Subject: [PATCH 02/14] fix version of assets-docker --- packages/@aws-cdk/assets-docker/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/assets-docker/package.json b/packages/@aws-cdk/assets-docker/package.json index 9eb23dd24bfc9..f995814a85d57 100644 --- a/packages/@aws-cdk/assets-docker/package.json +++ b/packages/@aws-cdk/assets-docker/package.json @@ -1,6 +1,6 @@ { "name": "@aws-cdk/assets-docker", - "version": "0.18.0", + "version": "0.18.1", "description": "Docker image assets", "main": "lib/index.js", "types": "lib/index.d.ts", @@ -73,4 +73,4 @@ "@aws-cdk/aws-s3": "^0.18.0", "@aws-cdk/cdk": "^0.18.0" } -} \ No newline at end of file +} From d987b8ee3c18f6fe514fbbd4c64b6e6e697df36d Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 22 Nov 2018 14:53:58 +0200 Subject: [PATCH 03/14] update package.json and expectations --- packages/@aws-cdk/assets-docker/package.json | 31 +- .../test/integ.defaults.expected.json | 118 +++++ .../test/integ.defaults.lit.expected.json | 118 +++++ .../test/integ.docker-asset.expected.json | 226 +++++++-- .../test/integ.docker-asset.lit.expected.json | 460 ++++++++++++++++++ 5 files changed, 904 insertions(+), 49 deletions(-) create mode 100644 packages/@aws-cdk/aws-codebuild/test/integ.defaults.expected.json create mode 100644 packages/@aws-cdk/aws-codebuild/test/integ.defaults.lit.expected.json create mode 100644 packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.lit.expected.json diff --git a/packages/@aws-cdk/assets-docker/package.json b/packages/@aws-cdk/assets-docker/package.json index f995814a85d57..1c331ca18da08 100644 --- a/packages/@aws-cdk/assets-docker/package.json +++ b/packages/@aws-cdk/assets-docker/package.json @@ -50,27 +50,28 @@ }, "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", + "@aws-cdk/assert": "^0.18.1", + "aws-cdk": "^0.18.1", + "cdk-build-tools": "^0.18.1", + "cdk-integ-tools": "^0.18.1", + "pkglint": "^0.18.1", "@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" + "@aws-cdk/aws-lambda": "^0.18.1", + "@aws-cdk/aws-cloudformation": "^0.18.1", + "@aws-cdk/aws-ecr": "^0.18.1", + "@aws-cdk/aws-iam": "^0.18.1", + "@aws-cdk/aws-s3": "^0.18.1", + "@aws-cdk/cdk": "^0.18.1", + "@aws-cdk/cx-api": "^0.18.1" }, "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" + "@aws-cdk/aws-iam": "^0.18.1", + "@aws-cdk/aws-s3": "^0.18.1", + "@aws-cdk/cdk": "^0.18.1", + "@aws-cdk/aws-ecr": "^0.18.1" } } diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.defaults.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.defaults.expected.json new file mode 100644 index 0000000000000..61b84b86ddb72 --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/integ.defaults.expected.json @@ -0,0 +1,118 @@ +{ + "Resources": { + "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": "aws/codebuild/ubuntu-base:14.04", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "MyProjectRole9BBE5233", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"echo \\\"Hello, CodeBuild!\\\"\"\n ]\n }\n }\n}", + "Type": "NO_SOURCE" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.defaults.lit.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.defaults.lit.expected.json new file mode 100644 index 0000000000000..61b84b86ddb72 --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/integ.defaults.lit.expected.json @@ -0,0 +1,118 @@ +{ + "Resources": { + "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": "aws/codebuild/ubuntu-base:14.04", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "MyProjectRole9BBE5233", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"echo \\\"Hello, CodeBuild!\\\"\"\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.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.expected.json index 4d7e14ed9c248..4ccc47693d3df 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.expected.json +++ b/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.expected.json @@ -1,12 +1,8 @@ { "Parameters": { - "MyImageRepository10239498": { + "MyImageImageName953AD232": { "Type": "String", - "Description": "Repository ARN for asset \"test-codebuild-docker-asset/MyImage\"" - }, - "MyImageTagFB60FD10": { - "Type": "String", - "Description": "Tag for asset \"test-codebuild-docker-asset/MyImage\"" + "Description": "ECR repository name and tag asset \"test-codebuild-docker-asset/MyImage\"" }, "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3Bucket92AB06B6": { "Type": "String", @@ -19,7 +15,7 @@ }, "Resources": { "MyImageAdoptRepository6CA902F6": { - "Type": "AWS::CloudFormation::CustomResource", + "Type": "Custom::CDKECRRepositoryAdoption", "Properties": { "ServiceToken": { "Fn::GetAtt": [ @@ -28,7 +24,54 @@ ] }, "RepositoryArn": { - "Ref": "MyImageRepository10239498" + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "MyImageImageName953AD232" + } + ] + } + ] + } + ] + ] + }, + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "ecr:BatchCheckLayerAvailability" + ], + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + }, + "Sid": "CodeBuild" + } + ], + "Version": "2012-10-17" } } }, @@ -78,7 +121,37 @@ ], "Effect": "Allow", "Resource": { - "Ref": "MyImageRepository10239498" + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "MyImageImageName953AD232" + } + ] + } + ] + } + ] + ] } } ], @@ -226,26 +299,6 @@ ] } ] - }, - { - "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" @@ -271,14 +324,119 @@ "", [ { - "Fn::GetAtt": [ - "MyImageAdoptRepository6CA902F6", - "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": "MyImageImageName953AD232" + } + ] + } + ] + } + ] + ] + } + ] + } + ] + }, + ".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": "MyImageImageName953AD232" + } + ] + } + ] + } + ] + ] + } + ] + } + ] + }, + ".amazonaws.com/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "MyImageImageName953AD232" + } + ] + } ] }, ":", { - "Ref": "MyImageTagFB60FD10" + "Fn::Select": [ + 1, + { + "Fn::Split": [ + ":", + { + "Ref": "MyImageImageName953AD232" + } + ] + } + ] } ] ] @@ -293,7 +451,7 @@ ] }, "Source": { - "BuildSpec": "{\n \"version\": \"2.0\",\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"ls\"\n ]\n }\n }\n}", + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"ls\"\n ]\n }\n }\n}", "Type": "NO_SOURCE" } } diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.lit.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.lit.expected.json new file mode 100644 index 0000000000000..4ccc47693d3df --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.lit.expected.json @@ -0,0 +1,460 @@ +{ + "Parameters": { + "MyImageImageName953AD232": { + "Type": "String", + "Description": "ECR repository name and tag 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": "Custom::CDKECRRepositoryAdoption", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c52BE89E9", + "Arn" + ] + }, + "RepositoryArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "MyImageImageName953AD232" + } + ] + } + ] + } + ] + ] + }, + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "ecr:BatchCheckLayerAvailability" + ], + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + }, + "Sid": "CodeBuild" + } + ], + "Version": "2012-10-17" + } + } + }, + "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": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "MyImageImageName953AD232" + } + ] + } + ] + } + ] + ] + } + } + ], + "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" + }, + ":*" + ] + ] + } + ] + } + ], + "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::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "MyImageImageName953AD232" + } + ] + } + ] + } + ] + ] + } + ] + } + ] + }, + ".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": "MyImageImageName953AD232" + } + ] + } + ] + } + ] + ] + } + ] + } + ] + }, + ".amazonaws.com/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "MyImageImageName953AD232" + } + ] + } + ] + }, + ":", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + ":", + { + "Ref": "MyImageImageName953AD232" + } + ] + } + ] + } + ] + ] + }, + "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 From 26cc8400e916a7adabfdf97fa9d9afea390f06d2 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 22 Nov 2018 15:06:41 +0200 Subject: [PATCH 04/14] Fix lit ref --- packages/@aws-cdk/aws-codebuild/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-codebuild/README.md b/packages/@aws-cdk/aws-codebuild/README.md index 2f0fe59874999..0716467da9158 100644 --- a/packages/@aws-cdk/aws-codebuild/README.md +++ b/packages/@aws-cdk/aws-codebuild/README.md @@ -75,7 +75,7 @@ on `XxxBuildImage`: The following example shows how to define an image from a Docker asset: -[Docker asset example](./test/integ.docker.asset.lit.ts) +[Docker asset example](./test/integ.docker-asset.lit.ts) The following example shows how to define an image from an ECR repository: From 2be8d1d247604448817f9836b74843115e67471c Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Sat, 24 Nov 2018 22:43:16 +0200 Subject: [PATCH 05/14] Use RepositoryName through custom resource as implicit dependency --- .../lib/adopt-repository/handler.js | 18 ++-- .../assets-docker/lib/adopted-repository.ts | 76 +++++++++++++++++ .../@aws-cdk/assets-docker/lib/image-asset.ts | 80 +----------------- .../assets-docker/test/test.image-asset.ts | 67 +++++++-------- .../aws-codebuild/test/demo-image/index.py | 33 +------- .../test/integ.docker-asset.expected.json | 83 +++++-------------- 6 files changed, 146 insertions(+), 211 deletions(-) create mode 100644 packages/@aws-cdk/assets-docker/lib/adopted-repository.ts diff --git a/packages/@aws-cdk/assets-docker/lib/adopt-repository/handler.js b/packages/@aws-cdk/assets-docker/lib/adopt-repository/handler.js index de35baa327ac2..a2db19be23d77 100644 --- a/packages/@aws-cdk/assets-docker/lib/adopt-repository/handler.js +++ b/packages/@aws-cdk/assets-docker/lib/adopt-repository/handler.js @@ -13,10 +13,6 @@ exports.handler = async function(event, context, _callback, respond) { Principal: "*" }; - function repoName(arn) { - return arn.split('/').slice(1).join('/'); - } - // The repository must already exist async function getAdopter(name) { try { @@ -30,7 +26,11 @@ exports.handler = async function(event, context, _callback, respond) { } } - const repo = repoName(event.ResourceProperties.RepositoryArn); + const repo = event.ResourceProperties.RepositoryName; + if (!repo) { + throw new Error('Missing required property "RepositoryName"'); + } + const adopter = await getAdopter(repo); if (event.RequestType === 'Delete') { if (adopter.Sid !== markerStatement.Sid) { @@ -76,8 +76,12 @@ exports.handler = async function(event, context, _callback, respond) { await ecr.setRepositoryPolicy({ repositoryName: repo, policyText: JSON.stringify(policy) }).promise(); } - const arn = event.ResourceProperties.RepositoryArn.split(':'); - await respond("SUCCESS", "OK", repo, {}); + // we reflect back the repository name as a resource attribute + // this will allow taking an implicit dependency in this custom resource by + // referencing this attribute via { "Fn::GetAtt": [ ID, "RepositoryName" ] } + await respond("SUCCESS", "OK", repo, { + RepositoryName: repo + }); } catch (e) { console.log(e); await respond("FAILED", e.message, context.logStreamName, {}); diff --git a/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts b/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts new file mode 100644 index 0000000000000..cf12c3c13340a --- /dev/null +++ b/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts @@ -0,0 +1,76 @@ +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 path = require('path'); + +interface AdoptedRepositoryProps { + /** + * An ECR repository to adopt. Once adopted, the repository will + * practically become part of this stack, so it will be removed when + * the stack is deleted. + */ + repositoryName: string; +} + +/** + * An internal class used to adopt an ECR 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. + */ +export class AdoptedRepository extends ecr.RepositoryBase { + public readonly repositoryName: string; + public readonly repositoryArn: string; + + private readonly policyDocument = new iam.PolicyDocument(); + + constructor(parent: cdk.Construct, id: string, props: AdoptedRepositoryProps) { + 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(ecr.Repository.arnForLocalRepository(props.repositoryName))); + + const adopter = new cfn.CustomResource(this, 'Resource', { + resourceType: 'Custom::ECRAdoptedRepository', + lambdaProvider: fn, + properties: { + RepositoryName: props.repositoryName, + PolicyDocument: this.policyDocument + } + }); + + // we use the Fn::GetAtt with the RepositoryName returned by the custom + // resource in order to implicitly create a dependency between consumers + // and the custom resource. + this.repositoryName = adopter.getAtt('RepositoryName').toString(); + + // this this repository is "local" to the stack (in the same region/account) + // we can render it's ARN from it's name. + this.repositoryArn = ecr.Repository.arnForLocalRepository(this.repositoryName); + } + + /** + * Adds a statement to the repository resource policy. + * + * Contrary to normal imported repositories, which no-op here, we can + * use the custom resource to modify the ECR resource policy if needed. + */ + public addToResourcePolicy(statement: iam.PolicyStatement) { + this.policyDocument.addStatement(statement); + } +} diff --git a/packages/@aws-cdk/assets-docker/lib/image-asset.ts b/packages/@aws-cdk/assets-docker/lib/image-asset.ts index cb803e6571a3e..7b76d91d20560 100644 --- a/packages/@aws-cdk/assets-docker/lib/image-asset.ts +++ b/packages/@aws-cdk/assets-docker/lib/image-asset.ts @@ -1,11 +1,9 @@ -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'); +import { AdoptedRepository } from './adopted-repository'; export interface DockerImageAssetProps { /** @@ -62,90 +60,18 @@ export class DockerImageAsset extends cdk.Construct { this.addMetadata(cxapi.ASSET_METADATA, asset); + // parse repository name and tag from the parameter (:) 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.repository = new AdoptedRepository(this, 'AdoptRepository', { repositoryName }); 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/test/test.image-asset.ts b/packages/@aws-cdk/assets-docker/test/test.image-asset.ts index 0ead553c21830..423ebc8603cf2 100644 --- a/packages/@aws-cdk/assets-docker/test/test.image-asset.ts +++ b/packages/@aws-cdk/assets-docker/test/test.image-asset.ts @@ -64,7 +64,12 @@ export = { ":", { "Ref": "AWS::AccountId" }, ":repository/", - { "Fn::Select": [ 0, { "Fn::Split": [ ":", { "Ref": "ImageImageName5E684353" } ] } ] } + { + "Fn::GetAtt": [ + "ImageAdoptRepositoryE1E84E35", + "RepositoryName" + ] + } ] ] } @@ -105,23 +110,9 @@ export = { .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" } ] } ] - } - ] - ] + expect(stack).to(haveResource('Custom::ECRAdoptedRepository', { + "RepositoryName": { + "Fn::Select": [ 0, { "Fn::Split": [ ":", { "Ref": "ImageImageName5E684353" } ] } ] }, "PolicyDocument": { "Statement": [ @@ -156,7 +147,7 @@ export = { await handler.handler({ StackId: 'StackId', ResourceProperties: { - RepositoryArn: 'RepositoryArn', + RepositoryName: 'RepositoryName', PolicyDocument: { Version: '2008-10-01', My: 'Document' @@ -179,8 +170,10 @@ export = { test.deepEqual(output, { responseStatus: 'SUCCESS', reason: 'OK', - physId: '', - data: { } + physId: 'RepositoryName', + data: { + RepositoryName: 'RepositoryName' + } }); test.done(); @@ -202,7 +195,7 @@ export = { await handler.handler({ StackId: 'StackId', ResourceProperties: { - RepositoryArn: 'RepositoryArn', + RepositoryName: 'RepositoryName', PolicyDocument: { Statement: { Action: 'boom' } } @@ -224,8 +217,10 @@ export = { test.deepEqual(output, { responseStatus: 'SUCCESS', reason: 'OK', - physId: '', - data: { } + physId: 'RepositoryName', + data: { + RepositoryName: 'RepositoryName' + } }); test.done(); @@ -247,7 +242,7 @@ export = { await handler.handler({ StackId: 'StackId', ResourceProperties: { - RepositoryArn: 'RepositoryArn', + RepositoryName: 'RepositoryName', PolicyDocument: { Statement: [ { Action: 'boom' }, { Resource: "foo" } ] } @@ -270,8 +265,10 @@ export = { test.deepEqual(output, { responseStatus: 'SUCCESS', reason: 'OK', - physId: '', - data: { } + physId: 'RepositoryName', + data: { + RepositoryName: 'RepositoryName' + } }); test.done(); @@ -293,7 +290,7 @@ export = { await handler.handler({ StackId: 'StackId', ResourceProperties: { - RepositoryArn: 'RepositoryArn', + RepositoryName: 'RepositoryName', }, RequestType: 'Create', ResponseURL: 'https://localhost/test' @@ -304,8 +301,10 @@ export = { test.deepEqual(output, { responseStatus: 'SUCCESS', reason: 'OK', - physId: '', - data: { } + physId: 'RepositoryName', + data: { + RepositoryName: 'RepositoryName' + } }); test.done(); @@ -324,7 +323,7 @@ export = { await handler.handler({ StackId: 'StackId', ResourceProperties: { - RepositoryArn: 'RepositoryArn', + RepositoryName: 'RepositoryName', }, RequestType: 'Delete', ResponseURL: 'https://localhost/test' @@ -335,8 +334,10 @@ export = { test.deepEqual(output, { responseStatus: 'SUCCESS', reason: 'OK', - physId: '', - data: { } + physId: 'RepositoryName', + data: { + RepositoryName: 'RepositoryName' + } }); test.done(); diff --git a/packages/@aws-cdk/aws-codebuild/test/demo-image/index.py b/packages/@aws-cdk/aws-codebuild/test/demo-image/index.py index 2ccedfce3ab76..25d040434dab1 100644 --- a/packages/@aws-cdk/aws-codebuild/test/demo-image/index.py +++ b/packages/@aws-cdk/aws-codebuild/test/demo-image/index.py @@ -1,33 +1,2 @@ #!/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() +print("Hello world from the built image") 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 index 4ccc47693d3df..887a952d0ef7c 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.expected.json +++ b/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.expected.json @@ -15,7 +15,7 @@ }, "Resources": { "MyImageAdoptRepository6CA902F6": { - "Type": "Custom::CDKECRRepositoryAdoption", + "Type": "Custom::ECRAdoptedRepository", "Properties": { "ServiceToken": { "Fn::GetAtt": [ @@ -23,37 +23,17 @@ "Arn" ] }, - "RepositoryArn": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":ecr:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":repository/", - { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - ":", - { - "Ref": "MyImageImageName953AD232" - } - ] - } - ] - } - ] + "RepositoryName": { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "MyImageImageName953AD232" + } + ] + } ] }, "PolicyDocument": { @@ -347,16 +327,9 @@ }, ":repository/", { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - ":", - { - "Ref": "MyImageImageName953AD232" - } - ] - } + "Fn::GetAtt": [ + "MyImageAdoptRepository6CA902F6", + "RepositoryName" ] } ] @@ -391,16 +364,9 @@ }, ":repository/", { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - ":", - { - "Ref": "MyImageImageName953AD232" - } - ] - } + "Fn::GetAtt": [ + "MyImageAdoptRepository6CA902F6", + "RepositoryName" ] } ] @@ -412,16 +378,9 @@ }, ".amazonaws.com/", { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - ":", - { - "Ref": "MyImageImageName953AD232" - } - ] - } + "Fn::GetAtt": [ + "MyImageAdoptRepository6CA902F6", + "RepositoryName" ] }, ":", From c1e5a31751b8f0ece898afdbae74e92aba032b3b Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Sat, 24 Nov 2018 23:38:51 +0200 Subject: [PATCH 06/14] Misc - Bump cxapi version to 0.19.0 (since protocol changed) - In integration tests, use local modules instead of "npm install" - Remove init template tests. Since they use "npm install" they can't really be validated like this against the local bits. We have integration tests for init template in our pipeline that work against the latest build. --- packages/@aws-cdk/assets-docker/README.md | 4 ++- packages/@aws-cdk/cx-api/lib/cxapi.ts | 2 +- packages/aws-cdk/integ-tests/common.bash | 10 +++++- .../aws-cdk/integ-tests/test-cdk-synth.sh | 6 ++++ .../aws-cdk/integ-tests/test-init-template.sh | 31 ------------------- 5 files changed, 19 insertions(+), 34 deletions(-) delete mode 100755 packages/aws-cdk/integ-tests/test-init-template.sh diff --git a/packages/@aws-cdk/assets-docker/README.md b/packages/@aws-cdk/assets-docker/README.md index 39b5f4f39dfbb..1ecd3a655fa63 100644 --- a/packages/@aws-cdk/assets-docker/README.md +++ b/packages/@aws-cdk/assets-docker/README.md @@ -35,4 +35,6 @@ 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)`. +To do that, you can use `asset.repository.addToResourcePolicy(statement)` to +grant the desired principal the following permissions: "ecr:GetDownloadUrlForLayer", +"ecr:BatchGetImage" and "ecr:BatchCheckLayerAvailability". diff --git a/packages/@aws-cdk/cx-api/lib/cxapi.ts b/packages/@aws-cdk/cx-api/lib/cxapi.ts index 61a0bd5481958..b3be97655b64d 100644 --- a/packages/@aws-cdk/cx-api/lib/cxapi.ts +++ b/packages/@aws-cdk/cx-api/lib/cxapi.ts @@ -20,7 +20,7 @@ import { Environment } from './environment'; * updated (as the current verison in package.json has already been released!) * - The request does not have versioning yet, only the response. */ -export const PROTO_RESPONSE_VERSION = '0.14.0'; +export const PROTO_RESPONSE_VERSION = '0.19.0'; export const OUTFILE_NAME = 'cdk.out'; export const OUTDIR_ENV = 'CDK_OUTDIR'; diff --git a/packages/aws-cdk/integ-tests/common.bash b/packages/aws-cdk/integ-tests/common.bash index fb767a8b92649..9ff7aaf1cd90f 100644 --- a/packages/aws-cdk/integ-tests/common.bash +++ b/packages/aws-cdk/integ-tests/common.bash @@ -30,7 +30,15 @@ function setup() { cp -R app/* /tmp/cdk-integ-test cd /tmp/cdk-integ-test - npm i --no-save @aws-cdk/cdk @aws-cdk/aws-sns + # "install" symlinks to the cdk core and SNS modules + # we don't use "npm install" here so that the modules will + # be from the same version as the toolkit we are testing + mkdir -p node_modules/@aws-cdk + ( + cd node_modules/@aws-cdk + ln -s ${scriptdir}/../../@aws-cdk/aws-sns + ln -s ${scriptdir}/../../@aws-cdk/cdk + ) } function fail() { diff --git a/packages/aws-cdk/integ-tests/test-cdk-synth.sh b/packages/aws-cdk/integ-tests/test-cdk-synth.sh index 50a7ebdbaa9bb..837d076248ace 100755 --- a/packages/aws-cdk/integ-tests/test-cdk-synth.sh +++ b/packages/aws-cdk/integ-tests/test-cdk-synth.sh @@ -10,6 +10,8 @@ assert "cdk synth cdk-toolkit-integration-test-1" < Date: Wed, 28 Nov 2018 05:56:39 -0800 Subject: [PATCH 07/14] CR fixes --- .../assets-docker/lib/adopted-repository.ts | 10 +- packages/@aws-cdk/aws-codebuild/README.md | 361 +++++++++--------- .../@aws-cdk/aws-codebuild/lib/project.ts | 23 +- .../aws-codebuild/test/integ.defaults.lit.ts | 8 +- .../@aws-cdk/aws-ecr/lib/repository-ref.ts | 43 ++- .../@aws-cdk/aws-ecr/test/test.repository.ts | 25 ++ 6 files changed, 269 insertions(+), 201 deletions(-) diff --git a/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts b/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts index cf12c3c13340a..3467bd8a94510 100644 --- a/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts +++ b/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts @@ -42,8 +42,14 @@ export class AdoptedRepository extends ecr.RepositoryBase { }); fn.addToRolePolicy(new iam.PolicyStatement() - .addActions('ecr:GetRepositoryPolicy', 'ecr:SetRepositoryPolicy', 'ecr:DeleteRepository', 'ecr:ListImages', 'ecr:BatchDeleteImage') - .addResource(ecr.Repository.arnForLocalRepository(props.repositoryName))); + .addResource(ecr.Repository.arnForLocalRepository(props.repositoryName)) + .addActions( + 'ecr:GetRepositoryPolicy', + 'ecr:SetRepositoryPolicy', + 'ecr:DeleteRepository', + 'ecr:ListImages', + 'ecr:BatchDeleteImage' + )); const adopter = new cfn.CustomResource(this, 'Resource', { resourceType: 'Custom::ECRAdoptedRepository', diff --git a/packages/@aws-cdk/aws-codebuild/README.md b/packages/@aws-cdk/aws-codebuild/README.md index 0716467da9158..eb70d5deb151c 100644 --- a/packages/@aws-cdk/aws-codebuild/README.md +++ b/packages/@aws-cdk/aws-codebuild/README.md @@ -33,45 +33,92 @@ 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` +#### `NoSource` + +This is the default and implies that no source will be associated with this +build project. + +The `buildSpec` option is required in this case. Here's an AWS CodeBuild project with no source which simply prints `Hello, CodeBuild!`: [Minimal Example](./test/integ.defaults.lit.ts) +#### `CodeCommitSource` + +Use an AWS CodeCommit repository as the source of this build: + +```ts +import codebuild = require('@aws-cdk/aws-codebuild'); +import codecommit = require('@aws-cdk/aws-codecommit'); + +const repository = new codecommit.Repository(this, 'MyRepo', { repositoryName: 'foo' }); +new codebuild.Project(this, 'MyFirstCodeCommitProject', { + source: new codebuild.CodeCommitSource({ repository }), +}); +``` + +#### `S3BucketSource` + +Create a CodeBuild project with an S3 bucket as the source: + +```ts +import codebuild = require('@aws-cdk/aws-codebuild'); +import s3 = require('@aws-cdk/aws-s3'); + +const bucket = new s3.Bucket(this, 'MyBucket'); +new codebuild.Project(this, 'MyProject', { + source: new codebuild.S3BucketSource({ + bucket: bucket, + path: 'path/to/file.zip', + }), +}); +``` + +#### `CodePipelineSource` + +Used as a special source type when an AWS CodeBuild project is used as an AWS +CodePipeline action. + +#### `GitHubSource` and `GitHubEnterpriseSource` + +These source types can be used to build code from a GitHub repository. + +#### `BitBucketSource` + +This source type can be used to build code from a BitBucket repository. + ### 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: +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. +* `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). +* `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. +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 +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`: +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. +* 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: @@ -81,45 +128,35 @@ 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 +### Events -#### CodeCommit +AWS CodeBuild projects can be used either as a source for events or be triggered +by events via an event rule. -Create a CodeBuild project with CodeCommit as the source: +#### Using Project as an event target -```ts -import codebuild = require('@aws-cdk/aws-codebuild'); -import codecommit = require('@aws-cdk/aws-codecommit'); +The `Project` construct implements the `IEventRuleTarget` interface. This means +that it can be used as a target for event rules: -const repo = new codecommit.Repository(this, 'MyRepo', { repositoryName: 'foo' }); -new codebuild.Project(this, 'MyFirstCodeCommitProject', { - source: new codebuild.CodeCommitSource({ - repository: repo, - }), -}); +```ts +// start build when a commit is pushed +codeCommitRepository.onCommit('OnCommit', project); ``` -#### S3 +#### Using Project as an event source -Create a CodeBuild project with an S3 bucket as the source: +To define CloudWatch event rules for build projects, use one of the `onXxx` +methods: ```ts -import codebuild = require('@aws-cdk/aws-codebuild'); -import s3 = require('@aws-cdk/aws-s3'); - -const bucket = new s3.Bucket(this, 'MyBucket'); -new codebuild.Project(this, 'MyProject', { - source: new codebuild.S3BucketSource({ - bucket: bucket, - path: 'path/to/file.zip', - }), -}); +const rule = project.onStateChange('BuildStateChange'); +rule.addTarget(lambdaFunction); ``` -#### CodePipeline -Example of a Project used in CodePipeline, -alongside CodeCommit: +### Using an AWS CodeBuild Project as an AWS CodePipeline action + +Example of a Project used in CodePipeline, alongside CodeCommit: ```ts import codebuild = require('@aws-cdk/aws-codebuild'); @@ -127,7 +164,7 @@ import codecommit = require('@aws-cdk/aws-codecommit'); import codepipeline = require('@aws-cdk/aws-codepipeline'); const repository = new codecommit.Repository(this, 'MyRepository', { - repositoryName: 'MyRepository', + repositoryName: 'MyRepository', }); const project = new codebuild.PipelineProject(this, 'MyProject'); @@ -139,19 +176,19 @@ repository.addToPipeline(sourceStage, 'CodeCommit'); const buildStage = pipeline.addStage('Build'); new codebuild.PipelineBuildAction(this, 'CodeBuild', { - stage: buildStage, - project, + stage: buildStage, + project, }); ``` -The `PipelineProject` utility class is a simple sugar around the `Project` class, -it's equivalent to: +The `PipelineProject` utility class is a simple sugar around the `Project` +class, it's equivalent to: ```ts const project = new codebuild.Project(this, 'MyProject', { - source: new codebuild.CodePipelineSource(), - artifacts: new codebuild.CodePipelineBuildArtifacts(), - // rest of the properties from PipelineProject are passed unchanged... + source: new codebuild.CodePipelineSource(), + artifacts: new codebuild.CodePipelineBuildArtifacts(), + // rest of the properties from PipelineProject are passed unchanged... } ``` @@ -162,175 +199,155 @@ You can also add the Project to the Pipeline directly: const buildAction = project.addBuildToPipeline(buildStage, 'CodeBuild'); ``` -In addition to the build Action, -there is also a test Action. -It works very similarly to the build Action, -the only difference is that the test Action does not always produce an output artifact. +In addition to the build Action, there is also a test Action. It works very +similarly to the build Action, the only difference is that the test Action does +not always produce an output artifact. Examples: ```ts new codebuild.PipelineTestAction(this, 'IntegrationTest', { - stage: buildStage, - project, - // outputArtifactName is optional - if you don't specify it, - // the Action will have an undefined `outputArtifact` property - outputArtifactName: 'IntegrationTestOutput', + stage: buildStage, + project, + // outputArtifactName is optional - if you don't specify it, + // the Action will have an undefined `outputArtifact` property + outputArtifactName: 'IntegrationTestOutput', }); // equivalent to the code above: project.addTestToPipeline(buildStage, 'IntegrationTest', { - // of course, this property is optional here as well - outputArtifactName: 'IntegrationTestOutput', + // of course, this property is optional here as well + outputArtifactName: 'IntegrationTestOutput', }); ``` -### Using Project as an event target - -The `Project` construct implements the `IEventRuleTarget` interface. This means that it can be -used as a target for event rules: - -```ts -// start build when a commit is pushed -codeCommitRepository.onCommit('OnCommit', project); -``` - -### Using Project as an event source - -To define CloudWatch event rules for build projects, use one of the `onXxx` methods: - -```ts -const rule = project.onStateChange('BuildStateChange'); -rule.addTarget(lambdaFunction); -``` - ### Secondary sources and artifacts -CodeBuild Projects can get their sources from multiple places, -and produce multiple outputs. For example: +CodeBuild Projects can get their sources from multiple places, and produce +multiple outputs. For example: ```ts const project = new codebuild.Project(this, 'MyProject', { - secondarySources: [ - new codebuild.CodeCommitSource({ - identifier: 'source2', - repository: repo, - }), - ], - secondaryArtifacts: [ - new codebuild.S3BucketBuildArtifacts({ - identifier: 'artifact2', - bucket: bucket, - path: 'some/path', - name: 'file.zip', - }), - ], - // ... + secondarySources: [ + new codebuild.CodeCommitSource({ + identifier: 'source2', + repository: repo, + }), + ], + secondaryArtifacts: [ + new codebuild.S3BucketBuildArtifacts({ + identifier: 'artifact2', + bucket: bucket, + path: 'some/path', + name: 'file.zip', + }), + ], + // ... }); ``` -Note that the `identifier` property is required for both secondary sources and artifacts. +Note that the `identifier` property is required for both secondary sources and +artifacts. -The contents of the secondary source will be available to the build under the directory -specified by the `CODEBUILD_SRC_DIR_` environment variable +The contents of the secondary source will be available to the build under the +directory specified by the `CODEBUILD_SRC_DIR_` environment variable (so, `CODEBUILD_SRC_DIR_source2` in the above case). -The secondary artifacts have their own section in the buildspec, -under the regular `artifacts` one. -Each secondary artifact has its own section, -beginning with their identifier. +The secondary artifacts have their own section in the buildspec, under the +regular `artifacts` one. Each secondary artifact has its own section, beginning +with their identifier. So, a buildspec for the above Project could look something like this: ```ts const project = new codebuild.Project(this, 'MyProject', { - // secondary sources and artifacts as above... - buildSpec: { - version: '0.2', - phases: { - build: { - commands: [ - 'cd $CODEBUILD_SRC_DIR_source2', - 'touch output2.txt', - ], - }, - }, - artifacts: { - 'secondary-artifacts': { - 'artifact2': { - 'base-directory': '$CODEBUILD_SRC_DIR_source2', - 'files': [ - 'output2.txt', - ], - }, - }, - }, + // secondary sources and artifacts as above... + buildSpec: { + version: '0.2', + phases: { + build: { + commands: [ + 'cd $CODEBUILD_SRC_DIR_source2', + 'touch output2.txt', + ], }, + }, + artifacts: { + 'secondary-artifacts': { + 'artifact2': { + 'base-directory': '$CODEBUILD_SRC_DIR_source2', + 'files': [ + 'output2.txt', + ], + }, + }, + }, + }, }); ``` #### Multiple inputs and outputs in CodePipeline -When you want to have multiple inputs and/or outputs for a Project used in a Pipeline, -instead of using the `secondarySources` and `secondaryArtifacts` properties, -you need to use the `additionalInputArtifacts` and `additionalOutputArtifactNames` -properties of the CodeBuild CodePipeline Actions. -Example: +When you want to have multiple inputs and/or outputs for a Project used in a +Pipeline, instead of using the `secondarySources` and `secondaryArtifacts` +properties, you need to use the `additionalInputArtifacts` and +`additionalOutputArtifactNames` properties of the CodeBuild CodePipeline +Actions. Example: ```ts const sourceStage = pipeline.addStage('Source'); const sourceAction1 = repository1.addToPipeline(sourceStage, 'Source1'); const sourceAction2 = repository2.addToPipeline(sourceStage, 'Source2', { - outputArtifactName: 'source2', + outputArtifactName: 'source2', }); const buildStage = pipeline.addStage('Build'); const buildAction = project.addBuildToPipeline(buildStage, 'Build', { - inputArtifact: sourceAction1.outputArtifact, - outputArtifactName: 'artifact1', // for better buildspec readability - see below - additionalInputArtifacts: [ - sourceAction2.outputArtifact, // this is where 'source2' comes from - ], - additionalOutputArtifactNames: [ - 'artifact2', - ], + inputArtifact: sourceAction1.outputArtifact, + outputArtifactName: 'artifact1', // for better buildspec readability - see below + additionalInputArtifacts: [ + sourceAction2.outputArtifact, // this is where 'source2' comes from + ], + additionalOutputArtifactNames: [ + 'artifact2', + ], }); ``` -**Note**: when a CodeBuild Action in a Pipeline has more than one output, -it will only use the `secondary-artifacts` field of the buildspec, -never the primary output specification directly under `artifacts`. -Because of that, it pays to name even your primary output artifact on the Pipeline, -like we did above, so that you know what name to use in the buildspec. +**Note**: when a CodeBuild Action in a Pipeline has more than one output, it +will only use the `secondary-artifacts` field of the buildspec, never the +primary output specification directly under `artifacts`. Because of that, it +pays to name even your primary output artifact on the Pipeline, like we did +above, so that you know what name to use in the buildspec. Example buildspec for the above project: ```ts const project = new codebuild.PipelineProject(this, 'MyProject', { - buildSpec: { - version: '0.2', - phases: { - build: { - commands: [ - // By default, you're in a directory with the contents of the repository from sourceAction1. - // Use the CODEBUILD_SRC_DIR_source2 environment variable - // to get a path to the directory with the contents of the second input repository. - ], - }, + buildSpec: { + version: '0.2', + phases: { + build: { + commands: [ + // By default, you're in a directory with the contents of the repository from sourceAction1. + // Use the CODEBUILD_SRC_DIR_source2 environment variable + // to get a path to the directory with the contents of the second input repository. + ], + }, + }, + artifacts: { + 'secondary-artifacts': { + 'artifact1': { + // primary Action output artifact, + // available as buildAction.outputArtifact }, - artifacts: { - 'secondary-artifacts': { - 'artifact1': { - // primary Action output artifact, - // available as buildAction.outputArtifact - }, - 'artifact2': { - // additional output artifact, - // available as buildAction.additionalOutputArtifact('artifact2') - }, - }, + 'artifact2': { + // additional output artifact, + // available as buildAction.additionalOutputArtifact('artifact2') }, + }, }, - // ... + }, + // ... }); ``` diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index a81dd239125b4..59ee321dbd513 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -799,9 +799,8 @@ export interface IBuildImage { /** * A CodeBuild image running Linux. + * * 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: * @@ -857,7 +856,7 @@ export class LinuxBuildImage implements IBuildImage { */ public static fromEcrRepository(repository: ecr.IRepository, tag: string = 'latest'): LinuxBuildImage { const image = new LinuxBuildImage(repository.repositoryUriForTag(tag)); - repository.addToResourcePolicy(resourcePolicyStatementForRepository()); + repository.addToResourcePolicy(ecrAccessForCodeBuildService()); return image; } @@ -870,7 +869,7 @@ export class LinuxBuildImage implements IBuildImage { // allow this codebuild to pull this image (CodeBuild doesn't use a role, so // we can't use `asset.grantUseImage()`. - asset.repository.addToResourcePolicy(resourcePolicyStatementForRepository()); + asset.repository.addToResourcePolicy(ecrAccessForCodeBuildService()); return image; } @@ -918,8 +917,6 @@ 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: * @@ -952,7 +949,7 @@ export class WindowsBuildImage implements IBuildImage { */ public static fromEcrRepository(repository: ecr.IRepository, tag: string = 'latest'): WindowsBuildImage { const image = new WindowsBuildImage(repository.repositoryUriForTag(tag)); - repository.addToResourcePolicy(resourcePolicyStatementForRepository()); + repository.addToResourcePolicy(ecrAccessForCodeBuildService()); return image; } @@ -965,7 +962,7 @@ export class WindowsBuildImage implements IBuildImage { // allow this codebuild to pull this image (CodeBuild doesn't use a role, so // we can't use `asset.grantUseImage()`. - asset.repository.addToResourcePolicy(resourcePolicyStatementForRepository()); + asset.repository.addToResourcePolicy(ecrAccessForCodeBuildService()); return image; } @@ -1062,11 +1059,13 @@ function extendBuildSpec(buildSpec: any, extend: any) { } } -function resourcePolicyStatementForRepository() { +function ecrAccessForCodeBuildService(): iam.PolicyStatement { return new iam.PolicyStatement() .describe('CodeBuild') .addServicePrincipal('codebuild.amazonaws.com') - .addAction('ecr:GetDownloadUrlForLayer') - .addAction('ecr:BatchGetImage') - .addAction('ecr:BatchCheckLayerAvailability'); + .addActions( + 'ecr:GetDownloadUrlForLayer', + 'ecr:BatchGetImage', + 'ecr:BatchCheckLayerAvailability' + ); } diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.defaults.lit.ts b/packages/@aws-cdk/aws-codebuild/test/integ.defaults.lit.ts index e8d44409e9813..f3d0355985489 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.defaults.lit.ts +++ b/packages/@aws-cdk/aws-codebuild/test/integ.defaults.lit.ts @@ -9,7 +9,13 @@ class TestStack extends cdk.Stack { new codebuild.Project(this, 'MyProject', { buildSpec: { version: '0.2', - phases: { build: { commands: [ 'echo "Hello, CodeBuild!"' ] } } + phases: { + build: { + commands: [ + 'echo "Hello, CodeBuild!"' + ] + } + } } }); /// !hide diff --git a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts index 60da5c5ecfc5a..3b4fa649a6b35 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts @@ -57,11 +57,13 @@ 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. + * At least one of `repositoryArn` or `repositoryName` is required. + * + * @default If you only have a repository name and the repository is in the same + * account/region as the current stack, you can set `repositoryName` instead + * and the ARN will be formatted with the current region and account. */ - repositoryArn: string; + repositoryArn?: string; /** * The full name of the repository to import. @@ -69,6 +71,9 @@ export interface ImportRepositoryProps { * 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`). + * + * If the repository is in the same region/account as the stack, it is sufficient + * to only specify the repository name. */ repositoryName?: string; } @@ -191,19 +196,29 @@ class ImportedRepository extends RepositoryBase { constructor(parent: cdk.Construct, id: string, props: ImportRepositoryProps) { super(parent, id); - this.repositoryArn = props.repositoryArn; - // 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'); - } + if (props.repositoryArn) { + this.repositoryArn = props.repositoryArn; + } else { + if (!props.repositoryName) { + throw new Error('If "repositoruyArn" is not specified, you must specify "repositoryName", ' + + 'which also implies that the repository resides in the same region/account as this stack'); + } + + this.repositoryArn = RepositoryBase.arnForLocalRepository(props.repositoryName); + } - this.repositoryName = props.repositoryName; + if (props.repositoryName) { + this.repositoryName = props.repositoryName; } else { - this.repositoryName = props.repositoryArn.split('/').slice(1).join('/'); + // 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(this.repositoryArn)) { + throw new Error('repositoryArn is a late-bound value, and therefore repositoryName is required'); + } + + this.repositoryName = this.repositoryArn.split('/').slice(1).join('/'); } } diff --git a/packages/@aws-cdk/aws-ecr/test/test.repository.ts b/packages/@aws-cdk/aws-ecr/test/test.repository.ts index b26b74c719b1f..d9193f01d827d 100644 --- a/packages/@aws-cdk/aws-ecr/test/test.repository.ts +++ b/packages/@aws-cdk/aws-ecr/test/test.repository.ts @@ -212,6 +212,31 @@ export = { test.done(); }, + 'import only with a repository name (arn is deduced)'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const repo = ecr.Repository.import(stack, 'Repo', { + repositoryName: 'my-repo' + }); + + // THEN + test.deepEqual(cdk.resolve(repo.repositoryArn), { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':ecr:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':repository/my-repo' ] + ] + }); + test.deepEqual(cdk.resolve(repo.repositoryName), 'my-repo'); + test.done(); + }, + 'arnForLocalRepository can be used to render an ARN for a local repository'(test: Test) { // GIVEN const stack = new cdk.Stack(); From e2173d2d59056340c0370b77700e6463ecd3655d Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 28 Nov 2018 06:16:34 -0800 Subject: [PATCH 08/14] Merge --- .../@aws-cdk/aws-ecr/lib/pipeline-action.ts | 4 +- .../@aws-cdk/aws-ecr/lib/repository-ref.ts | 108 ++++++++++-------- 2 files changed, 64 insertions(+), 48 deletions(-) diff --git a/packages/@aws-cdk/aws-ecr/lib/pipeline-action.ts b/packages/@aws-cdk/aws-ecr/lib/pipeline-action.ts index a08d4a5a16805..7d5af0917560e 100644 --- a/packages/@aws-cdk/aws-ecr/lib/pipeline-action.ts +++ b/packages/@aws-cdk/aws-ecr/lib/pipeline-action.ts @@ -1,7 +1,7 @@ import codepipeline = require('@aws-cdk/aws-codepipeline-api'); import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); -import { RepositoryRef } from './repository-ref'; +import { IRepository } from './repository-ref'; /** * Common properties for the {@link PipelineSourceAction CodePipeline source Action}, @@ -33,7 +33,7 @@ export interface PipelineSourceActionProps extends CommonPipelineSourceActionPro /** * The repository that will be watched for changes. */ - repository: RepositoryRef; + repository: IRepository; } /** diff --git a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts index 6e9bc37a51579..e68dc646ce9dd 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts @@ -8,52 +8,61 @@ import { CommonPipelineSourceActionProps, PipelineSourceAction } from './pipelin * Represents an ECR repository. */ 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; + /** + * 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; + + /** + * Defines an AWS CloudWatch event rule that can trigger a target when an image is pushed to this + * repository. + * @param name The name of the rule + * @param target An IEventRuleTarget to invoke when this event happens (you can add more targets using `addTarget`) + * @param imageTag Only trigger on the specific image tag + */ + onImagePushed(name: string, target?: events.IEventRuleTarget, imageTag?: string): events.EventRule; } export interface ImportRepositoryProps { @@ -170,6 +179,13 @@ export abstract class RepositoryBase extends cdk.Construct implements IRepositor }); } + /** + * Defines an AWS CloudWatch event rule that can trigger a target when an image is pushed to this + * repository. + * @param name The name of the rule + * @param target An IEventRuleTarget to invoke when this event happens (you can add more targets using `addTarget`) + * @param imageTag Only trigger on the specific image tag + */ public onImagePushed(name: string, target?: events.IEventRuleTarget, imageTag?: string): events.EventRule { return new events.EventRule(this, name, { targets: target ? [target] : undefined, From f3ffa945d7e0f8dce13fc3e4b6951dd169d9276d Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 28 Nov 2018 06:50:30 -0800 Subject: [PATCH 09/14] update app test --- packages/@aws-cdk/cdk/test/test.app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/cdk/test/test.app.ts b/packages/@aws-cdk/cdk/test/test.app.ts index 177ac8682219d..7d32d714da2db 100644 --- a/packages/@aws-cdk/cdk/test/test.app.ts +++ b/packages/@aws-cdk/cdk/test/test.app.ts @@ -70,7 +70,7 @@ export = { delete response.runtime; test.deepEqual(response, { - version: '0.14.0', + version: '0.19.0', stacks: [ { name: 'stack1', environment: From a720d2fec522b1aee6439abc106c6c66a2d49118 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 28 Nov 2018 08:39:57 -0800 Subject: [PATCH 10/14] update expectations --- .../aws-ecr/test/integ.basic.expected.json | 27 ++----------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/packages/@aws-cdk/aws-ecr/test/integ.basic.expected.json b/packages/@aws-cdk/aws-ecr/test/integ.basic.expected.json index 3fd615ddb218c..ded7bbb272c88 100644 --- a/packages/@aws-cdk/aws-ecr/test/integ.basic.expected.json +++ b/packages/@aws-cdk/aws-ecr/test/integ.basic.expected.json @@ -50,30 +50,7 @@ }, ".amazonaws.com/", { - "Fn::Select": [ - 1, - { - "Fn::Split": [ - "/", - { - "Fn::Select": [ - 5, - { - "Fn::Split": [ - ":", - { - "Fn::GetAtt": [ - "Repo02AC86CF", - "Arn" - ] - } - ] - } - ] - } - ] - } - ] + "Ref": "Repo02AC86CF" } ] ] @@ -83,4 +60,4 @@ } } } -} +} \ No newline at end of file From 094311bbdc1128eec02dc3ea8125b04685a7e0f6 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 29 Nov 2018 11:36:34 -0800 Subject: [PATCH 11/14] Improve custom resource coverage --- .../lib/adopt-repository/handler.js | 24 +- .../assets-docker/test/test.adpot-repo.ts | 371 ++++++++++++++++++ .../assets-docker/test/test.image-asset.ts | 277 +------------ 3 files changed, 386 insertions(+), 286 deletions(-) create mode 100644 packages/@aws-cdk/assets-docker/test/test.adpot-repo.ts diff --git a/packages/@aws-cdk/assets-docker/lib/adopt-repository/handler.js b/packages/@aws-cdk/assets-docker/lib/adopt-repository/handler.js index a2db19be23d77..abc883c0c108d 100644 --- a/packages/@aws-cdk/assets-docker/lib/adopt-repository/handler.js +++ b/packages/@aws-cdk/assets-docker/lib/adopt-repository/handler.js @@ -33,16 +33,20 @@ exports.handler = async function(event, context, _callback, respond) { const adopter = await getAdopter(repo); if (event.RequestType === 'Delete') { - if (adopter.Sid !== markerStatement.Sid) { - throw new Error(`This repository is already owned by another stack: ${adopter.Sid}`); - } - try { - console.log('Deleting', repo); - const ids = (await ecr.listImages({ repositoryName: repo }).promise()).imageIds; - await ecr.batchDeleteImage({ repositoryName: repo, imageIds: ids }).promise(); - await ecr.deleteRepository({ repositoryName: repo }).promise(); - } catch(e) { - if (e.code !== 'RepositoryNotFoundException') { throw e; } + if (!adopter.Sid) { + console.log(`Repository '${repo}' not found. Delete is no-op`); + } else { + if (adopter.Sid !== markerStatement.Sid) { + throw new Error(`This repository is already owned by another stack: ${adopter.Sid}`); + } + try { + console.log('Deleting', repo); + const ids = (await ecr.listImages({ repositoryName: repo }).promise()).imageIds; + await ecr.batchDeleteImage({ repositoryName: repo, imageIds: ids }).promise(); + await ecr.deleteRepository({ repositoryName: repo }).promise(); + } catch(e) { + if (e.code !== 'RepositoryNotFoundException') { throw e; } + } } } diff --git a/packages/@aws-cdk/assets-docker/test/test.adpot-repo.ts b/packages/@aws-cdk/assets-docker/test/test.adpot-repo.ts new file mode 100644 index 0000000000000..47023371869eb --- /dev/null +++ b/packages/@aws-cdk/assets-docker/test/test.adpot-repo.ts @@ -0,0 +1,371 @@ +import { Test } from 'nodeunit'; +import path = require('path'); +import proxyquire = require('proxyquire'); + +let ecrMock: any; + +export = { + 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: { + RepositoryName: 'RepositoryName', + 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: 'RepositoryName', + data: { + RepositoryName: 'RepositoryName' + } + }); + + 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: { + RepositoryName: 'RepositoryName', + 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: 'RepositoryName', + data: { + RepositoryName: 'RepositoryName' + } + }); + + 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: { + RepositoryName: 'RepositoryName', + 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: 'RepositoryName', + data: { + RepositoryName: 'RepositoryName' + } + }); + + 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: { + RepositoryName: 'RepositoryName', + }, + RequestType: 'Create', + ResponseURL: 'https://localhost/test' + }, { + logStreamName: 'xyz', + }, undefined, response); + + test.deepEqual(output, { + responseStatus: 'SUCCESS', + reason: 'OK', + physId: 'RepositoryName', + data: { + RepositoryName: 'RepositoryName' + } + }); + + 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: { + RepositoryName: 'RepositoryName', + }, + RequestType: 'Delete', + ResponseURL: 'https://localhost/test' + }, { + logStreamName: 'xyz', + }, undefined, response); + + test.deepEqual(output, { + responseStatus: 'SUCCESS', + reason: 'OK', + physId: 'RepositoryName', + data: { + RepositoryName: 'RepositoryName' + } + }); + + test.done(); + }, + + async 'exercise "delete" handler when repository doesnt exist'(test: Test) { + const handler = proxyquire(path.resolve(__dirname, '..', 'lib', 'adopt-repository', 'handler'), { + 'aws-sdk': { '@noCallThru': true, "ECR": ECRWithRepositoryNotFound } + }); + + let output; + async function response(responseStatus: string, reason: string, physId: string, data: any) { + output = { responseStatus, reason, physId, data }; + } + + await handler.handler({ + StackId: 'StackId', + ResourceProperties: { + RepositoryName: 'RepositoryName', + }, + RequestType: 'Delete', + ResponseURL: 'https://localhost/test' + }, { + logStreamName: 'xyz', + }, undefined, response); + + test.deepEqual(output, { + responseStatus: 'SUCCESS', + reason: 'OK', + physId: 'RepositoryName', + data: { + RepositoryName: 'RepositoryName' + } + }); + + test.done(); + }, + + async 'exercise "create" handler when repository doesnt exist'(test: Test) { + const handler = proxyquire(path.resolve(__dirname, '..', 'lib', 'adopt-repository', 'handler'), { + 'aws-sdk': { '@noCallThru': true, "ECR": ECRWithRepositoryNotFound } + }); + + let output; + async function response(responseStatus: string, reason: string, physId: string, data: any) { + output = { responseStatus, reason, physId, data }; + } + + await handler.handler({ + StackId: 'StackId', + ResourceProperties: { + RepositoryName: 'RepositoryName', + }, + RequestType: 'Create', + ResponseURL: 'https://localhost/test' + }, { + logStreamName: 'xyz', + }, undefined, response); + + test.deepEqual(output, { + responseStatus: 'FAILED', + reason: 'Simulated RepositoryPolicyNotFoundException', + physId: 'xyz', + 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: "*" + } + ] + }); +} + +function ECRWithRepositoryNotFound() { + const ecr = new ECR({}); + ecr.shouldThrowNotFound = true; + return ecr; +} + +class ECR { + public lastSetRepositoryPolicyRequest: any; + public shouldThrowNotFound = false; + + public constructor(private policy: any) { + } + + public getRepositoryPolicy() { + const self = this; + return { + async promise() { + if (self.shouldThrowNotFound) { return self.throwNotFound(); } + return { policyText: JSON.stringify(self.policy) }; + } + }; + } + + public setRepositoryPolicy(req: any) { + const self = this; + this.lastSetRepositoryPolicyRequest = req; + + return { + async promise() { + if (self.shouldThrowNotFound) { return self.throwNotFound(); } + return; + } + }; + } + + public listImages() { + return { + async promise() { + return { imageIds: [] }; + } + }; + } + + public batchDeleteImage() { + const self = this; + return { + async promise() { + if (self.shouldThrowNotFound) { return self.throwNotFound(); } + return {}; + } + }; + } + + public deleteRepository() { + const self = this; + return { + async promise() { + if (self.shouldThrowNotFound) { return self.throwNotFound(); } + return {}; + } + }; + } + + private throwNotFound() { + const err = new Error('Simulated RepositoryPolicyNotFoundException'); + (err as any).code = 'RepositoryPolicyNotFoundException'; + throw err; + } +} diff --git a/packages/@aws-cdk/assets-docker/test/test.image-asset.ts b/packages/@aws-cdk/assets-docker/test/test.image-asset.ts index 423ebc8603cf2..4c3edbef6c636 100644 --- a/packages/@aws-cdk/assets-docker/test/test.image-asset.ts +++ b/packages/@aws-cdk/assets-docker/test/test.image-asset.ts @@ -3,13 +3,10 @@ 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 @@ -129,277 +126,5 @@ export = { })); 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: { - RepositoryName: 'RepositoryName', - 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: 'RepositoryName', - data: { - RepositoryName: 'RepositoryName' - } - }); - - 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: { - RepositoryName: 'RepositoryName', - 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: 'RepositoryName', - data: { - RepositoryName: 'RepositoryName' - } - }); - - 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: { - RepositoryName: 'RepositoryName', - 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: 'RepositoryName', - data: { - RepositoryName: 'RepositoryName' - } - }); - - 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: { - RepositoryName: 'RepositoryName', - }, - RequestType: 'Create', - ResponseURL: 'https://localhost/test' - }, { - logStreamName: 'xyz', - }, undefined, response); - - test.deepEqual(output, { - responseStatus: 'SUCCESS', - reason: 'OK', - physId: 'RepositoryName', - data: { - RepositoryName: 'RepositoryName' - } - }); - - 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: { - RepositoryName: 'RepositoryName', - }, - RequestType: 'Delete', - ResponseURL: 'https://localhost/test' - }, { - logStreamName: 'xyz', - }, undefined, response); - - test.deepEqual(output, { - responseStatus: 'SUCCESS', - reason: 'OK', - physId: 'RepositoryName', - data: { - RepositoryName: 'RepositoryName' - } - }); - - 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 {}; - } }; - } -} +}; From 78d7506dac6bd13921a19e044ce7556e9f00f0ab Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 29 Nov 2018 14:20:52 -0800 Subject: [PATCH 12/14] Update expectation --- .../test/integ.docker-asset.lit.expected.json | 83 +++++-------------- 1 file changed, 21 insertions(+), 62 deletions(-) diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.lit.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.lit.expected.json index 4ccc47693d3df..887a952d0ef7c 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.lit.expected.json +++ b/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.lit.expected.json @@ -15,7 +15,7 @@ }, "Resources": { "MyImageAdoptRepository6CA902F6": { - "Type": "Custom::CDKECRRepositoryAdoption", + "Type": "Custom::ECRAdoptedRepository", "Properties": { "ServiceToken": { "Fn::GetAtt": [ @@ -23,37 +23,17 @@ "Arn" ] }, - "RepositoryArn": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":ecr:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":repository/", - { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - ":", - { - "Ref": "MyImageImageName953AD232" - } - ] - } - ] - } - ] + "RepositoryName": { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "MyImageImageName953AD232" + } + ] + } ] }, "PolicyDocument": { @@ -347,16 +327,9 @@ }, ":repository/", { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - ":", - { - "Ref": "MyImageImageName953AD232" - } - ] - } + "Fn::GetAtt": [ + "MyImageAdoptRepository6CA902F6", + "RepositoryName" ] } ] @@ -391,16 +364,9 @@ }, ":repository/", { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - ":", - { - "Ref": "MyImageImageName953AD232" - } - ] - } + "Fn::GetAtt": [ + "MyImageAdoptRepository6CA902F6", + "RepositoryName" ] } ] @@ -412,16 +378,9 @@ }, ".amazonaws.com/", { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - ":", - { - "Ref": "MyImageImageName953AD232" - } - ] - } + "Fn::GetAtt": [ + "MyImageAdoptRepository6CA902F6", + "RepositoryName" ] }, ":", From 41a543fb482da701928bc994e0b26ac442a17e96 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 29 Nov 2018 15:09:16 -0800 Subject: [PATCH 13/14] Update more expectations --- .../fargate/integ.asset-image.expected.json | 96 +++++-------------- 1 file changed, 24 insertions(+), 72 deletions(-) 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 eaaaec08ece2b..20d71e997cd3d 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 @@ -347,7 +347,7 @@ "Type": "AWS::ECS::Cluster" }, "ImageAdoptRepositoryE1E84E35": { - "Type": "Custom::CDKECRRepositoryAdoption", + "Type": "Custom::ECRAdoptedRepository", "Properties": { "ServiceToken": { "Fn::GetAtt": [ @@ -355,37 +355,17 @@ "Arn" ] }, - "RepositoryArn": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":ecr:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":repository/", - { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - ":", - { - "Ref": "ImageImageName5E684353" - } - ] - } - ] - } - ] + "RepositoryName": { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "ImageImageName5E684353" + } + ] + } ] } } @@ -587,16 +567,9 @@ }, ":repository/", { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - ":", - { - "Ref": "ImageImageName5E684353" - } - ] - } + "Fn::GetAtt": [ + "ImageAdoptRepositoryE1E84E35", + "RepositoryName" ] } ] @@ -631,16 +604,9 @@ }, ":repository/", { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - ":", - { - "Ref": "ImageImageName5E684353" - } - ] - } + "Fn::GetAtt": [ + "ImageAdoptRepositoryE1E84E35", + "RepositoryName" ] } ] @@ -652,16 +618,9 @@ }, ".amazonaws.com/", { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - ":", - { - "Ref": "ImageImageName5E684353" - } - ] - } + "Fn::GetAtt": [ + "ImageAdoptRepositoryE1E84E35", + "RepositoryName" ] }, ":", @@ -783,16 +742,9 @@ }, ":repository/", { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - ":", - { - "Ref": "ImageImageName5E684353" - } - ] - } + "Fn::GetAtt": [ + "ImageAdoptRepositoryE1E84E35", + "RepositoryName" ] } ] From 39b3d0f1eafef6591706bac8145088cb9b079f05 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 29 Nov 2018 15:09:44 -0800 Subject: [PATCH 14/14] Add cdk.json to gitignore --- packages/@aws-cdk/aws-ecs/test/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/@aws-cdk/aws-ecs/test/.gitignore diff --git a/packages/@aws-cdk/aws-ecs/test/.gitignore b/packages/@aws-cdk/aws-ecs/test/.gitignore new file mode 100644 index 0000000000000..30531cc5fb503 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/.gitignore @@ -0,0 +1 @@ +cdk.json