Skip to content
This repository was archived by the owner on Jan 28, 2025. It is now read-only.

Commit 0110f0a

Browse files
authored
feat(nextjs-component, aws-lambda): allow removing old lambda versions (#1884)
1 parent 7de14a4 commit 0110f0a

File tree

15 files changed

+297
-34
lines changed

15 files changed

+297
-34
lines changed

jest-sequencer.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const Sequencer = require("@jest/test-sequencer").default;
2+
3+
class CustomSequencer extends Sequencer {
4+
constructor() {
5+
super();
6+
}
7+
8+
sort(tests) {
9+
// Test structure information
10+
// https://github.com/facebook/jest/blob/6b8b1404a1d9254e7d5d90a8934087a9c9899dab/packages/jest-runner/src/types.ts#L17-L21
11+
const copyTests = Array.from(tests);
12+
return copyTests.sort((testA, testB) => {
13+
// FIXME: figure out why this test started failing if run after another test
14+
if (testA.path.includes("serverless-trace.test")) {
15+
return -1;
16+
}
17+
if (testB.path.includes("serverless-trace.test")) {
18+
return 1;
19+
}
20+
return testA.path > testB.path ? 1 : -1;
21+
});
22+
}
23+
}
24+
25+
module.exports = CustomSequencer;

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@
131131
],
132132
"modulePathIgnorePatterns": [
133133
"/sharp_node_modules/"
134-
]
134+
],
135+
"testSequencer": "<rootDir>/jest-sequencer.js"
135136
},
136137
"dependencies": {
137138
"opencollective-postinstall": "^2.0.3",

packages/e2e-tests/next-app/serverless.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
next-app:
22
component: "../../serverless-components/nextjs-component"
33
inputs:
4+
removeOldLambdaVersions: true
45
sqs:
56
tags:
67
foo: bar

packages/libs/lambda-at-edge/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"@types/react": "17.0.31",
5252
"@types/react-dom": "^17.0.10",
5353
"@types/sharp": "^0.29.2",
54+
"@types/uuid": "^8.3.1",
5455
"fetch-mock-jest": "^1.5.1",
5556
"klaw": "^3.0.0",
5657
"rimraf": "^3.0.2",
@@ -62,7 +63,8 @@
6263
"sharp": "^0.28.3",
6364
"ts-loader": "^9.2.6",
6465
"ts-node": "^10.3.0",
65-
"typescript": "^4.4.4"
66+
"typescript": "^4.4.4",
67+
"uuid": "^8.3.2"
6668
},
6769
"dependencies": {
6870
"@aws-sdk/client-s3": "^3.37.0",

packages/libs/lambda-at-edge/tests/serverless-trace/serverless-trace.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ import Builder, {
55
DEFAULT_LAMBDA_CODE_DIR,
66
API_LAMBDA_CODE_DIR
77
} from "../../src/build";
8+
import { jest } from "@jest/globals";
9+
import { v4 as uuidv4 } from "uuid";
810

911
describe("Serverless Trace", () => {
1012
const fixturePath = path.join(__dirname, "./fixture");
1113
let outputDir: string;
1214
let fseRemoveSpy: jest.SpyInstance;
1315

1416
beforeEach(async () => {
15-
outputDir = path.join(os.tmpdir(), `${Date.now()}`);
17+
outputDir = path.join(os.tmpdir(), `${uuidv4()}`);
1618

1719
fseRemoveSpy = jest.spyOn(fse, "remove").mockImplementation(() => {
1820
return;

packages/libs/lambda-at-edge/yarn.lock

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,31 +1362,12 @@
13621362
picomatch "^2.2.2"
13631363

13641364
"@sls-next/aws-common@link:../aws-common":
1365-
version "3.5.0-alpha.7"
1366-
dependencies:
1367-
"@aws-sdk/client-s3" "^3.37.0"
1368-
"@aws-sdk/client-sqs" "^3.37.0"
1369-
"@sls-next/core" "link:../core"
1365+
version "0.0.0"
1366+
uid ""
13701367

13711368
"@sls-next/core@link:../core":
1372-
version "3.5.0-alpha.7"
1373-
dependencies:
1374-
"@hapi/accept" "^5.0.1"
1375-
cookie "^0.4.1"
1376-
execa "^5.1.1"
1377-
fast-glob "^3.2.7"
1378-
fresh "^0.5.2"
1379-
fs-extra "^9.1.0"
1380-
is-animated "^2.0.1"
1381-
jsonwebtoken "^8.5.1"
1382-
next "^11.1.2"
1383-
node-fetch "2.6.5"
1384-
normalize-path "^3.0.0"
1385-
path-to-regexp "^6.1.0"
1386-
react "^17.0.2"
1387-
react-dom "^17.0.2"
1388-
send "^0.17.1"
1389-
sharp "^0.29.1"
1369+
version "0.0.0"
1370+
uid ""
13901371

13911372
"@tsconfig/node10@^1.0.7":
13921373
version "1.0.8"
@@ -1508,6 +1489,11 @@
15081489
dependencies:
15091490
"@types/node" "*"
15101491

1492+
"@types/uuid@^8.3.1":
1493+
version "8.3.1"
1494+
resolved "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f"
1495+
integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==
1496+
15111497
"@vercel/nft@^0.17.0":
15121498
version "0.17.0"
15131499
resolved "https://registry.npmjs.org/@vercel/nft/-/nft-0.17.0.tgz#28851fefe42fae7a116dc5e23a0a9da29929a18b"

packages/serverless-components/aws-lambda/__mocks__/aws-sdk.mock.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { jest } from "@jest/globals";
2+
13
const promisifyMock = (mockFn) => {
24
const promise = jest.fn();
35
mockFn.mockImplementation(() => ({
@@ -60,6 +62,12 @@ export const mockTagResource = jest.fn();
6062
export const mockTagResourcePromise = promisifyMock(mockTagResource);
6163
export const mockUntagResource = jest.fn();
6264
export const mockUntagResourcePromise = promisifyMock(mockUntagResource);
65+
export const mockListVersionsByFunction = jest.fn();
66+
export const mockListVersionsByFunctionPromise = promisifyMock(
67+
mockListVersionsByFunction
68+
);
69+
export const mockDeleteFunction = jest.fn();
70+
export const mockDeleteFunctionPromise = promisifyMock(mockDeleteFunction);
6371

6472
export default {
6573
SQS: jest.fn(() => ({
@@ -77,6 +85,8 @@ export default {
7785
updateFunctionConfiguration: mockUpdateFunctionConfiguration,
7886
listTags: mockListTags,
7987
tagResource: mockTagResource,
80-
untagResource: mockUntagResource
88+
untagResource: mockUntagResource,
89+
listVersionsByFunction: mockListVersionsByFunction,
90+
deleteFunction: mockDeleteFunction
8191
}))
8292
};
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import {
2+
mockGetFunctionConfigurationPromise,
3+
mockListVersionsByFunctionPromise,
4+
mockGetFunctionConfiguration,
5+
mockListVersionsByFunction,
6+
mockDeleteFunction,
7+
mockDeleteFunctionPromise
8+
} from "../__mocks__/aws-sdk.mock";
9+
import { removeLambdaVersions } from "../src/removeLambdaVersions";
10+
import { jest } from "@jest/globals";
11+
12+
jest.mock("aws-sdk", () => require("../__mocks__/aws-sdk.mock"));
13+
14+
describe("publishVersion", () => {
15+
it("removes all old lambda versions", async () => {
16+
mockGetFunctionConfigurationPromise.mockResolvedValue({
17+
FunctionName: "test-function",
18+
Version: "4"
19+
});
20+
21+
mockListVersionsByFunctionPromise.mockResolvedValue({
22+
Versions: [
23+
{
24+
FunctionName: "test-function",
25+
Version: "1"
26+
},
27+
{
28+
FunctionName: "test-function",
29+
Version: "2"
30+
},
31+
{
32+
FunctionName: "test-function",
33+
Version: "3"
34+
},
35+
{
36+
FunctionName: "test-function",
37+
Version: "4"
38+
}
39+
]
40+
});
41+
42+
mockDeleteFunctionPromise.mockResolvedValueOnce(undefined);
43+
mockDeleteFunctionPromise.mockResolvedValueOnce(undefined);
44+
// Simulate last function couldn't be deleted, but it will not fail the process.
45+
mockDeleteFunctionPromise.mockRejectedValueOnce({
46+
message: "Mocked error"
47+
});
48+
49+
await removeLambdaVersions(
50+
{
51+
debug: () => {
52+
// intentionally empty
53+
}
54+
},
55+
"test-function",
56+
"us-east-1"
57+
);
58+
59+
expect(mockDeleteFunction).toBeCalledWith({
60+
FunctionName: "test-function",
61+
Qualifier: "1"
62+
});
63+
64+
expect(mockDeleteFunction).toBeCalledWith({
65+
FunctionName: "test-function",
66+
Qualifier: "2"
67+
});
68+
69+
expect(mockDeleteFunction).toBeCalledWith({
70+
FunctionName: "test-function",
71+
Qualifier: "3"
72+
});
73+
74+
expect(mockDeleteFunction).toBeCalledTimes(3);
75+
76+
expect(mockGetFunctionConfiguration).toBeCalledWith({
77+
FunctionName: "test-function"
78+
});
79+
expect(mockGetFunctionConfiguration).toBeCalledTimes(1);
80+
81+
expect(mockListVersionsByFunction).toBeCalledWith({
82+
FunctionName: "test-function",
83+
MaxItems: 50
84+
});
85+
expect(mockListVersionsByFunction).toBeCalledTimes(1);
86+
});
87+
});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Cleanup Lambda code adapted from https://github.com/davidmenger/cleanup-lambda-versions/blob/master/src/cleanupVersions.js
2+
import AWS from "aws-sdk";
3+
import {
4+
FunctionConfiguration,
5+
ListVersionsByFunctionResponse
6+
} from "aws-sdk/clients/lambda";
7+
8+
async function listLambdaVersions(
9+
lambda: AWS.Lambda,
10+
fnName: string
11+
): Promise<ListVersionsByFunctionResponse> {
12+
return await lambda
13+
.listVersionsByFunction({
14+
FunctionName: fnName,
15+
MaxItems: 50
16+
})
17+
.promise();
18+
}
19+
20+
async function removeLambdaVersion(
21+
lambda: AWS.Lambda,
22+
fnName: string,
23+
version: string
24+
): Promise<unknown> {
25+
return await lambda
26+
.deleteFunction({ FunctionName: fnName, Qualifier: version })
27+
.promise();
28+
}
29+
30+
async function getLambdaFunction(
31+
lambda: AWS.Lambda,
32+
fnName: string
33+
): Promise<FunctionConfiguration> {
34+
return await lambda
35+
.getFunctionConfiguration({ FunctionName: fnName })
36+
.promise();
37+
}
38+
39+
/**
40+
* Clean up old lambda versions, up to 50 at a time.
41+
* Currently it just removes the version that's not the current version,
42+
* but if needed we could add support for preserving the latest X versions.
43+
* @param context
44+
* @param fnName
45+
* @param region
46+
*/
47+
export async function removeLambdaVersions(
48+
context: any,
49+
fnName: string,
50+
region: string
51+
) {
52+
const lambda: AWS.Lambda = new AWS.Lambda({ region });
53+
const fnConfig = await getLambdaFunction(lambda, fnName);
54+
55+
const versions = await listLambdaVersions(lambda, fnConfig.FunctionName);
56+
57+
for (const version of versions.Versions ?? []) {
58+
if (version.Version && version.Version !== fnConfig.Version) {
59+
try {
60+
context.debug(
61+
`Removing function: ${fnConfig.FunctionName} - ${version.Version}`
62+
);
63+
await removeLambdaVersion(
64+
lambda,
65+
fnConfig.FunctionName,
66+
version.Version
67+
);
68+
} catch (e) {
69+
context.debug(
70+
`Remove failed (${fnConfig.FunctionName} - ${version.Version}): ${e.message}`
71+
);
72+
}
73+
}
74+
}
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { jest } from "@jest/globals";
2+
3+
const mockRemoveLambdaVersions = jest.fn();
4+
5+
module.exports = {
6+
mockRemoveLambdaVersions,
7+
removeLambdaVersions: mockRemoveLambdaVersions
8+
};

packages/serverless-components/nextjs-component/__tests__/custom-inputs.test.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ import obtainDomains from "../src/lib/obtainDomains";
1313
import {
1414
DEFAULT_LAMBDA_CODE_DIR,
1515
API_LAMBDA_CODE_DIR,
16-
IMAGE_LAMBDA_CODE_DIR,
17-
REGENERATION_LAMBDA_CODE_DIR
16+
IMAGE_LAMBDA_CODE_DIR
1817
} from "../src/constants";
1918
import { cleanupFixtureDirectory } from "../src/lib/test-utils";
19+
import { mockRemoveLambdaVersions } from "@sls-next/aws-lambda/dist/removeLambdaVersions";
2020

2121
// unfortunately can't use __mocks__ because aws-sdk is being mocked in other
2222
// packages in the monorepo
@@ -474,6 +474,28 @@ describe("Custom inputs", () => {
474474
});
475475
});
476476

477+
describe("Old lambda function version removal", () => {
478+
let tmpCwd: string;
479+
const fixturePath = path.join(__dirname, "./fixtures/generic-fixture");
480+
481+
beforeEach(async () => {
482+
tmpCwd = process.cwd();
483+
process.chdir(fixturePath);
484+
485+
mockServerlessComponentDependencies({ expectedDomain: undefined });
486+
487+
const component = createNextComponent();
488+
489+
componentOutputs = await component.default({
490+
removeOldLambdaVersions: true
491+
});
492+
});
493+
494+
it("removes old versions of lambda functions", () => {
495+
expect(mockRemoveLambdaVersions).toBeCalledTimes(3); // 4 if there is regeneration lambda
496+
});
497+
});
498+
477499
describe.each`
478500
inputTimeout | expectedTimeout
479501
${undefined} | ${{ defaultTimeout: 10, apiTimeout: 10 }}

packages/serverless-components/nextjs-component/__tests__/deploy.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fse from "fs-extra";
33
import { mockS3 } from "@sls-next/aws-s3";
44
import { mockCloudFront } from "@sls-next/aws-cloudfront";
55
import { mockLambda, mockLambdaPublish } from "@sls-next/aws-lambda";
6+
import { mockRemoveLambdaVersions } from "@sls-next/aws-lambda/dist/removeLambdaVersions";
67
import {
78
mockCreateInvalidation,
89
mockCheckCloudFrontDistributionReady
@@ -447,6 +448,10 @@ describe.each`
447448
distributionId: "cloudfrontdistrib"
448449
});
449450
});
451+
452+
it("does not remove old versions of lambda functions by default", () => {
453+
expect(mockRemoveLambdaVersions).toBeCalledTimes(0);
454+
});
450455
});
451456

452457
it("uploads static assets to S3 correctly", () => {

0 commit comments

Comments
 (0)