Skip to content

Commit c6c09bf

Browse files
authoredSep 13, 2018
feat(aws-ecr): add support for ECR repositories (#697)
1 parent 4bd1cf2 commit c6c09bf

File tree

10 files changed

+654
-11
lines changed

10 files changed

+654
-11
lines changed
 

‎packages/@aws-cdk/aws-ecr/README.md

+25-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,25 @@
1-
## The CDK Construct Library for AWS Elastic Container Registry (ECR)
2-
This module is part of the [AWS Cloud Development Kit](https://github.com/awslabs/aws-cdk) project.
1+
## Amazon Elastic Container Registry Construct Library
2+
3+
This package contains constructs for working with Amazon Elastic Container Registry.
4+
5+
### Repositories
6+
7+
Define a repository by creating a new instance of `Repository`. A repository
8+
holds multiple verions of a single container image.
9+
10+
```ts
11+
const repository = new ecr.Repository(this, 'Repository');
12+
```
13+
14+
### Automatically clean up repositories
15+
16+
You can set life cycle rules to automatically clean up old images from your
17+
repository. The first life cycle rule that matches an image will be applied
18+
against that image. For example, the following deletes images older than
19+
30 days, while keeping all images tagged with prod (note that the order
20+
is important here):
21+
22+
```ts
23+
repository.addLifecycleRule({ tagPrefixList: ['prod'], maxImageCount: 9999 });
24+
repository.addLifecycleRule({ maxImageAgeDays: 30 });
25+
```
+4
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
// AWS::ECR CloudFormation Resources:
22
export * from './ecr.generated';
3+
4+
export * from './repository';
5+
export * from './repository-ref';
6+
export * from './lifecycle';
+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* An ECR life cycle rule
3+
*/
4+
export interface LifecycleRule {
5+
/**
6+
* Controls the order in which rules are evaluated (low to high)
7+
*
8+
* All rules must have a unique priority, where lower numbers have
9+
* higher precedence. The first rule that matches is applied to an image.
10+
*
11+
* There can only be one rule with a tagStatus of Any, and it must have
12+
* the highest rulePriority.
13+
*
14+
* All rules without a specified priority will have incrementing priorities
15+
* automatically assigned to them, higher than any rules that DO have priorities.
16+
*
17+
* @default Automatically assigned
18+
*/
19+
rulePriority?: number;
20+
21+
/**
22+
* Describes the purpose of the rule
23+
*
24+
* @default No description
25+
*/
26+
description?: string;
27+
28+
/**
29+
* Select images based on tags
30+
*
31+
* Only one rule is allowed to select untagged images, and it must
32+
* have the highest rulePriority.
33+
*
34+
* @default TagStatus.Tagged if tagPrefixList is given, TagStatus.Any otherwise
35+
*/
36+
tagStatus?: TagStatus;
37+
38+
/**
39+
* Select images that have ALL the given prefixes in their tag.
40+
*
41+
* Only if tagStatus == TagStatus.Tagged
42+
*/
43+
tagPrefixList?: string[];
44+
45+
/**
46+
* The maximum number of images to retain
47+
*
48+
* Specify exactly one of maxImageCount and maxImageAgeDays.
49+
*/
50+
maxImageCount?: number;
51+
52+
/**
53+
* The maximum age of images to retain
54+
*
55+
* Specify exactly one of maxImageCount and maxImageAgeDays.
56+
*/
57+
maxImageAgeDays?: number;
58+
}
59+
60+
/**
61+
* Select images based on tags
62+
*/
63+
export enum TagStatus {
64+
/**
65+
* Rule applies to all images
66+
*/
67+
Any = 'any',
68+
69+
/**
70+
* Rule applies to tagged images
71+
*/
72+
Tagged = 'tagged',
73+
74+
/**
75+
* Rule applies to untagged images
76+
*/
77+
Untagged = 'untagged',
78+
}
79+
80+
/**
81+
* Select images based on counts
82+
*/
83+
export enum CountType {
84+
/**
85+
* Set a limit on the number of images in your repository
86+
*/
87+
ImageCountMoreThan = 'imageCountMoreThan',
88+
89+
/**
90+
* Set an age limit on the images in your repository
91+
*/
92+
SinceImagePushed = 'sinceImagePushed',
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import cdk = require('@aws-cdk/cdk');
2+
import { RepositoryArn, RepositoryName } from './ecr.generated';
3+
4+
/**
5+
* An ECR repository
6+
*/
7+
export abstract class RepositoryRef extends cdk.Construct {
8+
/**
9+
* Import a repository
10+
*/
11+
public static import(parent: cdk.Construct, id: string, props: RepositoryRefProps): RepositoryRef {
12+
return new ImportedRepository(parent, id, props);
13+
}
14+
15+
/**
16+
* The name of the repository
17+
*/
18+
public abstract readonly repositoryName: RepositoryName;
19+
20+
/**
21+
* The ARN of the repository
22+
*/
23+
public abstract readonly repositoryArn: RepositoryArn;
24+
25+
/**
26+
* Add a policy statement to the repository's resource policy
27+
*/
28+
public abstract addToResourcePolicy(statement: cdk.PolicyStatement): void;
29+
30+
/**
31+
* Export this repository from the stack
32+
*/
33+
public export(): RepositoryRefProps {
34+
return {
35+
repositoryArn: new RepositoryArn(new cdk.Output(this, 'RepositoryArn', { value: this.repositoryArn }).makeImportValue()),
36+
};
37+
}
38+
39+
/**
40+
* The URI of the repository, for use in Docker/image references
41+
*/
42+
public get repositoryUri(): RepositoryUri {
43+
// Calculate this from the ARN
44+
const parts = cdk.Arn.parseToken(this.repositoryArn);
45+
return new RepositoryUri(`${parts.account}.dkr.ecr.${parts.region}.amazonaws.com/${parts.resourceName}`);
46+
}
47+
}
48+
49+
/**
50+
* URI of a repository
51+
*/
52+
export class RepositoryUri extends cdk.CloudFormationToken {
53+
}
54+
55+
export interface RepositoryRefProps {
56+
repositoryArn: RepositoryArn;
57+
}
58+
59+
/**
60+
* An already existing repository
61+
*/
62+
class ImportedRepository extends RepositoryRef {
63+
public readonly repositoryName: RepositoryName;
64+
public readonly repositoryArn: RepositoryArn;
65+
66+
constructor(parent: cdk.Construct, id: string, props: RepositoryRefProps) {
67+
super(parent, id);
68+
this.repositoryArn = props.repositoryArn;
69+
this.repositoryName = new RepositoryName(cdk.Arn.parseToken(props.repositoryArn).resourceName);
70+
}
71+
72+
public addToResourcePolicy(_statement: cdk.PolicyStatement) {
73+
// FIXME: Add annotation about policy we dropped on the floor
74+
}
75+
}
+190
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import cdk = require('@aws-cdk/cdk');
2+
import { cloudformation, RepositoryArn, RepositoryName } from './ecr.generated';
3+
import { CountType, LifecycleRule, TagStatus } from './lifecycle';
4+
import { RepositoryRef } from "./repository-ref";
5+
6+
export interface RepositoryProps {
7+
/**
8+
* Name for this repository
9+
*
10+
* @default Automatically generated name.
11+
*/
12+
repositoryName?: string;
13+
14+
/**
15+
* Life cycle rules to apply to this registry
16+
*
17+
* @default No life cycle rules
18+
*/
19+
lifecycleRules?: LifecycleRule[];
20+
21+
/**
22+
* The AWS account ID associated with the registry that contains the repository.
23+
*
24+
* @see https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_PutLifecyclePolicy.html
25+
* @default The default registry is assumed.
26+
*/
27+
lifecycleRegistryId?: string;
28+
29+
/**
30+
* Retain the repository on stack deletion
31+
*
32+
* If you don't set this to true, the registry must be empty, otherwise
33+
* your stack deletion will fail.
34+
*
35+
* @default false
36+
*/
37+
retain?: boolean;
38+
}
39+
40+
/**
41+
* Define an ECR repository
42+
*/
43+
export class Repository extends RepositoryRef {
44+
public readonly repositoryName: RepositoryName;
45+
public readonly repositoryArn: RepositoryArn;
46+
private readonly lifecycleRules = new Array<LifecycleRule>();
47+
private readonly registryId?: string;
48+
private policyDocument?: cdk.PolicyDocument;
49+
50+
constructor(parent: cdk.Construct, id: string, props: RepositoryProps = {}) {
51+
super(parent, id);
52+
53+
const resource = new cloudformation.RepositoryResource(this, 'Resource', {
54+
repositoryName: props.repositoryName,
55+
// It says "Text", but they actually mean "Object".
56+
repositoryPolicyText: this.policyDocument,
57+
lifecyclePolicy: new cdk.Token(() => this.renderLifecyclePolicy()),
58+
});
59+
60+
if (props.retain) {
61+
resource.options.deletionPolicy = cdk.DeletionPolicy.Retain;
62+
}
63+
64+
this.registryId = props.lifecycleRegistryId;
65+
if (props.lifecycleRules) {
66+
props.lifecycleRules.forEach(this.addLifecycleRule.bind(this));
67+
}
68+
69+
this.repositoryName = resource.ref;
70+
this.repositoryArn = resource.repositoryArn;
71+
}
72+
73+
public addToResourcePolicy(statement: cdk.PolicyStatement) {
74+
if (this.policyDocument === undefined) {
75+
this.policyDocument = new cdk.PolicyDocument();
76+
}
77+
this.policyDocument.addStatement(statement);
78+
}
79+
80+
/**
81+
* Add a life cycle rule to the repository
82+
*
83+
* Life cycle rules automatically expire images from the repository that match
84+
* certain conditions.
85+
*/
86+
public addLifecycleRule(rule: LifecycleRule) {
87+
// Validate rule here so users get errors at the expected location
88+
if (rule.tagStatus === undefined) {
89+
rule.tagStatus = rule.tagPrefixList === undefined ? TagStatus.Any : TagStatus.Tagged;
90+
}
91+
92+
if (rule.tagStatus === TagStatus.Tagged && (rule.tagPrefixList === undefined || rule.tagPrefixList.length === 0)) {
93+
throw new Error('TagStatus.Tagged requires the specification of a tagPrefixList');
94+
}
95+
if (rule.tagStatus !== TagStatus.Tagged && rule.tagPrefixList !== undefined) {
96+
throw new Error('tagPrefixList can only be specified when tagStatus is set to Tagged');
97+
}
98+
if ((rule.maxImageAgeDays !== undefined) === (rule.maxImageCount !== undefined)) {
99+
throw new Error(`Life cycle rule must contain exactly one of 'maxImageAgeDays' and 'maxImageCount', got: ${JSON.stringify(rule)}`);
100+
}
101+
102+
if (rule.tagStatus === TagStatus.Any && this.lifecycleRules.filter(r => r.tagStatus === TagStatus.Any).length > 0) {
103+
throw new Error('Life cycle can only have one TagStatus.Any rule');
104+
}
105+
106+
this.lifecycleRules.push({ ...rule });
107+
}
108+
109+
/**
110+
* Render the life cycle policy object
111+
*/
112+
private renderLifecyclePolicy(): cloudformation.RepositoryResource.LifecyclePolicyProperty | undefined {
113+
let lifecyclePolicyText: any;
114+
115+
if (this.lifecycleRules.length === 0 && !this.registryId) { return undefined; }
116+
117+
if (this.lifecycleRules.length > 0) {
118+
lifecyclePolicyText = JSON.stringify(cdk.resolve({
119+
rules: this.orderedLifecycleRules().map(renderLifecycleRule),
120+
}));
121+
}
122+
123+
return {
124+
lifecyclePolicyText,
125+
registryId: this.registryId,
126+
};
127+
}
128+
129+
/**
130+
* Return life cycle rules with automatic ordering applied.
131+
*
132+
* Also applies validation of the 'any' rule.
133+
*/
134+
private orderedLifecycleRules(): LifecycleRule[] {
135+
if (this.lifecycleRules.length === 0) { return []; }
136+
137+
const prioritizedRules = this.lifecycleRules.filter(r => r.rulePriority !== undefined && r.tagStatus !== TagStatus.Any);
138+
const autoPrioritizedRules = this.lifecycleRules.filter(r => r.rulePriority === undefined && r.tagStatus !== TagStatus.Any);
139+
const anyRules = this.lifecycleRules.filter(r => r.tagStatus === TagStatus.Any);
140+
if (anyRules.length > 0 && anyRules[0].rulePriority !== undefined && autoPrioritizedRules.length > 0) {
141+
// Supporting this is too complex for very little value. We just prohibit it.
142+
throw new Error("Cannot combine prioritized TagStatus.Any rule with unprioritized rules. Remove rulePriority from the 'Any' rule.");
143+
}
144+
145+
const prios = prioritizedRules.map(r => r.rulePriority!);
146+
let autoPrio = (prios.length > 0 ? Math.max(...prios) : 0) + 1;
147+
148+
const ret = new Array<LifecycleRule>();
149+
for (const rule of prioritizedRules.concat(autoPrioritizedRules).concat(anyRules)) {
150+
ret.push({
151+
...rule,
152+
rulePriority: rule.rulePriority !== undefined ? rule.rulePriority : autoPrio++
153+
});
154+
}
155+
156+
// Do validation on the final array--might still be wrong because the user supplied all prios, but incorrectly.
157+
validateAnyRuleLast(ret);
158+
return ret;
159+
}
160+
}
161+
162+
function validateAnyRuleLast(rules: LifecycleRule[]) {
163+
const anyRules = rules.filter(r => r.tagStatus === TagStatus.Any);
164+
if (anyRules.length === 1) {
165+
const maxPrio = Math.max(...rules.map(r => r.rulePriority!));
166+
if (anyRules[0].rulePriority !== maxPrio) {
167+
throw new Error(`TagStatus.Any rule must have highest priority, has ${anyRules[0].rulePriority} which is smaller than ${maxPrio}`);
168+
}
169+
}
170+
}
171+
172+
/**
173+
* Render the lifecycle rule to JSON
174+
*/
175+
function renderLifecycleRule(rule: LifecycleRule) {
176+
return {
177+
rulePriority: rule.rulePriority,
178+
description: rule.description,
179+
selection: {
180+
tagStatus: rule.tagStatus || TagStatus.Any,
181+
tagPrefixList: rule.tagPrefixList,
182+
countType: rule.maxImageAgeDays !== undefined ? CountType.SinceImagePushed : CountType.ImageCountMoreThan,
183+
countNumber: rule.maxImageAgeDays !== undefined ? rule.maxImageAgeDays : rule.maxImageCount,
184+
countUnit: rule.maxImageAgeDays !== undefined ? 'days' : undefined,
185+
},
186+
action: {
187+
type: 'expire'
188+
}
189+
};
190+
}

0 commit comments

Comments
 (0)