From f2fe4e94290291622d7f14e471194ceac965e1b5 Mon Sep 17 00:00:00 2001 From: Romain Marcadier-Muller Date: Tue, 6 Nov 2018 16:51:28 +0100 Subject: [PATCH] feat(app-delivery): CI/CD for CDK Stacks (#1022) This is a very simple construct that allows one to add actions to a CodePipeline to deploy a CDK stack. Currently comes with some pretty strong limitations (deployed stack & code pipeline must reside in the same account & region, assets are not supported). It does however lay the foundation for the API, as the external shape of the feature shouldn't change (or not much) when amending to support cross-account deployment and assets. --- packages/@aws-cdk/app-delivery/.gitignore | 11 + packages/@aws-cdk/app-delivery/.npmignore | 15 + packages/@aws-cdk/app-delivery/LICENSE | 201 +++++++++++++ packages/@aws-cdk/app-delivery/NOTICE | 2 + packages/@aws-cdk/app-delivery/README.md | 111 ++++++++ packages/@aws-cdk/app-delivery/lib/index.ts | 1 + .../lib/pipeline-deploy-stack-action.ts | 100 +++++++ packages/@aws-cdk/app-delivery/package.json | 64 +++++ .../test/integ.cicd.expected.json | 264 ++++++++++++++++++ .../@aws-cdk/app-delivery/test/integ.cicd.ts | 27 ++ .../test/test.pipeline-deploy-stack-action.ts | 103 +++++++ packages/@aws-cdk/cdk/lib/environment.ts | 10 + 12 files changed, 909 insertions(+) create mode 100644 packages/@aws-cdk/app-delivery/.gitignore create mode 100644 packages/@aws-cdk/app-delivery/.npmignore create mode 100644 packages/@aws-cdk/app-delivery/LICENSE create mode 100644 packages/@aws-cdk/app-delivery/NOTICE create mode 100644 packages/@aws-cdk/app-delivery/README.md create mode 100644 packages/@aws-cdk/app-delivery/lib/index.ts create mode 100644 packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts create mode 100644 packages/@aws-cdk/app-delivery/package.json create mode 100644 packages/@aws-cdk/app-delivery/test/integ.cicd.expected.json create mode 100644 packages/@aws-cdk/app-delivery/test/integ.cicd.ts create mode 100644 packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts diff --git a/packages/@aws-cdk/app-delivery/.gitignore b/packages/@aws-cdk/app-delivery/.gitignore new file mode 100644 index 0000000000000..c49007df54187 --- /dev/null +++ b/packages/@aws-cdk/app-delivery/.gitignore @@ -0,0 +1,11 @@ +dist +.LAST_PACKAGE +.LAST_BUILD +.jsii +.nyc_output +.nycrc +tsconfig.json +*.js +*.d.ts +*.snk +coverage diff --git a/packages/@aws-cdk/app-delivery/.npmignore b/packages/@aws-cdk/app-delivery/.npmignore new file mode 100644 index 0000000000000..de362b6dbebc8 --- /dev/null +++ b/packages/@aws-cdk/app-delivery/.npmignore @@ -0,0 +1,15 @@ + +dist +.LAST_PACKAGE +.LAST_BUILD +*.ts +!*.d.ts +!*.js +coverage +.nyc_output +*.tgz +*.snk + + +# Include .jsii +!.jsii diff --git a/packages/@aws-cdk/app-delivery/LICENSE b/packages/@aws-cdk/app-delivery/LICENSE new file mode 100644 index 0000000000000..1739faaebb745 --- /dev/null +++ b/packages/@aws-cdk/app-delivery/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/app-delivery/NOTICE b/packages/@aws-cdk/app-delivery/NOTICE new file mode 100644 index 0000000000000..95fd48569c743 --- /dev/null +++ b/packages/@aws-cdk/app-delivery/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/app-delivery/README.md b/packages/@aws-cdk/app-delivery/README.md new file mode 100644 index 0000000000000..f6790ea3e4302 --- /dev/null +++ b/packages/@aws-cdk/app-delivery/README.md @@ -0,0 +1,111 @@ +## Continuous Integration / Continuous Delivery for CDK Applications +This library includes a *CodePipeline* action for deploying AWS CDK Applications. + +This module is part of the [AWS Cloud Development Kit](https://github.com/awslabs/aws-cdk) project. + +### Limitations +The construct library in it's current form has the following limitations: +1. It can only deploy stacks that are hosted in the same AWS account and region as the *CodePipeline* is. +2. Stacks that make use of `Asset`s cannot be deployed successfully. + +### Getting Started +In order to add the `PipelineDeployStackAction` to your *CodePipeline*, you need to have a *CodePipeline* artifact that +contains the result of invoking `cdk synth -o ` on your *CDK App*. You can for example achieve this using a +*CodeBuild* project. + +The example below defines a *CDK App* that contains 3 stacks: +* `CodePipelineStack` manages the *CodePipeline* resources, and self-updates before deploying any other stack +* `ServiceStackA` and `ServiceStackB` are service infrastructure stacks, and need to be deployed in this order + +``` + ┏━━━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Source ┃ ┃ Build ┃ ┃ Self-Update ┃ ┃ Deploy ┃ + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ + ┃ ┌────────────┐ ┃ ┃ ┌────────────┐ ┃ ┃ ┌─────────────┐ ┃ ┃ ┌─────────────┐ ┌─────────────┐ ┃ + ┃ │ GitHub ┣━╋━━╋━▶ CodeBuild ┣━╋━━╋━▶Deploy Stack ┣━╋━━╋━▶Deploy Stack ┣━▶Deploy Stack │ ┃ + ┃ │ │ ┃ ┃ │ │ ┃ ┃ │PipelineStack│ ┃ ┃ │ServiceStackA│ │ServiceStackB│ ┃ + ┃ └────────────┘ ┃ ┃ └────────────┘ ┃ ┃ └─────────────┘ ┃ ┃ └─────────────┘ └─────────────┘ ┃ + ┗━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +``` + +#### `index.ts` +```ts +import codebuild = require('@aws-cdk/aws-codebuild'); +import codepipeline = require('@aws-cdk/aws-codepipeline'); +import cdk = require('@aws-cdk/cdk'); +import cicd = require('@aws-cdk/cicd'); + +const app = new cdk.App(); + +// We define a stack that contains the CodePipeline +const pipelineStack = new cdk.Stack(app, 'PipelineStack'); +const pipeline = new codepipeline.Pipeline(pipelineStack, 'CodePipeline', { + // Mutating a CodePipeline can cause the currently propagating state to be + // "lost". Ensure we re-run the latest change through the pipeline after it's + // been mutated so we're sure the latest state is fully deployed through. + restartExecutionOnUpdate: true, + /* ... */ +}); +// Configure the CodePipeline source - where your CDK App's source code is hosted +const source = new codepipeline.GitHubSourceAction(pipelineStack, 'GitHub', { + stage: pipeline.addStage('source'), + /* ... */ +}); +const project = new codebuild.PipelineProject(pipelineStack, 'CodeBuild', { + /* ... */ +}); +const synthesizedApp = project.outputArtifact; + +// Optionally, self-update the pipeline stack +const selfUpdateStage = pipeline.addStage('SelfUpdate'); +new cicd.PipelineDeployStackAction(pipelineStack, 'SelfUpdatePipeline', { + stage: selfUpdateStage, + stack: pipelineStack, + inputArtifact: synthesizedApp, +}); + +// Now add our service stacks +const deployStage = pipeline.addStage('Deploy'); +const serviceStackA = new MyServiceStackA(app, 'ServiceStackA', { /* ... */ }); +const serviceStackB = new MyServiceStackB(app, 'ServiceStackB', { /* ... */ }); +// Add actions to deploy the stacks in the deploy stage: +new cicd.PipelineDeployStackAction(pipelineStack, 'DeployServiceStackA', { + stage: deployStage, + stack: serviceStackA, + inputArtifact: synthesizedApp, +}); +new cicd.PipelineDeployStackAction(pipelineStack, 'DeployServiceStackB', { + stage: deployStage, + stack: serviceStackB, + inputArtifact: synthesizedApp, + createChangeSetRunOrder: 998, +}); +``` + +#### `buildspec.yml` +The `PipelineDeployStackAction` expects it's `inputArtifact` to contain the result of synthesizing a CDK App using the +`cdk synth -o ` command. + +For example, a *TypeScript* or *Javascript* CDK App can add the following `buildspec.yml` at the root of the repository +configured in the `Source` stage: + +```yml +version: 0.2 +phases: + install: + commands: + # Installs the npm dependencies as defined by the `package.json` file + # present in the root directory of the package + # (`cdk init app --language=typescript` would have created one for you) + - npm install + build: + commands: + # Builds the CDK App so it can be synthesized + - npm run build + # Synthesizes the CDK App and puts the resulting artifacts into `dist` + - npm run cdk synth -- -o dist +artifacts: + # The output artifact is all the files in the `dist` directory + base-directory: dist + files: '**/*' +``` diff --git a/packages/@aws-cdk/app-delivery/lib/index.ts b/packages/@aws-cdk/app-delivery/lib/index.ts new file mode 100644 index 0000000000000..5d0ab4f1eb92a --- /dev/null +++ b/packages/@aws-cdk/app-delivery/lib/index.ts @@ -0,0 +1 @@ +export * from './pipeline-deploy-stack-action'; diff --git a/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts new file mode 100644 index 0000000000000..8105d354771a8 --- /dev/null +++ b/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts @@ -0,0 +1,100 @@ + +import cfn = require('@aws-cdk/aws-cloudformation'); +import codepipeline = require('@aws-cdk/aws-codepipeline-api'); +import cdk = require('@aws-cdk/cdk'); +import cxapi = require('@aws-cdk/cx-api'); + +export interface PipelineDeployStackActionProps { + /** + * The CDK stack to be deployed. + */ + stack: cdk.Stack; + + /** + * The CodePipeline stage in which to perform the deployment. + */ + stage: codepipeline.IStage; + + /** + * The CodePipeline artifact that holds the synthesized app, which is the + * contents of the ```` when running ``cdk synth -o ``. + */ + inputArtifact: codepipeline.Artifact; + + /** + * The name to use when creating a ChangeSet for the stack. + * + * @default CDK-CodePipeline-ChangeSet + */ + changeSetName?: string; + + /** + * The runOrder for the CodePipeline action creating the ChangeSet. + * + * @default 1 + */ + createChangeSetRunOrder?: number; + + /** + * The runOrder for the CodePipeline action executing the ChangeSet. + * + * @default ``createChangeSetRunOrder + 1`` + */ + executeChangeSetRunOrder?: number; +} + +/** + * A CodePipeline action to deploy a stack that is part of a CDK App. This + * action takes care of preparing and executing a CloudFormation ChangeSet. + * + * It currently does *not* support stacks that make use of ``Asset``s, and + * requires the deployed stack is in the same account and region where the + * CodePipeline is hosted. + */ +export class PipelineDeployStackAction extends cdk.Construct { + private readonly stack: cdk.Stack; + + constructor(parent: cdk.Construct, id: string, props: PipelineDeployStackActionProps) { + super(parent, id); + + if (!cdk.environmentEquals(props.stack.env, cdk.Stack.find(this).env)) { + // FIXME: Add the necessary to extend to stacks in a different account + throw new Error(`Cross-environment deployment is not supported`); + } + + const createChangeSetRunOrder = props.createChangeSetRunOrder || 1; + const executeChangeSetRunOrder = props.executeChangeSetRunOrder || (createChangeSetRunOrder + 1); + + if (createChangeSetRunOrder >= executeChangeSetRunOrder) { + throw new Error(`createChangeSetRunOrder (${createChangeSetRunOrder}) must be < executeChangeSetRunOrder (${executeChangeSetRunOrder})`); + } + + this.stack = props.stack; + const changeSetName = props.changeSetName || 'CDK-CodePipeline-ChangeSet'; + + new cfn.PipelineCreateReplaceChangeSetAction(this, 'ChangeSet', { + changeSetName, + runOrder: createChangeSetRunOrder, + stackName: props.stack.name, + stage: props.stage, + templatePath: props.inputArtifact.atPath(`${props.stack.name}.template.yaml`), + }); + + new cfn.PipelineExecuteChangeSetAction(this, 'Execute', { + changeSetName, + runOrder: executeChangeSetRunOrder, + stackName: props.stack.name, + stage: props.stage, + }); + } + + public validate(): string[] { + const result = super.validate(); + const assets = this.stack.metadata.filter(md => md.type === cxapi.ASSET_METADATA); + if (assets.length > 0) { + // FIXME: Implement the necessary actions to publish assets + result.push(`Cannot deploy the stack ${this.stack.name} because it references ${assets.length} asset(s)`); + } + return result; + } +} diff --git a/packages/@aws-cdk/app-delivery/package.json b/packages/@aws-cdk/app-delivery/package.json new file mode 100644 index 0000000000000..979118b3765be --- /dev/null +++ b/packages/@aws-cdk/app-delivery/package.json @@ -0,0 +1,64 @@ +{ + "name": "@aws-cdk/app-delivery", + "description": "Continuous Integration / Continuous Delivery for CDK Applications", + "version": "0.14.1", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "targets": { + "java": { + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "cdk-app-delivery" + }, + "package": "software.amazon.awscdk.appdelivery" + }, + "sphinx": {}, + "dotnet": { + "namespace": "Amazon.CDK.AppDelivery", + "packageId": "Amazon.CDK.AppDelivery", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk" + } + }, + "outdir": "dist" + }, + "scripts": { + "build": "cdk-build", + "package": "cdk-package", + "pkglint": "pkglint -f", + "test": "cdk-test", + "watch": "cdk-watch", + "integ": "cdk-integ" + }, + "dependencies": { + "@aws-cdk/aws-cloudformation": "^0.14.1", + "@aws-cdk/aws-codebuild": "^0.14.1", + "@aws-cdk/aws-codepipeline-api": "^0.14.1", + "@aws-cdk/cdk": "^0.14.1", + "@aws-cdk/cx-api": "^0.14.1" + }, + "devDependencies": { + "@aws-cdk/aws-codepipeline": "^0.14.1", + "@aws-cdk/aws-s3": "^0.14.1", + "cdk-build-tools": "^0.14.1", + "cdk-integ-tools": "^0.14.1", + "fast-check": "^1.7.0", + "pkglint": "^0.14.1" + }, + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-cdk.git" + }, + "homepage": "https://github.com/awslabs/aws-cdk", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "keywords": [ + "aws", + "cdk" + ] +} diff --git a/packages/@aws-cdk/app-delivery/test/integ.cicd.expected.json b/packages/@aws-cdk/app-delivery/test/integ.cicd.expected.json new file mode 100644 index 0000000000000..e143489256939 --- /dev/null +++ b/packages/@aws-cdk/app-delivery/test/integ.cicd.expected.json @@ -0,0 +1,264 @@ +{ + "Resources": { + "ArtifactBucket7410C9EF": { + "Type": "AWS::S3::Bucket" + }, + "CodePipelineRoleB3A660B4": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codepipeline.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "CodePipelineRoleDefaultPolicy8D520A8D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ArtifactBucket7410C9EF", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ArtifactBucket7410C9EF", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "DeployStackChangeSetRole4923A126", + "Arn" + ] + } + }, + { + "Action": [ + "cloudformation:CreateChangeSet", + "cloudformation:DeleteChangeSet", + "cloudformation:DescribeChangeSet", + "cloudformation:DescribeStacks" + ], + "Condition": { + "StringEqualsIfExists": { + "cloudformation:ChangeSetName": "CICD-ChangeSet" + } + }, + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":stack/CICD/*" + ] + ] + } + }, + { + "Action": "cloudformation:ExecuteChangeSet", + "Condition": { + "StringEquals": { + "cloudformation:ChangeSetName": "CICD-ChangeSet" + } + }, + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":stack/CICD/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CodePipelineRoleDefaultPolicy8D520A8D", + "Roles": [ + { + "Ref": "CodePipelineRoleB3A660B4" + } + ] + } + }, + "CodePipelineB74E5936": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "ArtifactStore": { + "Location": { + "Ref": "ArtifactBucket7410C9EF" + }, + "Type": "S3" + }, + "RoleArn": { + "Fn::GetAtt": [ + "CodePipelineRoleB3A660B4", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "ThirdParty", + "Provider": "GitHub", + "Version": "1" + }, + "Configuration": { + "Owner": "awslabs", + "Repo": "aws-cdk", + "Branch": "master", + "OAuthToken": "DummyToken", + "PollForSourceChanges": true + }, + "InputArtifacts": [], + "Name": "GitHub", + "OutputArtifacts": [ + { + "Name": "Artifact_CICDGitHubF8BA7ADD" + } + ], + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "CICD", + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "CICD-ChangeSet", + "TemplatePath": "Artifact_CICDGitHubF8BA7ADD::CICD.template.yaml", + "RoleArn": { + "Fn::GetAtt": [ + "DeployStackChangeSetRole4923A126", + "Arn" + ] + } + }, + "InputArtifacts": [ + { + "Name": "Artifact_CICDGitHubF8BA7ADD" + } + ], + "Name": "ChangeSet", + "OutputArtifacts": [], + "RunOrder": 10 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "CICD", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "CICD-ChangeSet" + }, + "InputArtifacts": [], + "Name": "Execute", + "OutputArtifacts": [], + "RunOrder": 999 + } + ], + "Name": "Deploy" + } + ] + }, + "DependsOn": [ + "CodePipelineRoleB3A660B4", + "CodePipelineRoleDefaultPolicy8D520A8D" + ] + }, + "DeployStackChangeSetRole4923A126": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "cloudformation.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/app-delivery/test/integ.cicd.ts b/packages/@aws-cdk/app-delivery/test/integ.cicd.ts new file mode 100644 index 0000000000000..bf6948588f17e --- /dev/null +++ b/packages/@aws-cdk/app-delivery/test/integ.cicd.ts @@ -0,0 +1,27 @@ +import code = require('@aws-cdk/aws-codepipeline'); +import s3 = require('@aws-cdk/aws-s3'); +import cdk = require('@aws-cdk/cdk'); +import cicd = require('../lib'); + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'CICD'); +const pipeline = new code.Pipeline(stack, 'CodePipeline', { + artifactBucket: new s3.Bucket(stack, 'ArtifactBucket'), +}); +const source = new code.GitHubSourceAction(stack, 'GitHub', { + stage: pipeline.addStage('Source'), + owner: 'awslabs', + repo: 'aws-cdk', + oauthToken: new cdk.Secret('DummyToken'), +}); +new cicd.PipelineDeployStackAction(stack, 'DeployStack', { + stage: pipeline.addStage('Deploy'), + stack, + changeSetName: 'CICD-ChangeSet', + createChangeSetRunOrder: 10, + executeChangeSetRunOrder: 999, + inputArtifact: source.outputArtifact, +}); + +app.run(); diff --git a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts new file mode 100644 index 0000000000000..5357783f50052 --- /dev/null +++ b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts @@ -0,0 +1,103 @@ +import code = require('@aws-cdk/aws-codepipeline'); +import api = require('@aws-cdk/aws-codepipeline-api'); +import cdk = require('@aws-cdk/cdk'); +import cxapi = require('@aws-cdk/cx-api'); +import fc = require('fast-check'); +import nodeunit = require('nodeunit'); +import { PipelineDeployStackAction } from '../lib/pipeline-deploy-stack-action'; + +const accountId = fc.array(fc.integer(0, 9), 12, 12).map(arr => arr.join()); + +export = nodeunit.testCase({ + 'rejects cross-environment deployment'(test: nodeunit.Test) { + fc.assert( + fc.property( + accountId, accountId, + (pipelineAccount, stackAccount) => { + fc.pre(pipelineAccount !== stackAccount); + test.throws(() => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Test', { env: { account: pipelineAccount } }); + const pipeline = new code.Pipeline(stack, 'Pipeline'); + const fakeAction = new FakeAction(stack, 'Fake', pipeline); + new PipelineDeployStackAction(stack, 'Action', { + changeSetName: 'ChangeSet', + inputArtifact: fakeAction.outputArtifact, + stack: new cdk.Stack(app, 'DeployedStack', { env: { account: stackAccount } }), + stage: pipeline.addStage('DeployStage'), + }); + }, 'Cross-environment deployment is not supported'); + } + ) + ); + test.done(); + }, + + 'rejects createRunOrder >= executeRunOrder'(test: nodeunit.Test) { + fc.assert( + fc.property( + fc.integer(1, 999), fc.integer(1, 999), + (createRunOrder, executeRunOrder) => { + fc.pre(createRunOrder >= executeRunOrder); + test.throws(() => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Test'); + const pipeline = new code.Pipeline(stack, 'Pipeline'); + const fakeAction = new FakeAction(stack, 'Fake', pipeline); + new PipelineDeployStackAction(stack, 'Action', { + changeSetName: 'ChangeSet', + createChangeSetRunOrder: createRunOrder, + executeChangeSetRunOrder: executeRunOrder, + inputArtifact: fakeAction.outputArtifact, + stack: new cdk.Stack(app, 'DeployedStack'), + stage: pipeline.addStage('DeployStage'), + }); + }, 'createChangeSetRunOrder must be < executeChangeSetRunOrder'); + } + ) + ); + test.done(); + }, + + 'rejects stacks with assets'(test: nodeunit.Test) { + fc.assert( + fc.property( + fc.integer(1, 5), + (assetCount) => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Test'); + const pipeline = new code.Pipeline(stack, 'Pipeline'); + const fakeAction = new FakeAction(stack, 'Fake', pipeline); + const deployedStack = new cdk.Stack(app, 'DeployedStack'); + const action = new PipelineDeployStackAction(stack, 'Action', { + changeSetName: 'ChangeSet', + inputArtifact: fakeAction.outputArtifact, + stack: deployedStack, + stage: pipeline.addStage('DeployStage'), + }); + for (let i = 0 ; i < assetCount ; i++) { + deployedStack.addMetadata(cxapi.ASSET_METADATA, {}); + } + test.deepEqual(action.validate(), + [`Cannot deploy the stack DeployedStack because it references ${assetCount} asset(s)`]); + } + ) + ); + test.done(); + } +}); + +class FakeAction extends api.Action { + public readonly outputArtifact: api.Artifact; + + constructor(parent: cdk.Construct, id: string, pipeline: code.Pipeline) { + super(parent, id, { + artifactBounds: api.defaultBounds(), + category: api.ActionCategory.Test, + provider: 'Test', + stage: pipeline.addStage('FakeStage'), + }); + + this.outputArtifact = new api.Artifact(this, 'OutputArtifact'); + } +} diff --git a/packages/@aws-cdk/cdk/lib/environment.ts b/packages/@aws-cdk/cdk/lib/environment.ts index 2bad88f641cce..e3e0cd76eca21 100644 --- a/packages/@aws-cdk/cdk/lib/environment.ts +++ b/packages/@aws-cdk/cdk/lib/environment.ts @@ -14,3 +14,13 @@ export interface Environment { */ region?: string; } + +/** + * Checks whether two environments are equal. + * @param left one of the environments to compare. + * @param right the other environment. + * @returns ``true`` if both environments are guaranteed to be in the same account and region. + */ +export function environmentEquals(left: Environment, right: Environment): boolean { + return left.account === right.account && left.region === right.region; +}