serverless-tag-resources 1.2.51 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -20
- package/index.js +60 -575
- package/package.json +22 -7
- package/src/aws-clients.js +59 -0
- package/src/post-deploy/apigatewayv2.js +42 -0
- package/src/post-deploy/ec2-related.js +78 -0
- package/src/post-deploy/firehose.js +20 -0
- package/src/post-deploy/pinpoint.js +25 -0
- package/src/post-deploy/rds.js +16 -0
- package/src/post-deploy/ssm.js +16 -0
- package/src/post-deploy-tagger.js +95 -0
- package/src/resource-classifier.js +381 -0
- package/src/tags.js +78 -0
- package/src/template-tagger.js +90 -0
- package/src/validation.js +76 -0
- package/awsCloudFormation.js +0 -20
- package/awsSSM.js +0 -22
- package/bitbucket-pipelines.yml +0 -17
package/package.json
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "serverless-tag-resources",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Datamart
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Datamart: Tag all AWS resources with dual legacy + datamart:* tag support",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js",
|
|
8
|
+
"src/"
|
|
9
|
+
],
|
|
6
10
|
"scripts": {
|
|
7
|
-
"test": "
|
|
11
|
+
"test": "jest",
|
|
12
|
+
"test:verbose": "jest --verbose",
|
|
13
|
+
"test:coverage": "jest --coverage"
|
|
8
14
|
},
|
|
9
15
|
"repository": {
|
|
10
16
|
"type": "git",
|
|
@@ -13,16 +19,25 @@
|
|
|
13
19
|
"keywords": [
|
|
14
20
|
"serverless",
|
|
15
21
|
"tags",
|
|
16
|
-
"stage",
|
|
17
22
|
"plugin",
|
|
18
|
-
"tag",
|
|
19
23
|
"aws",
|
|
20
24
|
"resource",
|
|
21
|
-
"tagging"
|
|
25
|
+
"tagging",
|
|
26
|
+
"datamart",
|
|
27
|
+
"finops"
|
|
22
28
|
],
|
|
23
29
|
"author": "Yunier Saborit <ysaborit@gmail.com>",
|
|
24
30
|
"license": "ISC",
|
|
25
31
|
"dependencies": {
|
|
26
|
-
"aws-sdk": "^
|
|
32
|
+
"@aws-sdk/client-apigatewayv2": "^3.700.0",
|
|
33
|
+
"@aws-sdk/client-cloudformation": "^3.700.0",
|
|
34
|
+
"@aws-sdk/client-ec2": "^3.700.0",
|
|
35
|
+
"@aws-sdk/client-firehose": "^3.700.0",
|
|
36
|
+
"@aws-sdk/client-pinpoint": "^3.700.0",
|
|
37
|
+
"@aws-sdk/client-rds": "^3.700.0",
|
|
38
|
+
"@aws-sdk/client-ssm": "^3.700.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"jest": "^30.3.0"
|
|
27
42
|
}
|
|
28
43
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AWS SDK v3 client factory.
|
|
5
|
+
* Lazily creates and caches clients per service+region.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const clientCache = new Map();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get or create an AWS SDK v3 client.
|
|
12
|
+
*
|
|
13
|
+
* @param {Function} ClientClass - SDK v3 client constructor (e.g. CloudFormationClient)
|
|
14
|
+
* @param {object} config - { region, credentials }
|
|
15
|
+
* @returns {object} cached client instance
|
|
16
|
+
*/
|
|
17
|
+
function getClient(ClientClass, config) {
|
|
18
|
+
const key = `${ClientClass.name}:${config.region}`;
|
|
19
|
+
if (!clientCache.has(key)) {
|
|
20
|
+
clientCache.set(key, new ClientClass(config));
|
|
21
|
+
}
|
|
22
|
+
return clientCache.get(key);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Clear client cache (useful for testing).
|
|
27
|
+
*/
|
|
28
|
+
function clearClients() {
|
|
29
|
+
clientCache.clear();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Extract SDK v3 compatible config from Serverless provider.
|
|
34
|
+
*
|
|
35
|
+
* @param {object} awsProvider - serverless.getProvider('aws')
|
|
36
|
+
* @returns {object} { region, credentials }
|
|
37
|
+
*/
|
|
38
|
+
function configFromProvider(awsProvider) {
|
|
39
|
+
const region = awsProvider.getRegion();
|
|
40
|
+
const creds = awsProvider.getCredentials();
|
|
41
|
+
|
|
42
|
+
// Serverless v3 provides credentials in different formats
|
|
43
|
+
// depending on the provider. Normalize for SDK v3.
|
|
44
|
+
const config = { region };
|
|
45
|
+
|
|
46
|
+
if (creds.credentials) {
|
|
47
|
+
config.credentials = creds.credentials;
|
|
48
|
+
} else if (creds.accessKeyId) {
|
|
49
|
+
config.credentials = {
|
|
50
|
+
accessKeyId: creds.accessKeyId,
|
|
51
|
+
secretAccessKey: creds.secretAccessKey,
|
|
52
|
+
sessionToken: creds.sessionToken,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return config;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { getClient, clearClients, configFromProvider };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
ApiGatewayV2Client,
|
|
5
|
+
TagResourceCommand,
|
|
6
|
+
} = require("@aws-sdk/client-apigatewayv2");
|
|
7
|
+
const { getClient } = require("../aws-clients");
|
|
8
|
+
|
|
9
|
+
async function tagApiGatewayV2(config, resource, tagsMap, partition, region, allStackResources) {
|
|
10
|
+
const client = getClient(ApiGatewayV2Client, config);
|
|
11
|
+
|
|
12
|
+
switch (resource.ResourceType) {
|
|
13
|
+
case "AWS::ApiGatewayV2::Api": {
|
|
14
|
+
const arn = `arn:${partition}:apigateway:${region}::/apis/${resource.PhysicalResourceId}`;
|
|
15
|
+
await client.send(new TagResourceCommand({ ResourceArn: arn, Tags: tagsMap }));
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
case "AWS::ApiGatewayV2::Stage": {
|
|
19
|
+
// Find the parent API in the same stack
|
|
20
|
+
const apiResource = allStackResources.find(
|
|
21
|
+
(r) => r.ResourceType === "AWS::ApiGatewayV2::Api"
|
|
22
|
+
);
|
|
23
|
+
if (apiResource) {
|
|
24
|
+
const arn = `arn:${partition}:apigateway:${region}::/apis/${apiResource.PhysicalResourceId}/stages/${resource.PhysicalResourceId}`;
|
|
25
|
+
await client.send(new TagResourceCommand({ ResourceArn: arn, Tags: tagsMap }));
|
|
26
|
+
}
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case "AWS::ApiGatewayV2::DomainName": {
|
|
30
|
+
const arn = `arn:${partition}:apigateway:${region}::/domainnames/${resource.PhysicalResourceId}`;
|
|
31
|
+
await client.send(new TagResourceCommand({ ResourceArn: arn, Tags: tagsMap }));
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
case "AWS::ApiGatewayV2::VpcLink": {
|
|
35
|
+
const arn = `arn:${partition}:apigateway:${region}::/vpclinks/${resource.PhysicalResourceId}`;
|
|
36
|
+
await client.send(new TagResourceCommand({ ResourceArn: arn, Tags: tagsMap }));
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { tagApiGatewayV2 };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
EC2Client,
|
|
5
|
+
DescribeInstancesCommand,
|
|
6
|
+
DescribeAddressesCommand,
|
|
7
|
+
CreateTagsCommand,
|
|
8
|
+
} = require("@aws-sdk/client-ec2");
|
|
9
|
+
const { getClient, } = require("../aws-clients");
|
|
10
|
+
const { excludeAwsTags } = require("../tags");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Tag EC2 instance related resources: EBS volumes, ENIs, EIPs, Security Groups.
|
|
14
|
+
*/
|
|
15
|
+
async function tagEC2RelatedResources(config, resource, tags) {
|
|
16
|
+
const client = getClient(EC2Client, config);
|
|
17
|
+
|
|
18
|
+
const describeResult = await client.send(
|
|
19
|
+
new DescribeInstancesCommand({
|
|
20
|
+
InstanceIds: [resource.PhysicalResourceId],
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const resourceIds = [];
|
|
25
|
+
|
|
26
|
+
for (const reservation of describeResult.Reservations || []) {
|
|
27
|
+
const ownerId = reservation.OwnerId;
|
|
28
|
+
|
|
29
|
+
for (const instance of reservation.Instances || []) {
|
|
30
|
+
// EBS Volumes
|
|
31
|
+
for (const bdm of instance.BlockDeviceMappings || []) {
|
|
32
|
+
if (bdm.Ebs && bdm.Ebs.VolumeId) {
|
|
33
|
+
resourceIds.push(bdm.Ebs.VolumeId);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Network Interfaces
|
|
38
|
+
for (const eni of instance.NetworkInterfaces || []) {
|
|
39
|
+
resourceIds.push(eni.NetworkInterfaceId);
|
|
40
|
+
|
|
41
|
+
// Elastic IPs (only if owned by same account)
|
|
42
|
+
if (eni.Association && eni.Association.IpOwnerId === ownerId) {
|
|
43
|
+
const eipResult = await client.send(
|
|
44
|
+
new DescribeAddressesCommand({
|
|
45
|
+
PublicIps: [eni.Association.PublicIp],
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
for (const addr of eipResult.Addresses || []) {
|
|
49
|
+
resourceIds.push(addr.AllocationId);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Security Groups
|
|
54
|
+
for (const sg of eni.Groups || []) {
|
|
55
|
+
resourceIds.push(sg.GroupId);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (resourceIds.length > 0) {
|
|
62
|
+
// Use existing instance tags (excluding aws: reserved) as base
|
|
63
|
+
const instanceTags =
|
|
64
|
+
describeResult.Reservations?.[0]?.Instances?.[0]?.Tags || [];
|
|
65
|
+
const filteredTags = excludeAwsTags(instanceTags);
|
|
66
|
+
|
|
67
|
+
await client.send(
|
|
68
|
+
new CreateTagsCommand({
|
|
69
|
+
Resources: resourceIds,
|
|
70
|
+
Tags: filteredTags.length > 0 ? filteredTags : tags,
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return resourceIds;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { tagEC2RelatedResources };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
FirehoseClient,
|
|
5
|
+
TagDeliveryStreamCommand,
|
|
6
|
+
} = require("@aws-sdk/client-firehose");
|
|
7
|
+
const { getClient } = require("../aws-clients");
|
|
8
|
+
|
|
9
|
+
async function tagFirehoseStream(config, resource, tags) {
|
|
10
|
+
const client = getClient(FirehoseClient, config);
|
|
11
|
+
|
|
12
|
+
await client.send(
|
|
13
|
+
new TagDeliveryStreamCommand({
|
|
14
|
+
DeliveryStreamName: resource.PhysicalResourceId,
|
|
15
|
+
Tags: tags,
|
|
16
|
+
})
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { tagFirehoseStream };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
PinpointClient,
|
|
5
|
+
GetAppCommand,
|
|
6
|
+
TagResourceCommand,
|
|
7
|
+
} = require("@aws-sdk/client-pinpoint");
|
|
8
|
+
const { getClient } = require("../aws-clients");
|
|
9
|
+
|
|
10
|
+
async function tagPinpointApp(config, resource, tagsMap) {
|
|
11
|
+
const client = getClient(PinpointClient, config);
|
|
12
|
+
|
|
13
|
+
const app = await client.send(
|
|
14
|
+
new GetAppCommand({ ApplicationId: resource.PhysicalResourceId })
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
await client.send(
|
|
18
|
+
new TagResourceCommand({
|
|
19
|
+
ResourceArn: app.ApplicationResponse.Arn,
|
|
20
|
+
TagsModel: { tags: tagsMap },
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = { tagPinpointApp };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { RDSClient, AddTagsToResourceCommand } = require("@aws-sdk/client-rds");
|
|
4
|
+
const { getClient } = require("../aws-clients");
|
|
5
|
+
|
|
6
|
+
async function tagRDSCluster(config, resource, tags, partition, region) {
|
|
7
|
+
const client = getClient(RDSClient, config);
|
|
8
|
+
const accountId = resource.StackId.split(":")[4];
|
|
9
|
+
const arn = `arn:${partition}:rds:${region}:${accountId}:cluster:${resource.PhysicalResourceId}`;
|
|
10
|
+
|
|
11
|
+
await client.send(
|
|
12
|
+
new AddTagsToResourceCommand({ ResourceName: arn, Tags: tags })
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { tagRDSCluster };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { SSMClient, AddTagsToResourceCommand } = require("@aws-sdk/client-ssm");
|
|
4
|
+
const { getClient } = require("../aws-clients");
|
|
5
|
+
|
|
6
|
+
async function tagSSMParameter(config, resource, tags) {
|
|
7
|
+
const client = getClient(SSMClient, config);
|
|
8
|
+
const command = new AddTagsToResourceCommand({
|
|
9
|
+
ResourceId: resource.PhysicalResourceId,
|
|
10
|
+
ResourceType: "Parameter",
|
|
11
|
+
Tags: tags,
|
|
12
|
+
});
|
|
13
|
+
await client.send(command);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { tagSSMParameter };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
CloudFormationClient,
|
|
5
|
+
DescribeStackResourcesCommand,
|
|
6
|
+
} = require("@aws-sdk/client-cloudformation");
|
|
7
|
+
const { getClient } = require("./aws-clients");
|
|
8
|
+
const { buildListTags, buildDictTags } = require("./tags");
|
|
9
|
+
const { needsPostDeployTagging, DICT_BASED_TYPES, API_ONLY_TYPES, RELATED_TYPES } = require("./resource-classifier");
|
|
10
|
+
|
|
11
|
+
const { tagSSMParameter } = require("./post-deploy/ssm");
|
|
12
|
+
const { tagPinpointApp } = require("./post-deploy/pinpoint");
|
|
13
|
+
const { tagApiGatewayV2 } = require("./post-deploy/apigatewayv2");
|
|
14
|
+
const { tagRDSCluster } = require("./post-deploy/rds");
|
|
15
|
+
const { tagFirehoseStream } = require("./post-deploy/firehose");
|
|
16
|
+
const { tagEC2RelatedResources } = require("./post-deploy/ec2-related");
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Post-Deploy Tagger — Hook: after:deploy:deploy
|
|
20
|
+
*
|
|
21
|
+
* Tags resources that cannot be fully tagged in the CF template:
|
|
22
|
+
* dict-based, API-only, and related resources.
|
|
23
|
+
*/
|
|
24
|
+
async function updateTagsPostDeploy(config, stackName, stackTags, stage, partition, region, log) {
|
|
25
|
+
const cfnClient = getClient(CloudFormationClient, config);
|
|
26
|
+
|
|
27
|
+
const result = await cfnClient.send(
|
|
28
|
+
new DescribeStackResourcesCommand({ StackName: stackName })
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const allResources = result.StackResources || [];
|
|
32
|
+
|
|
33
|
+
for (const resource of allResources) {
|
|
34
|
+
const type = resource.ResourceType;
|
|
35
|
+
if (!needsPostDeployTagging(type)) continue;
|
|
36
|
+
|
|
37
|
+
const logicalId = resource.LogicalResourceId;
|
|
38
|
+
const listTags = buildListTags(stackTags, stage, logicalId);
|
|
39
|
+
const dictTags = buildDictTags(stackTags, stage, logicalId);
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Dict-based types: need API call because CF template tagging
|
|
43
|
+
// may not fully propagate for some of these types
|
|
44
|
+
if (DICT_BASED_TYPES.has(type)) {
|
|
45
|
+
switch (type) {
|
|
46
|
+
case "AWS::SSM::Parameter":
|
|
47
|
+
await tagSSMParameter(config, resource, listTags);
|
|
48
|
+
log(`TAGGING: post-deploy SSM Parameter ${logicalId}`);
|
|
49
|
+
break;
|
|
50
|
+
case "AWS::Pinpoint::App":
|
|
51
|
+
await tagPinpointApp(config, resource, dictTags);
|
|
52
|
+
log(`TAGGING: post-deploy Pinpoint App ${logicalId}`);
|
|
53
|
+
break;
|
|
54
|
+
case "AWS::ApiGatewayV2::Api":
|
|
55
|
+
case "AWS::ApiGatewayV2::Stage":
|
|
56
|
+
case "AWS::ApiGatewayV2::DomainName":
|
|
57
|
+
case "AWS::ApiGatewayV2::VpcLink":
|
|
58
|
+
await tagApiGatewayV2(config, resource, dictTags, partition, region, allResources);
|
|
59
|
+
log(`TAGGING: post-deploy ${type} ${logicalId}`);
|
|
60
|
+
break;
|
|
61
|
+
// Glue and Batch are tagged in template (dict-based), no post-deploy needed
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// API-only types: not tagged in template at all
|
|
66
|
+
if (API_ONLY_TYPES.has(type)) {
|
|
67
|
+
switch (type) {
|
|
68
|
+
case "AWS::RDS::DBCluster":
|
|
69
|
+
await tagRDSCluster(config, resource, listTags, partition, region);
|
|
70
|
+
log(`TAGGING: post-deploy RDS Cluster ${logicalId}`);
|
|
71
|
+
break;
|
|
72
|
+
case "AWS::KinesisFirehose::DeliveryStream":
|
|
73
|
+
await tagFirehoseStream(config, resource, listTags);
|
|
74
|
+
log(`TAGGING: post-deploy Firehose ${logicalId}`);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Related types: tag associated resources (volumes, ENIs, etc.)
|
|
80
|
+
if (RELATED_TYPES.has(type)) {
|
|
81
|
+
switch (type) {
|
|
82
|
+
case "AWS::EC2::Instance": {
|
|
83
|
+
const relatedIds = await tagEC2RelatedResources(config, resource, listTags);
|
|
84
|
+
log(`TAGGING: post-deploy EC2 related resources for ${logicalId}: ${relatedIds.length} resources`);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
log(`TAGGING: ERROR post-deploy ${type} ${logicalId}: ${err.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { updateTagsPostDeploy };
|