Skip to content

Commit

Permalink
feat(pipelines): ECR source action (#16385)
Browse files Browse the repository at this point in the history
Closes #16378 

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
alexpulver committed Feb 24, 2022
1 parent 040238e commit fc11ae2
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 3 deletions.
10 changes: 10 additions & 0 deletions packages/@aws-cdk/pipelines/README.md
Expand Up @@ -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
Expand Down
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
*/
Expand Down
@@ -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';
Expand Down Expand Up @@ -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', {
Expand All @@ -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', {
Expand Down

0 comments on commit fc11ae2

Please sign in to comment.