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

Commit 74721c2

Browse files
authored
feat(nextjs-component, aws-lambda): support adding tags to lambdas (#1300)
1 parent e824146 commit 74721c2

File tree

12 files changed

+204
-19
lines changed

12 files changed

+204
-19
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,9 @@ The exhaustive list of AWS actions required for a deployment:
377377
"lambda:PublishVersion",
378378
"lambda:UpdateFunctionCode",
379379
"lambda:UpdateFunctionConfiguration",
380+
"lambda:ListTags", // for tagging lambdas
381+
"lambda:TagResource", // for tagging lambdas
382+
"lambda:UntagResource", // for tagging lambdas
380383
"route53:ChangeResourceRecordSets", // only for custom domains
381384
"route53:ListHostedZonesByName",
382385
"route53:ListResourceRecordSets", // only for custom domains
@@ -503,6 +506,7 @@ The fourth cache behaviour handles next API requests `api/*`.
503506
| roleArn | `string\|object` | null | The arn of role that will be assigned to both lambdas. |
504507
| runtime | `string\|object` | `nodejs12.x` | When assigned a value, both the default and api lambdas will be assigned the runtime defined in the value. When assigned to an object, values for the default and api lambdas can be separately defined |
505508
| memory | `number\|object` | `512` | When assigned a number, both the default and api lambdas will be assigned memory of that value. When assigned to an object, values for the default and api lambdas can be separately defined |
509+
| tags | `object` | `undefined` | Tags to assign to a Lambda. If undefined, the component will not update any tags. If set to an empty object, it will remove all tags. |
506510
| timeout | `number\|object` | `10` | Same as above |
507511
| handler | `string` | `index.handler` | When assigned a value, overrides the default function handler to allow for configuration. Copies `handler.js` in route into the Lambda folders. Your handler MUST still call the `default-handler` afterwards or your function won't work with Next.JS |
508512
| name | `string\|object` | / | When assigned a string, both the default and api lambdas will assigned name of that value. When assigned to an object, values for the default and api lambdas can be separately defined |

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,12 @@ next-app:
1010
api/*:
1111
forward:
1212
headers: [Authorization]
13+
tags:
14+
defaultLambda:
15+
tag1: val1
16+
apiLambda:
17+
tag2: val2
18+
imageLambda:
19+
tag3: val3
20+
regenerationLambda:
21+
tag4: val4

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ const mockGetCallerIdentityMappingPromise = promisifyMock(
5050
mockGetCallerIdentityMapping
5151
);
5252

53+
const mockListTags = jest.fn();
54+
const mockListTagsPromise = promisifyMock(mockListTags);
55+
const mockTagResource = jest.fn();
56+
const mockTagResourcePromise = promisifyMock(mockTagResource);
57+
const mockUntagResource = jest.fn();
58+
const mockUntagResourcePromise = promisifyMock(mockUntagResource);
59+
5360
module.exports = {
5461
mockCreateQueuePromise,
5562
mockGetQueueAttributesPromise,
@@ -80,6 +87,12 @@ module.exports = {
8087
mockUpdateFunctionCodePromise,
8188
mockUpdateFunctionConfiguration,
8289
mockUpdateFunctionConfigurationPromise,
90+
mockListTags,
91+
mockListTagsPromise,
92+
mockTagResource,
93+
mockTagResourcePromise,
94+
mockUntagResource,
95+
mockUntagResourcePromise,
8396

8497
Lambda: jest.fn(() => ({
8598
listEventSourceMappings: mockListEventSourceMappings,
@@ -88,6 +101,9 @@ module.exports = {
88101
publishVersion: mockPublishVersion,
89102
getFunctionConfiguration: mockGetFunctionConfiguration,
90103
updateFunctionCode: mockUpdateFunctionCode,
91-
updateFunctionConfiguration: mockUpdateFunctionConfiguration
104+
updateFunctionConfiguration: mockUpdateFunctionConfiguration,
105+
listTags: mockListTags,
106+
tagResource: mockTagResource,
107+
untagResource: mockUntagResource
92108
}))
93109
};

packages/serverless-components/aws-lambda/__tests__/publishVersion.test.js

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
const { createComponent, createTmpDir } = require("../test-utils");
22

33
const {
4+
mockCreateFunction,
45
mockCreateFunctionPromise,
56
mockPublishVersion,
67
mockPublishVersionPromise,
78
mockGetFunctionConfigurationPromise,
89
mockUpdateFunctionCodePromise,
9-
mockUpdateFunctionConfigurationPromise
10+
mockUpdateFunctionConfigurationPromise,
11+
mockListTags,
12+
mockListTagsPromise,
13+
mockTagResource,
14+
mockUntagResource
1015
} = require("aws-sdk");
1116

1217
jest.mock("aws-sdk", () => require("../__mocks__/aws-sdk.mock"));
@@ -47,7 +52,8 @@ describe("publishVersion", () => {
4752
const tmpFolder = await createTmpDir();
4853

4954
await component.default({
50-
code: tmpFolder
55+
code: tmpFolder,
56+
tags: { new: "tag" }
5157
});
5258

5359
const versionResult = await component.publishVersion();
@@ -60,6 +66,12 @@ describe("publishVersion", () => {
6066
expect(versionResult).toEqual({
6167
version: "v2"
6268
});
69+
70+
expect(mockCreateFunction).toBeCalledWith(
71+
expect.objectContaining({
72+
Tags: { new: "tag" }
73+
})
74+
);
6375
});
6476

6577
it("publishes new version of lambda that was updated", async () => {
@@ -79,7 +91,11 @@ describe("publishVersion", () => {
7991
CodeSha256: "LQT0VA="
8092
});
8193
mockUpdateFunctionConfigurationPromise.mockResolvedValueOnce({
82-
CodeSha256: "XYZ0VA="
94+
CodeSha256: "XYZ0VA=",
95+
FunctionArn: "arn:aws:lambda:us-east-1:123456789012:function:my-func"
96+
});
97+
mockListTagsPromise.mockResolvedValueOnce({
98+
Tags: { foo: "bar" }
8399
});
84100

85101
const tmpFolder = await createTmpDir();
@@ -89,7 +105,8 @@ describe("publishVersion", () => {
89105
});
90106

91107
await component.default({
92-
code: tmpFolder
108+
code: tmpFolder,
109+
tags: { new: "tag" }
93110
});
94111

95112
const versionResult = await component.publishVersion();
@@ -99,6 +116,20 @@ describe("publishVersion", () => {
99116
CodeSha256: "XYZ0VA=" // compare against the hash received from the function update, *not* create
100117
});
101118

119+
expect(mockListTags).toBeCalledWith({
120+
Resource: "arn:aws:lambda:us-east-1:123456789012:function:my-func"
121+
});
122+
123+
expect(mockUntagResource).toBeCalledWith({
124+
Resource: "arn:aws:lambda:us-east-1:123456789012:function:my-func",
125+
TagKeys: ["foo"]
126+
});
127+
128+
expect(mockTagResource).toBeCalledWith({
129+
Resource: "arn:aws:lambda:us-east-1:123456789012:function:my-func",
130+
Tags: { new: "tag" }
131+
});
132+
102133
expect(versionResult).toEqual({
103134
version: "v2"
104135
});

packages/serverless-components/aws-lambda/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"archiver": "^5.3.0",
2525
"fs-extra": "^9.1.0",
2626
"globby": "^11.0.1",
27+
"lodash": "^4.17.21",
2728
"ramda": "^0.27.0"
2829
},
2930
"peerDependencies": {

packages/serverless-components/aws-lambda/serverless.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ const outputsList = [
2626
"env",
2727
"role",
2828
"arn",
29-
"region"
29+
"region",
30+
"tags"
3031
];
3132

3233
const defaults = {
@@ -39,7 +40,8 @@ const defaults = {
3940
handler: "handler.hello",
4041
runtime: "nodejs10.x",
4142
env: {},
42-
region: "us-east-1"
43+
region: "us-east-1",
44+
tags: undefined
4345
};
4446

4547
class AwsLambda extends Component {
@@ -49,6 +51,7 @@ class AwsLambda extends Component {
4951
const config = mergeDeepRight(defaults, inputs);
5052

5153
config.name = inputs.name || this.state.name || this.context.resourceId();
54+
config.tags = inputs.tags || this.state.tags;
5255

5356
this.context.debug(
5457
`Starting deployment of lambda ${config.name} to the ${config.region} region.`

packages/serverless-components/aws-lambda/utils.js

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const globby = require("globby");
55
const { contains, isNil, last, split, equals, not, pick } = require("ramda");
66
const { readFile, createReadStream, createWriteStream } = require("fs-extra");
77
const { utils } = require("@serverless/core");
8+
const _ = require("lodash");
89

910
const VALID_FORMATS = ["zip", "tar"];
1011
const isValidFormat = (format) => contains(format, VALID_FORMATS);
@@ -88,7 +89,8 @@ const createLambda = async ({
8889
zipPath,
8990
bucket,
9091
role,
91-
layer
92+
layer,
93+
tags
9294
}) => {
9395
const params = {
9496
FunctionName: name,
@@ -102,7 +104,8 @@ const createLambda = async ({
102104
Timeout: timeout,
103105
Environment: {
104106
Variables: env
105-
}
107+
},
108+
Tags: tags
106109
};
107110

108111
if (layer && layer.arn) {
@@ -131,7 +134,8 @@ const updateLambdaConfig = async ({
131134
env,
132135
description,
133136
role,
134-
layer
137+
layer,
138+
tags
135139
}) => {
136140
const functionConfigParams = {
137141
FunctionName: name,
@@ -154,6 +158,33 @@ const updateLambdaConfig = async ({
154158
.updateFunctionConfiguration(functionConfigParams)
155159
.promise();
156160

161+
// Get and update Lambda tags only if tags are specified (for backwards compatibility and avoiding unneeded updates)
162+
if (tags) {
163+
const listTagsResponse = await lambda
164+
.listTags({ Resource: res.FunctionArn })
165+
.promise();
166+
const currentTags = listTagsResponse.Tags;
167+
168+
// If tags are not the same then update them
169+
if (!_.isEqual(currentTags, tags)) {
170+
if (currentTags && Object.keys(currentTags).length > 0)
171+
await lambda
172+
.untagResource({
173+
Resource: res.FunctionArn,
174+
TagKeys: Object.keys(currentTags)
175+
})
176+
.promise();
177+
178+
if (Object.keys(tags).length > 0)
179+
await lambda
180+
.tagResource({
181+
Resource: res.FunctionArn,
182+
Tags: tags
183+
})
184+
.promise();
185+
}
186+
}
187+
157188
return { arn: res.FunctionArn, hash: res.CodeSha256 };
158189
};
159190

packages/serverless-components/aws-lambda/yarn.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1097,7 +1097,7 @@ lodash.union@^4.6.0:
10971097
resolved "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88"
10981098
integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=
10991099

1100-
lodash@^4.17.14:
1100+
lodash@^4.17.14, lodash@^4.17.21:
11011101
version "4.17.21"
11021102
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
11031103
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==

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

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import { mockSQS } from "@sls-next/aws-sqs";
1010

1111
import NextjsComponent, { DeploymentResult } from "../src/component";
1212
import obtainDomains from "../src/lib/obtainDomains";
13-
import { DEFAULT_LAMBDA_CODE_DIR, API_LAMBDA_CODE_DIR } from "../src/constants";
13+
import {
14+
DEFAULT_LAMBDA_CODE_DIR,
15+
API_LAMBDA_CODE_DIR,
16+
IMAGE_LAMBDA_CODE_DIR,
17+
REGENERATION_LAMBDA_CODE_DIR
18+
} from "../src/constants";
1419
import { cleanupFixtureDirectory } from "../src/lib/test-utils";
1520

1621
// unfortunately can't use __mocks__ because aws-sdk is being mocked in other
@@ -414,6 +419,61 @@ describe("Custom inputs", () => {
414419
});
415420
});
416421

422+
describe.each([
423+
{
424+
defaultLambda: { tag1: "val1" },
425+
apiLambda: { tag2: "val2" },
426+
imageLambda: { tag3: "val3" }
427+
}
428+
])("Lambda tags input", (tags) => {
429+
const fixturePath = path.join(__dirname, "./fixtures/generic-fixture");
430+
let tmpCwd: string;
431+
432+
beforeEach(async () => {
433+
tmpCwd = process.cwd();
434+
process.chdir(fixturePath);
435+
436+
mockServerlessComponentDependencies({ expectedDomain: undefined });
437+
438+
const component = createNextComponent();
439+
440+
componentOutputs = await component.default({
441+
tags: tags
442+
});
443+
});
444+
445+
afterEach(() => {
446+
process.chdir(tmpCwd);
447+
return cleanupFixtureDirectory(fixturePath);
448+
});
449+
450+
it(`sets lambda tags to ${JSON.stringify(tags)}`, () => {
451+
// default Lambda
452+
expect(mockLambda).toBeCalledWith(
453+
expect.objectContaining({
454+
code: path.join(fixturePath, DEFAULT_LAMBDA_CODE_DIR),
455+
tags: tags.defaultLambda
456+
})
457+
);
458+
459+
// api Lambda
460+
expect(mockLambda).toBeCalledWith(
461+
expect.objectContaining({
462+
code: path.join(fixturePath, API_LAMBDA_CODE_DIR),
463+
tags: tags.apiLambda
464+
})
465+
);
466+
467+
// image lambda
468+
expect(mockLambda).toBeCalledWith(
469+
expect.objectContaining({
470+
code: path.join(fixturePath, IMAGE_LAMBDA_CODE_DIR),
471+
tags: tags.imageLambda
472+
})
473+
);
474+
});
475+
});
476+
417477
describe.each`
418478
inputTimeout | expectedTimeout
419479
${undefined} | ${{ defaultTimeout: 10, apiTimeout: 10 }}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ describe.each`
124124
runtime: "nodejs12.x",
125125
name: "bucket-xyz",
126126
region: "us-east-1",
127+
tags: undefined,
127128
role: {
128129
service: ["lambda.amazonaws.com"],
129130
policy: {
@@ -175,6 +176,7 @@ describe.each`
175176
memory: 512,
176177
timeout: 10,
177178
runtime: "nodejs12.x",
179+
tags: undefined,
178180
role: {
179181
service: ["lambda.amazonaws.com", "edgelambda.amazonaws.com"],
180182
policy: {
@@ -221,6 +223,7 @@ describe.each`
221223
memory: 512,
222224
timeout: 10,
223225
runtime: "nodejs12.x",
226+
tags: undefined,
224227
role: {
225228
service: ["lambda.amazonaws.com", "edgelambda.amazonaws.com"],
226229
policy: {
@@ -267,6 +270,7 @@ describe.each`
267270
memory: 512,
268271
timeout: 10,
269272
runtime: "nodejs12.x",
273+
tags: undefined,
270274
role: {
271275
service: ["lambda.amazonaws.com", "edgelambda.amazonaws.com"],
272276
policy: {

0 commit comments

Comments
 (0)