From 123cd8ff2acd23592a46add8585c78454653f12e Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Thu, 18 Apr 2019 17:36:19 -0700 Subject: [PATCH] refactor(codepipeline): unify the Action bind() signature, and introduce IAction interface. --- .../lib/pipeline-deploy-stack-action.ts | 70 ++-- packages/@aws-cdk/app-delivery/package.json | 2 + .../@aws-cdk/app-delivery/test/integ.cicd.ts | 5 +- .../test/test.pipeline-deploy-stack-action.ts | 97 +++-- .../aws-codepipeline-actions/lib/action.ts | 82 ++++ .../lib/alexa-ask/deploy-action.ts | 27 +- .../lib/cloudformation/pipeline-actions.ts | 204 ++++++---- .../lib/codebuild/build-action.ts | 32 +- .../lib/codecommit/source-action.ts | 39 +- .../lib/codedeploy/server-deploy-action.ts | 26 +- .../lib/ecr/source-action.ts | 26 +- .../lib/ecs/deploy-action.ts | 28 +- .../lib/github/source-action.ts | 31 +- .../lib/jenkins/jenkins-action.ts | 26 +- .../lib/lambda/invoke-action.ts | 23 +- .../lib/manual-approval-action.ts | 27 +- .../lib/s3/deploy-action.ts | 26 +- .../lib/s3/source-action.ts | 28 +- .../cloudformation/test.pipeline-actions.ts | 69 ++-- .../test/ecs/test.ecs-deploy-action.ts | 24 +- .../test/test.pipeline.ts | 15 +- .../@aws-cdk/aws-codepipeline/lib/action.ts | 359 +++--------------- .../lib/full-action-descriptor.ts | 56 +++ .../@aws-cdk/aws-codepipeline/lib/pipeline.ts | 89 ++--- .../@aws-cdk/aws-codepipeline/lib/stage.ts | 50 ++- .../@aws-cdk/aws-codepipeline/package.json | 3 +- .../test/fake-build-action.ts | 19 +- .../test/fake-source-action.ts | 22 +- .../aws-codepipeline/test/test.action.ts | 55 ++- .../integ.pipeline-event-target.ts | 25 +- .../test/codepipeline/pipeline.test.ts | 17 +- 31 files changed, 816 insertions(+), 786 deletions(-) create mode 100644 packages/@aws-cdk/aws-codepipeline-actions/lib/action.ts create mode 100644 packages/@aws-cdk/aws-codepipeline/lib/full-action-descriptor.ts 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 index 77afcadb2acd6..eac5752eac137 100644 --- a/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts +++ b/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts @@ -1,6 +1,7 @@ import cfn = require('@aws-cdk/aws-cloudformation'); import codepipeline = require('@aws-cdk/aws-codepipeline'); import cpactions = require('@aws-cdk/aws-codepipeline-actions'); +import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/core'); import cxapi = require('@aws-cdk/cx-api'); @@ -11,11 +12,6 @@ export interface PipelineDeployStackActionProps { */ readonly stack: cdk.Stack; - /** - * The CodePipeline stage in which to perform the deployment. - */ - readonly stage: codepipeline.IStage; - /** * The CodePipeline artifact that holds the synthesized app, which is the * contents of the ```` when running ``cdk synth -o ``. @@ -86,42 +82,40 @@ export interface PipelineDeployStackActionProps { } /** - * A Construct to deploy a stack that is part of a CDK App, using CodePipeline. + * A class to deploy a stack that is part of a CDK App, using CodePipeline. * This composite 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 { - +export class PipelineDeployStackAction implements codepipeline.IAction { /** * The role used by CloudFormation for the deploy action */ - public readonly deploymentRole: iam.IRole; + private _deploymentRole: iam.IRole; private readonly stack: cdk.Stack; + private readonly prepareChangeSetAction: cpactions.CloudFormationCreateReplaceChangeSetAction; + private readonly executeChangeSetAction: cpactions.CloudFormationExecuteChangeSetAction; - constructor(scope: cdk.Construct, id: string, props: PipelineDeployStackActionProps) { - super(scope, id); - - if (props.stack.environment !== cdk.Stack.of(this).environment) { - // FIXME: Add the necessary to extend to stacks in a different account - throw new Error(`Cross-environment deployment is not supported`); + constructor(props: PipelineDeployStackActionProps) { + this.stack = props.stack; + const assets = this.stack.node.metadata.filter(md => md.type === cxapi.ASSET_METADATA); + if (assets.length > 0) { + // FIXME: Implement the necessary actions to publish assets + throw new Error(`Cannot deploy the stack ${this.stack.stackName} because it references ${assets.length} asset(s)`); } 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'; - const capabilities = cfnCapabilities(props.adminPermissions, props.capabilities); - const changeSetAction = new cpactions.CloudFormationCreateReplaceChangeSetAction({ + this.prepareChangeSetAction = new cpactions.CloudFormationCreateReplaceChangeSetAction({ actionName: 'ChangeSet', changeSetName, runOrder: createChangeSetRunOrder, @@ -131,15 +125,29 @@ export class PipelineDeployStackAction extends cdk.Construct { deploymentRole: props.role, capabilities, }); - props.stage.addAction(changeSetAction); - props.stage.addAction(new cpactions.CloudFormationExecuteChangeSetAction({ + this.executeChangeSetAction = new cpactions.CloudFormationExecuteChangeSetAction({ actionName: 'Execute', changeSetName, runOrder: executeChangeSetRunOrder, - stackName: props.stack.stackName, - })); + stackName: this.stack.stackName, + }); + } + + public bind(scope: cdk.Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { + if (this.stack.environment !== cdk.Stack.of(scope).environment) { + // FIXME: Add the necessary to extend to stacks in a different account + throw new Error(`Cross-environment deployment is not supported`); + } - this.deploymentRole = changeSetAction.deploymentRole; + stage.addAction(this.prepareChangeSetAction); + this._deploymentRole = this.prepareChangeSetAction.deploymentRole; + + return this.executeChangeSetAction.bind(scope, stage, options); + } + + public get deploymentRole(): iam.IRole { + return this._deploymentRole; } /** @@ -155,14 +163,12 @@ export class PipelineDeployStackAction extends cdk.Construct { this.deploymentRole.addToPolicy(statement); } - protected validate(): string[] { - const result = super.validate(); - const assets = this.stack.node.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.stackName} because it references ${assets.length} asset(s)`); - } - return result; + public onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule { + return this.executeChangeSetAction.onStateChange(name, target, options); + } + + public get actionProperties(): codepipeline.ActionProperties { + return this.executeChangeSetAction.actionProperties; } } diff --git a/packages/@aws-cdk/app-delivery/package.json b/packages/@aws-cdk/app-delivery/package.json index 2c0fa38e3be50..a12f508a4c077 100644 --- a/packages/@aws-cdk/app-delivery/package.json +++ b/packages/@aws-cdk/app-delivery/package.json @@ -42,6 +42,7 @@ "@aws-cdk/aws-codebuild": "^0.35.0", "@aws-cdk/aws-codepipeline": "^0.35.0", "@aws-cdk/aws-codepipeline-actions": "^0.35.0", + "@aws-cdk/aws-events": "^0.35.0", "@aws-cdk/aws-iam": "^0.35.0", "@aws-cdk/core": "^0.35.0", "@aws-cdk/cx-api": "^0.35.0" @@ -75,6 +76,7 @@ "@aws-cdk/aws-codebuild": "^0.35.0", "@aws-cdk/aws-codepipeline": "^0.35.0", "@aws-cdk/aws-codepipeline-actions": "^0.35.0", + "@aws-cdk/aws-events": "^0.35.0", "@aws-cdk/aws-iam": "^0.35.0", "@aws-cdk/core": "^0.35.0", "@aws-cdk/cx-api": "^0.35.0" diff --git a/packages/@aws-cdk/app-delivery/test/integ.cicd.ts b/packages/@aws-cdk/app-delivery/test/integ.cicd.ts index c1d71ea2d8f6b..bf26b60ac8aed 100644 --- a/packages/@aws-cdk/app-delivery/test/integ.cicd.ts +++ b/packages/@aws-cdk/app-delivery/test/integ.cicd.ts @@ -27,8 +27,7 @@ pipeline.addStage({ actions: [source], }); const stage = pipeline.addStage({ stageName: 'Deploy' }); -new cicd.PipelineDeployStackAction(stack, 'DeployStack', { - stage, +stage.addAction(new cicd.PipelineDeployStackAction({ stack, changeSetName: 'CICD-ChangeSet', createChangeSetRunOrder: 10, @@ -36,6 +35,6 @@ new cicd.PipelineDeployStackAction(stack, 'DeployStack', { input: sourceOutput, adminPermissions: false, capabilities: [cfn.CloudFormationCapabilities.NONE], -}); +})); app.synth(); 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 index 5a78f6b1d6e68..99f4091e5f743 100644 --- 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 @@ -3,10 +3,10 @@ import cfn = require('@aws-cdk/aws-cloudformation'); import codebuild = require('@aws-cdk/aws-codebuild'); import codepipeline = require('@aws-cdk/aws-codepipeline'); import cpactions = require('@aws-cdk/aws-codepipeline-actions'); +import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import s3 = require('@aws-cdk/aws-s3'); import cdk = require('@aws-cdk/core'); -import { ConstructNode } from '@aws-cdk/core'; import cxapi = require('@aws-cdk/cx-api'); import fc = require('fast-check'); import nodeunit = require('nodeunit'); @@ -34,13 +34,14 @@ export = nodeunit.testCase({ stageName: 'FakeStage', actions: [fakeAction], }); - new PipelineDeployStackAction(stack, 'Action', { + + const deployStage = pipeline.addStage({ stageName: 'DeployStage' }); + deployStage.addAction(new PipelineDeployStackAction({ changeSetName: 'ChangeSet', input: fakeAction.outputArtifact, stack: new cdk.Stack(app, 'DeployedStack', { env: { account: stackAccount } }), - stage: pipeline.addStage({ stageName: 'DeployStage' }), adminPermissions: false, - }); + })); }, 'Cross-environment deployment is not supported'); } ) @@ -63,15 +64,15 @@ export = nodeunit.testCase({ stageName: 'FakeStage', actions: [fakeAction], }); - new PipelineDeployStackAction(stack, 'Action', { + const deployStage = pipeline.addStage({ stageName: 'DeployStage' }); + deployStage.addAction(new PipelineDeployStackAction({ changeSetName: 'ChangeSet', createChangeSetRunOrder: createRunOrder, executeChangeSetRunOrder: executeRunOrder, input: fakeAction.outputArtifact, stack: new cdk.Stack(app, 'DeployedStack'), - stage: pipeline.addStage({ stageName: 'DeployStage' }), adminPermissions: false, - }); + })); }, 'createChangeSetRunOrder must be < executeChangeSetRunOrder'); } ) @@ -102,41 +103,36 @@ export = nodeunit.testCase({ const selfUpdateStage4 = pipeline.addStage({ stageName: 'SelfUpdate4' }); const selfUpdateStage5 = pipeline.addStage({ stageName: 'SelfUpdate5' }); - new PipelineDeployStackAction(pipelineStack, 'SelfUpdatePipeline', { - stage: selfUpdateStage1, + selfUpdateStage1.addAction(new PipelineDeployStackAction({ stack: pipelineStack, input: selfUpdatingStack.synthesizedApp, capabilities: [cfn.CloudFormationCapabilities.NAMED_IAM], adminPermissions: false, - }); - new PipelineDeployStackAction(pipelineStack, 'DeployStack', { - stage: selfUpdateStage2, + })); + selfUpdateStage2.addAction(new PipelineDeployStackAction({ stack: stackWithNoCapability, input: selfUpdatingStack.synthesizedApp, capabilities: [cfn.CloudFormationCapabilities.NONE], adminPermissions: false, - }); - new PipelineDeployStackAction(pipelineStack, 'DeployStack2', { - stage: selfUpdateStage3, + })); + selfUpdateStage3.addAction(new PipelineDeployStackAction({ stack: stackWithAnonymousCapability, input: selfUpdatingStack.synthesizedApp, capabilities: [cfn.CloudFormationCapabilities.ANONYMOUS_IAM], adminPermissions: false, - }); - new PipelineDeployStackAction(pipelineStack, 'DeployStack3', { - stage: selfUpdateStage4, + })); + selfUpdateStage4.addAction(new PipelineDeployStackAction({ stack: stackWithAutoExpandCapability, input: selfUpdatingStack.synthesizedApp, capabilities: [cfn.CloudFormationCapabilities.AUTO_EXPAND], adminPermissions: false, - }); - new PipelineDeployStackAction(pipelineStack, 'DeployStack4', { - stage: selfUpdateStage5, + })); + selfUpdateStage5.addAction(new PipelineDeployStackAction({ stack: stackWithAnonymousAndAutoExpandCapability, input: selfUpdatingStack.synthesizedApp, capabilities: [cfn.CloudFormationCapabilities.ANONYMOUS_IAM, cfn.CloudFormationCapabilities.AUTO_EXPAND], adminPermissions: false, - }); + })); expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ Configuration: { StackName: "TestStack", @@ -193,12 +189,11 @@ export = nodeunit.testCase({ const pipeline = selfUpdatingStack.pipeline; const selfUpdateStage = pipeline.addStage({ stageName: 'SelfUpdate' }); - new PipelineDeployStackAction(pipelineStack, 'SelfUpdatePipeline', { - stage: selfUpdateStage, + selfUpdateStage.addAction(new PipelineDeployStackAction({ stack: pipelineStack, input: selfUpdatingStack.synthesizedApp, adminPermissions: true, - }); + })); expect(pipelineStack).to(haveResource('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', @@ -229,13 +224,13 @@ export = nodeunit.testCase({ }); const pipeline = selfUpdatingStack.pipeline; const selfUpdateStage = pipeline.addStage({ stageName: 'SelfUpdate' }); - const deployAction = new PipelineDeployStackAction(pipelineStack, 'SelfUpdatePipeline', { - stage: selfUpdateStage, + const deployAction = new PipelineDeployStackAction({ stack: pipelineStack, input: selfUpdatingStack.synthesizedApp, adminPermissions: false, role }); + selfUpdateStage.addAction(deployAction); test.same(deployAction.deploymentRole, role); test.done(); }, @@ -252,12 +247,12 @@ export = nodeunit.testCase({ // WHEN // // this our app/service/infra to deploy const deployStage = pipeline.addStage({ stageName: 'Deploy' }); - const deployAction = new PipelineDeployStackAction(pipelineStack, 'DeployServiceStackA', { - stage: deployStage, + const deployAction = new PipelineDeployStackAction({ stack: emptyStack, input: selfUpdatingStack.synthesizedApp, adminPermissions: false, }); + deployStage.addAction(deployAction); // we might need to add permissions deployAction.addToDeploymentRolePolicy(new iam.PolicyStatement({ actions: [ @@ -309,27 +304,20 @@ export = nodeunit.testCase({ fc.integer(1, 5), (assetCount) => { const app = new cdk.App(); - const stack = new cdk.Stack(app, 'Test'); - const pipeline = new codepipeline.Pipeline(stack, 'Pipeline'); - const fakeAction = new FakeAction('Fake'); - pipeline.addStage({ - stageName: 'FakeStage', - actions: [fakeAction], - }); + const deployedStack = new cdk.Stack(app, 'DeployedStack'); - const deployStage = pipeline.addStage({ stageName: 'DeployStage' }); - const action = new PipelineDeployStackAction(stack, 'Action', { - changeSetName: 'ChangeSet', - input: fakeAction.outputArtifact, - stack: deployedStack, - stage: deployStage, - adminPermissions: false, - }); for (let i = 0 ; i < assetCount ; i++) { deployedStack.node.addMetadata(cxapi.ASSET_METADATA, {}); } - test.deepEqual(ConstructNode.validate(action.node).map(x => x.message), - [`Cannot deploy the stack DeployedStack because it references ${assetCount} asset(s)`]); + + test.throws(() => { + new PipelineDeployStackAction({ + changeSetName: 'ChangeSet', + input: new codepipeline.Artifact(), + stack: deployedStack, + adminPermissions: false, + }); + }, /Cannot deploy the stack DeployedStack because it references/); } ) ); @@ -337,22 +325,27 @@ export = nodeunit.testCase({ } }); -class FakeAction extends codepipeline.Action { +class FakeAction implements codepipeline.IAction { + public readonly actionProperties: codepipeline.ActionProperties; public readonly outputArtifact: codepipeline.Artifact; constructor(actionName: string) { - super({ + this.actionProperties = { actionName, artifactBounds: { minInputs: 0, maxInputs: 5, minOutputs: 0, maxOutputs: 5 }, category: codepipeline.ActionCategory.TEST, provider: 'Test', - }); - + }; this.outputArtifact = new codepipeline.Artifact('OutputArtifact'); } - protected bind(_info: codepipeline.ActionBind): void { - // do nothing + public bind(_scope: cdk.Construct, _stage: codepipeline.IStage, _options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { + return {}; + } + + public onStateChange(_name: string, _target?: events.IRuleTarget, _options?: events.RuleProps): events.Rule { + throw new Error('onStateChange() is not available on FakeAction'); } } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/action.ts new file mode 100644 index 0000000000000..24e3b8e09ca28 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/action.ts @@ -0,0 +1,82 @@ +import codepipeline = require('@aws-cdk/aws-codepipeline'); +import events = require('@aws-cdk/aws-events'); +import { Construct } from '@aws-cdk/core'; + +/** + * Low-level class for generic CodePipeline Actions. + * + * @internal + */ +export abstract class Action implements codepipeline.IAction { + private _pipeline?: codepipeline.IPipeline; + private _stage?: codepipeline.IStage; + private _scope?: Construct; + + constructor(public readonly actionProperties: codepipeline.ActionProperties) { + // nothing to do + } + + public bind(scope: Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { + this._pipeline = stage.pipeline; + this._stage = stage; + this._scope = scope; + + return this.bound(scope, stage, options); + } + + public onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { + const rule = new events.Rule(this.scope, name, options); + rule.addTarget(target); + rule.addEventPattern({ + detailType: [ 'CodePipeline Stage Execution State Change' ], + source: [ 'aws.codepipeline' ], + resources: [ this.pipeline.pipelineArn ], + detail: { + stage: [ this.stage.stageName ], + action: [ this.actionProperties.actionName ], + }, + }); + return rule; + } + + /** + * The method called when an Action is attached to a Pipeline. + * This method is guaranteed to be called only once for each Action instance. + * + * @param options an instance of the {@link ActionBindOptions} class, + * that contains the necessary information for the Action + * to configure itself, like a reference to the Role, etc. + */ + protected abstract bound(scope: Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig; + + private get pipeline(): codepipeline.IPipeline { + if (this._pipeline) { + return this._pipeline; + } else { + throw new Error('Action must be added to a stage that is part of a pipeline before using onStateChange'); + } + } + + private get stage(): codepipeline.IStage { + if (this._stage) { + return this._stage; + } else { + throw new Error('Action must be added to a stage that is part of a pipeline before using onStateChange'); + } + } + + /** + * Retrieves the Construct scope of this Action. + * Only available after the Action has been added to a Stage, + * and that Stage to a Pipeline. + */ + private get scope(): Construct { + if (this._scope) { + return this._scope; + } else { + throw new Error('Action must be added to a stage that is part of a pipeline first'); + } + } +} diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/alexa-ask/deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/alexa-ask/deploy-action.ts index a949ef46fa1cf..42ff7aa6f9493 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/alexa-ask/deploy-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/alexa-ask/deploy-action.ts @@ -1,5 +1,6 @@ import codepipeline = require('@aws-cdk/aws-codepipeline'); -import { SecretValue } from '@aws-cdk/core'; +import { Construct, SecretValue } from '@aws-cdk/core'; +import { Action } from '../action'; /** * Construction properties of the {@link AlexaSkillDeployAction Alexa deploy Action}. @@ -39,7 +40,9 @@ export interface AlexaSkillDeployActionProps extends codepipeline.CommonActionPr /** * Deploys the skill to Alexa */ -export class AlexaSkillDeployAction extends codepipeline.Action { +export class AlexaSkillDeployAction extends Action { + private readonly props: AlexaSkillDeployActionProps; + constructor(props: AlexaSkillDeployActionProps) { super({ ...props, @@ -53,17 +56,21 @@ export class AlexaSkillDeployAction extends codepipeline.Action { maxOutputs: 0, }, inputs: getInputs(props), - configuration: { - ClientId: props.clientId, - ClientSecret: props.clientSecret, - RefreshToken: props.refreshToken, - SkillId: props.skillId, - }, }); + + this.props = props; } - protected bind(_info: codepipeline.ActionBind): void { - // nothing to do + protected bound(_scope: Construct, _stage: codepipeline.IStage, _options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { + return { + configuration: { + ClientId: this.props.clientId, + ClientSecret: this.props.clientSecret, + RefreshToken: this.props.refreshToken, + SkillId: this.props.skillId, + }, + }; } } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/pipeline-actions.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/pipeline-actions.ts index 0da996dd01245..b5226c3f132b7 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/pipeline-actions.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/pipeline-actions.ts @@ -4,6 +4,7 @@ import codepipeline = require('@aws-cdk/aws-codepipeline'); import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/core'); import { Stack } from '@aws-cdk/core'; +import { Action } from '../action'; /** * Properties common to all CloudFormation actions @@ -60,28 +61,37 @@ interface CloudFormationActionProps extends codepipeline.CommonActionProps { /** * Base class for Actions that execute CloudFormation */ -abstract class CloudFormationAction extends codepipeline.Action { - constructor(props: CloudFormationActionProps, configuration?: any) { +abstract class CloudFormationAction extends Action { + private readonly props: CloudFormationActionProps; + + constructor(props: CloudFormationActionProps, inputs: codepipeline.Artifact[] | undefined) { super({ ...props, - region: props.region, + provider: 'CloudFormation', + category: codepipeline.ActionCategory.DEPLOY, artifactBounds: { minInputs: 0, maxInputs: 10, minOutputs: 0, maxOutputs: 1, }, + inputs, outputs: props.outputFileName ? [props.output || new codepipeline.Artifact(`${props.actionName}_${props.stackName}_Artifact`)] : undefined, - provider: 'CloudFormation', - category: codepipeline.ActionCategory.DEPLOY, - configuration: { - StackName: props.stackName, - OutputFileName: props.outputFileName, - ...configuration, - } }); + + this.props = props; + } + + protected bound(_scope: cdk.Construct, _stage: codepipeline.IStage, _options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { + return { + configuration: { + StackName: this.props.stackName, + OutputFileName: this.props.outputFileName, + }, + }; } } @@ -99,19 +109,27 @@ export interface CloudFormationExecuteChangeSetActionProps extends CloudFormatio * CodePipeline action to execute a prepared change set. */ export class CloudFormationExecuteChangeSetAction extends CloudFormationAction { - private readonly props: CloudFormationExecuteChangeSetActionProps; + private readonly props2: CloudFormationExecuteChangeSetActionProps; constructor(props: CloudFormationExecuteChangeSetActionProps) { - super(props, { - ActionMode: 'CHANGE_SET_EXECUTE', - ChangeSetName: props.changeSetName, - }); + super(props, undefined); - this.props = props; + this.props2 = props; } - protected bind(info: codepipeline.ActionBind): void { - SingletonPolicy.forRole(info.role).grantExecuteChangeSet(this.props); + protected bound(scope: cdk.Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { + SingletonPolicy.forRole(options.role).grantExecuteChangeSet(this.props2); + + const actionConfig = super.bound(scope, stage, options); + return { + ...actionConfig, + configuration: { + ...actionConfig.configuration, + ActionMode: 'CHANGE_SET_EXECUTE', + ChangeSetName: this.props2.changeSetName, + }, + }; } } @@ -218,27 +236,12 @@ interface CloudFormationDeployActionProps extends CloudFormationActionProps { */ abstract class CloudFormationDeployAction extends CloudFormationAction { private _deploymentRole?: iam.IRole; - private readonly props: CloudFormationDeployActionProps; + private readonly props2: CloudFormationDeployActionProps; - constructor(props: CloudFormationDeployActionProps, configuration: any) { - const capabilities = props.adminPermissions && props.capabilities === undefined - ? [cloudformation.CloudFormationCapabilities.NAMED_IAM] - : props.capabilities; - super(props, { - ...configuration, - // None evaluates to empty string which is falsey and results in undefined - Capabilities: parseCapabilities(capabilities), - RoleArn: cdk.Lazy.stringValue({ produce: () => this.deploymentRole.roleArn }), - ParameterOverrides: cdk.Lazy.stringValue({ produce: () => Stack.of(this.scope).toJsonString(props.parameterOverrides) }), - TemplateConfiguration: props.templateConfiguration ? props.templateConfiguration.location : undefined, - StackName: props.stackName, - }); - - this.props = props; + constructor(props: CloudFormationDeployActionProps, inputs: codepipeline.Artifact[] | undefined) { + super(props, (props.extraInputs || []).concat(inputs || [])); - for (const inputArtifact of props.extraInputs || []) { - this.addInputArtifact(inputArtifact); - } + this.props2 = props; } /** @@ -252,15 +255,16 @@ abstract class CloudFormationDeployAction extends CloudFormationAction { return this.getDeploymentRole('property role()'); } - protected bind(info: codepipeline.ActionBind): void { - if (this.props.deploymentRole) { - this._deploymentRole = this.props.deploymentRole; + protected bound(scope: cdk.Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { + if (this.props2.deploymentRole) { + this._deploymentRole = this.props2.deploymentRole; } else { - this._deploymentRole = new iam.Role(info.scope, 'Role', { + this._deploymentRole = new iam.Role(scope, 'Role', { assumedBy: new iam.ServicePrincipal('cloudformation.amazonaws.com') }); - if (this.props.adminPermissions) { + if (this.props2.adminPermissions) { this._deploymentRole.addToPolicy(new iam.PolicyStatement({ actions: ['*'], resources: ['*'], @@ -268,7 +272,27 @@ abstract class CloudFormationDeployAction extends CloudFormationAction { } } - SingletonPolicy.forRole(info.role).grantPassRole(this._deploymentRole); + SingletonPolicy.forRole(options.role).grantPassRole(this._deploymentRole); + + const capabilities = this.props2.adminPermissions && this.props2.capabilities === undefined + ? [cloudformation.CloudFormationCapabilities.NAMED_IAM] + : this.props2.capabilities; + + const actionConfig = super.bound(scope, stage, options); + return { + ...actionConfig, + configuration: { + ...actionConfig.configuration, + // None evaluates to empty string which is falsey and results in undefined + Capabilities: parseCapabilities(capabilities), + RoleArn: this.deploymentRole.roleArn, + ParameterOverrides: Stack.of(scope).toJsonString(this.props2.parameterOverrides), + TemplateConfiguration: this.props2.templateConfiguration + ? this.props2.templateConfiguration.location + : undefined, + StackName: this.props2.stackName, + }, + }; } private getDeploymentRole(member: string): iam.IRole { @@ -302,27 +326,32 @@ export interface CloudFormationCreateReplaceChangeSetActionProps extends CloudFo * If the change set exists, AWS CloudFormation deletes it, and then creates a new one. */ export class CloudFormationCreateReplaceChangeSetAction extends CloudFormationDeployAction { - private readonly props2: CloudFormationCreateReplaceChangeSetActionProps; + private readonly props3: CloudFormationCreateReplaceChangeSetActionProps; constructor(props: CloudFormationCreateReplaceChangeSetActionProps) { - super(props, { - ActionMode: 'CHANGE_SET_REPLACE', - ChangeSetName: props.changeSetName, - TemplatePath: props.templatePath.location, - }); + super(props, props.templateConfiguration + ? [props.templatePath.artifact, props.templateConfiguration.artifact] + : [props.templatePath.artifact]); - this.addInputArtifact(props.templatePath.artifact); - if (props.templateConfiguration) { - this.addInputArtifact(props.templateConfiguration.artifact); - } - - this.props2 = props; + this.props3 = props; } - protected bind(info: codepipeline.ActionBind): void { - super.bind(info); + protected bound(scope: cdk.Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { + // the super call order is to preserve the existing order of statements in policies + const actionConfig = super.bound(scope, stage, options); + + SingletonPolicy.forRole(options.role).grantCreateReplaceChangeSet(this.props3); - SingletonPolicy.forRole(info.role).grantCreateReplaceChangeSet(this.props2); + return { + ...actionConfig, + configuration: { + ...actionConfig.configuration, + ActionMode: 'CHANGE_SET_REPLACE', + ChangeSetName: this.props3.changeSetName, + TemplatePath: this.props3.templatePath.location, + }, + }; } } @@ -366,26 +395,31 @@ export interface CloudFormationCreateUpdateStackActionProps extends CloudFormati * troubleshooting them. You would typically choose this mode for testing. */ export class CloudFormationCreateUpdateStackAction extends CloudFormationDeployAction { - private readonly props2: CloudFormationCreateUpdateStackActionProps; + private readonly props3: CloudFormationCreateUpdateStackActionProps; constructor(props: CloudFormationCreateUpdateStackActionProps) { - super(props, { - ActionMode: props.replaceOnFailure ? 'REPLACE_ON_FAILURE' : 'CREATE_UPDATE', - TemplatePath: props.templatePath.location - }); + super(props, props.templateConfiguration + ? [props.templatePath.artifact, props.templateConfiguration.artifact] + : [props.templatePath.artifact]); - this.addInputArtifact(props.templatePath.artifact); - if (props.templateConfiguration) { - this.addInputArtifact(props.templateConfiguration.artifact); - } - - this.props2 = props; + this.props3 = props; } - protected bind(info: codepipeline.ActionBind): void { - super.bind(info); + protected bound(scope: cdk.Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { + // the super call order is to preserve the existing order of statements in policies + const actionConfig = super.bound(scope, stage, options); - SingletonPolicy.forRole(info.role).grantCreateUpdateStack(this.props2); + SingletonPolicy.forRole(options.role).grantCreateUpdateStack(this.props3); + + return { + ...actionConfig, + configuration: { + ...actionConfig.configuration, + ActionMode: this.props3.replaceOnFailure ? 'REPLACE_ON_FAILURE' : 'CREATE_UPDATE', + TemplatePath: this.props3.templatePath.location, + }, + }; } } @@ -403,20 +437,28 @@ export interface CloudFormationDeleteStackActionProps extends CloudFormationDepl * without deleting a stack. */ export class CloudFormationDeleteStackAction extends CloudFormationDeployAction { - private readonly props2: CloudFormationDeleteStackActionProps; + private readonly props3: CloudFormationDeleteStackActionProps; constructor(props: CloudFormationDeleteStackActionProps) { - super(props, { - ActionMode: 'DELETE_ONLY', - }); + super(props, undefined); - this.props2 = props; + this.props3 = props; } - protected bind(info: codepipeline.ActionBind): void { - super.bind(info); + protected bound(scope: cdk.Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { + // the super call order is to preserve the existing order of statements in policies + const actionConfig = super.bound(scope, stage, options); + + SingletonPolicy.forRole(options.role).grantDeleteStack(this.props3); - SingletonPolicy.forRole(info.role).grantDeleteStack(this.props2); + return { + ...actionConfig, + configuration: { + ...actionConfig.configuration, + ActionMode: 'DELETE_ONLY', + }, + }; } } @@ -556,4 +598,4 @@ function parseCapabilities(capabilities: CloudFormationCapabilities[] | undefine } return undefined; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts index 52684af0857cc..c666beea98ea5 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts @@ -2,6 +2,7 @@ import codebuild = require('@aws-cdk/aws-codebuild'); import codepipeline = require('@aws-cdk/aws-codepipeline'); import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/core'); +import { Action } from '../action'; /** * The type of the CodeBuild action that determines its CodePipeline Category - @@ -64,7 +65,7 @@ export interface CodeBuildActionProps extends codepipeline.CommonActionProps { /** * CodePipeline build action that uses AWS CodeBuild. */ -export class CodeBuildAction extends codepipeline.Action { +export class CodeBuildAction extends Action { private readonly props: CodeBuildActionProps; constructor(props: CodeBuildActionProps) { @@ -77,21 +78,15 @@ export class CodeBuildAction extends codepipeline.Action { artifactBounds: { minInputs: 1, maxInputs: 5, minOutputs: 0, maxOutputs: 5 }, inputs: [props.input, ...props.extraInputs || []], resource: props.project, - configuration: { - ProjectName: props.project.projectName, - }, }); this.props = props; - - if (this.inputs.length > 1) { - this.configuration.PrimarySource = cdk.Lazy.stringValue({ produce: () => this.inputs[0].artifactName }); - } } - protected bind(info: codepipeline.ActionBind): void { + protected bound(_scope: cdk.Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { // grant the Pipeline role the required permissions to this Project - info.role.addToPolicy(new iam.PolicyStatement({ + options.role.addToPolicy(new iam.PolicyStatement({ resources: [this.props.project.projectArn], actions: [ 'codebuild:BatchGetBuilds', @@ -101,10 +96,21 @@ export class CodeBuildAction extends codepipeline.Action { })); // allow the Project access to the Pipeline's artifact Bucket - if (this.outputs.length > 0) { - info.pipeline.grantBucketReadWrite(this.props.project); + if ((this.actionProperties.outputs || []).length > 0) { + stage.pipeline.artifactBucket.grantReadWrite(this.props.project); } else { - info.pipeline.grantBucketRead(this.props.project); + stage.pipeline.artifactBucket.grantRead(this.props.project); + } + + const configuration: any = { + ProjectName: this.props.project.projectName, + }; + if ((this.actionProperties.inputs || []).length > 1) { + // lazy, because the Artifact name might be generated lazily + configuration.PrimarySource = cdk.Lazy.stringValue({ produce: () => this.props.input.artifactName }); } + return { + configuration, + }; } } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts index e7ae52163284b..7613c6bb7bb56 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts @@ -2,6 +2,8 @@ import codecommit = require('@aws-cdk/aws-codecommit'); import codepipeline = require('@aws-cdk/aws-codepipeline'); import targets = require('@aws-cdk/aws-events-targets'); import iam = require('@aws-cdk/aws-iam'); +import { Construct } from '@aws-cdk/core'; +import { Action } from '../action'; import { sourceArtifactBounds } from '../common'; /** @@ -57,10 +59,9 @@ export interface CodeCommitSourceActionProps extends codepipeline.CommonActionPr /** * CodePipeline Source that is provided by an AWS CodeCommit repository. */ -export class CodeCommitSourceAction extends codepipeline.Action { - private readonly repository: codecommit.IRepository; +export class CodeCommitSourceAction extends Action { private readonly branch: string; - private readonly createEvent: boolean; + private readonly props: CodeCommitSourceActionProps; constructor(props: CodeCommitSourceActionProps) { const branch = props.branch || 'master'; @@ -71,30 +72,26 @@ export class CodeCommitSourceAction extends codepipeline.Action { provider: 'CodeCommit', artifactBounds: sourceArtifactBounds(), outputs: [props.output], - configuration: { - RepositoryName: props.repository.repositoryName, - BranchName: branch, - PollForSourceChanges: props.trigger === CodeCommitTrigger.POLL, - }, }); - this.repository = props.repository; this.branch = branch; - this.createEvent = props.trigger === undefined || - props.trigger === CodeCommitTrigger.EVENTS; + this.props = props; } - protected bind(info: codepipeline.ActionBind): void { - if (this.createEvent) { - this.repository.onCommit(info.pipeline.node.uniqueId + 'EventRule', { - target: new targets.CodePipeline(info.pipeline), + protected bound(_scope: Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { + const createEvent = this.props.trigger === undefined || + this.props.trigger === CodeCommitTrigger.EVENTS; + if (createEvent) { + this.props.repository.onCommit(stage.pipeline.node.uniqueId + 'EventRule', { + target: new targets.CodePipeline(stage.pipeline), branches: [this.branch], }); } // https://docs.aws.amazon.com/codecommit/latest/userguide/auth-and-access-control-permissions-reference.html#aa-acp - info.role.addToPolicy(new iam.PolicyStatement({ - resources: [this.repository.repositoryArn], + options.role.addToPolicy(new iam.PolicyStatement({ + resources: [this.props.repository.repositoryArn], actions: [ 'codecommit:GetBranch', 'codecommit:GetCommit', @@ -103,5 +100,13 @@ export class CodeCommitSourceAction extends codepipeline.Action { 'codecommit:CancelUploadArchive', ], })); + + return { + configuration: { + RepositoryName: this.props.repository.repositoryName, + BranchName: this.branch, + PollForSourceChanges: this.props.trigger === CodeCommitTrigger.POLL, + }, + }; } } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/codedeploy/server-deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/codedeploy/server-deploy-action.ts index 7d1b0b9c0e04c..c5e3f9e732a62 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/codedeploy/server-deploy-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/codedeploy/server-deploy-action.ts @@ -1,6 +1,8 @@ import codedeploy = require('@aws-cdk/aws-codedeploy'); import codepipeline = require('@aws-cdk/aws-codepipeline'); import iam = require('@aws-cdk/aws-iam'); +import { Construct } from '@aws-cdk/core'; +import { Action } from '../action'; import { deployArtifactBounds } from '../common'; /** @@ -18,7 +20,7 @@ export interface CodeDeployServerDeployActionProps extends codepipeline.CommonAc readonly deploymentGroup: codedeploy.IServerDeploymentGroup; } -export class CodeDeployServerDeployAction extends codepipeline.Action { +export class CodeDeployServerDeployAction extends Action { private readonly deploymentGroup: codedeploy.IServerDeploymentGroup; constructor(props: CodeDeployServerDeployActionProps) { @@ -28,37 +30,41 @@ export class CodeDeployServerDeployAction extends codepipeline.Action { provider: 'CodeDeploy', artifactBounds: deployArtifactBounds(), inputs: [props.input], - configuration: { - ApplicationName: props.deploymentGroup.application.applicationName, - DeploymentGroupName: props.deploymentGroup.deploymentGroupName, - }, }); this.deploymentGroup = props.deploymentGroup; } - protected bind(info: codepipeline.ActionBind): void { + protected bound(_scope: Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { // permissions, based on: // https://docs.aws.amazon.com/codedeploy/latest/userguide/auth-and-access-control-permissions-reference.html - info.role.addToPolicy(new iam.PolicyStatement({ + options.role.addToPolicy(new iam.PolicyStatement({ resources: [this.deploymentGroup.application.applicationArn], actions: ['codedeploy:GetApplicationRevision', 'codedeploy:RegisterApplicationRevision'] })); - info.role.addToPolicy(new iam.PolicyStatement({ + options.role.addToPolicy(new iam.PolicyStatement({ resources: [this.deploymentGroup.deploymentGroupArn], actions: ['codedeploy:CreateDeployment', 'codedeploy:GetDeployment'], })); - info.role.addToPolicy(new iam.PolicyStatement({ + options.role.addToPolicy(new iam.PolicyStatement({ resources: [this.deploymentGroup.deploymentConfig.deploymentConfigArn], actions: ['codedeploy:GetDeploymentConfig'] })); // grant the ASG Role permissions to read from the Pipeline Bucket for (const asg of this.deploymentGroup.autoScalingGroups || []) { - info.pipeline.grantBucketRead(asg.role); + stage.pipeline.artifactBucket.grantRead(asg.role); } + + return { + configuration: { + ApplicationName: this.deploymentGroup.application.applicationName, + DeploymentGroupName: this.deploymentGroup.deploymentGroupName, + }, + }; } } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/ecr/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/ecr/source-action.ts index 4ec55df41d1ed..5a739a3528228 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/ecr/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/ecr/source-action.ts @@ -2,6 +2,8 @@ import codepipeline = require('@aws-cdk/aws-codepipeline'); import ecr = require('@aws-cdk/aws-ecr'); import targets = require('@aws-cdk/aws-events-targets'); import iam = require('@aws-cdk/aws-iam'); +import { Construct } from '@aws-cdk/core'; +import { Action } from '../action'; import { sourceArtifactBounds } from '../common'; /** @@ -33,7 +35,7 @@ export interface EcrSourceActionProps extends codepipeline.CommonActionProps { * changes, but only if there is a CloudTrail Trail in the account that * captures the ECR event. */ -export class EcrSourceAction extends codepipeline.Action { +export class EcrSourceAction extends Action { private readonly props: EcrSourceActionProps; constructor(props: EcrSourceActionProps) { @@ -43,24 +45,28 @@ export class EcrSourceAction extends codepipeline.Action { provider: 'ECR', artifactBounds: sourceArtifactBounds(), outputs: [props.output], - configuration: { - RepositoryName: props.repository.repositoryName, - ImageTag: props.imageTag, - }, }); this.props = props; } - protected bind(info: codepipeline.ActionBind): void { - info.role.addToPolicy(new iam.PolicyStatement({ + protected bound(_scope: Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { + options.role.addToPolicy(new iam.PolicyStatement({ actions: ['ecr:DescribeImages'], resources: [this.props.repository.repositoryArn] })); - this.props.repository.onCloudTrailImagePushed(info.pipeline.node.uniqueId + 'SourceEventRule', { - target: new targets.CodePipeline(info.pipeline), - imageTag: this.props.imageTag + this.props.repository.onCloudTrailImagePushed(stage.pipeline.node.uniqueId + 'SourceEventRule', { + target: new targets.CodePipeline(stage.pipeline), + imageTag: this.props.imageTag }); + + return { + configuration: { + RepositoryName: this.props.repository.repositoryName, + ImageTag: this.props.imageTag, + }, + }; } } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/ecs/deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/ecs/deploy-action.ts index e52a2e1db8935..e8bd9a5bd12c0 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/ecs/deploy-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/ecs/deploy-action.ts @@ -1,6 +1,8 @@ import codepipeline = require('@aws-cdk/aws-codepipeline'); import ecs = require('@aws-cdk/aws-ecs'); import iam = require('@aws-cdk/aws-iam'); +import { Construct } from '@aws-cdk/core'; +import { Action } from '../action'; import { deployArtifactBounds } from '../common'; /** @@ -43,7 +45,9 @@ export interface EcsDeployActionProps extends codepipeline.CommonActionProps { /** * CodePipeline Action to deploy an ECS Service. */ -export class EcsDeployAction extends codepipeline.Action { +export class EcsDeployAction extends Action { + private readonly props: EcsDeployActionProps; + constructor(props: EcsDeployActionProps) { super({ ...props, @@ -51,18 +55,16 @@ export class EcsDeployAction extends codepipeline.Action { provider: 'ECS', artifactBounds: deployArtifactBounds(), inputs: [determineInputArtifact(props)], - configuration: { - ClusterName: props.service.cluster.clusterName, - ServiceName: props.service.serviceName, - FileName: props.imageFile && props.imageFile.fileName, - }, }); + + this.props = props; } - protected bind(info: codepipeline.ActionBind): void { + protected bound(_scope: Construct, _stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { // permissions based on CodePipeline documentation: // https://docs.aws.amazon.com/codepipeline/latest/userguide/how-to-custom-role.html#how-to-update-role-new-services - info.role.addToPolicy(new iam.PolicyStatement({ + options.role.addToPolicy(new iam.PolicyStatement({ actions: [ 'ecs:DescribeServices', 'ecs:DescribeTaskDefinition', @@ -74,7 +76,7 @@ export class EcsDeployAction extends codepipeline.Action { resources: ['*'] })); - info.role.addToPolicy(new iam.PolicyStatement({ + options.role.addToPolicy(new iam.PolicyStatement({ actions: ['iam:PassRole'], resources: ['*'], conditions: { @@ -86,6 +88,14 @@ export class EcsDeployAction extends codepipeline.Action { } } })); + + return { + configuration: { + ClusterName: this.props.service.cluster.clusterName, + ServiceName: this.props.service.serviceName, + FileName: this.props.imageFile && this.props.imageFile.fileName, + }, + }; } } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/github/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/github/source-action.ts index 208c5330b41e6..973370bfe028c 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/github/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/github/source-action.ts @@ -1,5 +1,6 @@ import codepipeline = require('@aws-cdk/aws-codepipeline'); -import { SecretValue } from '@aws-cdk/core'; +import { Construct, SecretValue } from '@aws-cdk/core'; +import { Action } from '../action'; import { sourceArtifactBounds } from '../common'; /** @@ -62,7 +63,7 @@ export interface GitHubSourceActionProps extends codepipeline.CommonActionProps /** * Source that is provided by a GitHub repository. */ -export class GitHubSourceAction extends codepipeline.Action { +export class GitHubSourceAction extends Action { private readonly props: GitHubSourceActionProps; constructor(props: GitHubSourceActionProps) { @@ -73,21 +74,15 @@ export class GitHubSourceAction extends codepipeline.Action { provider: 'GitHub', artifactBounds: sourceArtifactBounds(), outputs: [props.output], - configuration: { - Owner: props.owner, - Repo: props.repo, - Branch: props.branch || "master", - OAuthToken: props.oauthToken.toString(), - PollForSourceChanges: props.trigger === GitHubTrigger.POLL, - }, }); this.props = props; } - protected bind(info: codepipeline.ActionBind): void { + protected bound(scope: Construct, stage: codepipeline.IStage, _options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { if (!this.props.trigger || this.props.trigger === GitHubTrigger.WEBHOOK) { - new codepipeline.CfnWebhook(info.scope, 'WebhookResource', { + new codepipeline.CfnWebhook(scope, 'WebhookResource', { authentication: 'GITHUB_HMAC', authenticationConfiguration: { secretToken: this.props.oauthToken.toString(), @@ -98,11 +93,21 @@ export class GitHubSourceAction extends codepipeline.Action { matchEquals: 'refs/heads/{Branch}', }, ], - targetAction: this.actionName, - targetPipeline: info.pipeline.pipelineName, + targetAction: this.actionProperties.actionName, + targetPipeline: stage.pipeline.pipelineName, targetPipelineVersion: 1, registerWithThirdParty: true, }); } + + return { + configuration: { + Owner: this.props.owner, + Repo: this.props.repo, + Branch: this.props.branch || "master", + OAuthToken: this.props.oauthToken.toString(), + PollForSourceChanges: this.props.trigger === GitHubTrigger.POLL, + }, + }; } } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/jenkins/jenkins-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/jenkins/jenkins-action.ts index f23dfbb4bff63..8b96c40357092 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/jenkins/jenkins-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/jenkins/jenkins-action.ts @@ -1,4 +1,6 @@ import codepipeline = require('@aws-cdk/aws-codepipeline'); +import { Construct } from '@aws-cdk/core'; +import { Action } from '../action'; import { IJenkinsProvider, jenkinsArtifactsBounds } from "./jenkins-provider"; /** @@ -57,8 +59,8 @@ export interface JenkinsActionProps extends codepipeline.CommonActionProps { * * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/tutorials-four-stage-pipeline.html */ -export class JenkinsAction extends codepipeline.Action { - private readonly jenkinsProvider: IJenkinsProvider; +export class JenkinsAction extends Action { + private readonly props: JenkinsActionProps; constructor(props: JenkinsActionProps) { super({ @@ -70,19 +72,23 @@ export class JenkinsAction extends codepipeline.Action { owner: 'Custom', artifactBounds: jenkinsArtifactsBounds, version: props.jenkinsProvider.version, - configuration: { - ProjectName: props.projectName, - }, }); - this.jenkinsProvider = props.jenkinsProvider; + this.props = props; } - protected bind(_info: codepipeline.ActionBind): void { - if (this.category === codepipeline.ActionCategory.BUILD) { - this.jenkinsProvider._registerBuildProvider(); + protected bound(_scope: Construct, _stage: codepipeline.IStage, _options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { + if (this.actionProperties.category === codepipeline.ActionCategory.BUILD) { + this.props.jenkinsProvider._registerBuildProvider(); } else { - this.jenkinsProvider._registerTestProvider(); + this.props.jenkinsProvider._registerTestProvider(); } + + return { + configuration: { + ProjectName: this.props.projectName, + }, + }; } } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/lambda/invoke-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/lambda/invoke-action.ts index 9132e29416986..9349d7ca42926 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/lambda/invoke-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/lambda/invoke-action.ts @@ -1,7 +1,8 @@ import codepipeline = require('@aws-cdk/aws-codepipeline'); import iam = require('@aws-cdk/aws-iam'); import lambda = require('@aws-cdk/aws-lambda'); -import { Stack } from '@aws-cdk/core'; +import { Construct, Stack } from "@aws-cdk/core"; +import { Action } from '../action'; /** * Construction properties of the {@link LambdaInvokeAction Lambda invoke CodePipeline Action}. @@ -53,7 +54,7 @@ export interface LambdaInvokeActionProps extends codepipeline.CommonActionProps * * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/actions-invoke-lambda-function.html */ -export class LambdaInvokeAction extends codepipeline.Action { +export class LambdaInvokeAction extends Action { private readonly props: LambdaInvokeActionProps; constructor(props: LambdaInvokeActionProps) { @@ -67,24 +68,21 @@ export class LambdaInvokeAction extends codepipeline.Action { minOutputs: 0, maxOutputs: 5, }, - configuration: { - FunctionName: props.lambda.functionName, - UserParameters: Stack.of(props.lambda).toJsonString(props.userParameters), - }, }); this.props = props; } - protected bind(info: codepipeline.ActionBind): void { + protected bound(scope: Construct, _stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { // allow pipeline to list functions - info.role.addToPolicy(new iam.PolicyStatement({ + options.role.addToPolicy(new iam.PolicyStatement({ actions: ['lambda:ListFunctions'], resources: ['*'] })); // allow pipeline to invoke this lambda functionn - info.role.addToPolicy(new iam.PolicyStatement({ + options.role.addToPolicy(new iam.PolicyStatement({ actions: ['lambda:InvokeFunction'], resources: [this.props.lambda.functionArn] })); @@ -96,5 +94,12 @@ export class LambdaInvokeAction extends codepipeline.Action { resources: ['*'], actions: ['codepipeline:PutJobSuccessResult', 'codepipeline:PutJobFailureResult'] })); + + return { + configuration: { + FunctionName: this.props.lambda.functionName, + UserParameters: Stack.of(scope).toJsonString(this.props.userParameters), + }, + }; } } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/manual-approval-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/manual-approval-action.ts index daa690f6aa0a0..246c1f3b24d26 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/manual-approval-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/manual-approval-action.ts @@ -2,6 +2,7 @@ import codepipeline = require('@aws-cdk/aws-codepipeline'); import sns = require('@aws-cdk/aws-sns'); import subs = require('@aws-cdk/aws-sns-subscriptions'); import cdk = require('@aws-cdk/core'); +import { Action } from './action'; /** * Construction properties of the {@link ManualApprovalAction}. @@ -28,7 +29,7 @@ export interface ManualApprovalActionProps extends codepipeline.CommonActionProp /** * Manual approval action. */ -export class ManualApprovalAction extends codepipeline.Action { +export class ManualApprovalAction extends Action { /** * The SNS Topic passed when constructing the Action. * If no Topic was passed, but `notifyEmails` were provided, @@ -43,7 +44,6 @@ export class ManualApprovalAction extends codepipeline.Action { category: codepipeline.ActionCategory.APPROVAL, provider: 'Manual', artifactBounds: { minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0 }, - configuration: cdk.Lazy.anyValue({ produce: () => this.actionConfiguration() }), }); this.props = props; @@ -53,27 +53,28 @@ export class ManualApprovalAction extends codepipeline.Action { return this._notificationTopic; } - protected bind(info: codepipeline.ActionBind): void { + protected bound(scope: cdk.Construct, _stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { if (this.props.notificationTopic) { this._notificationTopic = this.props.notificationTopic; } else if ((this.props.notifyEmails || []).length > 0) { - this._notificationTopic = new sns.Topic(info.scope, 'TopicResource'); + this._notificationTopic = new sns.Topic(scope, 'TopicResource'); } if (this._notificationTopic) { - this._notificationTopic.grantPublish(info.role); + this._notificationTopic.grantPublish(options.role); for (const notifyEmail of this.props.notifyEmails || []) { this._notificationTopic.addSubscription(new subs.EmailSubscription(notifyEmail)); } } - } - private actionConfiguration(): any { - return this._notificationTopic - ? { - NotificationArn: this._notificationTopic.topicArn, - CustomData: this.props.additionalInformation, - } - : undefined; + return { + configuration: this._notificationTopic + ? { + NotificationArn: this._notificationTopic.topicArn, + CustomData: this.props.additionalInformation, + } + : undefined, + }; } } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/deploy-action.ts index 59c96ea4f883a..de49e45088035 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/deploy-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/deploy-action.ts @@ -1,5 +1,7 @@ import codepipeline = require('@aws-cdk/aws-codepipeline'); import s3 = require('@aws-cdk/aws-s3'); +import { Construct } from '@aws-cdk/core'; +import { Action } from '../action'; import { deployArtifactBounds } from '../common'; /** @@ -32,8 +34,8 @@ export interface S3DeployActionProps extends codepipeline.CommonActionProps { /** * Deploys the sourceArtifact to Amazon S3. */ -export class S3DeployAction extends codepipeline.Action { - private readonly bucket: s3.IBucket; +export class S3DeployAction extends Action { + private readonly props: S3DeployActionProps; constructor(props: S3DeployActionProps) { super({ @@ -42,18 +44,22 @@ export class S3DeployAction extends codepipeline.Action { provider: 'S3', artifactBounds: deployArtifactBounds(), inputs: [props.input], - configuration: { - BucketName: props.bucket.bucketName, - Extract: (props.extract === false) ? 'false' : 'true', - ObjectKey: props.objectKey, - }, }); - this.bucket = props.bucket; + this.props = props; } - protected bind(info: codepipeline.ActionBind): void { + protected bound(_scope: Construct, _stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { // pipeline needs permissions to write to the S3 bucket - this.bucket.grantWrite(info.role); + this.props.bucket.grantWrite(options.role); + + return { + configuration: { + BucketName: this.props.bucket.bucketName, + Extract: this.props.extract === false ? 'false' : 'true', + ObjectKey: this.props.objectKey, + }, + }; } } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/source-action.ts index f6ee7a7253006..6c875cba6b1c2 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/source-action.ts @@ -1,6 +1,8 @@ import codepipeline = require('@aws-cdk/aws-codepipeline'); import targets = require('@aws-cdk/aws-events-targets'); import s3 = require('@aws-cdk/aws-s3'); +import { Construct } from '@aws-cdk/core'; +import { Action } from '../action'; import { sourceArtifactBounds } from '../common'; /** @@ -66,37 +68,39 @@ export interface S3SourceActionProps extends codepipeline.CommonActionProps { * Will trigger the pipeline as soon as the S3 object changes, but only if there is * a CloudTrail Trail in the account that captures the S3 event. */ -export class S3SourceAction extends codepipeline.Action { +export class S3SourceAction extends Action { private readonly props: S3SourceActionProps; constructor(props: S3SourceActionProps) { - const pollForSourceChanges = props.trigger && props.trigger === S3Trigger.POLL; - super({ ...props, category: codepipeline.ActionCategory.SOURCE, provider: 'S3', artifactBounds: sourceArtifactBounds(), outputs: [props.output], - configuration: { - S3Bucket: props.bucket.bucketName, - S3ObjectKey: props.bucketKey, - PollForSourceChanges: pollForSourceChanges, - }, }); this.props = props; } - protected bind(info: codepipeline.ActionBind): void { + protected bound(_scope: Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { if (this.props.trigger === S3Trigger.EVENTS) { - this.props.bucket.onCloudTrailPutObject(info.pipeline.node.uniqueId + 'SourceEventRule', { - target: new targets.CodePipeline(info.pipeline), + this.props.bucket.onCloudTrailPutObject(stage.pipeline.node.uniqueId + 'SourceEventRule', { + target: new targets.CodePipeline(stage.pipeline), paths: [this.props.bucketKey] }); } // pipeline needs permissions to read from the S3 bucket - this.props.bucket.grantRead(info.role); + this.props.bucket.grantRead(options.role); + + return { + configuration: { + S3Bucket: this.props.bucket.bucketName, + S3ObjectKey: this.props.bucketKey, + PollForSourceChanges: this.props.trigger && this.props.trigger === S3Trigger.POLL, + }, + }; } } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.pipeline-actions.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.pipeline-actions.ts index 57b935c192601..a817699b795bd 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.pipeline-actions.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.pipeline-actions.ts @@ -1,6 +1,7 @@ import codepipeline = require('@aws-cdk/aws-codepipeline'); import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); +import s3 = require('@aws-cdk/aws-s3'); import cdk = require('@aws-cdk/core'); import { Stack } from '@aws-cdk/core'; import _ = require('lodash'); @@ -35,10 +36,10 @@ export = nodeunit.testCase({ _assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DeleteChangeSet', stackArn, changeSetCondition); // TODO: revert "as any" once we move all actions into a single package. - test.deepEqual(action.inputs, [artifact], + test.deepEqual(stage.actions[0].actionProperties.inputs, [artifact], 'The input was correctly registered'); - _assertActionMatches(test, stage.actions, 'AWS', 'CloudFormation', 'Deploy', { + _assertActionMatches(test, stage.actions, 'CloudFormation', 'Deploy', { ActionMode: 'CHANGE_SET_CREATE_REPLACE', StackName: 'MyStack', ChangeSetName: 'MyChangeSet' @@ -124,7 +125,7 @@ export = nodeunit.testCase({ _assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:ExecuteChangeSet', stackArn, { StringEquals: { 'cloudformation:ChangeSetName': 'MyChangeSet' } }); - _assertActionMatches(test, stage.actions, 'AWS', 'CloudFormation', 'Deploy', { + _assertActionMatches(test, stage.actions, 'CloudFormation', 'Deploy', { ActionMode: 'CHANGE_SET_EXECUTE', StackName: 'MyStack', ChangeSetName: 'MyChangeSet' @@ -230,30 +231,31 @@ interface PolicyStatementJson { } function _assertActionMatches(test: nodeunit.Test, - actions: codepipeline.Action[], - owner: string, + actions: FullAction[], provider: string, category: string, configuration?: { [key: string]: any }) { const configurationStr = configuration - ? `configuration including ${JSON.stringify(resolve(configuration), null, 2)}` + ? `, configuration including ${JSON.stringify(resolve(configuration), null, 2)}` : ''; const actionsStr = JSON.stringify(actions.map(a => - ({ owner: a.owner, provider: a.provider, category: a.category, configuration: resolve(a.configuration) }) + ({ owner: a.actionProperties.owner, provider: a.actionProperties.provider, + category: a.actionProperties.category, configuration: resolve(a.actionConfig.configuration) + }) ), null, 2); - test.ok(_hasAction(actions, owner, provider, category, configuration), - `Expected to find an action with owner ${owner}, provider ${provider}, category ${category}${configurationStr}, but found ${actionsStr}`); + test.ok(_hasAction(actions, provider, category, configuration), + `Expected to find an action with provider ${provider}, category ${category}${configurationStr}, but found ${actionsStr}`); } -function _hasAction(actions: codepipeline.Action[], owner: string, provider: string, category: string, configuration?: { [key: string]: any}) { +function _hasAction(actions: FullAction[], provider: string, category: string, + configuration?: { [key: string]: any}) { for (const action of actions) { - if (action.owner !== owner) { continue; } - if (action.provider !== provider) { continue; } - if (action.category !== category) { continue; } - if (configuration && !action.configuration) { continue; } + if (action.actionProperties.provider !== provider) { continue; } + if (action.actionProperties.category !== category) { continue; } + if (configuration && !action.actionConfig.configuration) { continue; } if (configuration) { for (const key of Object.keys(configuration)) { - if (!_.isEqual(resolve(action.configuration[key]), resolve(configuration[key]))) { + if (!_.isEqual(resolve(action.actionConfig.configuration[key]), resolve(configuration[key]))) { continue; } } @@ -314,16 +316,8 @@ class PipelineDouble extends cdk.Resource implements codepipeline.IPipeline { this.role = role; } - public bind(_rule: events.IRule): events.RuleTargetConfig { - throw new Error('asRuleTarget() is unsupported in PipelineDouble'); - } - - public grantBucketRead(_identity?: iam.IGrantable): iam.Grant { - throw new Error('grantBucketRead() is unsupported in PipelineDouble'); - } - - public grantBucketReadWrite(_identity?: iam.IGrantable): iam.Grant { - throw new Error('grantBucketReadWrite() is unsupported in PipelineDouble'); + public get artifactBucket(): s3.IBucket { + throw new Error('artifactBucket is unsupported in PipelineDouble'); } public onEvent(_id: string, _options: events.OnEventOptions): events.Rule { @@ -334,33 +328,38 @@ class PipelineDouble extends cdk.Resource implements codepipeline.IPipeline { } } +class FullAction { + constructor(readonly actionProperties: codepipeline.ActionProperties, + readonly actionConfig: codepipeline.ActionConfig) { + // empty + } +} + class StageDouble implements codepipeline.IStage { public readonly stageName: string; public readonly pipeline: codepipeline.IPipeline; - public readonly actions: codepipeline.Action[]; + public readonly actions: FullAction[]; public get node(): cdk.ConstructNode { throw new Error('StageDouble is not a real construct'); } - constructor({ name, pipeline, actions }: { name?: string, pipeline: PipelineDouble, actions: codepipeline.Action[] }) { + constructor({ name, pipeline, actions }: { name?: string, pipeline: PipelineDouble, actions: codepipeline.IAction[] }) { this.stageName = name || 'TestStage'; this.pipeline = pipeline; const stageParent = new cdk.Construct(pipeline, this.stageName); + const fullActions = new Array(); for (const action of actions) { - const actionParent = new cdk.Construct(stageParent, action.actionName); - (action as any)._actionAttachedToPipeline({ - pipeline, - stage: this, - scope: actionParent, + const actionParent = new cdk.Construct(stageParent, action.actionProperties.actionName); + fullActions.push(new FullAction(action.actionProperties, action.bind(actionParent, this, { role: pipeline.role, - }); + }))); } - this.actions = actions; + this.actions = fullActions; } - public addAction(_action: codepipeline.Action): void { + public addAction(_action: codepipeline.IAction): void { throw new Error('addAction() is not supported on StageDouble'); } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/test.ecs-deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/test.ecs-deploy-action.ts index 9b163ab4b282f..c668283c225b3 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/test.ecs-deploy-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/test.ecs-deploy-action.ts @@ -24,14 +24,14 @@ export = { const service = anyEcsService(); const artifact = new codepipeline.Artifact('Artifact'); - const action = new cpactions.EcsDeployAction({ - actionName: 'ECS', - service, - input: artifact, + test.doesNotThrow(() => { + new cpactions.EcsDeployAction({ + actionName: 'ECS', + service, + input: artifact, + }); }); - test.equal(action.configuration.FileName, undefined); - test.done(); }, @@ -39,14 +39,14 @@ export = { const service = anyEcsService(); const artifact = new codepipeline.Artifact('Artifact'); - const action = new cpactions.EcsDeployAction({ - actionName: 'ECS', - service, - imageFile: artifact.atPath('imageFile.json'), + test.doesNotThrow(() => { + new cpactions.EcsDeployAction({ + actionName: 'ECS', + service, + imageFile: artifact.atPath('imageFile.json'), + }); }); - test.equal(action.configuration.FileName, 'imageFile.json'); - test.done(); }, diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/test.pipeline.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/test.pipeline.ts index 7a37e4eef7791..17da422ea0e10 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/test.pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/test.pipeline.ts @@ -527,7 +527,7 @@ export = { ] })); - test.equal(lambdaAction.outputs.length, 3); + test.equal((lambdaAction.actionProperties.outputs || []).length, 3); expect(stack, /* skip validation */ true).to(haveResource('AWS::IAM::Policy', { "PolicyDocument": { @@ -897,19 +897,6 @@ export = { test.done(); }, }, - - 'Pipeline.fromPipelineArn'(test: Test) { - // GIVEN - const stack = new Stack(); - - // WHEN - const pl = codepipeline.Pipeline.fromPipelineArn(stack, 'imported', 'arn:aws:codepipeline:us-east-1:123456789012:MyDemoPipeline'); - - // THEN - test.deepEqual(pl.pipelineArn, 'arn:aws:codepipeline:us-east-1:123456789012:MyDemoPipeline'); - test.deepEqual(pl.pipelineName, 'MyDemoPipeline'); - test.done(); - } }; function stageForTesting(stack: Stack): codepipeline.IStage { diff --git a/packages/@aws-cdk/aws-codepipeline/lib/action.ts b/packages/@aws-cdk/aws-codepipeline/lib/action.ts index 9a0b3dbcc6f57..a137f6e741ab6 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/action.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/action.ts @@ -1,8 +1,8 @@ import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); +import s3 = require('@aws-cdk/aws-s3'); import { Construct, IResource } from '@aws-cdk/core'; import { Artifact } from './artifact'; -import validation = require('./validation'); export enum ActionCategory { SOURCE = 'Source', @@ -27,34 +27,68 @@ export interface ActionArtifactBounds { readonly maxOutputs: number; } -/** - * The interface used in the {@link Action#bind()} callback. - */ -export interface ActionBind { +export interface ActionProperties { + readonly actionName: string; + readonly role?: iam.IRole; + + /** + * The AWS region the given Action resides in. + * Note that a cross-region Pipeline requires replication buckets to function correctly. + * You can provide their names with the {@link PipelineProps#crossRegionReplicationBuckets} property. + * If you don't, the CodePipeline Construct will create new Stacks in your CDK app containing those buckets, + * that you will need to `cdk deploy` before deploying the main, Pipeline-containing Stack. + * + * @default the Action resides in the same region as the Pipeline + */ + readonly region?: string; + /** - * The pipeline this action has been added to. + * The optional resource that is backing this Action. + * This is used for automatically handling Actions backed by + * resources from a different account and/or region. */ - readonly pipeline: IPipeline; + readonly resource?: IResource; /** - * The stage this action has been added to. + * The category of the action. + * The category defines which action type the owner + * (the entity that performs the action) performs. */ - readonly stage: IStage; + readonly category: ActionCategory; /** - * The scope construct for this action. - * Can be used by the action implementation to create any resources it needs to work correctly. + * The service provider that the action calls. */ - readonly scope: Construct; + readonly provider: string; + readonly owner?: string; + readonly version?: string; /** - * The IAM Role to add the necessary permissions to. + * The order in which AWS CodePipeline runs this action. + * For more information, see the AWS CodePipeline User Guide. + * + * https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#action-requirements */ + readonly runOrder?: number; + readonly artifactBounds: ActionArtifactBounds; + readonly inputs?: Artifact[]; + readonly outputs?: Artifact[]; +} + +export interface ActionBindOptions { readonly role: iam.IRole; } -interface ExtendedActionBind extends ActionBind { - readonly actionRole?: iam.IRole; +export interface ActionConfig { + readonly configuration?: any; +} + +export interface IAction { + readonly actionProperties: ActionProperties; + + bind(scope: Construct, stage: IStage, options: ActionBindOptions): ActionConfig; + + onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; } /** @@ -77,19 +111,7 @@ export interface IPipeline extends IResource { */ readonly pipelineArn: string; - /** - * Grants read permissions to the Pipeline's S3 Bucket to the given Identity. - * - * @param identity the IAM Identity to grant the permissions to - */ - grantBucketRead(identity: iam.IGrantable): iam.Grant; - - /** - * Grants read & write permissions to the Pipeline's S3 Bucket to the given Identity. - * - * @param identity the IAM Identity to grant the permissions to - */ - grantBucketReadWrite(identity: iam.IGrantable): iam.Grant; + readonly artifactBucket: s3.IBucket; /** * Define an event rule triggered by this CodePipeline. @@ -118,7 +140,9 @@ export interface IStage { */ readonly stageName: string; - addAction(action: Action): void; + readonly pipeline: IPipeline; + + addAction(action: IAction): void; onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; } @@ -142,280 +166,3 @@ export interface CommonActionProps { */ readonly runOrder?: number; } - -/** - * Construction properties of the low-level {@link Action Action class}. - */ -export interface ActionProps extends CommonActionProps { - readonly category: ActionCategory; - readonly provider: string; - - /** - * The region this Action resides in. - * - * @default the Action resides in the same region as the Pipeline - */ - readonly region?: string; - - /** - * The service role that is assumed during execution of action. - * This role is not mandatory, however more advanced configuration - * may require specifying it. - * - * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codepipeline-pipeline-stages-actions.html - */ - readonly role?: iam.IRole; - - readonly artifactBounds: ActionArtifactBounds; - readonly inputs?: Artifact[]; - readonly outputs?: Artifact[]; - readonly configuration?: any; - readonly version?: string; - readonly owner?: string; - - /** - * The optional resource that is backing this Action. - * This is used for automatically handling Actions backed by - * resources from a different account and/or region. - * - * @default the Action is not backed by any resource - */ - readonly resource?: IResource; -} - -/** - * Low-level class for generic CodePipeline Actions. - */ -export abstract class Action { - /** - * The category of the action. - * The category defines which action type the owner - * (the entity that performs the action) performs. - */ - public readonly category: ActionCategory; - - /** - * The service provider that the action calls. - */ - public readonly provider: string; - - /** - * The AWS region the given Action resides in. - * Note that a cross-region Pipeline requires replication buckets to function correctly. - * You can provide their names with the {@link PipelineProps#crossRegionReplicationBuckets} property. - * If you don't, the CodePipeline Construct will create new Stacks in your CDK app containing those buckets, - * that you will need to `cdk deploy` before deploying the main, Pipeline-containing Stack. - * - * @default the Action resides in the same region as the Pipeline - */ - public readonly region?: string; - - /** - * The optional resource that is backing this Action. - * This is used for automatically handling Actions backed by - * resources from a different account and/or region. - */ - public readonly resource?: IResource; - - /** - * The action's configuration. These are key-value pairs that specify input values for an action. - * For more information, see the AWS CodePipeline User Guide. - * - * http://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#action-requirements - */ - public readonly configuration?: any; - - /** - * The order in which AWS CodePipeline runs this action. - * For more information, see the AWS CodePipeline User Guide. - * - * https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#action-requirements - */ - public readonly runOrder: number; - - public readonly owner: string; - public readonly version: string; - public readonly actionName: string; - - private readonly _actionInputArtifacts = new Array(); - private readonly _actionOutputArtifacts = new Array(); - private readonly artifactBounds: ActionArtifactBounds; - - private _role?: iam.IRole; - private _pipeline?: IPipeline; - private _stage?: IStage; - private _scope?: Construct; - - constructor(props: ActionProps) { - validation.validateName('Action', props.actionName); - - this.owner = props.owner || 'AWS'; - this.version = props.version || '1'; - this.category = props.category; - this.provider = props.provider; - this.region = props.region; - this.configuration = props.configuration; - this.artifactBounds = props.artifactBounds; - this.runOrder = props.runOrder === undefined ? 1 : props.runOrder; - this.actionName = props.actionName; - this._role = props.role; - this.resource = props.resource; - - for (const inputArtifact of props.inputs || []) { - this.addInputArtifact(inputArtifact); - } - - for (const outputArtifact of props.outputs || []) { - this.addOutputArtifact(outputArtifact); - } - } - - public onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { - const rule = new events.Rule(this.scope, name, options); - rule.addTarget(target); - rule.addEventPattern({ - detailType: [ 'CodePipeline Stage Execution State Change' ], - source: [ 'aws.codepipeline' ], - resources: [ this.pipeline.pipelineArn ], - detail: { - stage: [ this.stage.stageName ], - action: [ this.actionName ], - }, - }); - return rule; - } - - /** - * The service role that is assumed during execution of this action. - * If this is undefined, the Action will execute in the context of the Pipeline Role. - */ - public get role(): iam.IRole | undefined { - return this._role; - } - - public get inputs(): Artifact[] { - return this._actionInputArtifacts.slice(); - } - - public get outputs(): Artifact[] { - return this._actionOutputArtifacts.slice(); - } - - /** @internal */ - public _validate(): string[] { - return validation.validateArtifactBounds('input', this.inputs, this.artifactBounds.minInputs, - this.artifactBounds.maxInputs, this.category, this.provider) - .concat(validation.validateArtifactBounds('output', this.outputs, this.artifactBounds.minOutputs, - this.artifactBounds.maxOutputs, this.category, this.provider) - ); - } - - /** @internal */ - public _actionAttachedToPipeline(info: ExtendedActionBind): void { - if (this._stage) { - throw new Error(`Action '${this.actionName}' has been added to a pipeline twice`); - } - - this._pipeline = info.pipeline; - this._stage = info.stage; - this._scope = info.scope; - if (!this._role) { - this._role = info.actionRole; - } - - this.bind(info); - } - - protected addInputArtifact(artifact: Artifact): void { - this.addToArtifacts(artifact, this._actionInputArtifacts); - } - - /** - * Retrieves the Construct scope of this Action. - * Only available after the Action has been added to a Stage, - * and that Stage to a Pipeline. - */ - protected get scope(): Construct { - if (this._scope) { - return this._scope; - } else { - throw new Error('Action must be added to a stage that is part of a pipeline first'); - } - } - - /** - * The method called when an Action is attached to a Pipeline. - * This method is guaranteed to be called only once for each Action instance. - * - * @info an instance of the {@link ActionBind} class, - * that contains the necessary information for the Action - * to configure itself, like a reference to the Pipeline, Stage, Role, etc. - */ - protected abstract bind(info: ActionBind): void; - - private addOutputArtifact(artifact: Artifact): void { - this.addToArtifacts(artifact, this._actionOutputArtifacts); - } - - private addToArtifacts(artifact: Artifact, artifacts: Artifact[]): void { - // adding the same Artifact, or a different Artifact, but with the same name, - // multiple times, doesn't do anything - - // addToArtifacts is idempotent - if (artifact.artifactName) { - if (artifacts.find(a => a.artifactName === artifact.artifactName)) { - return; - } - } else { - if (artifacts.find(a => a === artifact)) { - return; - } - } - - artifacts.push(artifact); - } - - private get pipeline(): IPipeline { - if (this._pipeline) { - return this._pipeline; - } else { - throw new Error('Action must be added to a stage that is part of a pipeline before using onStateChange'); - } - } - - private get stage(): IStage { - if (this._stage) { - return this._stage; - } else { - throw new Error('Action must be added to a stage that is part of a pipeline before using onStateChange'); - } - } -} - -// export class ElasticBeanstalkDeploy extends DeployAction { -// constructor(scope: Stage, id: string, applicationName: string, environmentName: string) { -// super(scope, id, 'ElasticBeanstalk', { minInputs: 1, maxInputs: 1, minOutputs: 0, maxOutputs: 0 }, { -// ApplicationName: applicationName, -// EnvironmentName: environmentName -// }); -// } -// } - -// export class OpsWorksDeploy extends DeployAction { -// constructor(scope: Stage, id: string, app: string, stack: string, layer?: string) { -// super(scope, id, 'OpsWorks', { minInputs: 1, maxInputs: 1, minOutputs: 0, maxOutputs: 0 }, { -// Stack: stack, -// App: app, -// Layer: layer, -// }); -// } -// } - -// export class ECSDeploy extends DeployAction { -// constructor(scope: Stage, id: string, clusterName: string, serviceName: string, fileName?: string) { -// super(scope, id, 'ECS', { minInputs: 1, maxInputs: 1, minOutputs: 0, maxOutputs: 0 }, { -// ClusterName: clusterName, -// ServiceName: serviceName, -// FileName: fileName, -// }); -// } -// } diff --git a/packages/@aws-cdk/aws-codepipeline/lib/full-action-descriptor.ts b/packages/@aws-cdk/aws-codepipeline/lib/full-action-descriptor.ts new file mode 100644 index 0000000000000..c1a130e767797 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline/lib/full-action-descriptor.ts @@ -0,0 +1,56 @@ +import iam = require('@aws-cdk/aws-iam'); +import { ActionArtifactBounds, ActionCategory, ActionConfig, IAction } from './action'; +import { Artifact } from './artifact'; + +/** + * This class is private to the aws-codepipeline package. + */ +export class FullActionDescriptor { + public readonly actionName: string; + public readonly category: ActionCategory; + public readonly owner: string; + public readonly provider: string; + public readonly version: string; + public readonly runOrder: number; + public readonly artifactBounds: ActionArtifactBounds; + public readonly inputs: Artifact[]; + public readonly outputs: Artifact[]; + public readonly region?: string; + public readonly role?: iam.IRole; + public readonly configuration: any; + + constructor(action: IAction, actionConfig: ActionConfig, actionRole: iam.IRole | undefined) { + const actionProperties = action.actionProperties; + this.actionName = actionProperties.actionName; + this.category = actionProperties.category; + this.owner = actionProperties.owner || 'AWS'; + this.provider = actionProperties.provider; + this.version = actionProperties.version || '1'; + this.runOrder = actionProperties.runOrder === undefined ? 1 : actionProperties.runOrder; + this.artifactBounds = actionProperties.artifactBounds; + this.inputs = deduplicateArtifacts(actionProperties.inputs); + this.outputs = deduplicateArtifacts(actionProperties.outputs); + this.region = actionProperties.region; + this.role = actionProperties.role !== undefined ? actionProperties.role : actionRole; + + this.configuration = actionConfig.configuration; + } +} + +function deduplicateArtifacts(artifacts?: Artifact[]): Artifact[] { + const ret = new Array(); + for (const artifact of artifacts || []) { + if (artifact.artifactName) { + if (ret.find(a => a.artifactName === artifact.artifactName)) { + continue; + } + } else { + if (ret.find(a => a === artifact)) { + continue; + } + } + + ret.push(artifact); + } + return ret; +} diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index e274dc84df550..9245c6bb90edd 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -3,9 +3,10 @@ import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); import s3 = require('@aws-cdk/aws-s3'); import { App, Construct, Lazy, PhysicalName, RemovalPolicy, Resource, Stack, Token } from '@aws-cdk/core'; -import { Action, IPipeline, IStage } from "./action"; +import { IAction, IPipeline, IStage } from "./action"; import { CfnPipeline } from './codepipeline.generated'; import { CrossRegionSupportStack } from './cross-region-support-stack'; +import { FullActionDescriptor } from './full-action-descriptor'; import { Stage } from './stage'; import { validateName, validateSourceAction } from "./validation"; @@ -16,7 +17,6 @@ import { validateName, validateSourceAction } from "./validation"; * * @see #rightBefore * @see #justAfter - * @see #atIndex */ export interface StagePlacement { /** @@ -45,7 +45,7 @@ export interface StageProps { * The list of Actions to create this Stage with. * You can always add more Actions later by calling {@link IStage#addAction}. */ - readonly actions?: Action[]; + readonly actions?: IAction[]; } export interface StageOptions extends StageProps { @@ -104,8 +104,7 @@ export interface PipelineProps { abstract class PipelineBase extends Resource implements IPipeline { public abstract pipelineName: string; public abstract pipelineArn: string; - public abstract grantBucketRead(identity: iam.IGrantable): iam.Grant; - public abstract grantBucketReadWrite(identity: iam.IGrantable): iam.Grant; + public abstract artifactBucket: s3.IBucket; /** * Defines an event rule triggered by this CodePipeline. @@ -160,30 +159,6 @@ abstract class PipelineBase extends Resource implements IPipeline { * // ... add more stages */ export class Pipeline extends PipelineBase { - - /** - * Import a pipeline into this app. - * @param scope the scope into which to import this pipeline - * @param pipelineArn The ARN of the pipeline (e.g. `arn:aws:codepipeline:us-east-1:123456789012:MyDemoPipeline`) - */ - public static fromPipelineArn(scope: Construct, id: string, pipelineArn: string): IPipeline { - - class Import extends PipelineBase { - public pipelineName = Stack.of(scope).parseArn(pipelineArn).resource; - public pipelineArn = pipelineArn; - - public grantBucketRead(identity: iam.IGrantable): iam.Grant { - return iam.Grant.drop(identity, `grant read permissions to the artifacts bucket of ${pipelineArn}`); - } - - public grantBucketReadWrite(identity: iam.IGrantable): iam.Grant { - return iam.Grant.drop(identity, `grant read/write permissions to the artifacts bucket of ${pipelineArn}`); - } - } - - return new Import(scope, id); - } - /** * The IAM role AWS CodePipeline will use to perform actions or assume roles for actions with * a more specific IAM role. @@ -315,14 +290,6 @@ export class Pipeline extends PipelineBase { return this.stages.length; } - public grantBucketRead(identity: iam.IGrantable): iam.Grant { - return this.artifactBucket.grantRead(identity); - } - - public grantBucketReadWrite(identity: iam.IGrantable): iam.Grant { - return this.artifactBucket.grantReadWrite(identity); - } - /** * Returns all of the {@link CrossRegionSupportStack}s that were generated automatically * when dealing with Actions that reside in a different region than the Pipeline itself. @@ -335,6 +302,22 @@ export class Pipeline extends PipelineBase { return ret; } + /** @internal */ + public _attachActionToPipeline(stage: Stage, action: IAction, actionScope: Construct): FullActionDescriptor { + // handle cross-region actions here + this.ensureReplicationBucketExistsFor(action.actionProperties.region); + + // get the role for the given action + const actionRole = this.getRoleForAction(stage, action); + + // bind the Action + const actionDescriptor = action.bind(actionScope, stage, { + role: actionRole ? actionRole : this.role, + }); + + return new FullActionDescriptor(action, actionDescriptor, actionRole); + } + /** * Validate the pipeline structure * @@ -352,25 +335,6 @@ export class Pipeline extends PipelineBase { ]; } - // ignore unused private method (it's actually used in Stage) - // @ts-ignore - private _attachActionToPipeline(stage: Stage, action: Action, actionScope: cdk.Construct): void { - // handle cross-region actions here - this.ensureReplicationBucketExistsFor(action.region); - - // get the role for the given action - const actionRole = this.getRoleForAction(stage, action); - - // call the action callback which eventually calls bind() - action._actionAttachedToPipeline({ - pipeline: this, - stage, - scope: actionScope, - role: actionRole ? actionRole : this.role, - actionRole, - }); - } - private requireRegion(): string { const region = Stack.of(this).region; if (Token.isUnresolved(region)) { @@ -431,14 +395,14 @@ export class Pipeline extends PipelineBase { * @param stage the stage the action belongs to * @param action the action to return/create a role for */ - private getRoleForAction(stage: Stage, action: Action): iam.IRole | undefined { + private getRoleForAction(stage: Stage, action: IAction): iam.IRole | undefined { let actionRole: iam.IRole | undefined; - if (action.role) { - actionRole = action.role; - } else if (action.resource) { + if (action.actionProperties.role) { + actionRole = action.actionProperties.role; + } else if (action.actionProperties.resource) { const pipelineStack = Stack.of(this); - const resourceStack = Stack.of(action.resource); + const resourceStack = Stack.of(action.actionProperties.resource); // check if resource is from a different account if (pipelineStack.environment !== resourceStack.environment) { // if it is, the pipeline's bucket must have a KMS key @@ -450,7 +414,8 @@ export class Pipeline extends PipelineBase { } // generate a role in the other stack, that the Pipeline will assume for executing this action - actionRole = new iam.Role(resourceStack, `${this.node.uniqueId}-${stage.stageName}-${action.actionName}-ActionRole`, { + actionRole = new iam.Role(resourceStack, + `${this.node.uniqueId}-${stage.stageName}-${action.actionProperties.actionName}-ActionRole`, { assumedBy: new iam.AccountPrincipal(pipelineStack.account), roleName: PhysicalName.GENERATE_IF_NEEDED, }); diff --git a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts index 0e3ef20eaa507..d4f24951eea6b 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts @@ -1,10 +1,11 @@ import events = require('@aws-cdk/aws-events'); import cdk = require('@aws-cdk/core'); -import { Action, IPipeline, IStage } from "./action"; +import { IAction, IPipeline, IStage } from "./action"; import { Artifact } from "./artifact"; import { CfnPipeline } from './codepipeline.generated'; +import { FullActionDescriptor } from './full-action-descriptor'; import { Pipeline, StageProps } from './pipeline'; -import { validateName } from "./validation"; +import validation = require('./validation'); /** * A Stage in a Pipeline. @@ -18,19 +19,19 @@ export class Stage implements IStage { /** * The Pipeline this Stage is a part of. */ - public readonly pipeline: IPipeline; public readonly stageName: string; private readonly scope: cdk.Construct; - private readonly _actions = new Array(); + private readonly _pipeline: Pipeline; + private readonly _actions = new Array(); /** * Create a new Stage. */ constructor(props: StageProps, pipeline: Pipeline) { - validateName('Stage', props.stageName); + validation.validateName('Stage', props.stageName); this.stageName = props.stageName; - this.pipeline = pipeline; + this._pipeline = pipeline; this.scope = new cdk.Construct(pipeline, this.stageName); for (const action of props.actions || []) { @@ -41,10 +42,14 @@ export class Stage implements IStage { /** * Get a duplicate of this stage's list of actions. */ - public get actions(): Action[] { + public get actions(): FullActionDescriptor[] { return this._actions.slice(); } + public get pipeline(): IPipeline { + return this._pipeline; + } + public render(): CfnPipeline.StageDeclarationProperty { // first, assign names to output Artifacts who don't have one for (const action of this.actions) { @@ -68,14 +73,17 @@ export class Stage implements IStage { }; } - public addAction(action: Action): void { + public addAction(action: IAction): void { + const actionName = action.actionProperties.actionName; + // validate the name + validation.validateName('Action', actionName); + // check for duplicate Actions and names - if (this._actions.find(a => a.actionName === action.actionName)) { - throw new Error(`Stage ${this.stageName} already contains an action with name '${action.actionName}'`); + if (this._actions.find(a => a.actionName === actionName)) { + throw new Error(`Stage ${this.stageName} already contains an action with name '${actionName}'`); } - this._actions.push(action); - this.attachActionToPipeline(action); + this._actions.push(this.attachActionToPipeline(action)); } public onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule { @@ -109,18 +117,26 @@ export class Stage implements IStage { private validateActions(): string[] { const ret = new Array(); for (const action of this.actions) { - ret.push(...action._validate()); + ret.push(...this.validateAction(action)); } return ret; } - private attachActionToPipeline(action: Action) { + private validateAction(action: FullActionDescriptor): string[] { + return validation.validateArtifactBounds('input', action.inputs, action.artifactBounds.minInputs, + action.artifactBounds.maxInputs, action.category, action.provider) + .concat(validation.validateArtifactBounds('output', action.outputs, action.artifactBounds.minOutputs, + action.artifactBounds.maxOutputs, action.category, action.provider) + ); + } + + private attachActionToPipeline(action: IAction): FullActionDescriptor { // notify the Pipeline of the new Action - const actionScope = new cdk.Construct(this.scope, action.actionName); - (this.pipeline as any)._attachActionToPipeline(this, action, actionScope); + const actionScope = new cdk.Construct(this.scope, action.actionProperties.actionName); + return this._pipeline._attachActionToPipeline(this, action, actionScope); } - private renderAction(action: Action): CfnPipeline.ActionDeclarationProperty { + private renderAction(action: FullActionDescriptor): CfnPipeline.ActionDeclarationProperty { const outputArtifacts = this.renderArtifacts(action.outputs); const inputArtifacts = this.renderArtifacts(action.inputs); return { diff --git a/packages/@aws-cdk/aws-codepipeline/package.json b/packages/@aws-cdk/aws-codepipeline/package.json index 882df5b1f4a5b..dca0b7d9a3f91 100644 --- a/packages/@aws-cdk/aws-codepipeline/package.json +++ b/packages/@aws-cdk/aws-codepipeline/package.json @@ -100,7 +100,8 @@ "construct-ctor:@aws-cdk/aws-codepipeline.CrossRegionScaffoldStack..params[1]", "export:@aws-cdk/aws-codepipeline.IPipeline", "import-props-interface:@aws-cdk/aws-codepipeline.PipelineImportProps", - "resource-attribute:@aws-cdk/aws-codepipeline.IPipeline.pipelineVersion" + "resource-attribute:@aws-cdk/aws-codepipeline.IPipeline.pipelineVersion", + "from-method:@aws-cdk/aws-codepipeline.Pipeline" ] }, "stability": "experimental" diff --git a/packages/@aws-cdk/aws-codepipeline/test/fake-build-action.ts b/packages/@aws-cdk/aws-codepipeline/test/fake-build-action.ts index a04c7acd7ef1b..fef3d112b3f9d 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/fake-build-action.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/fake-build-action.ts @@ -1,3 +1,5 @@ +import events = require('@aws-cdk/aws-events'); +import { Construct } from '@aws-cdk/core'; import codepipeline = require('../lib'); export interface FakeBuildActionProps extends codepipeline.CommonActionProps { @@ -8,19 +10,26 @@ export interface FakeBuildActionProps extends codepipeline.CommonActionProps { extraInputs?: codepipeline.Artifact[]; } -export class FakeBuildAction extends codepipeline.Action { +export class FakeBuildAction implements codepipeline.IAction { + public readonly actionProperties: codepipeline.ActionProperties; + constructor(props: FakeBuildActionProps) { - super({ + this.actionProperties = { ...props, category: codepipeline.ActionCategory.BUILD, provider: 'Fake', artifactBounds: { minInputs: 1, maxInputs: 3, minOutputs: 0, maxOutputs: 1 }, inputs: [props.input, ...props.extraInputs || []], outputs: props.output ? [props.output] : undefined, - }); + }; + } + + public bind(_scope: Construct, _stage: codepipeline.IStage, _options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { + return {}; } - protected bind(_info: codepipeline.ActionBind): void { - // do nothing + public onStateChange(_name: string, _target?: events.IRuleTarget, _options?: events.RuleProps): events.Rule { + throw new Error('onStateChange() is not available on FakeBuildAction'); } } diff --git a/packages/@aws-cdk/aws-codepipeline/test/fake-source-action.ts b/packages/@aws-cdk/aws-codepipeline/test/fake-source-action.ts index 8f1f1f7c968f1..1bfeed49746ce 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/fake-source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/fake-source-action.ts @@ -1,3 +1,5 @@ +import events = require('@aws-cdk/aws-events'); +import { Construct } from '@aws-cdk/core'; import codepipeline = require('../lib'); export interface FakeSourceActionProps extends codepipeline.CommonActionProps { @@ -6,18 +8,28 @@ export interface FakeSourceActionProps extends codepipeline.CommonActionProps { extraOutputs?: codepipeline.Artifact[]; } -export class FakeSourceAction extends codepipeline.Action { +export class FakeSourceAction implements codepipeline.IAction { + public readonly inputs?: codepipeline.Artifact[]; + public readonly outputs?: codepipeline.Artifact[]; + + public readonly actionProperties: codepipeline.ActionProperties; + constructor(props: FakeSourceActionProps) { - super({ + this.actionProperties = { ...props, category: codepipeline.ActionCategory.SOURCE, provider: 'Fake', artifactBounds: { minInputs: 0, maxInputs: 0, minOutputs: 1, maxOutputs: 4 }, outputs: [props.output, ...props.extraOutputs || []], - }); + }; + } + + public bind(_scope: Construct, _stage: codepipeline.IStage, _options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { + return {}; } - protected bind(_info: codepipeline.ActionBind): void { - // do nothing + public onStateChange(_name: string, _target?: events.IRuleTarget, _options?: events.RuleProps): events.Rule { + throw new Error('onStateChange() is not available on FakeSourceAction'); } } diff --git a/packages/@aws-cdk/aws-codepipeline/test/test.action.ts b/packages/@aws-cdk/aws-codepipeline/test/test.action.ts index 4eebfcf135b8d..253a4b66e9cf8 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/test.action.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/test.action.ts @@ -61,6 +61,24 @@ export = { }, }, + 'action name validation': { + 'throws an exception when adding an Action with an empty name to the Pipeline'(test: Test) { + const stack = new cdk.Stack(); + const action = new FakeSourceAction({ + actionName: '', + output: new codepipeline.Artifact(), + }); + + const pipeline = new codepipeline.Pipeline(stack, 'Pipeline'); + const stage = pipeline.addStage({ stageName: 'Source' }); + test.throws(() => { + stage.addAction(action); + }, /Action name must match regular expression:/); + + test.done(); + }, + }, + 'action Artifacts validation': { 'validates that input Artifacts are within bounds'(test: Test) { const stack = new cdk.Stack(); @@ -221,7 +239,7 @@ export = { test.done(); }, - 'the same Action cannot be added to 2 different Stages'(test: Test) { + 'the same Action can be safely added to 2 different Stages'(test: Test) { const stack = new cdk.Stack(); const sourceOutput = new codepipeline.Artifact(); @@ -249,8 +267,8 @@ export = { actions: [action], }; - pipeline.addStage(stage2); // fine - test.throws(() => { + pipeline.addStage(stage2); + test.doesNotThrow(() => { pipeline.addStage(stage3); }, /FakeAction/); @@ -260,13 +278,14 @@ export = { 'input Artifacts': { 'can be added multiple times to an Action safely'(test: Test) { const artifact = new codepipeline.Artifact('SomeArtifact'); - const action = new FakeBuildAction({ - actionName: 'CodeBuild', - input: artifact, - extraInputs: [artifact], - }); - test.equal(action.inputs.length, 1); + test.doesNotThrow(() => { + new FakeBuildAction({ + actionName: 'CodeBuild', + input: artifact, + extraInputs: [artifact], + }); + }); test.done(); }, @@ -289,17 +308,17 @@ export = { 'output Artifacts': { 'accept multiple Artifacts with the same name safely'(test: Test) { - const action = new FakeSourceAction({ - actionName: 'CodeBuild', - output: new codepipeline.Artifact('Artifact1'), - extraOutputs: [ - new codepipeline.Artifact('Artifact1'), - new codepipeline.Artifact('Artifact1'), - ], + test.doesNotThrow(() => { + new FakeSourceAction({ + actionName: 'CodeBuild', + output: new codepipeline.Artifact('Artifact1'), + extraOutputs: [ + new codepipeline.Artifact('Artifact1'), + new codepipeline.Artifact('Artifact1'), + ], + }); }); - test.equal(action.outputs.length, 1); - test.done(); }, }, diff --git a/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.ts b/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.ts index 9324c65d5f109..e8d0bfd6fa0db 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.ts +++ b/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.ts @@ -4,9 +4,28 @@ import events = require('@aws-cdk/aws-events'); import cdk = require('@aws-cdk/core'); import targets = require('../../lib'); -class MockAction extends codepipeline.Action { - protected bind(_info: codepipeline.ActionBind): void { - // void +interface MockActionProps extends codepipeline.ActionProperties { + configuration?: any; +} + +class MockAction implements codepipeline.IAction { + public readonly actionProperties: codepipeline.ActionProperties; + private readonly configuration: any; + + constructor(props: MockActionProps) { + this.actionProperties = props; + this.configuration = props.configuration; + } + + public bind(_scope: cdk.Construct, _stage: codepipeline.IStage, _options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { + return { + configuration: this.configuration, + }; + } + + public onStateChange(_name: string, _target?: events.IRuleTarget, _options?: events.RuleProps): events.Rule { + throw new Error('onStateChange() is not available on MockAction'); } } diff --git a/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts b/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts index 60bd93005f3ca..f56c513df9618 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts @@ -1,7 +1,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import codepipeline = require('@aws-cdk/aws-codepipeline'); import events = require('@aws-cdk/aws-events'); -import { Stack } from '@aws-cdk/core'; +import { Construct, Stack } from '@aws-cdk/core'; import targets = require('../../lib'); test('use codebuild project as an eventrule target', () => { @@ -76,8 +76,17 @@ test('use codebuild project as an eventrule target', () => { })); }); -class TestAction extends codepipeline.Action { - protected bind(_info: codepipeline.ActionBind): void { - // void +class TestAction implements codepipeline.IAction { + constructor(public readonly actionProperties: codepipeline.ActionProperties) { + // nothing to do + } + + public bind(_scope: Construct, _stage: codepipeline.IStage, _options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { + return {}; + } + + public onStateChange(_name: string, _target?: events.IRuleTarget, _options?: events.RuleProps): events.Rule { + throw new Error('onStateChange() is not available on MockAction'); } }