From c61861494aff72044417510141b43d7ea84643f6 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 8 Aug 2018 16:45:11 +0200 Subject: [PATCH] CodePipeline: add CloudFormation actions (#525) Introducing a new package, `@aws-cdk/aws-cloudformation-codepipeline`, with CodePipeline actions to deploy CloudFormation templates and create change sets. (Contributed by @hallmaxw and @mindstorms6) --- .../.gitignore | 15 + .../.npmignore | 11 + .../aws-cloudformation-codepipeline/LICENSE | 201 +++++++++++ .../aws-cloudformation-codepipeline/NOTICE | 2 + .../aws-cloudformation-codepipeline/README.md | 24 ++ .../lib/index.ts | 1 + .../lib/pipeline-actions.ts | 328 +++++++++++++++++ .../package.json | 67 ++++ .../test/integ.pipeline.expected.json | 215 ++++++++++++ .../test/integ.pipeline.ts | 42 +++ ...integ.template-from-repo.lit.expected.json | 253 ++++++++++++++ .../test/integ.template-from-repo.lit.ts | 43 +++ .../test/test.pipeline-actions.ts | 330 ++++++++++++++++++ .../tslint.json | 38 ++ .../@aws-cdk/aws-codepipeline/lib/actions.ts | 20 +- .../lib/cloudformation-actions.ts | 186 ---------- 16 files changed, 1580 insertions(+), 196 deletions(-) create mode 100644 packages/@aws-cdk/aws-cloudformation-codepipeline/.gitignore create mode 100644 packages/@aws-cdk/aws-cloudformation-codepipeline/.npmignore create mode 100644 packages/@aws-cdk/aws-cloudformation-codepipeline/LICENSE create mode 100644 packages/@aws-cdk/aws-cloudformation-codepipeline/NOTICE create mode 100644 packages/@aws-cdk/aws-cloudformation-codepipeline/README.md create mode 100644 packages/@aws-cdk/aws-cloudformation-codepipeline/lib/index.ts create mode 100644 packages/@aws-cdk/aws-cloudformation-codepipeline/lib/pipeline-actions.ts create mode 100644 packages/@aws-cdk/aws-cloudformation-codepipeline/package.json create mode 100644 packages/@aws-cdk/aws-cloudformation-codepipeline/test/integ.pipeline.expected.json create mode 100644 packages/@aws-cdk/aws-cloudformation-codepipeline/test/integ.pipeline.ts create mode 100644 packages/@aws-cdk/aws-cloudformation-codepipeline/test/integ.template-from-repo.lit.expected.json create mode 100644 packages/@aws-cdk/aws-cloudformation-codepipeline/test/integ.template-from-repo.lit.ts create mode 100644 packages/@aws-cdk/aws-cloudformation-codepipeline/test/test.pipeline-actions.ts create mode 100644 packages/@aws-cdk/aws-cloudformation-codepipeline/tslint.json delete mode 100644 packages/@aws-cdk/aws-codepipeline/lib/cloudformation-actions.ts diff --git a/packages/@aws-cdk/aws-cloudformation-codepipeline/.gitignore b/packages/@aws-cdk/aws-cloudformation-codepipeline/.gitignore new file mode 100644 index 0000000000000..acfc6e27248fe --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation-codepipeline/.gitignore @@ -0,0 +1,15 @@ +*.js +tsconfig.json +tslint.json +*.js.map +*.d.ts +*.generated.ts +dist +lib/generated/resources.ts +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudformation-codepipeline/.npmignore b/packages/@aws-cdk/aws-cloudformation-codepipeline/.npmignore new file mode 100644 index 0000000000000..f536f042b8cb6 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation-codepipeline/.npmignore @@ -0,0 +1,11 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudformation-codepipeline/LICENSE b/packages/@aws-cdk/aws-cloudformation-codepipeline/LICENSE new file mode 100644 index 0000000000000..1739faaebb745 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation-codepipeline/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/aws-cloudformation-codepipeline/NOTICE b/packages/@aws-cdk/aws-cloudformation-codepipeline/NOTICE new file mode 100644 index 0000000000000..95fd48569c743 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation-codepipeline/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/aws-cloudformation-codepipeline/README.md b/packages/@aws-cdk/aws-cloudformation-codepipeline/README.md new file mode 100644 index 0000000000000..4960c60e24674 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation-codepipeline/README.md @@ -0,0 +1,24 @@ +## AWS CodePipline Actions for AWS CloudFormation + +This module contains Actions that allows you to deploy to AWS CloudFormation from AWS CodePipeline. + +For example, the following code fragment defines a pipeline that automatically deploys a CloudFormation template +directly from a CodeCommit repository, with a manual approval step in between to confirm the changes: + +[example Pipeline to deploy CloudFormation](test/integ.template-from-repo.lit.ts) + +See [the AWS documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/continuous-delivery-codepipeline.html) +for more details about using CloudFormation in CodePipeline. + +### Actions defined by this package + +This package defines the following actions: + +* **CreateUpdateStack** - Deploy a CloudFormation template directly from the pipeline. The indicated stack is created, + or updated if it already exists. If the stack is in a failure state, deployment will fail (unless `replaceOnFailure` + is set to `true`, in which case it will be destroyed and recreated). +* **DeleteStackOnly** - Delete the stack with the given name. +* **CreateReplaceChangeSet** - Prepare a change set to be applied later. You will typically use change sets if you want + to manually verify the changes that are being staged, or if you want to separate the people (or system) preparing the + changes from the people (or system) applying the changes. +* **ExecuteChangeSet** - Execute a change set prepared previously. diff --git a/packages/@aws-cdk/aws-cloudformation-codepipeline/lib/index.ts b/packages/@aws-cdk/aws-cloudformation-codepipeline/lib/index.ts new file mode 100644 index 0000000000000..9b3db900186ee --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation-codepipeline/lib/index.ts @@ -0,0 +1 @@ +export * from './pipeline-actions'; diff --git a/packages/@aws-cdk/aws-cloudformation-codepipeline/lib/pipeline-actions.ts b/packages/@aws-cdk/aws-cloudformation-codepipeline/lib/pipeline-actions.ts new file mode 100644 index 0000000000000..7b5dee76168eb --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation-codepipeline/lib/pipeline-actions.ts @@ -0,0 +1,328 @@ +import codepipeline = require('@aws-cdk/aws-codepipeline'); +import iam = require('@aws-cdk/aws-iam'); +import cdk = require('@aws-cdk/cdk'); + +/** + * Properties common to all CloudFormation actions + */ +export interface CloudFormationCommonProps { + /** + * The name of the stack to apply this action to + */ + stackName: string; + + /** + * A name for the filename in the output artifact to store the AWS CloudFormation call's result. + * + * The file will contain the result of the call to AWS CloudFormation (for example + * the call to UpdateStack or CreateChangeSet). + * + * AWS CodePipeline adds the file to the output artifact after performing + * the specified action. + * + * @default No output artifact generated + */ + outputFileName?: string; + + /** + * The name of the output artifact to generate + * + * Only applied if `outputFileName` is set as well. + * + * @default Automatically generated artifact name. + */ + outputArtifactName?: string; +} + +/** + * Base class for Actions that execute CloudFormation + */ +export abstract class CloudFormationAction extends codepipeline.DeployAction { + /** + * Output artifact containing the CloudFormation call response + * + * Only present if configured by passing `outputFileName`. + */ + public artifact?: codepipeline.Artifact; + + constructor(parent: codepipeline.Stage, id: string, props: CloudFormationCommonProps, configuration?: any) { + super(parent, id, 'CloudFormation', { minInputs: 0, maxInputs: 10, minOutputs: 0, maxOutputs: 1 }, { + ...configuration, + StackName: props.stackName, + OutputFileName: props.outputFileName, + }); + + if (props.outputFileName) { + this.artifact = this.addOutputArtifact(props.outputArtifactName || (parent.name + this.name + 'Artifact')); + } + } +} + +/** + * Properties for the ExecuteChangeSet action. + */ +export interface ExecuteChangeSetProps extends CloudFormationCommonProps { + /** + * Name of the change set to execute. + */ + changeSetName: string; +} + +/** + * CodePipeline action to execute a prepared change set. + */ +export class ExecuteChangeSet extends CloudFormationAction { + constructor(parent: codepipeline.Stage, id: string, props: ExecuteChangeSetProps) { + super(parent, id, props, { + ActionMode: 'CHANGE_SET_EXECUTE', + ChangeSetName: props.changeSetName, + }); + } +} + +// tslint:disable:max-line-length Because of long URLs in documentation +/** + * Properties common to CloudFormation actions that stage deployments + */ +export interface CloudFormationDeploymentActionCommonProps extends CloudFormationCommonProps { + /** + * IAM role to assume when deploying changes. + * + * If not specified, a fresh role is created. The role is created with zero + * permissions unless `trustTemplate` is true, in which case the role will have + * full permissions. + * + * @default A fresh role with full or no permissions (depending on the value of `trustTemplate`). + */ + role?: iam.Role; + + /** + * Acknowledge certain changes made as part of deployment + * + * For stacks that contain certain resources, explicit acknowledgement that AWS CloudFormation + * might create or update those resources. For example, you must specify CAPABILITY_IAM if your + * stack template contains AWS Identity and Access Management (IAM) resources. For more + * information, see [Acknowledging IAM Resources in AWS CloudFormation Templates](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-template.html#using-iam-capabilities). + * + * @default No capabitilities passed, unless `trustTemplate` is true + */ + capabilities?: CloudFormationCapabilities[]; + + /** + * Whether to grant full permissions to CloudFormation while deploying this template. + * + * Setting this to `true` affects the defaults for `role` and `capabilities`, if you + * don't specify any alternatives. + * + * The default role that will be created for you will have full (i.e., `*`) + * permissions on all resources, and the deployment will have named IAM + * capabilities (i.e., able to create all IAM resources). + * + * This is a shorthand that you can use if you fully trust the templates that + * are deployed in this pipeline. If you want more fine-grained permissions, + * use `addToRolePolicy` and `capabilities` to control what the CloudFormation + * deployment is allowed to do. + * + * @default false + */ + fullPermissions?: boolean; + + /** + * Input artifact to use for template parameters values and stack policy. + * + * The template configuration file should contain a JSON object that should look like this: + * `{ "Parameters": {...}, "Tags": {...}, "StackPolicy": {... }}`. For more information, + * see [AWS CloudFormation Artifacts](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/continuous-delivery-codepipeline-cfn-artifacts.html). + * + * Note that if you include sensitive information, such as passwords, restrict access to this + * file. + * + * @default No template configuration based on input artifacts + */ + templateConfiguration?: codepipeline.ArtifactPath; + + /** + * Additional template parameters. + * + * Template parameters specified here take precedence over template parameters + * found in the artifact specified by the `templateConfiguration` property. + * + * We recommend that you use the template configuration file to specify + * most of your parameter values. Use parameter overrides to specify only + * dynamic parameter values (values that are unknown until you run the + * pipeline). + * + * All parameter names must be present in the stack template. + * + * Note: the entire object cannot be more than 1kB. + * + * @default No overrides + */ + parameterOverrides?: { [name: string]: any }; +} +// tslint:enable:max-line-length + +/** + * Base class for all CloudFormation actions that execute or stage deployments. + */ +export abstract class CloudFormationDeploymentAction extends CloudFormationAction { + public readonly role: iam.Role; + + constructor(parent: codepipeline.Stage, id: string, props: CloudFormationDeploymentActionCommonProps, configuration: any) { + const capabilities = props.fullPermissions && props.capabilities === undefined ? [CloudFormationCapabilities.NamedIAM] : props.capabilities; + + super(parent, id, props, { + ...configuration, + // This must be a string, so flatten the list to a comma-separated string. + Capabilities: (capabilities && capabilities.join(',')) || undefined, + RoleArn: new cdk.Token(() => this.role.roleArn), + ParameterOverrides: props.parameterOverrides, + TemplateConfiguration: props.templateConfiguration, + StackName: props.stackName, + }); + + if (props.role) { + this.role = props.role; + } else { + this.role = new iam.Role(this, 'Role', { + assumedBy: new cdk.ServicePrincipal('cloudformation.amazonaws.com') + }); + + if (props.fullPermissions) { + this.role.addToPolicy(new cdk.PolicyStatement().addAction('*').addAllResources()); + } + } + } + + /** + * Add statement to the service role assumed by CloudFormation while executing this action. + */ + public addToRolePolicy(statement: cdk.PolicyStatement) { + return this.role.addToPolicy(statement); + } +} + +/** + * Properties for the CreateReplaceChangeSet action. + */ +export interface CreateReplaceChangeSetProps extends CloudFormationDeploymentActionCommonProps { + /** + * Name of the change set to create or update. + */ + changeSetName: string; + + /** + * Input artifact with the ChangeSet's CloudFormation template + */ + templatePath: codepipeline.ArtifactPath; +} + +/** + * CodePipeline action to prepare a change set. + * + * Creates the change set if it doesn't exist based on the stack name and template that you submit. + * If the change set exists, AWS CloudFormation deletes it, and then creates a new one. + */ +export class CreateReplaceChangeSet extends CloudFormationDeploymentAction { + constructor(parent: codepipeline.Stage, id: string, props: CreateReplaceChangeSetProps) { + super(parent, id, props, { + ActionMode: 'CHANGE_SET_REPLACE', + ChangeSetName: props.changeSetName, + TemplatePath: props.templatePath.location, + }); + + this.addInputArtifact(props.templatePath.artifact); + } +} + +/** + * Properties for the CreateUpdate action + */ +export interface CreateUpdateProps extends CloudFormationDeploymentActionCommonProps { + /** + * Input artifact with the CloudFormation template to deploy + */ + templatePath: codepipeline.ArtifactPath; + + /** + * Replace the stack if it's in a failed state. + * + * If this is set to true and the stack is in a failed state (one of + * ROLLBACK_COMPLETE, ROLLBACK_FAILED, CREATE_FAILED, DELETE_FAILED, or + * UPDATE_ROLLBACK_FAILED), AWS CloudFormation deletes the stack and then + * creates a new stack. + * + * If this is not set to true and the stack is in a failed state, + * the deployment fails. + * + * @default false + */ + replaceOnFailure?: boolean; +} + +/** + * CodePipeline action to deploy a stack. + * + * Creates the stack if the specified stack doesn't exist. If the stack exists, + * AWS CloudFormation updates the stack. Use this action to update existing + * stacks. + * + * AWS CodePipeline won't replace the stack, and will fail deployment if the + * stack is in a failed state. Use `ReplaceOnFailure` for an action that + * will delete and recreate the stack to try and recover from failed states. + * + * Use this action to automatically replace failed stacks without recovering or + * troubleshooting them. You would typically choose this mode for testing. + */ +export class CreateUpdateStack extends CloudFormationDeploymentAction { + constructor(parent: codepipeline.Stage, id: string, props: CreateUpdateProps) { + super(parent, id, props, { + ActionMode: props.replaceOnFailure ? 'REPLACE_ON_FAILURE' : 'CREATE_UPDATE', + TemplatePath: props.templatePath.location + }); + this.addInputArtifact(props.templatePath.artifact); + } +} + +/** + * Properties for the DeleteOnly action + */ +// tslint:disable-next-line:no-empty-interface +export interface DeleteStackOnlyProps extends CloudFormationDeploymentActionCommonProps { +} + +/** + * CodePipeline action to delete a stack. + * + * Deletes a stack. If you specify a stack that doesn't exist, the action completes successfully + * without deleting a stack. + */ +export class DeleteStackOnly extends CloudFormationDeploymentAction { + constructor(parent: codepipeline.Stage, id: string, props: DeleteStackOnlyProps) { + super(parent, id, props, { + ActionMode: 'DELETE_ONLY', + }); + } +} + +/** + * Capabilities that affect whether CloudFormation is allowed to change IAM resources + */ +export enum CloudFormationCapabilities { + /** + * Capability to create anonymous IAM resources + * + * Pass this capability if you're only creating anonymous resources. + */ + IAM = 'CAPABILITY_IAM', + + /** + * Capability to create named IAM resources. + * + * Pass this capability if you're creating IAM resources that have physical + * names. + * + * `CloudFormationCapabilities.NamedIAM` implies `CloudFormationCapabilities.IAM`; you don't have to pass both. + */ + NamedIAM = 'CAPABILITY_NAMED_IAM' +} diff --git a/packages/@aws-cdk/aws-cloudformation-codepipeline/package.json b/packages/@aws-cdk/aws-cloudformation-codepipeline/package.json new file mode 100644 index 0000000000000..833d099c56086 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation-codepipeline/package.json @@ -0,0 +1,67 @@ +{ + "name": "@aws-cdk/aws-cloudformation-codepipeline", + "version": "0.8.0", + "description": "AWS CodePipeline Actions for AWS CloudFormation", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "sphinx": {}, + "java": { + "package": "software.amazon.awscdk.services.cloudformation.codepipeline", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "cloudformation-codepipeline" + } + } + } + }, + "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" + }, + "nyc": { + "lines": 60, + "branches": 30 + }, + "keywords": [ + "aws", + "cdk", + "codepipeline", + "constructs", + "cloudformation" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assert": "^0.8.0", + "cdk-build-tools": "^0.8.0", + "cdk-integ-tools": "^0.8.0", + "pkglint": "^0.8.0", + "@aws-cdk/aws-codebuild-codepipeline": "^0.8.0", + "@aws-cdk/aws-s3": "^0.8.0", + "@aws-cdk/aws-codebuild": "^0.8.0", + "@aws-cdk/aws-codecommit": "^0.8.0", + "@aws-cdk/aws-codecommit-codepipeline": "^0.8.0" + }, + "dependencies": { + "@aws-cdk/aws-codepipeline": "^0.8.0", + "@aws-cdk/aws-iam": "^0.8.0", + "@aws-cdk/cdk": "^0.8.0" + }, + "homepage": "https://github.com/awslabs/aws-cdk" +} diff --git a/packages/@aws-cdk/aws-cloudformation-codepipeline/test/integ.pipeline.expected.json b/packages/@aws-cdk/aws-cloudformation-codepipeline/test/integ.pipeline.expected.json new file mode 100644 index 0000000000000..f9ad5997041fd --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation-codepipeline/test/integ.pipeline.expected.json @@ -0,0 +1,215 @@ +{ + "Resources": { + "PipelineArtifactsBucket22248F97": { + "Type": "AWS::S3::Bucket", + "DeletionPolicy": "Retain" + }, + "PipelineRoleD68726F7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codepipeline.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleDefaultPolicyC7A05455": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:PutObject*", + "s3:DeleteObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/", + "*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + "/", + "*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineRoleDefaultPolicyC7A05455", + "Roles": [ + { + "Ref": "PipelineRoleD68726F7" + } + ] + } + }, + "PipelineC660917D": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "ArtifactStore": { + "Location": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "Type": "S3" + }, + "RoleArn": { + "Fn::GetAtt": [ + "PipelineRoleD68726F7", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "AWS", + "Provider": "S3", + "Version": "1" + }, + "Configuration": { + "S3Bucket": { + "Ref": "PipelineBucketB967BD35" + }, + "S3ObjectKey": "key", + "PollForSourceChanges": true + }, + "InputArtifacts": [], + "Name": "Source", + "OutputArtifacts": [ + { + "Name": "SourceArtifact" + } + ], + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "ChangeSetIntegTest", + "TemplatePath": "SourceArtifact::test.yaml", + "RoleArn": { + "Fn::GetAtt": [ + "CfnChangeSetRole6F05F6FC", + "Arn" + ] + }, + "StackName": "IntegTest-TestActionStack" + }, + "InputArtifacts": [ + { + "Name": "SourceArtifact" + } + ], + "Name": "DeployCFN", + "OutputArtifacts": [], + "RunOrder": 1 + } + ], + "Name": "CFN" + } + ] + }, + "DependsOn": [ + "PipelineRoleD68726F7", + "PipelineRoleDefaultPolicyC7A05455" + ] + }, + "PipelineBucketB967BD35": { + "Type": "AWS::S3::Bucket", + "Properties": { + "VersioningConfiguration": { + "Status": "Enabled" + } + } + }, + "CfnChangeSetRole6F05F6FC": { + "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/aws-cloudformation-codepipeline/test/integ.pipeline.ts b/packages/@aws-cdk/aws-cloudformation-codepipeline/test/integ.pipeline.ts new file mode 100644 index 0000000000000..cceeb7f2a45a6 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation-codepipeline/test/integ.pipeline.ts @@ -0,0 +1,42 @@ +import codepipeline = require('@aws-cdk/aws-codepipeline'); +import { ArtifactPath } from '@aws-cdk/aws-codepipeline'; +import { Role } from '@aws-cdk/aws-iam'; +import s3 = require('@aws-cdk/aws-s3'); +import cdk = require('@aws-cdk/cdk'); +import { ServicePrincipal } from '@aws-cdk/cdk'; + +import cfn_codepipeline = require('../lib'); + +const app = new cdk.App(process.argv); + +const stack = new cdk.Stack(app, 'aws-cdk-codepipeline-cloudformation'); + +const pipeline = new codepipeline.Pipeline(stack, 'Pipeline'); + +const sourceStage = new codepipeline.Stage(pipeline, 'Source'); +const bucket = new s3.Bucket(stack, 'PipelineBucket', { + versioned: true, +}); +const source = new codepipeline.AmazonS3Source(sourceStage, 'Source', { + artifactName: 'SourceArtifact', + bucket, + bucketKey: 'key', +}); + +const cfnStage = new codepipeline.Stage(pipeline, 'CFN'); + +const changeSetName = "ChangeSetIntegTest"; +const stackName = "IntegTest-TestActionStack"; + +const role = new Role(stack, 'CfnChangeSetRole', { + assumedBy: new ServicePrincipal('cloudformation.amazonaws.com'), +}); + +new cfn_codepipeline.CreateReplaceChangeSet(cfnStage, 'DeployCFN', { + changeSetName, + stackName, + role, + templatePath: new ArtifactPath(source.artifact, 'test.yaml') +}); + +process.stdout.write(app.run()); diff --git a/packages/@aws-cdk/aws-cloudformation-codepipeline/test/integ.template-from-repo.lit.expected.json b/packages/@aws-cdk/aws-cloudformation-codepipeline/test/integ.template-from-repo.lit.expected.json new file mode 100644 index 0000000000000..91fde8381e96d --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation-codepipeline/test/integ.template-from-repo.lit.expected.json @@ -0,0 +1,253 @@ +{ + "Resources": { + "PipelineArtifactsBucket22248F97": { + "Type": "AWS::S3::Bucket", + "DeletionPolicy": "Retain" + }, + "PipelineRoleD68726F7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codepipeline.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleDefaultPolicyC7A05455": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:PutObject*", + "s3:DeleteObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/", + "*" + ] + ] + } + ] + }, + { + "Action": [ + "codecommit:GetBranch", + "codecommit:GetCommit", + "codecommit:UploadArchive", + "codecommit:GetUploadArchiveStatus", + "codecommit:CancelUploadArchive" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TemplateRepo2326F199", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineRoleDefaultPolicyC7A05455", + "Roles": [ + { + "Ref": "PipelineRoleD68726F7" + } + ] + } + }, + "PipelineC660917D": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "ArtifactStore": { + "Location": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "Type": "S3" + }, + "RoleArn": { + "Fn::GetAtt": [ + "PipelineRoleD68726F7", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "AWS", + "Provider": "CodeCommit", + "Version": "1" + }, + "Configuration": { + "RepositoryName": { + "Fn::GetAtt": [ + "TemplateRepo2326F199", + "Name" + ] + }, + "BranchName": "master", + "PollForSourceChanges": true + }, + "InputArtifacts": [], + "Name": "Source", + "OutputArtifacts": [ + { + "Name": "SourceArtifact" + } + ], + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "StagedChangeSet", + "TemplatePath": "SourceArtifact::template.yaml", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineDeployPrepareChangesRoleD28C853C", + "Arn" + ] + }, + "StackName": "OurStack" + }, + "InputArtifacts": [ + { + "Name": "SourceArtifact" + } + ], + "Name": "PrepareChanges", + "OutputArtifacts": [], + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Approval", + "Owner": "AWS", + "Provider": "Manual", + "Version": "1" + }, + "InputArtifacts": [], + "Name": "ApproveChanges", + "OutputArtifacts": [], + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "StagedChangeSet", + "StackName": "OurStack" + }, + "InputArtifacts": [], + "Name": "ExecuteChanges", + "OutputArtifacts": [], + "RunOrder": 1 + } + ], + "Name": "Deploy" + } + ] + }, + "DependsOn": [ + "PipelineRoleD68726F7", + "PipelineRoleDefaultPolicyC7A05455" + ] + }, + "PipelineDeployPrepareChangesRoleD28C853C": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "cloudformation.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineDeployPrepareChangesRoleDefaultPolicy8CDCCD73": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "*", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineDeployPrepareChangesRoleDefaultPolicy8CDCCD73", + "Roles": [ + { + "Ref": "PipelineDeployPrepareChangesRoleD28C853C" + } + ] + } + }, + "TemplateRepo2326F199": { + "Type": "AWS::CodeCommit::Repository", + "Properties": { + "RepositoryName": "template-repo", + "Triggers": [] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudformation-codepipeline/test/integ.template-from-repo.lit.ts b/packages/@aws-cdk/aws-cloudformation-codepipeline/test/integ.template-from-repo.lit.ts new file mode 100644 index 0000000000000..033c4143c4210 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation-codepipeline/test/integ.template-from-repo.lit.ts @@ -0,0 +1,43 @@ +import codecommit = require('@aws-cdk/aws-codecommit'); +import codecommitpl = require('@aws-cdk/aws-codecommit-codepipeline'); +import codepipeline = require('@aws-cdk/aws-codepipeline'); +import cdk = require('@aws-cdk/cdk'); +import cfnpl = require('../lib'); + +const app = new cdk.App(process.argv); +const stack = new cdk.Stack(app, 'aws-cdk-codepipeline-cloudformation'); + +/// !show +const pipeline = new codepipeline.Pipeline(stack, 'Pipeline'); + +// Source stage: read from repository +const repo = new codecommit.Repository(stack, 'TemplateRepo', { + repositoryName: 'template-repo' +}); +const sourceStage = new codepipeline.Stage(pipeline, 'Source'); +const source = new codecommitpl.PipelineSource(sourceStage, 'Source', { + repository: repo, + artifactName: 'SourceArtifact', +}); + +// Deployment stage: create and deploy changeset with manual approval +const prodStage = new codepipeline.Stage(pipeline, 'Deploy'); +const stackName = 'OurStack'; +const changeSetName = 'StagedChangeSet'; + +new cfnpl.CreateReplaceChangeSet(prodStage, 'PrepareChanges', { + stackName, + changeSetName, + fullPermissions: true, + templatePath: source.artifact.subartifact('template.yaml'), +}); + +new codepipeline.ApprovalAction(prodStage, 'ApproveChanges'); + +new cfnpl.ExecuteChangeSet(prodStage, 'ExecuteChanges', { + stackName, + changeSetName, +}); +/// !hide + +process.stdout.write(app.run()); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudformation-codepipeline/test/test.pipeline-actions.ts b/packages/@aws-cdk/aws-cloudformation-codepipeline/test/test.pipeline-actions.ts new file mode 100644 index 0000000000000..51d6f0bfc41d6 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation-codepipeline/test/test.pipeline-actions.ts @@ -0,0 +1,330 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import { CodePipelineBuildArtifacts, CodePipelineSource, Project } from '@aws-cdk/aws-codebuild'; +import { PipelineBuildAction } from '@aws-cdk/aws-codebuild-codepipeline'; +import { Repository } from '@aws-cdk/aws-codecommit'; +import { PipelineSource } from '@aws-cdk/aws-codecommit-codepipeline'; +import { ArtifactPath, Pipeline, Stage } from '@aws-cdk/aws-codepipeline'; +import { Role } from '@aws-cdk/aws-iam'; +import cdk = require('@aws-cdk/cdk'); +import { PolicyStatement, ServicePrincipal } from '@aws-cdk/cdk'; +import { Test } from 'nodeunit'; +import { CreateReplaceChangeSet, CreateUpdateStack, ExecuteChangeSet } from '../lib/pipeline-actions'; + +// tslint:disable:object-literal-key-quotes + +export = { + 'CreateChangeSetAction can be used to make a change set from a CodePipeline'(test: Test) { + const stack = new cdk.Stack(); + + const pipeline = new Pipeline(stack, 'MagicPipeline'); + + const changeSetExecRole = new Role(stack, 'ChangeSetRole', { + assumedBy: new ServicePrincipal('cloudformation.amazonaws.com'), + }); + + /** Source! */ + const repo = new Repository(stack, 'MyVeryImportantRepo', { repositoryName: 'my-very-important-repo' }); + + const sourceStage = new Stage(pipeline, 'source'); + + const source = new PipelineSource(sourceStage, 'source', { + artifactName: 'SourceArtifact', + repository: repo, + }); + + /** Build! */ + + const buildStage = new Stage(pipeline, 'build'); + const buildArtifacts = new CodePipelineBuildArtifacts(); + const project = new Project(stack, 'MyBuildProject', { + source: new CodePipelineSource(), + artifacts: buildArtifacts, + }); + + const buildAction = new PipelineBuildAction(buildStage, 'build', { + project, + inputArtifact: source.artifact, + artifactName: "OutputYo" + }); + + /** Deploy! */ + + // To execute a change set - yes, you probably do need *:* 🤷‍♀️ + changeSetExecRole.addToPolicy(new PolicyStatement().addAllResources().addAction("*")); + + const prodStage = new Stage(pipeline, 'prod'); + const stackName = 'BrelandsStack'; + const changeSetName = 'MyMagicalChangeSet'; + + new CreateReplaceChangeSet(prodStage, 'BuildChangeSetProd', { + stackName, + changeSetName, + role: changeSetExecRole, + templatePath: new ArtifactPath(buildAction.artifact!, 'template.yaml'), + }); + + new ExecuteChangeSet(prodStage, 'ExecuteChangeSetProd', { + stackName, + changeSetName, + }); + + expect(stack).to(haveResource('AWS::CodePipeline::Pipeline', { + "ArtifactStore": { + "Location": { + "Ref": "MagicPipelineArtifactsBucket212FE7BF" + }, + "Type": "S3" + }, "RoleArn": { + "Fn::GetAtt": ["MagicPipelineRoleFB2BD6DE", + "Arn" + ] + }, + "Stages": [{ + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "AWS", "Provider": "CodeCommit", "Version": "1" + }, + "Configuration": { + "RepositoryName": { + "Fn::GetAtt": [ + "MyVeryImportantRepo11BC3EBD", + "Name" + ] + }, + "BranchName": "master", + "PollForSourceChanges": true + }, + "InputArtifacts": [], + "Name": "source", + "OutputArtifacts": [ + { + "Name": "SourceArtifact" + } + ], + "RunOrder": 1 + } + ], + "Name": "source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "MyBuildProject30DB9D6E" + } + }, + "InputArtifacts": [ + { + "Name": "SourceArtifact" + } + ], + "Name": "build", + "OutputArtifacts": [ + { + "Name": "OutputYo" + } + ], + "RunOrder": 1 + } + ], + "Name": "build" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "MyMagicalChangeSet", + "RoleArn": { + "Fn::GetAtt": [ + "ChangeSetRole0BCF99E6", + "Arn" + ] + }, + "StackName": "BrelandsStack", + "TemplatePath": "OutputYo::template.yaml" + }, + "InputArtifacts": [{"Name": "OutputYo"}], + "Name": "BuildChangeSetProd", + "OutputArtifacts": [], + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "MyMagicalChangeSet" + }, + "InputArtifacts": [], + "Name": "ExecuteChangeSetProd", + "OutputArtifacts": [], + "RunOrder": 1 + } + ], + "Name": "prod" + } + ] + })); + + test.done(); + + }, + + 'fullPermissions leads to admin role and full IAM capabilities'(test: Test) { + // GIVEN + const stack = new TestFixture(); + + // WHEN + new CreateUpdateStack(stack.deployStage, 'CreateUpdate', { + stackName: 'MyStack', + templatePath: stack.source.artifact.subartifact('template.yaml'), + fullPermissions: true, + }); + + const roleId = "PipelineDeployCreateUpdateRole515CB7D4"; + + // THEN: Action in Pipeline has named IAM capabilities + expect(stack).to(haveResource('AWS::CodePipeline::Pipeline', { + "Stages": [ + { "Name": "Source" /* don't care about the rest */ }, + { + "Name": "Deploy", + "Actions": [ + { + "Configuration": { + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { "Fn::GetAtt": [ roleId, "Arn" ] }, + "ActionMode": "CREATE_UPDATE", + "StackName": "MyStack", + "TemplatePath": "SourceArtifact::template.yaml" + }, + "InputArtifacts": [{"Name": "SourceArtifact"}], + "Name": "CreateUpdate", + }, + ], + } + ] + })); + + // THEN: Role is created with full permissions + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: "*", + Resource: "*" + } + ], + }, + Roles: [{ Ref: roleId }] + })); + + test.done(); + }, + + 'outputFileName leads to creation of output artifact'(test: Test) { + // GIVEN + const stack = new TestFixture(); + + // WHEN + new CreateUpdateStack(stack.deployStage, 'CreateUpdate', { + stackName: 'MyStack', + templatePath: stack.source.artifact.subartifact('template.yaml'), + outputFileName: 'CreateResponse.json', + }); + + // THEN: Action has output artifacts + expect(stack).to(haveResource('AWS::CodePipeline::Pipeline', { + "Stages": [ + { "Name": "Source" /* don't care about the rest */ }, + { + "Name": "Deploy", + "Actions": [ + { + "OutputArtifacts": [{"Name": "DeployCreateUpdateArtifact"}], + "Name": "CreateUpdate", + }, + ], + } + ] + })); + + test.done(); + }, + + 'replaceOnFailure switches action type'(test: Test) { + // GIVEN + const stack = new TestFixture(); + + // WHEN + new CreateUpdateStack(stack.deployStage, 'CreateUpdate', { + stackName: 'MyStack', + templatePath: stack.source.artifact.subartifact('template.yaml'), + replaceOnFailure: true, + }); + + // THEN: Action has output artifacts + expect(stack).to(haveResource('AWS::CodePipeline::Pipeline', { + "Stages": [ + { "Name": "Source" /* don't care about the rest */ }, + { + "Name": "Deploy", + "Actions": [ + { + "Configuration": { + "ActionMode": "REPLACE_ON_FAILURE", + }, + "Name": "CreateUpdate", + }, + ], + } + ] + })); + + test.done(); + }, +}; + +/** + * A test stack with a half-prepared pipeline ready to add CloudFormation actions to + */ +class TestFixture extends cdk.Stack { + public readonly pipeline: Pipeline; + public readonly sourceStage: Stage; + public readonly deployStage: Stage; + public readonly repo: Repository; + public readonly source: PipelineSource; + + constructor() { + super(); + + this.pipeline = new Pipeline(this, 'Pipeline'); + this.sourceStage = new Stage(this.pipeline, 'Source'); + this.deployStage = new Stage(this.pipeline, 'Deploy'); + this.repo = new Repository(this, 'MyVeryImportantRepo', { repositoryName: 'my-very-important-repo' }); + this.source = new PipelineSource(this.sourceStage, 'Source', { + artifactName: 'SourceArtifact', + repository: this.repo, + }); + } +} diff --git a/packages/@aws-cdk/aws-cloudformation-codepipeline/tslint.json b/packages/@aws-cdk/aws-cloudformation-codepipeline/tslint.json new file mode 100644 index 0000000000000..ddd9bc8e0f437 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation-codepipeline/tslint.json @@ -0,0 +1,38 @@ +{ + "extends": "tslint:recommended", + "rules": { + "semicolon": [ + true, + "always", + "ignore-interfaces" + ], + "no-invalid-template-strings": false, + "quotemark": false, + "interface-name": false, + "max-classes-per-file": false, + "member-access": { + "severity": "warning" + }, + "interface-over-type-literal": false, + "eofline": false, + "arrow-parens": false, + "no-namespace": false, + "max-line-length": [ + true, + 150 + ], + "object-literal-sort-keys": false, + "trailing-comma": false, + "no-unused-expression": [ + true, + "allow-new" + ], + "variable-name": [ + true, + "ban-keywords", + "check-format", + "allow-leading-underscore", + "allow-pascal-case" + ] + } +} diff --git a/packages/@aws-cdk/aws-codepipeline/lib/actions.ts b/packages/@aws-cdk/aws-codepipeline/lib/actions.ts index 0a25581f778d7..5b89af927f753 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/actions.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/actions.ts @@ -460,16 +460,16 @@ export class ApprovalAction extends Action { // } // } -// export class DeployAction extends Action { -// constructor(parent: Stage, name: string, provider: string, artifactBounds: ActionArtifactBounds, configuration?: any) { -// super(parent, name, { -// category: ActionCategory.Deploy, -// provider, -// artifactBounds, -// configuration -// }); -// } -// } +export class DeployAction extends Action { + constructor(parent: Stage, name: string, provider: string, artifactBounds: ActionArtifactBounds, configuration?: any) { + super(parent, name, { + category: ActionCategory.Deploy, + provider, + artifactBounds, + configuration + }); + } +} // export class CodeDeploy extends DeployAction { // constructor(parent: Stage, name: string, applicationName: string, deploymentGroupName: string) { diff --git a/packages/@aws-cdk/aws-codepipeline/lib/cloudformation-actions.ts b/packages/@aws-cdk/aws-codepipeline/lib/cloudformation-actions.ts deleted file mode 100644 index 4db9b930e7c7f..0000000000000 --- a/packages/@aws-cdk/aws-codepipeline/lib/cloudformation-actions.ts +++ /dev/null @@ -1,186 +0,0 @@ -// import { iam } from '@aws-cdk/resources'; -// import { ArtifactPath } from '.'; -// import { DeployAction } from './actions'; -// import { Stage } from './pipeline'; - -// // TODO: rework these according to new model - -// export enum CloudFormationCapabilities { -// IAM = 'CAPABILITY_IAM', -// NamedIAM = 'CAPABILITY_NAMED_IAM' -// } - -// export class CloudFormationCommonOptions { -// /** -// * For stacks that contain certain resources, explicit acknowledgement that AWS CloudFormation -// * might create or update those resources. For example, you must specify CAPABILITY_IAM if your -// * stack template contains AWS Identity and Access Management (IAM) resources. For more -// * information, see Acknowledging IAM Resources in AWS CloudFormation Templates. -// */ -// public capabilities?: CloudFormationCapabilities[]; - -// /** -// * A name for the output file, such as CreateStackOutput.json. AWS CodePipeline adds the file to -// * the output artifact after performing the specified action. -// */ -// public outputFileName?: string; - -// /** -// * A JSON object that specifies values for template parameters. If you specify parameters that -// * are also specified in the template configuration file, these values override them. All -// * parameter names must be present in the stack template. -// * -// * Note: There is a maximum size limit of 1 kilobyte for the JSON object that can be stored in -// * the ParameterOverrides property. -// * -// * We recommend that you use the template configuration file to specify most of your parameter -// * values. Use parameter overrides to specify only dynamic parameter values (values that are -// * unknown until you run the pipeline). -// */ -// public parameterOverrides?: { [name: string]: any }; - -// /** -// * The template configuration file can contain template parameter values and a stack policy. -// * Note that if you include sensitive information, such as passwords, restrict access to this -// * file. For more information, see AWS CloudFormation Artifacts. -// */ -// public templateConfiguration?: ArtifactPath; -// } - -// export class CloudFormationAction extends DeployAction { -// constructor(parent: Stage, name: string, configuration?: any) { -// super(parent, name, 'CloudFormation', { minInputs: 0, maxInputs: 10, minOutputs: 0, maxOutputs: 1 }, configuration); -// } -// } - -// export class ExecuteChangeSetOptions extends CloudFormationCommonOptions { -// public stackName: string; - -// /** -// * The name of an existing change set or a new change set that you want to create for the -// * specified stack. -// */ -// public changeSetName: string; -// } - -// /** -// * Executes a change set. -// */ -// export class ExecuteChangeSet extends CloudFormationAction { -// constructor(parent: Stage, name: string, options: ExecuteChangeSetOptions) { -// super(parent, name, { -// ActionMode: 'CHANGE_SET_EXECUTE', -// Capabilities: options.capabilities, -// ChangeSetName: options.changeSetName, -// OutputFileName: options.outputFileName, -// }); -// } -// } - -// export class ChangeSetReplaceOptions extends CloudFormationCommonOptions { -// public stackName: string; - -// /** -// * The name of an existing change set or a new change set that you want to create for the -// * specified stack. -// */ -// public changeSetName: string; -// public roleArn: iam.RoleArnAttribute; -// public templatePath: ArtifactPath; -// } - -// /** -// * Creates the change set if it doesn't exist based on the stack name and template that you submit. -// * If the change set exists, AWS CloudFormation deletes it, and then creates a new one. -// */ -// export class ChangeSetReplace extends CloudFormationAction { -// constructor(parent: Stage, name: string, options: ChangeSetReplaceOptions) { -// super(parent, name, { -// ActionMode: 'CHANGE_SET_REPLACE', -// Capabilities: options.capabilities, -// ChangeSetName: options.changeSetName, -// OutputFileName: options.outputFileName, -// ParameterOverrides: options.parameterOverrides, -// RoleArn: options.roleArn, -// StackName: options.stackName, -// TemplateConfiguration: options.templateConfiguration, -// TemplatePath: options.templatePath, -// }); -// } -// } - -// export class CreateUpdateOptions extends CloudFormationCommonOptions { -// public roleArn: iam.RoleArnAttribute; -// public stackName: string; -// public templatePath: ArtifactPath; -// } - -// /** -// * Creates the stack if the specified stack doesn't exist. If the stack exists, AWS CloudFormation -// * updates the stack. Use this action to update existing stacks. AWS CodePipeline won't replace the -// * stack. -// */ -// export class CreateUpdate extends CloudFormationAction { -// constructor(parent: Stage, name: string, options: CreateUpdateOptions) { -// super(parent, name, { -// ActionMode: 'CREATE_UPDATE', -// Capabilities: options.capabilities, -// OutputFileName: options.outputFileName, -// ParameterOverrides: options.parameterOverrides, -// RoleArn: options.roleArn, -// StackName: options.stackName, -// TemplateConfiguration: options.templateConfiguration, -// TemplatePath: options.templatePath -// }); -// } -// } - -// export class DeleteOnlyOptions extends CloudFormationCommonOptions { -// public roleArn: iam.RoleArnAttribute; -// public stackName: string; -// } - -// /** -// * Deletes a stack. If you specify a stack that doesn't exist, the action completes successfully -// * without deleting a stack. -// */ -// export class DeleteOnly extends CloudFormationAction { -// constructor(parent: Stage, name: string, options: DeleteOnlyOptions) { -// super(parent, name, { -// ActionMode: 'DELETE_ONLY', -// Capabilities: options.capabilities, -// OutputFileName: options.outputFileName, -// RoleArn: options.roleArn, -// StackName: options.stackName, -// }); -// } -// } - -// export class ReplaceOnFailureOptions extends CloudFormationCommonOptions { -// public roleArn: iam.RoleArnAttribute; -// public stackName: string; -// public templatePath: ArtifactPath; -// } - -// /** -// * Creates a stack if the specified stack doesn't exist. If the stack exists and is in a failed -// * state (reported as ROLLBACK_COMPLETE, ROLLBACK_FAILED, CREATE_FAILED, DELETE_FAILED, or -// * UPDATE_ROLLBACK_FAILED), AWS CloudFormation deletes the stack and then creates a new stack. If -// * the stack isn't in a failed state, AWS CloudFormation updates it. Use this action to -// * automatically replace failed stacks without recovering or troubleshooting them. You would -// * typically choose this mode for testing. -// */ -// export class ReplaceOnFailure extends CloudFormationAction { -// constructor(parent: Stage, name: string, options: ReplaceOnFailureOptions) { -// super(parent, name, { -// ActionMode: 'REPLACE_ON_FAILURE', -// Capabilities: options.capabilities, -// OutputFileName: options.outputFileName, -// ParameterOverrides: options.parameterOverrides, -// RoleArn: options.roleArn, -// StackName: options.stackName, -// TemplateConfiguration: options.templateConfiguration, -// TemplatePath: options.templatePath, -// }); -// } -// }