serverless-tag-resources 3.1.0 → 3.1.1
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 +70 -45
- package/src/post-deploy-tagger.js +1 -7
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.1",
|
|
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,119 @@ 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");
|
|
8
13
|
|
|
9
14
|
/**
|
|
10
|
-
* Remove
|
|
11
|
-
* Uses Resource Groups Tagging API
|
|
15
|
+
* Remove unwanted tags from the stack itself and all its resources.
|
|
16
|
+
* Uses Resource Groups Tagging API (UntagResources) which works on
|
|
17
|
+
* both CF stacks and individual resources.
|
|
12
18
|
*
|
|
13
|
-
* This
|
|
14
|
-
* that
|
|
19
|
+
* This runs post-deploy to clean up tags injected by SFW (e.g., STAGE)
|
|
20
|
+
* that we cannot prevent at template/deploy time.
|
|
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
|
|
26
|
+
// Collect ARNs: stack + all resources
|
|
20
27
|
const arns = [];
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
|
|
29
|
+
// 1. Get stack ARN
|
|
30
|
+
const stackResult = await cfnClient.send(
|
|
31
|
+
new DescribeStacksCommand({ StackName: stackName })
|
|
32
|
+
);
|
|
33
|
+
const stackArn = stackResult.Stacks?.[0]?.StackId;
|
|
34
|
+
if (stackArn) arns.push(stackArn);
|
|
35
|
+
|
|
36
|
+
// 2. Get all resource ARNs
|
|
37
|
+
const resourceResult = await cfnClient.send(
|
|
38
|
+
new DescribeStackResourcesCommand({ StackName: stackName })
|
|
39
|
+
);
|
|
40
|
+
for (const resource of resourceResult.StackResources || []) {
|
|
41
|
+
const arn = resolveArn(resource);
|
|
23
42
|
if (arn) arns.push(arn);
|
|
24
43
|
}
|
|
25
44
|
|
|
26
|
-
if (arns.length === 0)
|
|
27
|
-
log("TAGGING: No resources with ARNs to untag");
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
45
|
+
if (arns.length === 0) return;
|
|
30
46
|
|
|
31
47
|
// UntagResources accepts max 20 ARNs per call
|
|
32
48
|
const batchSize = 20;
|
|
33
49
|
let untagged = 0;
|
|
50
|
+
let failed = 0;
|
|
34
51
|
|
|
35
52
|
for (let i = 0; i < arns.length; i += batchSize) {
|
|
36
53
|
const batch = arns.slice(i, i + batchSize);
|
|
37
54
|
try {
|
|
38
|
-
await taggingClient.send(
|
|
55
|
+
const result = await taggingClient.send(
|
|
39
56
|
new UntagResourcesCommand({
|
|
40
57
|
ResourceARNList: batch,
|
|
41
58
|
TagKeys: tagKeysToRemove,
|
|
42
59
|
})
|
|
43
60
|
);
|
|
44
|
-
|
|
61
|
+
const failures = Object.keys(result.FailedResourcesMap || {}).length;
|
|
62
|
+
untagged += batch.length - failures;
|
|
63
|
+
failed += failures;
|
|
45
64
|
} catch (err) {
|
|
46
|
-
// Some resource types don't support tagging API — skip
|
|
47
|
-
|
|
65
|
+
// Some resource types don't support tagging API — skip
|
|
66
|
+
failed += batch.length;
|
|
48
67
|
}
|
|
49
68
|
}
|
|
50
69
|
|
|
51
|
-
log(`TAGGING: Removed [${tagKeysToRemove.join(", ")}] from ${untagged} resources`);
|
|
70
|
+
log(`TAGGING: Removed [${tagKeysToRemove.join(", ")}] from ${untagged} resources (${failed} skipped)`);
|
|
52
71
|
}
|
|
53
72
|
|
|
54
73
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
74
|
+
* Resolve the ARN of a stack resource.
|
|
75
|
+
* PhysicalResourceId is sometimes the ARN, sometimes just the name/ID.
|
|
57
76
|
*/
|
|
58
|
-
function
|
|
77
|
+
function resolveArn(resource) {
|
|
59
78
|
const physicalId = resource.PhysicalResourceId;
|
|
60
79
|
if (!physicalId) return null;
|
|
61
80
|
|
|
62
|
-
//
|
|
81
|
+
// Already an ARN
|
|
63
82
|
if (physicalId.startsWith("arn:")) return physicalId;
|
|
64
83
|
|
|
65
|
-
//
|
|
66
|
-
const
|
|
67
|
-
const
|
|
84
|
+
// Extract account from stack ARN
|
|
85
|
+
const stackId = resource.StackId || "";
|
|
86
|
+
const parts = stackId.split(":");
|
|
87
|
+
if (parts.length < 5) return null;
|
|
88
|
+
const partition = parts[1];
|
|
89
|
+
const region = parts[3];
|
|
90
|
+
const account = parts[4];
|
|
68
91
|
|
|
69
|
-
|
|
92
|
+
// Build ARN by resource type
|
|
93
|
+
const type = resource.ResourceType;
|
|
94
|
+
const builders = {
|
|
70
95
|
"AWS::Lambda::Function": () =>
|
|
71
|
-
`arn:${partition}:lambda:${region}:${
|
|
96
|
+
`arn:${partition}:lambda:${region}:${account}:function:${physicalId}`,
|
|
72
97
|
"AWS::SNS::Topic": () =>
|
|
73
|
-
`arn:${partition}:sns:${region}:${
|
|
74
|
-
"AWS::SQS::Queue": () => null, // Queue URL, not name — skip
|
|
98
|
+
`arn:${partition}:sns:${region}:${account}:${physicalId}`,
|
|
75
99
|
"AWS::Events::EventBus": () =>
|
|
76
|
-
`arn:${partition}:events:${region}:${
|
|
77
|
-
"AWS::Events::Rule": () => null, // Complex ARN — skip
|
|
78
|
-
"AWS::ApiGateway::RestApi": () =>
|
|
79
|
-
`arn:${partition}:apigateway:${region}::/restapis/${physicalId}`,
|
|
100
|
+
`arn:${partition}:events:${region}:${account}:event-bus/${physicalId}`,
|
|
80
101
|
"AWS::Logs::LogGroup": () =>
|
|
81
|
-
`arn:${partition}:logs:${region}:${
|
|
102
|
+
`arn:${partition}:logs:${region}:${account}:log-group:${physicalId}`,
|
|
103
|
+
"AWS::IAM::Role": () =>
|
|
104
|
+
`arn:${partition}:iam::${account}:role/${physicalId}`,
|
|
105
|
+
"AWS::IAM::ManagedPolicy": () =>
|
|
106
|
+
`arn:${partition}:iam::${account}:policy/${physicalId}`,
|
|
107
|
+
"AWS::S3::Bucket": () =>
|
|
108
|
+
`arn:${partition}:s3:::${physicalId}`,
|
|
109
|
+
"AWS::SSM::Parameter": () =>
|
|
110
|
+
`arn:${partition}:ssm:${region}:${account}:parameter${physicalId.startsWith("/") ? "" : "/"}${physicalId}`,
|
|
111
|
+
"AWS::KMS::Key": () =>
|
|
112
|
+
`arn:${partition}:kms:${region}:${account}:key/${physicalId}`,
|
|
113
|
+
"AWS::KMS::Alias": () => null, // aliases don't support tagging
|
|
114
|
+
"AWS::CodeBuild::Project": () =>
|
|
115
|
+
`arn:${partition}:codebuild:${region}:${account}:project/${physicalId}`,
|
|
82
116
|
};
|
|
83
117
|
|
|
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] : "";
|
|
118
|
+
const builder = builders[type];
|
|
119
|
+
return builder ? builder() : null;
|
|
95
120
|
}
|
|
96
121
|
|
|
97
|
-
module.exports = {
|
|
122
|
+
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 };
|