serverless-tag-resources 3.1.0 → 3.1.2
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/index.js +13 -0
- package/package.json +3 -2
- package/src/post-deploy/untag.js +77 -45
- package/src/post-deploy-tagger.js +1 -7
- package/src/resource-classifier.js +1 -0
package/index.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const { tagResources } = require("./src/template-tagger");
|
|
4
4
|
const { updateTagsPostDeploy } = require("./src/post-deploy-tagger");
|
|
5
|
+
const { removeUnwantedTags } = require("./src/post-deploy/untag");
|
|
5
6
|
const { validateTags } = require("./src/validation");
|
|
6
7
|
const { configFromProvider } = require("./src/aws-clients");
|
|
8
|
+
const { TAGS_TO_REMOVE } = require("./src/tags");
|
|
7
9
|
|
|
8
10
|
class TagResourcesServerlessPlugin {
|
|
9
11
|
constructor(serverless, options) {
|
|
@@ -77,6 +79,7 @@ class TagResourcesServerlessPlugin {
|
|
|
77
79
|
const awsProvider = this.serverless.getProvider("aws");
|
|
78
80
|
const stackName = awsProvider.naming.getStackName();
|
|
79
81
|
|
|
82
|
+
// Phase 1: Tag resources that CF doesn't cover natively
|
|
80
83
|
await updateTagsPostDeploy(
|
|
81
84
|
this.awsConfig,
|
|
82
85
|
stackName,
|
|
@@ -87,6 +90,16 @@ class TagResourcesServerlessPlugin {
|
|
|
87
90
|
this._log.bind(this)
|
|
88
91
|
);
|
|
89
92
|
|
|
93
|
+
// Phase 2: Remove unwanted tags (STAGE injected by SFW)
|
|
94
|
+
if (TAGS_TO_REMOVE.length > 0) {
|
|
95
|
+
await removeUnwantedTags(
|
|
96
|
+
this.awsConfig,
|
|
97
|
+
stackName,
|
|
98
|
+
TAGS_TO_REMOVE,
|
|
99
|
+
this._log.bind(this)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
90
103
|
this._log("TAGGING: Post-deploy tagging complete");
|
|
91
104
|
}
|
|
92
105
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "serverless-tag-resources",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"description": "Datamart: Tag all AWS resources with dual legacy + datamart:* tag support",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"@aws-sdk/client-firehose": "^3.700.0",
|
|
36
36
|
"@aws-sdk/client-pinpoint": "^3.700.0",
|
|
37
37
|
"@aws-sdk/client-rds": "^3.700.0",
|
|
38
|
-
"@aws-sdk/client-ssm": "^3.700.0"
|
|
38
|
+
"@aws-sdk/client-ssm": "^3.700.0",
|
|
39
|
+
"@aws-sdk/client-resource-groups-tagging-api": "^3.700.0"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
42
|
"jest": "^30.3.0"
|
package/src/post-deploy/untag.js
CHANGED
|
@@ -4,94 +4,126 @@ const {
|
|
|
4
4
|
ResourceGroupsTaggingAPIClient,
|
|
5
5
|
UntagResourcesCommand,
|
|
6
6
|
} = require("@aws-sdk/client-resource-groups-tagging-api");
|
|
7
|
+
const {
|
|
8
|
+
CloudFormationClient,
|
|
9
|
+
DescribeStacksCommand,
|
|
10
|
+
DescribeStackResourcesCommand,
|
|
11
|
+
} = require("@aws-sdk/client-cloudformation");
|
|
7
12
|
const { getClient } = require("../aws-clients");
|
|
13
|
+
const { SKIP_TYPES } = require("../resource-classifier");
|
|
8
14
|
|
|
9
15
|
/**
|
|
10
|
-
* Remove
|
|
11
|
-
* Uses Resource Groups Tagging API
|
|
16
|
+
* Remove unwanted tags from the stack itself and all its resources.
|
|
17
|
+
* Uses Resource Groups Tagging API (UntagResources) which works on
|
|
18
|
+
* both CF stacks and individual resources.
|
|
12
19
|
*
|
|
13
|
-
*
|
|
14
|
-
* that conflict with the datamart:* tagging policy.
|
|
20
|
+
* No UpdateStack — only tag removal, zero infrastructure changes.
|
|
15
21
|
*/
|
|
16
|
-
async function
|
|
22
|
+
async function removeUnwantedTags(config, stackName, tagKeysToRemove, log) {
|
|
23
|
+
const cfnClient = getClient(CloudFormationClient, config);
|
|
17
24
|
const taggingClient = getClient(ResourceGroupsTaggingAPIClient, config);
|
|
18
25
|
|
|
19
|
-
// Collect ARNs of all stack resources
|
|
20
26
|
const arns = [];
|
|
21
|
-
for (const resource of stackResources) {
|
|
22
|
-
const arn = buildArn(resource, partition, region);
|
|
23
|
-
if (arn) arns.push(arn);
|
|
24
|
-
}
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
// 1. Get stack ARN
|
|
29
|
+
const stackResult = await cfnClient.send(
|
|
30
|
+
new DescribeStacksCommand({ StackName: stackName })
|
|
31
|
+
);
|
|
32
|
+
const stackArn = stackResult.Stacks?.[0]?.StackId;
|
|
33
|
+
if (stackArn) arns.push(stackArn);
|
|
34
|
+
|
|
35
|
+
// 2. Get resource ARNs (deduplicated, excluding types that don't support tags)
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
const resourceResult = await cfnClient.send(
|
|
38
|
+
new DescribeStackResourcesCommand({ StackName: stackName })
|
|
39
|
+
);
|
|
40
|
+
for (const resource of resourceResult.StackResources || []) {
|
|
41
|
+
if (SKIP_TYPES.has(resource.ResourceType)) continue;
|
|
42
|
+
const arn = resolveArn(resource);
|
|
43
|
+
if (arn && !seen.has(arn)) {
|
|
44
|
+
seen.add(arn);
|
|
45
|
+
arns.push(arn);
|
|
46
|
+
}
|
|
29
47
|
}
|
|
30
48
|
|
|
49
|
+
if (arns.length === 0) return;
|
|
50
|
+
|
|
31
51
|
// UntagResources accepts max 20 ARNs per call
|
|
32
52
|
const batchSize = 20;
|
|
33
53
|
let untagged = 0;
|
|
54
|
+
let skipped = 0;
|
|
34
55
|
|
|
35
56
|
for (let i = 0; i < arns.length; i += batchSize) {
|
|
36
57
|
const batch = arns.slice(i, i + batchSize);
|
|
37
58
|
try {
|
|
38
|
-
await taggingClient.send(
|
|
59
|
+
const result = await taggingClient.send(
|
|
39
60
|
new UntagResourcesCommand({
|
|
40
61
|
ResourceARNList: batch,
|
|
41
62
|
TagKeys: tagKeysToRemove,
|
|
42
63
|
})
|
|
43
64
|
);
|
|
44
|
-
|
|
65
|
+
const failedMap = result.FailedResourcesMap || {};
|
|
66
|
+
const failures = Object.keys(failedMap).length;
|
|
67
|
+
untagged += batch.length - failures;
|
|
68
|
+
skipped += failures;
|
|
45
69
|
} catch (err) {
|
|
46
|
-
|
|
47
|
-
log(`TAGGING: WARN untag batch ${i}-${i + batch.length}: ${err.message}`);
|
|
70
|
+
skipped += batch.length;
|
|
48
71
|
}
|
|
49
72
|
}
|
|
50
73
|
|
|
51
|
-
log(`TAGGING: Removed [${tagKeysToRemove.join(", ")}] from ${untagged} resources`);
|
|
74
|
+
log(`TAGGING: Removed [${tagKeysToRemove.join(", ")}] from ${untagged} resources (${skipped} skipped)`);
|
|
52
75
|
}
|
|
53
76
|
|
|
54
77
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
78
|
+
* Resolve the ARN of a stack resource.
|
|
79
|
+
* PhysicalResourceId is sometimes the ARN, sometimes just the name/ID.
|
|
57
80
|
*/
|
|
58
|
-
function
|
|
81
|
+
function resolveArn(resource) {
|
|
59
82
|
const physicalId = resource.PhysicalResourceId;
|
|
60
83
|
if (!physicalId) return null;
|
|
61
84
|
|
|
62
|
-
//
|
|
85
|
+
// Already an ARN
|
|
63
86
|
if (physicalId.startsWith("arn:")) return physicalId;
|
|
64
87
|
|
|
65
|
-
//
|
|
66
|
-
const
|
|
67
|
-
const
|
|
88
|
+
// Extract account from stack ARN
|
|
89
|
+
const stackId = resource.StackId || "";
|
|
90
|
+
const parts = stackId.split(":");
|
|
91
|
+
if (parts.length < 5) return null;
|
|
92
|
+
const partition = parts[1];
|
|
93
|
+
const region = parts[3];
|
|
94
|
+
const account = parts[4];
|
|
68
95
|
|
|
69
|
-
|
|
96
|
+
// Build ARN by resource type
|
|
97
|
+
const type = resource.ResourceType;
|
|
98
|
+
const builders = {
|
|
70
99
|
"AWS::Lambda::Function": () =>
|
|
71
|
-
`arn:${partition}:lambda:${region}:${
|
|
100
|
+
`arn:${partition}:lambda:${region}:${account}:function:${physicalId}`,
|
|
72
101
|
"AWS::SNS::Topic": () =>
|
|
73
|
-
`arn:${partition}:sns:${region}:${
|
|
74
|
-
"AWS::SQS::Queue": () => null, // Queue URL, not name — skip
|
|
102
|
+
`arn:${partition}:sns:${region}:${account}:${physicalId}`,
|
|
75
103
|
"AWS::Events::EventBus": () =>
|
|
76
|
-
`arn:${partition}:events:${region}:${
|
|
77
|
-
"AWS::Events::Rule": () =>
|
|
104
|
+
`arn:${partition}:events:${region}:${account}:event-bus/${physicalId}`,
|
|
105
|
+
"AWS::Events::Rule": () =>
|
|
106
|
+
`arn:${partition}:events:${region}:${account}:rule/${physicalId}`,
|
|
107
|
+
"AWS::Logs::LogGroup": () =>
|
|
108
|
+
`arn:${partition}:logs:${region}:${account}:log-group:${physicalId}`,
|
|
109
|
+
"AWS::IAM::Role": () =>
|
|
110
|
+
`arn:${partition}:iam::${account}:role/${physicalId}`,
|
|
111
|
+
"AWS::S3::Bucket": () =>
|
|
112
|
+
`arn:${partition}:s3:::${physicalId}`,
|
|
113
|
+
"AWS::SSM::Parameter": () =>
|
|
114
|
+
`arn:${partition}:ssm:${region}:${account}:parameter${physicalId.startsWith("/") ? "" : "/"}${physicalId}`,
|
|
115
|
+
"AWS::KMS::Key": () =>
|
|
116
|
+
`arn:${partition}:kms:${region}:${account}:key/${physicalId}`,
|
|
117
|
+
"AWS::CodeBuild::Project": () =>
|
|
118
|
+
`arn:${partition}:codebuild:${region}:${account}:project/${physicalId}`,
|
|
78
119
|
"AWS::ApiGateway::RestApi": () =>
|
|
79
120
|
`arn:${partition}:apigateway:${region}::/restapis/${physicalId}`,
|
|
80
|
-
"AWS::
|
|
81
|
-
`arn:${partition}:
|
|
121
|
+
"AWS::WAFv2::WebACL": () =>
|
|
122
|
+
`arn:${partition}:wafv2:${region}:${account}:regional/webacl/${physicalId}`,
|
|
82
123
|
};
|
|
83
124
|
|
|
84
|
-
const builder =
|
|
85
|
-
|
|
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] : "";
|
|
125
|
+
const builder = builders[type];
|
|
126
|
+
return builder ? builder() : null;
|
|
95
127
|
}
|
|
96
128
|
|
|
97
|
-
module.exports = {
|
|
129
|
+
module.exports = { removeUnwantedTags };
|
|
@@ -5,9 +5,8 @@ const {
|
|
|
5
5
|
DescribeStackResourcesCommand,
|
|
6
6
|
} = require("@aws-sdk/client-cloudformation");
|
|
7
7
|
const { getClient } = require("./aws-clients");
|
|
8
|
-
const { buildListTags, buildDictTags
|
|
8
|
+
const { buildListTags, buildDictTags } = require("./tags");
|
|
9
9
|
const { needsPostDeployTagging, DICT_BASED_TYPES, API_ONLY_TYPES, RELATED_TYPES } = require("./resource-classifier");
|
|
10
|
-
const { removeTagsFromStackResources } = require("./post-deploy/untag");
|
|
11
10
|
|
|
12
11
|
const { tagSSMParameter } = require("./post-deploy/ssm");
|
|
13
12
|
const { tagPinpointApp } = require("./post-deploy/pinpoint");
|
|
@@ -92,11 +91,6 @@ async function updateTagsPostDeploy(config, stackName, stackTags, stage, partiti
|
|
|
92
91
|
}
|
|
93
92
|
}
|
|
94
93
|
|
|
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
94
|
}
|
|
101
95
|
|
|
102
96
|
module.exports = { updateTagsPostDeploy };
|