serverless-tag-resources 1.2.51 → 3.1.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 +23 -8
- 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/untag.js +97 -0
- package/src/post-deploy-tagger.js +102 -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,28 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "serverless-tag-resources",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Datamart
|
|
3
|
+
"version": "3.1.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",
|
|
11
|
-
"url": "git+ssh://git@bitbucket.org
|
|
17
|
+
"url": "git+ssh://git@bitbucket.org/datamartcl/dm-serverless-tag-resources-plugin.git"
|
|
12
18
|
},
|
|
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,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
ResourceGroupsTaggingAPIClient,
|
|
5
|
+
UntagResourcesCommand,
|
|
6
|
+
} = require("@aws-sdk/client-resource-groups-tagging-api");
|
|
7
|
+
const { getClient } = require("../aws-clients");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Remove specific tags from all resources in a CloudFormation stack.
|
|
11
|
+
* Uses Resource Groups Tagging API for broad coverage across resource types.
|
|
12
|
+
*
|
|
13
|
+
* This is used to clean up tags injected by Serverless Framework (e.g., STAGE)
|
|
14
|
+
* that conflict with the datamart:* tagging policy.
|
|
15
|
+
*/
|
|
16
|
+
async function removeTagsFromStackResources(config, stackResources, tagKeysToRemove, partition, region, log) {
|
|
17
|
+
const taggingClient = getClient(ResourceGroupsTaggingAPIClient, config);
|
|
18
|
+
|
|
19
|
+
// Collect ARNs of all stack resources
|
|
20
|
+
const arns = [];
|
|
21
|
+
for (const resource of stackResources) {
|
|
22
|
+
const arn = buildArn(resource, partition, region);
|
|
23
|
+
if (arn) arns.push(arn);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (arns.length === 0) {
|
|
27
|
+
log("TAGGING: No resources with ARNs to untag");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// UntagResources accepts max 20 ARNs per call
|
|
32
|
+
const batchSize = 20;
|
|
33
|
+
let untagged = 0;
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < arns.length; i += batchSize) {
|
|
36
|
+
const batch = arns.slice(i, i + batchSize);
|
|
37
|
+
try {
|
|
38
|
+
await taggingClient.send(
|
|
39
|
+
new UntagResourcesCommand({
|
|
40
|
+
ResourceARNList: batch,
|
|
41
|
+
TagKeys: tagKeysToRemove,
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
untagged += batch.length;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
// Some resource types don't support tagging API — skip gracefully
|
|
47
|
+
log(`TAGGING: WARN untag batch ${i}-${i + batch.length}: ${err.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
log(`TAGGING: Removed [${tagKeysToRemove.join(", ")}] from ${untagged} resources`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build ARN from CloudFormation StackResource.
|
|
56
|
+
* Uses PhysicalResourceId which is usually the ARN or resource ID.
|
|
57
|
+
*/
|
|
58
|
+
function buildArn(resource, partition, region) {
|
|
59
|
+
const physicalId = resource.PhysicalResourceId;
|
|
60
|
+
if (!physicalId) return null;
|
|
61
|
+
|
|
62
|
+
// If it's already an ARN, use it directly
|
|
63
|
+
if (physicalId.startsWith("arn:")) return physicalId;
|
|
64
|
+
|
|
65
|
+
// For some resource types, we can construct the ARN
|
|
66
|
+
const accountId = extractAccountId(resource);
|
|
67
|
+
const type = resource.ResourceType;
|
|
68
|
+
|
|
69
|
+
const arnBuilders = {
|
|
70
|
+
"AWS::Lambda::Function": () =>
|
|
71
|
+
`arn:${partition}:lambda:${region}:${accountId}:function:${physicalId}`,
|
|
72
|
+
"AWS::SNS::Topic": () =>
|
|
73
|
+
`arn:${partition}:sns:${region}:${accountId}:${physicalId}`,
|
|
74
|
+
"AWS::SQS::Queue": () => null, // Queue URL, not name — skip
|
|
75
|
+
"AWS::Events::EventBus": () =>
|
|
76
|
+
`arn:${partition}:events:${region}:${accountId}:event-bus/${physicalId}`,
|
|
77
|
+
"AWS::Events::Rule": () => null, // Complex ARN — skip
|
|
78
|
+
"AWS::ApiGateway::RestApi": () =>
|
|
79
|
+
`arn:${partition}:apigateway:${region}::/restapis/${physicalId}`,
|
|
80
|
+
"AWS::Logs::LogGroup": () =>
|
|
81
|
+
`arn:${partition}:logs:${region}:${accountId}:log-group:${physicalId}`,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const builder = arnBuilders[type];
|
|
85
|
+
if (builder) return builder();
|
|
86
|
+
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function extractAccountId(resource) {
|
|
91
|
+
// StackId contains the account ID
|
|
92
|
+
const stackId = resource.StackId || "";
|
|
93
|
+
const parts = stackId.split(":");
|
|
94
|
+
return parts.length >= 5 ? parts[4] : "";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { removeTagsFromStackResources };
|
|
@@ -0,0 +1,102 @@
|
|
|
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, TAGS_TO_REMOVE } = require("./tags");
|
|
9
|
+
const { needsPostDeployTagging, DICT_BASED_TYPES, API_ONLY_TYPES, RELATED_TYPES } = require("./resource-classifier");
|
|
10
|
+
const { removeTagsFromStackResources } = require("./post-deploy/untag");
|
|
11
|
+
|
|
12
|
+
const { tagSSMParameter } = require("./post-deploy/ssm");
|
|
13
|
+
const { tagPinpointApp } = require("./post-deploy/pinpoint");
|
|
14
|
+
const { tagApiGatewayV2 } = require("./post-deploy/apigatewayv2");
|
|
15
|
+
const { tagRDSCluster } = require("./post-deploy/rds");
|
|
16
|
+
const { tagFirehoseStream } = require("./post-deploy/firehose");
|
|
17
|
+
const { tagEC2RelatedResources } = require("./post-deploy/ec2-related");
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Post-Deploy Tagger — Hook: after:deploy:deploy
|
|
21
|
+
*
|
|
22
|
+
* Tags resources that cannot be fully tagged in the CF template:
|
|
23
|
+
* dict-based, API-only, and related resources.
|
|
24
|
+
*/
|
|
25
|
+
async function updateTagsPostDeploy(config, stackName, stackTags, stage, partition, region, log) {
|
|
26
|
+
const cfnClient = getClient(CloudFormationClient, config);
|
|
27
|
+
|
|
28
|
+
const result = await cfnClient.send(
|
|
29
|
+
new DescribeStackResourcesCommand({ StackName: stackName })
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const allResources = result.StackResources || [];
|
|
33
|
+
|
|
34
|
+
for (const resource of allResources) {
|
|
35
|
+
const type = resource.ResourceType;
|
|
36
|
+
if (!needsPostDeployTagging(type)) continue;
|
|
37
|
+
|
|
38
|
+
const logicalId = resource.LogicalResourceId;
|
|
39
|
+
const listTags = buildListTags(stackTags, stage, logicalId);
|
|
40
|
+
const dictTags = buildDictTags(stackTags, stage, logicalId);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Dict-based types: need API call because CF template tagging
|
|
44
|
+
// may not fully propagate for some of these types
|
|
45
|
+
if (DICT_BASED_TYPES.has(type)) {
|
|
46
|
+
switch (type) {
|
|
47
|
+
case "AWS::SSM::Parameter":
|
|
48
|
+
await tagSSMParameter(config, resource, listTags);
|
|
49
|
+
log(`TAGGING: post-deploy SSM Parameter ${logicalId}`);
|
|
50
|
+
break;
|
|
51
|
+
case "AWS::Pinpoint::App":
|
|
52
|
+
await tagPinpointApp(config, resource, dictTags);
|
|
53
|
+
log(`TAGGING: post-deploy Pinpoint App ${logicalId}`);
|
|
54
|
+
break;
|
|
55
|
+
case "AWS::ApiGatewayV2::Api":
|
|
56
|
+
case "AWS::ApiGatewayV2::Stage":
|
|
57
|
+
case "AWS::ApiGatewayV2::DomainName":
|
|
58
|
+
case "AWS::ApiGatewayV2::VpcLink":
|
|
59
|
+
await tagApiGatewayV2(config, resource, dictTags, partition, region, allResources);
|
|
60
|
+
log(`TAGGING: post-deploy ${type} ${logicalId}`);
|
|
61
|
+
break;
|
|
62
|
+
// Glue and Batch are tagged in template (dict-based), no post-deploy needed
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// API-only types: not tagged in template at all
|
|
67
|
+
if (API_ONLY_TYPES.has(type)) {
|
|
68
|
+
switch (type) {
|
|
69
|
+
case "AWS::RDS::DBCluster":
|
|
70
|
+
await tagRDSCluster(config, resource, listTags, partition, region);
|
|
71
|
+
log(`TAGGING: post-deploy RDS Cluster ${logicalId}`);
|
|
72
|
+
break;
|
|
73
|
+
case "AWS::KinesisFirehose::DeliveryStream":
|
|
74
|
+
await tagFirehoseStream(config, resource, listTags);
|
|
75
|
+
log(`TAGGING: post-deploy Firehose ${logicalId}`);
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Related types: tag associated resources (volumes, ENIs, etc.)
|
|
81
|
+
if (RELATED_TYPES.has(type)) {
|
|
82
|
+
switch (type) {
|
|
83
|
+
case "AWS::EC2::Instance": {
|
|
84
|
+
const relatedIds = await tagEC2RelatedResources(config, resource, listTags);
|
|
85
|
+
log(`TAGGING: post-deploy EC2 related resources for ${logicalId}: ${relatedIds.length} resources`);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
log(`TAGGING: ERROR post-deploy ${type} ${logicalId}: ${err.message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Remove unwanted tags injected by SFW (STAGE)
|
|
96
|
+
if (TAGS_TO_REMOVE.length > 0) {
|
|
97
|
+
log(`TAGGING: Removing unwanted tags: ${TAGS_TO_REMOVE.join(", ")}`);
|
|
98
|
+
await removeTagsFromStackResources(config, allResources, TAGS_TO_REMOVE, partition, region, log);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { updateTagsPostDeploy };
|