-
Notifications
You must be signed in to change notification settings - Fork 4.4k
feat(codepipeline): allow cross-account CloudFormation Actions #3208
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -375,10 +375,7 @@ export class Pipeline extends PipelineBase { | |
| throw new Error("You need to specify an explicit account when using CodePipeline's cross-region support"); | ||
| } | ||
|
|
||
| const app = this.node.root; | ||
| if (!app || !App.isApp(app)) { | ||
| throw new Error(`Pipeline stack which uses cross region actions must be part of a CDK app`); | ||
| } | ||
| const app = this.requireApp(); | ||
| const crossRegionScaffoldStack = new CrossRegionSupportStack(app, `cross-region-stack-${pipelineAccount}:${region}`, { | ||
| pipelineStackName: pipelineStack.stackName, | ||
| region, | ||
|
|
@@ -404,44 +401,16 @@ export class Pipeline extends PipelineBase { | |
|
|
||
| /** | ||
| * Gets the role used for this action, | ||
| * including handling the case when the action is supposed to be cross-region. | ||
| * including handling the case when the action is supposed to be cross-account. | ||
| * | ||
| * @param stage the stage the action belongs to | ||
| * @param action the action to return/create a role for | ||
| * @param actionScope the scope, unique to the action, to create new resources in | ||
| */ | ||
| private getRoleForAction(stage: Stage, action: IAction, actionScope: Construct): iam.IRole | undefined { | ||
| const pipelineStack = Stack.of(this); | ||
|
|
||
| let actionRole: iam.IRole | undefined; | ||
| if (action.actionProperties.role) { | ||
| if (!this.isAwsOwned(action)) { | ||
| throw new Error("Specifying a Role is not supported for actions with an owner different than 'AWS' - " + | ||
| `got '${action.actionProperties.owner}' (Action: '${action.actionProperties.actionName}' in Stage: '${stage.stageName}')`); | ||
| } | ||
| actionRole = action.actionProperties.role; | ||
| } else if (action.actionProperties.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 | ||
| if (!this.artifactBucket.encryptionKey) { | ||
| throw new Error('The Pipeline is being used in a cross-account manner, ' + | ||
| 'but its artifact bucket does not have a KMS key defined. ' + | ||
| 'A KMS key is required for a cross-account Pipeline. ' + | ||
| 'Make sure to pass a Bucket with a Key when creating the Pipeline'); | ||
| } | ||
|
|
||
| // 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.actionProperties.actionName}-ActionRole`, { | ||
| assumedBy: new iam.AccountPrincipal(pipelineStack.account), | ||
| roleName: PhysicalName.GENERATE_IF_NEEDED, | ||
| }); | ||
|
|
||
| // the other stack has to be deployed before the pipeline stack | ||
| pipelineStack.addDependency(resourceStack); | ||
| } | ||
| } | ||
| let actionRole = this.getRoleFromActionPropsOrGenerateIfCrossAccount(stage, action); | ||
|
|
||
| if (!actionRole && this.isAwsOwned(action)) { | ||
| // generate a Role for this specific Action | ||
|
|
@@ -461,6 +430,107 @@ export class Pipeline extends PipelineBase { | |
| return actionRole; | ||
| } | ||
|
|
||
| private getRoleFromActionPropsOrGenerateIfCrossAccount(stage: Stage, action: IAction): iam.IRole | undefined { | ||
| const pipelineStack = Stack.of(this); | ||
|
|
||
| // if a Role has been passed explicitly, always use it | ||
| // (even if the backing resource is from a different account - | ||
| // this is how the user can override our default support logic) | ||
| if (action.actionProperties.role) { | ||
skinny85 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (this.isAwsOwned(action)) { | ||
| // the role has to be deployed before the pipeline | ||
| const roleStack = Stack.of(action.actionProperties.role); | ||
| pipelineStack.addDependency(roleStack); | ||
|
|
||
| return action.actionProperties.role; | ||
| } else { | ||
| // ...except if the Action is not owned by 'AWS', | ||
| // as that would be rejected by CodePipeline at deploy time | ||
| throw new Error("Specifying a Role is not supported for actions with an owner different than 'AWS' - " + | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The terminology "owner" here is extremely confusing (especially since we use it to indicate "owned resources"). How about something like "The action X does not support specifying a role".
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to be clear: I'm talking here about this property. Given that, does the error message makes sense? (I'm not worried about the EDIT: maybe this is a better explanation of the |
||
| `got '${action.actionProperties.owner}' (Action: '${action.actionProperties.actionName}' in Stage: '${stage.stageName}')`); | ||
| } | ||
| } | ||
|
|
||
| // if we don't have a Role passed, | ||
| // and the action is cross-account, | ||
| // generate a Role in that other account stack | ||
| const otherAccountStack = this.getOtherStackIfActionIsCrossAccount(action); | ||
| if (!otherAccountStack) { | ||
| return undefined; | ||
| } | ||
|
|
||
| // if we have a cross-account action, the pipeline's bucket must have a KMS key | ||
| if (!this.artifactBucket.encryptionKey) { | ||
| throw new Error('The Pipeline is being used in a cross-account manner, ' + | ||
| 'but its artifact bucket does not have a KMS key defined. ' + | ||
| 'A KMS key is required for a cross-account Pipeline. ' + | ||
| 'Make sure to pass a Bucket with a Key when creating the Pipeline'); | ||
| } | ||
|
|
||
| // generate a role in the other stack, that the Pipeline will assume for executing this action | ||
| const ret = new iam.Role(otherAccountStack, | ||
| `${this.node.uniqueId}-${stage.stageName}-${action.actionProperties.actionName}-ActionRole`, { | ||
| assumedBy: new iam.AccountPrincipal(pipelineStack.account), | ||
| roleName: PhysicalName.GENERATE_IF_NEEDED, | ||
| }); | ||
| // the other stack with the role has to be deployed before the pipeline stack | ||
| // (CodePipeline verifies you can assume the action Role on creation) | ||
| pipelineStack.addDependency(otherAccountStack); | ||
|
|
||
| return ret; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the Stack this Action belongs to if this is a cross-account Action. | ||
| * If this Action is not cross-account (i.e., it lives in the same account as the Pipeline), | ||
| * it returns undefined. | ||
| * | ||
| * @param action the Action to return the Stack for | ||
| */ | ||
| private getOtherStackIfActionIsCrossAccount(action: IAction): Stack | undefined { | ||
skinny85 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const pipelineStack = Stack.of(this); | ||
|
|
||
| if (action.actionProperties.resource) { | ||
skinny85 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const resourceStack = Stack.of(action.actionProperties.resource); | ||
| // check if resource is from a different account | ||
| return pipelineStack.account === resourceStack.account | ||
| ? undefined | ||
| : resourceStack; | ||
| } | ||
|
|
||
| if (!action.actionProperties.account) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const targetAccount = action.actionProperties.account; | ||
| // check whether the account is a static string | ||
| if (Token.isUnresolved(targetAccount)) { | ||
| throw new Error(`The 'account' property must be a concrete value (action: '${action.actionProperties.actionName}')`); | ||
| } | ||
| // check whether the pipeline account is a static string | ||
| if (Token.isUnresolved(pipelineStack.account)) { | ||
| throw new Error("Pipeline stack which uses cross-environment actions must have an explicitly set account"); | ||
| } | ||
|
|
||
| if (pipelineStack.account === targetAccount) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const stackId = `cross-account-support-stack-${targetAccount}`; | ||
| const app = this.requireApp(); | ||
| let targetAccountStack = app.node.tryFindChild(stackId) as Stack; | ||
| if (!targetAccountStack) { | ||
| targetAccountStack = new Stack(app, stackId, { | ||
| stackName: `${pipelineStack.stackName}-support-${targetAccount}`, | ||
| env: { | ||
| account: targetAccount, | ||
| region: action.actionProperties.region ? action.actionProperties.region : pipelineStack.region, | ||
| }, | ||
| }); | ||
| } | ||
| return targetAccountStack; | ||
| } | ||
|
|
||
| private isAwsOwned(action: IAction) { | ||
| const owner = action.actionProperties.owner; | ||
| return !owner || owner === 'AWS'; | ||
|
|
@@ -626,10 +696,18 @@ export class Pipeline extends PipelineBase { | |
| private requireRegion(): string { | ||
| const region = Stack.of(this).region; | ||
| if (Token.isUnresolved(region)) { | ||
| throw new Error(`You need to specify an explicit region when using CodePipeline's cross-region support`); | ||
| throw new Error(`Pipeline stack which uses cross-environment actions must have an explicitly set region`); | ||
| } | ||
| return region; | ||
| } | ||
|
|
||
| private requireApp(): App { | ||
| const app = this.node.root; | ||
| if (!app || !App.isApp(app)) { | ||
| throw new Error(`Pipeline stack which uses cross-environment actions must be part of a CDK app`); | ||
| } | ||
| return app; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.