diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 47c95ba410975..8571bd5cf2576 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -426,6 +426,16 @@ const bucket = s3.Bucket.fromBucketName(this, 'Bucket', 'my-bucket'); pipelines.CodePipelineSource.s3(bucket, 'my/source.zip'); ``` +##### ECR + +You can use a Docker image in ECR as the source of the pipeline. The pipeline will be +triggered every time an image is pushed to ECR: + +```ts +const repository = new ecr.Repository(this, 'Repository'); +pipelines.CodePipelineSource.ecr(repository); +``` + #### Additional inputs `ShellStep` allows passing in more than one input: additional diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts index ee815a6f94492..062df5b4b446f 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts @@ -3,9 +3,10 @@ import * as cp from '@aws-cdk/aws-codepipeline'; import { Artifact } from '@aws-cdk/aws-codepipeline'; import * as cp_actions from '@aws-cdk/aws-codepipeline-actions'; import { Action, CodeCommitTrigger, GitHubTrigger, S3Trigger } from '@aws-cdk/aws-codepipeline-actions'; +import { IRepository } from '@aws-cdk/aws-ecr'; import * as iam from '@aws-cdk/aws-iam'; import { IBucket } from '@aws-cdk/aws-s3'; -import { SecretValue, Token } from '@aws-cdk/core'; +import { Fn, SecretValue, Token } from '@aws-cdk/core'; import { Node } from 'constructs'; import { FileSet, Step } from '../blueprint'; import { CodePipelineActionFactoryResult, ProduceActionOptions, ICodePipelineActionFactory } from './codepipeline-action-factory'; @@ -57,6 +58,22 @@ export abstract class CodePipelineSource extends Step implements ICodePipelineAc return new S3Source(bucket, objectKey, props); } + /** + * Returns an ECR source. + * + * @param repository The repository that will be watched for changes. + * @param props The options, which include the image tag to be checked for changes. + * + * @example + * declare const repository: ecr.IRepository; + * pipelines.CodePipelineSource.ecr(repository, { + * imageTag: 'latest', + * }); + */ + public static ecr(repository: IRepository, props: ECRSourceOptions = {}): CodePipelineSource { + return new ECRSource(repository, props); + } + /** * Returns a CodeStar connection source. A CodeStar connection allows AWS CodePipeline to * access external resources, such as repositories in GitHub, GitHub Enterprise or @@ -264,6 +281,46 @@ class S3Source extends CodePipelineSource { } } +/** + * Options for ECR sources + */ +export interface ECRSourceOptions { + /** + * The image tag that will be checked for changes. + * + * @default latest + */ + readonly imageTag?: string; + + /** + * The action name used for this source in the CodePipeline + * + * @default - The repository name + */ + readonly actionName?: string; +} + +class ECRSource extends CodePipelineSource { + constructor(readonly repository: IRepository, readonly props: ECRSourceOptions) { + super(Node.of(repository).addr); + + this.configurePrimaryOutput(new FileSet('Source', this)); + } + + protected getAction(output: Artifact, _actionName: string, runOrder: number, variablesNamespace: string) { + // RepositoryName can contain '/' that is not a valid ActionName character, use '_' instead + const formattedRepositoryName = Fn.join('_', Fn.split('/', this.repository.repositoryName)); + return new cp_actions.EcrSourceAction({ + output, + actionName: this.props.actionName ?? formattedRepositoryName, + runOrder, + repository: this.repository, + imageTag: this.props.imageTag, + variablesNamespace, + }); + } +} + /** * Configuration options for CodeStar source */ diff --git a/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts b/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts index 721c65d6d8b21..93284d031d11c 100644 --- a/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts +++ b/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts @@ -1,6 +1,7 @@ import { Capture, Match, Template } from '@aws-cdk/assertions'; import * as ccommit from '@aws-cdk/aws-codecommit'; import { CodeCommitTrigger, GitHubTrigger } from '@aws-cdk/aws-codepipeline-actions'; +import * as ecr from '@aws-cdk/aws-ecr'; import { AnyPrincipal, Role } from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; import { SecretValue, Stack, Token } from '@aws-cdk/core'; @@ -75,9 +76,9 @@ test('CodeCommit source honors all valid properties', () => { }); test('S3 source handles tokenized names correctly', () => { - const buckit = new s3.Bucket(pipelineStack, 'Buckit'); + const bucket = new s3.Bucket(pipelineStack, 'Bucket'); new ModernTestGitHubNpmPipeline(pipelineStack, 'Pipeline', { - input: cdkp.CodePipelineSource.s3(buckit, 'thefile.zip'), + input: cdkp.CodePipelineSource.s3(bucket, 'thefile.zip'), }); Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { @@ -96,6 +97,40 @@ test('S3 source handles tokenized names correctly', () => { }); }); +test('ECR source handles tokenized and namespaced names correctly', () => { + const repository = new ecr.Repository(pipelineStack, 'Repository', { repositoryName: 'namespace/repo' }); + new ModernTestGitHubNpmPipeline(pipelineStack, 'Pipeline', { + input: cdkp.CodePipelineSource.ecr(repository), + }); + + const template = Template.fromStack(pipelineStack); + template.hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ + Name: 'Source', + Actions: [ + Match.objectLike({ + Configuration: Match.objectLike({ + RepositoryName: { Ref: Match.anyValue() }, + }), + Name: Match.objectLike({ + 'Fn::Join': [ + '_', + { + 'Fn::Split': [ + '/', + { + Ref: Match.anyValue(), + }, + ], + }, + ], + }), + }), + ], + }]), + }); +}); + test('GitHub source honors all valid properties', () => { new ModernTestGitHubNpmPipeline(pipelineStack, 'Pipeline', { input: cdkp.CodePipelineSource.gitHub('owner/repo', 'main', {