From d169688f35bc78c88c44ff9a7d8fa0dfea71f904 Mon Sep 17 00:00:00 2001 From: Alban Esc Date: Thu, 21 Jan 2021 14:58:57 -0800 Subject: [PATCH] feat(aws-codepipeline-actions): Add Full Clone support for CodeCommit (#12558) Add `codeBuildCloneOutput` property to the CodeCommit source action. It automatically adds the `codecommit:GetRepository` permission to the CodeCommitSourceAction role. It will also add the `codecommit:GitPull` permission to any CodeBuildAction using the artifact from CodeCommitSourceAction as input. Closes #12236 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-codepipeline-actions/README.md | 20 ++++ .../lib/codebuild/build-action.ts | 18 ++- .../lib/codecommit/source-action.ts | 31 ++++- .../test.codecommit-source-action.ts | 107 +++++++++++++++++- 4 files changed, 171 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-codepipeline-actions/README.md b/packages/@aws-cdk/aws-codepipeline-actions/README.md index e241c5104ff22..cdc6cf1028ce3 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/README.md +++ b/packages/@aws-cdk/aws-codepipeline-actions/README.md @@ -57,6 +57,26 @@ const sourceAction = new codepipeline_actions.CodeCommitSourceAction({ }); ``` +If you want to clone the entire CodeCommit repository (only available for CodeBuild actions), +you can set the `codeBuildCloneOutput` property to `true`: + +```ts +const sourceOutput = new codepipeline.Artifact(); +const sourceAction = new codepipeline_actions.CodeCommitSourceAction({ + actionName: 'CodeCommit', + repository: repo, + output: sourceOutput, + codeBuildCloneOutput: true, +}); + +const buildAction = new codepipeline_actions.CodeBuildAction({ + actionName: 'CodeBuild', + project, + input: sourceOutput, // The build action must use the CodeCommitSourceAction output as input. + outputs: [new codepipeline.Artifact()], // optional +}); +``` + The CodeCommit source action emits variables: ```ts 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 b55d9742c514b..468684664cc5f 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 @@ -4,6 +4,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import { BitBucketSourceAction } from '..'; import { Action } from '../action'; +import { CodeCommitSourceAction } from '../codecommit/source-action'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main // eslint-disable-next-line no-duplicate-imports, import/order @@ -176,10 +177,10 @@ export class CodeBuildAction extends Action { }); } - // if any of the inputs come from the BitBucketSourceAction - // with codeBuildCloneOutput=true, - // grant the Project's Role to use the connection for (const inputArtifact of this.actionProperties.inputs || []) { + // if any of the inputs come from the BitBucketSourceAction + // with codeBuildCloneOutput=true, + // grant the Project's Role to use the connection const connectionArn = inputArtifact.getMetadata(BitBucketSourceAction._CONNECTION_ARN_PROPERTY); if (connectionArn) { this.props.project.addToRolePolicy(new iam.PolicyStatement({ @@ -187,6 +188,17 @@ export class CodeBuildAction extends Action { resources: [connectionArn], })); } + + // if any of the inputs come from the CodeCommitSourceAction + // with codeBuildCloneOutput=true, + // grant the Project's Role git pull access to the repository + const codecommitRepositoryArn = inputArtifact.getMetadata(CodeCommitSourceAction._FULL_CLONE_ARN_PROPERTY); + if (codecommitRepositoryArn) { + this.props.project.addToRolePolicy(new iam.PolicyStatement({ + actions: ['codecommit:GitPull'], + resources: [codecommitRepositoryArn], + })); + } } const configuration: any = { 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 9935bcb5be4d7..2b18bb9db6071 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 @@ -85,12 +85,33 @@ export interface CodeCommitSourceActionProps extends codepipeline.CommonAwsActio * @default a new role will be created. */ readonly eventRole?: iam.IRole; + + /** + * Whether the output should be the contents of the repository + * (which is the default), + * or a link that allows CodeBuild to clone the repository before building. + * + * **Note**: if this option is true, + * then only CodeBuild actions can use the resulting {@link output}. + * + * @default false + * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-CodeCommit.html + */ + readonly codeBuildCloneOutput?: boolean; } /** * CodePipeline Source that is provided by an AWS CodeCommit repository. */ export class CodeCommitSourceAction extends Action { + /** + * The name of the property that holds the ARN of the CodeCommit Repository + * inside of the CodePipeline Artifact's metadata. + * + * @internal + */ + public static readonly _FULL_CLONE_ARN_PROPERTY = 'CodeCommitCloneRepositoryArn'; + private readonly branch: string; private readonly props: CodeCommitSourceActionProps; @@ -100,6 +121,10 @@ export class CodeCommitSourceAction extends Action { throw new Error("'branch' parameter cannot be an empty string"); } + if (props.codeBuildCloneOutput === true) { + props.output.setMetadata(CodeCommitSourceAction._FULL_CLONE_ARN_PROPERTY, props.repository.repositoryArn); + } + super({ ...props, resource: props.repository, @@ -144,7 +169,7 @@ export class CodeCommitSourceAction extends Action { options.bucket.grantReadWrite(options.role); // https://docs.aws.amazon.com/codecommit/latest/userguide/auth-and-access-control-permissions-reference.html#aa-acp - options.role.addToPolicy(new iam.PolicyStatement({ + options.role.addToPrincipalPolicy(new iam.PolicyStatement({ resources: [this.props.repository.repositoryArn], actions: [ 'codecommit:GetBranch', @@ -152,6 +177,7 @@ export class CodeCommitSourceAction extends Action { 'codecommit:UploadArchive', 'codecommit:GetUploadArchiveStatus', 'codecommit:CancelUploadArchive', + ...(this.props.codeBuildCloneOutput === true ? ['codecommit:GetRepository'] : []), ], })); @@ -160,6 +186,9 @@ export class CodeCommitSourceAction extends Action { RepositoryName: this.props.repository.repositoryName, BranchName: this.branch, PollForSourceChanges: this.props.trigger === CodeCommitTrigger.POLL, + OutputArtifactFormat: this.props.codeBuildCloneOutput === true + ? 'CODEBUILD_CLONE_REF' + : undefined, }, }; } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/test.codecommit-source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/test.codecommit-source-action.ts index 9aa0ac65a72c9..3dc5b1c739d4e 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/test.codecommit-source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/test.codecommit-source-action.ts @@ -1,4 +1,4 @@ -import { countResources, expect, haveResourceLike, not } from '@aws-cdk/assert'; +import { arrayWith, countResources, expect, haveResourceLike, not, objectLike } from '@aws-cdk/assert'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codecommit from '@aws-cdk/aws-codecommit'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; @@ -269,6 +269,111 @@ export = { test.done(); }, + 'allows to enable full clone'(test: Test) { + const stack = new Stack(); + + const sourceOutput = new codepipeline.Artifact(); + new codepipeline.Pipeline(stack, 'P', { + stages: [ + { + stageName: 'Source', + actions: [ + new cpactions.CodeCommitSourceAction({ + actionName: 'CodeCommit', + repository: new codecommit.Repository(stack, 'R', { + repositoryName: 'repository', + }), + branch: Lazy.string({ produce: () => 'my-branch' }), + output: sourceOutput, + codeBuildCloneOutput: true, + }), + ], + }, + { + stageName: 'Build', + actions: [ + new cpactions.CodeBuildAction({ + actionName: 'Build', + project: new codebuild.PipelineProject(stack, 'CodeBuild'), + input: sourceOutput, + }), + ], + }, + ], + }); + + expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + 'Stages': [ + { + 'Name': 'Source', + 'Actions': [{ + 'Configuration': { + 'OutputArtifactFormat': 'CODEBUILD_CLONE_REF', + }, + }], + }, + { + 'Name': 'Build', + 'Actions': [ + { + 'Name': 'Build', + }, + ], + }, + ], + })); + + expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + 'PolicyDocument': { + 'Statement': arrayWith( + objectLike({ + 'Action': [ + 'logs:CreateLogGroup', + 'logs:CreateLogStream', + 'logs:PutLogEvents', + ], + }), + objectLike({ + 'Action': 'codecommit:GitPull', + 'Effect': 'Allow', + 'Resource': { + 'Fn::GetAtt': [ + 'RC21A1702', + 'Arn', + ], + }, + }), + ), + }, + })); + + expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + 'PolicyDocument': { + 'Statement': arrayWith( + objectLike({ + 'Action': [ + 'codecommit:GetBranch', + 'codecommit:GetCommit', + 'codecommit:UploadArchive', + 'codecommit:GetUploadArchiveStatus', + 'codecommit:CancelUploadArchive', + 'codecommit:GetRepository', + ], + 'Effect': 'Allow', + 'Resource': { + 'Fn::GetAtt': [ + 'RC21A1702', + 'Arn', + ], + }, + }), + ), + }, + })); + + test.done(); + }, + 'uses the role when passed'(test: Test) { const stack = new Stack();