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..1ecd3a655fa63 --- /dev/null +++ b/packages/@aws-cdk/assets-docker/README.md @@ -0,0 +1,40 @@ +# 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)` to +grant the desired principal the following permissions: "ecr:GetDownloadUrlForLayer", +"ecr:BatchGetImage" and "ecr:BatchCheckLayerAvailability". 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 61% 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..abc883c0c108d 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,10 +13,6 @@ exports.handler = async function(event, context, _callback, respond) { Principal: "*" }; - function repoName(props) { - return props.RepositoryArn.split('/').slice(1).join('/'); - } - // The repository must already exist async function getAdopter(name) { try { @@ -30,19 +26,27 @@ exports.handler = async function(event, context, _callback, respond) { } } - const repo = repoName(event.ResourceProperties); + 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) { - 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; } + } } } @@ -51,15 +55,36 @@ 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(':'); + // 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, { - RepositoryUri: `${arn[4]}.dkr.ecr.${arn[3]}.amazonaws.com/${repoName(event.ResourceProperties)}` + RepositoryName: repo }); } catch (e) { console.log(e); 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..3467bd8a94510 --- /dev/null +++ b/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts @@ -0,0 +1,82 @@ +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() + .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', + 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 new file mode 100644 index 0000000000000..7b76d91d20560 --- /dev/null +++ b/packages/@aws-cdk/assets-docker/lib/image-asset.ts @@ -0,0 +1,77 @@ +import ecr = require('@aws-cdk/aws-ecr'); +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 { + /** + * 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); + + // 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(); + + // 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 AdoptedRepository(this, 'AdoptRepository', { repositoryName }); + this.imageUri = this.repository.repositoryUriForTag(imageTag); + } +} 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..1c331ca18da08 --- /dev/null +++ b/packages/@aws-cdk/assets-docker/package.json @@ -0,0 +1,77 @@ +{ + "name": "@aws-cdk/assets-docker", + "version": "0.18.1", + "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.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.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.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/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.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 new file mode 100644 index 0000000000000..4c3edbef6c636 --- /dev/null +++ b/packages/@aws-cdk/assets-docker/test/test.image-asset.ts @@ -0,0 +1,130 @@ +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 { DockerImageAsset } from '../lib'; + +// tslint:disable:object-literal-key-quotes + +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::GetAtt": [ + "ImageAdoptRepositoryE1E84E35", + "RepositoryName" + ] + } + ] + ] + } + }, + { + "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::ECRAdoptedRepository', { + "RepositoryName": { + "Fn::Select": [ 0, { "Fn::Split": [ ":", { "Ref": "ImageImageName5E684353" } ] } ] + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "BOOM", + "Effect": "Allow", + "Principal": { + "Service": "DAMN" + } + } + ], + "Version": "2012-10-17" + } + })); + + test.done(); + } +}; diff --git a/packages/@aws-cdk/aws-codebuild/README.md b/packages/@aws-cdk/aws-codebuild/README.md index 1a7ca3b2fb30c..1edd0f58e6011 100644 --- a/packages/@aws-cdk/aws-codebuild/README.md +++ b/packages/@aws-cdk/aws-codebuild/README.md @@ -1,26 +1,65 @@ ## 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. -### Using CodeBuild with other AWS services +### Installation -#### CodeCommit +Install the module: -Create a CodeBuild project with CodeCommit as the source: +```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` + +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 repo = new codecommit.Repository(this, 'MyRepo', { repositoryName: 'foo' }); +const repository = new codecommit.Repository(this, 'MyRepo', { repositoryName: 'foo' }); new codebuild.Project(this, 'MyFirstCodeCommitProject', { - source: new codebuild.CodeCommitSource({ - repository: repo, - }), + source: new codebuild.CodeCommitSource({ repository }), }); ``` -#### S3 +#### `S3BucketSource` Create a CodeBuild project with an S3 bucket as the source: @@ -30,17 +69,94 @@ 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', - }), + source: new codebuild.S3BucketSource({ + bucket: bucket, + path: 'path/to/file.zip', + }), }); ``` -#### CodePipeline +#### `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: + +* `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) + +### Events + +AWS CodeBuild projects can be used either as a source for events or be triggered +by events via an event rule. + +#### 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); +``` + -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'); @@ -48,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'); @@ -60,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... } ``` @@ -83,20 +199,19 @@ You can also add the Project to the Pipeline directly: const buildAction = project.addToPipeline(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: @@ -106,103 +221,84 @@ project.addToPipelineAsTest(buildStage, 'IntegrationTest', { }); ``` -### 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'); @@ -218,40 +314,40 @@ const buildAction = project.addToPipeline(buildStage, 'Build', { }); ``` -**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 8407eba4cd0ad..df61a04ab8fc1 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'); @@ -797,9 +799,15 @@ 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: + * + * - 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 */ @@ -828,10 +836,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(ecrAccessForCodeBuildService()); + 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(ecrAccessForCodeBuildService()); + + 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 +915,61 @@ 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(ecrAccessForCodeBuildService()); + 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(ecrAccessForCodeBuildService()); + + 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 +1058,14 @@ function extendBuildSpec(buildSpec: any, extend: any) { phase.commands.push(...extend.phases[phaseName].commands); } } + +function ecrAccessForCodeBuildService(): iam.PolicyStatement { + return new iam.PolicyStatement() + .describe('CodeBuild') + .addServicePrincipal('codebuild.amazonaws.com') + .addActions( + 'ecr:GetDownloadUrlForLayer', + 'ecr:BatchGetImage', + '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..25d040434dab1 --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/demo-image/index.py @@ -0,0 +1,2 @@ +#!/usr/bin/python +print("Hello world from the built image") 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.defaults.lit.ts b/packages/@aws-cdk/aws-codebuild/test/integ.defaults.lit.ts new file mode 100644 index 0000000000000..f3d0355985489 --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/integ.defaults.lit.ts @@ -0,0 +1,29 @@ +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..887a952d0ef7c --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.expected.json @@ -0,0 +1,419 @@ +{ + "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::ECRAdoptedRepository", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c52BE89E9", + "Arn" + ] + }, + "RepositoryName": { + "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::GetAtt": [ + "MyImageAdoptRepository6CA902F6", + "RepositoryName" + ] + } + ] + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::GetAtt": [ + "MyImageAdoptRepository6CA902F6", + "RepositoryName" + ] + } + ] + ] + } + ] + } + ] + }, + ".amazonaws.com/", + { + "Fn::GetAtt": [ + "MyImageAdoptRepository6CA902F6", + "RepositoryName" + ] + }, + ":", + { + "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 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..887a952d0ef7c --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.lit.expected.json @@ -0,0 +1,419 @@ +{ + "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::ECRAdoptedRepository", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c52BE89E9", + "Arn" + ] + }, + "RepositoryName": { + "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::GetAtt": [ + "MyImageAdoptRepository6CA902F6", + "RepositoryName" + ] + } + ] + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::GetAtt": [ + "MyImageAdoptRepository6CA902F6", + "RepositoryName" + ] + } + ] + ] + } + ] + } + ] + }, + ".amazonaws.com/", + { + "Fn::GetAtt": [ + "MyImageAdoptRepository6CA902F6", + "RepositoryName" + ] + }, + ":", + { + "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 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/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 960606f8fac04..e68dc646ce9dd 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts @@ -5,16 +5,114 @@ import cdk = require('@aws-cdk/cdk'); import { CommonPipelineSourceActionProps, PipelineSourceAction } from './pipeline-action'; /** - * 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; + + /** + * 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 { + /** + * The ARN of the repository to import. + * + * 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; + + /** + * 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`). + * + * If the repository is in the same region/account as the stack, it is sufficient + * to only specify the repository name. + */ + 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 */ @@ -31,21 +129,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() + }; } /** @@ -66,6 +179,13 @@ export abstract class RepositoryRef extends cdk.Construct { }); } + /** + * 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, @@ -101,7 +221,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) { @@ -110,23 +230,53 @@ 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 (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); + } + + if (props.repositoryName) { + this.repositoryName = props.repositoryName; + } else { + // 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('/'); + } } public addToResourcePolicy(_statement: iam.PolicyStatement) { 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/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 diff --git a/packages/@aws-cdk/aws-ecr/test/test.repository.ts b/packages/@aws-cdk/aws-ecr/test/test.repository.ts index c670eec05a699..7408d10afea12 100644 --- a/packages/@aws-cdk/aws-ecr/test/test.repository.ts +++ b/packages/@aws-cdk/aws-ecr/test/test.repository.ts @@ -142,7 +142,7 @@ export = { '.dkr.ecr.', { 'Fn::Select': [ 3, arnSplit ] }, '.amazonaws.com/', - { 'Fn::Select': [ 1, { 'Fn::Split': [ '/', { 'Fn::Select': [ 5, arnSplit ] } ] } ] } + { Ref: 'Repo02AC86CF' } ]]}); test.done(); @@ -156,13 +156,113 @@ 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(); + }, + + '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(); + 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/.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 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..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 @@ -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::ECRAdoptedRepository", "Properties": { "ServiceToken": { "Fn::GetAtt": [ @@ -355,8 +355,18 @@ "Arn" ] }, - "RepositoryArn": { - "Ref": "ImageRepositoryC2BE7AD4" + "RepositoryName": { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "ImageImageName5E684353" + } + ] + } + ] } } }, @@ -406,7 +416,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" + } + ] + } + ] + } + ] + ] } } ], @@ -503,15 +543,99 @@ "Fn::Join": [ "", [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::GetAtt": [ + "ImageAdoptRepositoryE1E84E35", + "RepositoryName" + ] + } + ] + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::GetAtt": [ + "ImageAdoptRepositoryE1E84E35", + "RepositoryName" + ] + } + ] + ] + } + ] + } + ] + }, + ".amazonaws.com/", { "Fn::GetAtt": [ "ImageAdoptRepositoryE1E84E35", - "RepositoryUri" + "RepositoryName" ] }, ":", { - "Ref": "ImageTagE17D8A6B" + "Fn::Select": [ + 1, + { + "Fn::Split": [ + ":", + { + "Ref": "ImageImageName5E684353" + } + ] + } + ] } ] ] @@ -525,8 +649,8 @@ "Devices": [], "Tmpfs": [] }, - "LogConfiguration":{ - "LogDriver":"awslogs", + "LogConfiguration": { + "LogDriver": "awslogs", "Options": { "awslogs-group": { "Ref": "FargateServiceLoggingLogGroup9B16742A" @@ -601,7 +725,30 @@ ], "Effect": "Allow", "Resource": { - "Ref": "ImageRepositoryC2BE7AD4" + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::GetAtt": [ + "ImageAdoptRepositoryE1E84E35", + "RepositoryName" + ] + } + ] + ] } }, { @@ -625,11 +772,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 +964,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 +1001,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/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: diff --git a/packages/@aws-cdk/cx-api/lib/cxapi.ts b/packages/@aws-cdk/cx-api/lib/cxapi.ts index 5e91a7b296586..51bf4388dff31 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/cx-api/lib/metadata/assets.ts b/packages/@aws-cdk/cx-api/lib/metadata/assets.ts index 524539166b724..e3645d4302f22 100644 --- a/packages/@aws-cdk/cx-api/lib/metadata/assets.ts +++ b/packages/@aws-cdk/cx-api/lib/metadata/assets.ts @@ -44,14 +44,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; \ No newline at end of file +export type AssetMetadataEntry = FileAssetMetadataEntry | ContainerImageAssetMetadataEntry; 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-init-template.sh b/packages/aws-cdk/integ-tests/test-init-template.sh deleted file mode 100755 index a17bad0b3ce29..0000000000000 --- a/packages/aws-cdk/integ-tests/test-init-template.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- - -rm -rf /tmp/cdk-integ-test -mkdir -p /tmp/cdk-integ-test -cd /tmp/cdk-integ-test - -cdk init app -l typescript -npm run build -cdk synth - -rm -rf /tmp/cdk-integ-test -mkdir -p /tmp/cdk-integ-test -cd /tmp/cdk-integ-test - -cdk init sample-app -l typescript -npm run build -cdk synth - -rm -rf /tmp/cdk-integ-test -mkdir -p /tmp/cdk-integ-test -cd /tmp/cdk-integ-test - -cdk init lib -l typescript -npm run build - -echo "✅ success" - 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') {