Skip to content
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

feat(pipelines): ECR source action #16385

Merged
merged 9 commits into from Feb 24, 2022
Merged
10 changes: 10 additions & 0 deletions packages/@aws-cdk/pipelines/README.md
Expand Up @@ -355,6 +355,16 @@ const bucket = s3.Bucket.fromBucketName(this, 'Bucket', 'my-bucket');
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 = ecr.Repository(this, 'Repository');
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 @@ -51,16 +52,37 @@ export abstract class CodePipelineSource extends Step implements ICodePipelineAc
* Example:
*
* ```ts
* const bucket: IBucket = ...
* CodePipelineSource.s3(bucket, {
* key: 'path/to/file.zip',
* import * as ecr from '@aws-cdk/aws-ecr';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexpulver : should the following comments have been modified ? I think they still apply to the s3 factory method.

*
* declare const repository: ecr.IRepository;
* pipelines.CodePipelineSource.ecr(repository, {
* imageTag: 'latest',
* });
* ```
*/
public static s3(bucket: IBucket, objectKey: string, props: S3SourceOptions = {}): CodePipelineSource {
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:
*
* ```ts
* const repository: IRepository = ...
* 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 @@ -238,6 +260,45 @@ 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) {
// 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,
});
}
}

/**
* Configuration options for CodeStar source
*/
Expand Down Expand Up @@ -363,4 +424,4 @@ class CodeCommitSource extends CodePipelineSource {
codeBuildCloneOutput: this.props.codeBuildCloneOutput,
});
}
}
}
@@ -1,7 +1,8 @@
import { anything, arrayWith, Capture, objectLike } from '@aws-cdk/assert-internal';
import { anything, arrayWith, Capture, deepObjectLike, objectLike } from '@aws-cdk/assert-internal';
import '@aws-cdk/assert-internal/jest';
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 @@ -76,9 +77,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'),
});

expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', {
Expand All @@ -97,6 +98,39 @@ 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),
});

expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', {
Stages: arrayWith({
Name: 'Source',
Actions: [
objectLike({
Configuration: objectLike({
RepositoryName: { Ref: anything() },
}),
Name: deepObjectLike({
'Fn::Join': [
'_',
{
'Fn::Split': [
'/',
{
Ref: anything(),
},
],
},
],
}),
}),
],
}),
});
});

test('GitHub source honors all valid properties', () => {
new ModernTestGitHubNpmPipeline(pipelineStack, 'Pipeline', {
input: cdkp.CodePipelineSource.gitHub('owner/repo', 'main', {
Expand Down