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

feat(nextjs-component, aws-lambda): support adding tags to lambdas #1300

Merged
merged 7 commits into from
Jun 26, 2021
Merged
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,9 @@ The exhaustive list of AWS actions required for a deployment:
"lambda:PublishVersion",
"lambda:UpdateFunctionCode",
"lambda:UpdateFunctionConfiguration",
"lambda:ListTags", // for tagging lambdas
"lambda:TagResource", // for tagging lambdas
"lambda:UntagResource", // for tagging lambdas
"route53:ChangeResourceRecordSets", // only for custom domains
"route53:ListHostedZonesByName",
"route53:ListResourceRecordSets", // only for custom domains
Expand Down Expand Up @@ -503,6 +506,7 @@ The fourth cache behaviour handles next API requests `api/*`.
| roleArn | `string\|object` | null | The arn of role that will be assigned to both lambdas. |
| 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 |
| 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 |
| 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. |
| timeout | `number\|object` | `10` | Same as above |
| 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 |
| 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 |
Expand Down
9 changes: 9 additions & 0 deletions packages/e2e-tests/next-app/serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,12 @@ next-app:
api/*:
forward:
headers: [Authorization]
tags:
defaultLambda:
tag1: val1
apiLambda:
tag2: val2
imageLambda:
tag3: val3
regenerationLambda:
tag4: val4
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ const mockGetCallerIdentityMappingPromise = promisifyMock(
mockGetCallerIdentityMapping
);

const mockListTags = jest.fn();
const mockListTagsPromise = promisifyMock(mockListTags);
const mockTagResource = jest.fn();
const mockTagResourcePromise = promisifyMock(mockTagResource);
const mockUntagResource = jest.fn();
const mockUntagResourcePromise = promisifyMock(mockUntagResource);

module.exports = {
mockCreateQueuePromise,
mockGetQueueAttributesPromise,
Expand Down Expand Up @@ -80,6 +87,12 @@ module.exports = {
mockUpdateFunctionCodePromise,
mockUpdateFunctionConfiguration,
mockUpdateFunctionConfigurationPromise,
mockListTags,
mockListTagsPromise,
mockTagResource,
mockTagResourcePromise,
mockUntagResource,
mockUntagResourcePromise,

Lambda: jest.fn(() => ({
listEventSourceMappings: mockListEventSourceMappings,
Expand All @@ -88,6 +101,9 @@ module.exports = {
publishVersion: mockPublishVersion,
getFunctionConfiguration: mockGetFunctionConfiguration,
updateFunctionCode: mockUpdateFunctionCode,
updateFunctionConfiguration: mockUpdateFunctionConfiguration
updateFunctionConfiguration: mockUpdateFunctionConfiguration,
listTags: mockListTags,
tagResource: mockTagResource,
untagResource: mockUntagResource
}))
};
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
const { createComponent, createTmpDir } = require("../test-utils");

const {
mockCreateFunction,
mockCreateFunctionPromise,
mockPublishVersion,
mockPublishVersionPromise,
mockGetFunctionConfigurationPromise,
mockUpdateFunctionCodePromise,
mockUpdateFunctionConfigurationPromise
mockUpdateFunctionConfigurationPromise,
mockListTags,
mockListTagsPromise,
mockTagResource,
mockUntagResource
} = require("aws-sdk");

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

await component.default({
code: tmpFolder
code: tmpFolder,
tags: { new: "tag" }
});

const versionResult = await component.publishVersion();
Expand All @@ -60,6 +66,12 @@ describe("publishVersion", () => {
expect(versionResult).toEqual({
version: "v2"
});

expect(mockCreateFunction).toBeCalledWith(
expect.objectContaining({
Tags: { new: "tag" }
})
);
});

it("publishes new version of lambda that was updated", async () => {
Expand All @@ -79,7 +91,11 @@ describe("publishVersion", () => {
CodeSha256: "LQT0VA="
});
mockUpdateFunctionConfigurationPromise.mockResolvedValueOnce({
CodeSha256: "XYZ0VA="
CodeSha256: "XYZ0VA=",
FunctionArn: "arn:aws:lambda:us-east-1:123456789012:function:my-func"
});
mockListTagsPromise.mockResolvedValueOnce({
Tags: { foo: "bar" }
});

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

await component.default({
code: tmpFolder
code: tmpFolder,
tags: { new: "tag" }
});

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

expect(mockListTags).toBeCalledWith({
Resource: "arn:aws:lambda:us-east-1:123456789012:function:my-func"
});

expect(mockUntagResource).toBeCalledWith({
Resource: "arn:aws:lambda:us-east-1:123456789012:function:my-func",
TagKeys: ["foo"]
});

expect(mockTagResource).toBeCalledWith({
Resource: "arn:aws:lambda:us-east-1:123456789012:function:my-func",
Tags: { new: "tag" }
});

expect(versionResult).toEqual({
version: "v2"
});
Expand Down
1 change: 1 addition & 0 deletions packages/serverless-components/aws-lambda/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"archiver": "^5.3.0",
"fs-extra": "^9.1.0",
"globby": "^11.0.1",
"lodash": "^4.17.21",
"ramda": "^0.27.0"
},
"peerDependencies": {
Expand Down
7 changes: 5 additions & 2 deletions packages/serverless-components/aws-lambda/serverless.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ const outputsList = [
"env",
"role",
"arn",
"region"
"region",
"tags"
];

const defaults = {
Expand All @@ -39,7 +40,8 @@ const defaults = {
handler: "handler.hello",
runtime: "nodejs10.x",
env: {},
region: "us-east-1"
region: "us-east-1",
tags: undefined
};

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

config.name = inputs.name || this.state.name || this.context.resourceId();
config.tags = inputs.tags || this.state.tags;

this.context.debug(
`Starting deployment of lambda ${config.name} to the ${config.region} region.`
Expand Down
37 changes: 34 additions & 3 deletions packages/serverless-components/aws-lambda/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const globby = require("globby");
const { contains, isNil, last, split, equals, not, pick } = require("ramda");
const { readFile, createReadStream, createWriteStream } = require("fs-extra");
const { utils } = require("@serverless/core");
const _ = require("lodash");

const VALID_FORMATS = ["zip", "tar"];
const isValidFormat = (format) => contains(format, VALID_FORMATS);
Expand Down Expand Up @@ -88,7 +89,8 @@ const createLambda = async ({
zipPath,
bucket,
role,
layer
layer,
tags
}) => {
const params = {
FunctionName: name,
Expand All @@ -102,7 +104,8 @@ const createLambda = async ({
Timeout: timeout,
Environment: {
Variables: env
}
},
Tags: tags
};

if (layer && layer.arn) {
Expand Down Expand Up @@ -131,7 +134,8 @@ const updateLambdaConfig = async ({
env,
description,
role,
layer
layer,
tags
}) => {
const functionConfigParams = {
FunctionName: name,
Expand All @@ -154,6 +158,33 @@ const updateLambdaConfig = async ({
.updateFunctionConfiguration(functionConfigParams)
.promise();

// Get and update Lambda tags only if tags are specified (for backwards compatibility and avoiding unneeded updates)
if (tags) {
const listTagsResponse = await lambda
.listTags({ Resource: res.FunctionArn })
.promise();
const currentTags = listTagsResponse.Tags;

// If tags are not the same then update them
if (!_.isEqual(currentTags, tags)) {
if (currentTags && Object.keys(currentTags).length > 0)
await lambda
.untagResource({
Resource: res.FunctionArn,
TagKeys: Object.keys(currentTags)
})
.promise();

if (Object.keys(tags).length > 0)
await lambda
.tagResource({
Resource: res.FunctionArn,
Tags: tags
})
.promise();
}
}

return { arn: res.FunctionArn, hash: res.CodeSha256 };
};

Expand Down
2 changes: 1 addition & 1 deletion packages/serverless-components/aws-lambda/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1097,7 +1097,7 @@ lodash.union@^4.6.0:
resolved "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88"
integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=

lodash@^4.17.14:
lodash@^4.17.14, lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import { mockSQS } from "@sls-next/aws-sqs";

import NextjsComponent, { DeploymentResult } from "../src/component";
import obtainDomains from "../src/lib/obtainDomains";
import { DEFAULT_LAMBDA_CODE_DIR, API_LAMBDA_CODE_DIR } from "../src/constants";
import {
DEFAULT_LAMBDA_CODE_DIR,
API_LAMBDA_CODE_DIR,
IMAGE_LAMBDA_CODE_DIR,
REGENERATION_LAMBDA_CODE_DIR
} from "../src/constants";
import { cleanupFixtureDirectory } from "../src/lib/test-utils";

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

describe.each([
{
defaultLambda: { tag1: "val1" },
apiLambda: { tag2: "val2" },
imageLambda: { tag3: "val3" }
}
])("Lambda tags input", (tags) => {
const fixturePath = path.join(__dirname, "./fixtures/generic-fixture");
let tmpCwd: string;

beforeEach(async () => {
tmpCwd = process.cwd();
process.chdir(fixturePath);

mockServerlessComponentDependencies({ expectedDomain: undefined });

const component = createNextComponent();

componentOutputs = await component.default({
tags: tags
});
});

afterEach(() => {
process.chdir(tmpCwd);
return cleanupFixtureDirectory(fixturePath);
});

it(`sets lambda tags to ${JSON.stringify(tags)}`, () => {
// default Lambda
expect(mockLambda).toBeCalledWith(
expect.objectContaining({
code: path.join(fixturePath, DEFAULT_LAMBDA_CODE_DIR),
tags: tags.defaultLambda
})
);

// api Lambda
expect(mockLambda).toBeCalledWith(
expect.objectContaining({
code: path.join(fixturePath, API_LAMBDA_CODE_DIR),
tags: tags.apiLambda
})
);

// image lambda
expect(mockLambda).toBeCalledWith(
expect.objectContaining({
code: path.join(fixturePath, IMAGE_LAMBDA_CODE_DIR),
tags: tags.imageLambda
})
);
});
});

describe.each`
inputTimeout | expectedTimeout
${undefined} | ${{ defaultTimeout: 10, apiTimeout: 10 }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ describe.each`
runtime: "nodejs12.x",
name: "bucket-xyz",
region: "us-east-1",
tags: undefined,
role: {
service: ["lambda.amazonaws.com"],
policy: {
Expand Down Expand Up @@ -175,6 +176,7 @@ describe.each`
memory: 512,
timeout: 10,
runtime: "nodejs12.x",
tags: undefined,
role: {
service: ["lambda.amazonaws.com", "edgelambda.amazonaws.com"],
policy: {
Expand Down Expand Up @@ -221,6 +223,7 @@ describe.each`
memory: 512,
timeout: 10,
runtime: "nodejs12.x",
tags: undefined,
role: {
service: ["lambda.amazonaws.com", "edgelambda.amazonaws.com"],
policy: {
Expand Down Expand Up @@ -267,6 +270,7 @@ describe.each`
memory: 512,
timeout: 10,
runtime: "nodejs12.x",
tags: undefined,
role: {
service: ["lambda.amazonaws.com", "edgelambda.amazonaws.com"],
policy: {
Expand Down
Loading