Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions lib/plugins/aws/provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -1143,6 +1143,7 @@ class AwsProvider {
type: 'object',
properties: {
scanOnPush: { type: 'boolean' },
maxImageCount: { type: 'integer', minimum: 1 },
images: {
type: 'object',
patternProperties: {
Expand Down Expand Up @@ -2337,7 +2338,7 @@ Object.defineProperties(
{ promise: true }
),
getOrCreateEcrRepository: d(
async function (scanOnPush) {
async function (scanOnPush, maxImageCount) {
const registryId = await this.getAccountId();
const repositoryName = this.naming.getEcrRepositoryName();
let repositoryUri;
Expand All @@ -2357,6 +2358,27 @@ Object.defineProperties(
});
repositoryUri = result.repository.repositoryUri;
}

// Set ECR Lifecycle policy. See https://docs.aws.amazon.com/AmazonECR/latest/userguide/LifecyclePolicies.html
if (maxImageCount > 0) {
await this.request('ECR', 'putLifecyclePolicy', {
repositoryName,
lifecyclePolicyText: JSON.stringify({
rules: [
{
rulePriority: 1,
action: { type: 'expire' },
selection: {
tagStatus: 'any',
countType: 'imageCountMoreThan',
countNumber: maxImageCount,
},
},
],
}),
});
}

return {
repositoryUri,
repositoryName,
Expand All @@ -2375,6 +2397,7 @@ Object.defineProperties(
platform,
provenance,
scanOnPush,
maxImageCount,
}) {
const imageProgress = progress.get(`containerImage:${imageName}`);
await this.ensureDockerIsAvailable();
Expand All @@ -2395,7 +2418,10 @@ Object.defineProperties(
);
}

const { repositoryUri, repositoryName } = await this.getOrCreateEcrRepository(scanOnPush);
const { repositoryUri, repositoryName } = await this.getOrCreateEcrRepository(
scanOnPush,
maxImageCount
);

const localTag = `${repositoryName}:${imageName}`;
const remoteTag = `${repositoryUri}:${imageName}`;
Expand Down Expand Up @@ -2561,6 +2587,7 @@ Object.defineProperties(
const defaultScanOnPush = false;
const defaultPlatform = '';
const defaultProvenance = '';
const defaultMaxImageCount = 0;

if (imageUri) {
return await this.resolveImageUriAndShaFromUri(imageUri);
Expand All @@ -2577,6 +2604,12 @@ Object.defineProperties(
defaultScanOnPush
);

const maxImageCountProvider = _.get(
this.serverless.service.provider,
'ecr.maxImageCount',
defaultMaxImageCount
);

if (!imageDefinedInProvider) {
throw new ServerlessError(
`Referenced "${imageName}" not defined in "provider.ecr.images"`,
Expand Down Expand Up @@ -2638,6 +2671,7 @@ Object.defineProperties(
platform: imageDefinedInProvider.platform || defaultPlatform,
provenance: imageDefinedInProvider.provenance || defaultProvenance,
scanOnPush: imageScanDefinedInProvider,
maxImageCount: maxImageCountProvider,
});
}
return await this.resolveImageUriAndShaFromUri(imageDefinedInProvider.uri);
Expand All @@ -2655,6 +2689,7 @@ Object.defineProperties(
platform: imageDefinedInProvider.platform || defaultPlatform,
provenance: imageDefinedInProvider.provenance || defaultProvenance,
scanOnPush: imageScanDefinedInProvider,
maxImageCount: maxImageCountProvider,
});
},
{ promise: true }
Expand Down
44 changes: 44 additions & 0 deletions test/unit/lib/plugins/aws/provider.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1255,6 +1255,7 @@ aws_secret_access_key = CUSTOMSECRET
const describeRepositoriesStub = sinon.stub();
const createRepositoryStub = sinon.stub();
const createRepositoryStubScanOnPush = sinon.stub();
const putLifecyclePolicyStub = sinon.stub();
const baseAwsRequestStubMap = {
STS: {
getCallerIdentity: {
Expand Down Expand Up @@ -1420,6 +1421,49 @@ aws_secret_access_key = CUSTOMSECRET
expect(versionCfConfig.CodeSha256).to.equal(imageSha);
expect(describeRepositoriesStub).to.be.calledOnce;
expect(createRepositoryStub).to.be.calledOnce;
expect(putLifecyclePolicyStub).to.not.have.been.called;
});

it('should set ECR lifecycle policy correctly', async () => {
const awsRequestStubMap = {
...baseAwsRequestStubMap,
ECR: {
...baseAwsRequestStubMap.ECR,
describeRepositories: describeRepositoriesStub.throws({
providerError: { code: 'RepositoryNotFoundException' },
}),
createRepository: createRepositoryStub.resolves({ repository: { repositoryUri } }),
putLifecyclePolicy: putLifecyclePolicyStub.resolves(),
},
};

await runServerless({
fixture: 'ecr',
command: 'package',
awsRequestStubMap,
modulesCacheStub,
configExt: {
provider: {
ecr: {
maxImageCount: 10,
},
},
},
});

expect(JSON.parse(putLifecyclePolicyStub.args[0][0].lifecyclePolicyText)).to.deep.equal({
rules: [
{
rulePriority: 1,
action: { type: 'expire' },
selection: {
tagStatus: 'any',
countType: 'imageCountMoreThan',
countNumber: 10,
},
},
],
});
});

it('should login and retry when docker push fails with no basic auth credentials error', async () => {
Expand Down