serverless-tag-resources 3.0.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 +4 -3
- package/src/post-deploy/untag.js +122 -0
- package/src/post-deploy-tagger.js +1 -0
- package/src/tags.js +12 -12
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.
|
|
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": [
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
17
|
-
"url": "git+ssh://git@bitbucket.org
|
|
17
|
+
"url": "git+ssh://git@bitbucket.org/datamartcl/dm-serverless-tag-resources-plugin.git"
|
|
18
18
|
},
|
|
19
19
|
"keywords": [
|
|
20
20
|
"serverless",
|
|
@@ -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"
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
ResourceGroupsTaggingAPIClient,
|
|
5
|
+
UntagResourcesCommand,
|
|
6
|
+
} = require("@aws-sdk/client-resource-groups-tagging-api");
|
|
7
|
+
const {
|
|
8
|
+
CloudFormationClient,
|
|
9
|
+
DescribeStacksCommand,
|
|
10
|
+
DescribeStackResourcesCommand,
|
|
11
|
+
} = require("@aws-sdk/client-cloudformation");
|
|
12
|
+
const { getClient } = require("../aws-clients");
|
|
13
|
+
|
|
14
|
+
/**
|
|
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.
|
|
18
|
+
*
|
|
19
|
+
* This runs post-deploy to clean up tags injected by SFW (e.g., STAGE)
|
|
20
|
+
* that we cannot prevent at template/deploy time.
|
|
21
|
+
*/
|
|
22
|
+
async function removeUnwantedTags(config, stackName, tagKeysToRemove, log) {
|
|
23
|
+
const cfnClient = getClient(CloudFormationClient, config);
|
|
24
|
+
const taggingClient = getClient(ResourceGroupsTaggingAPIClient, config);
|
|
25
|
+
|
|
26
|
+
// Collect ARNs: stack + all resources
|
|
27
|
+
const arns = [];
|
|
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);
|
|
42
|
+
if (arn) arns.push(arn);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (arns.length === 0) return;
|
|
46
|
+
|
|
47
|
+
// UntagResources accepts max 20 ARNs per call
|
|
48
|
+
const batchSize = 20;
|
|
49
|
+
let untagged = 0;
|
|
50
|
+
let failed = 0;
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < arns.length; i += batchSize) {
|
|
53
|
+
const batch = arns.slice(i, i + batchSize);
|
|
54
|
+
try {
|
|
55
|
+
const result = await taggingClient.send(
|
|
56
|
+
new UntagResourcesCommand({
|
|
57
|
+
ResourceARNList: batch,
|
|
58
|
+
TagKeys: tagKeysToRemove,
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
const failures = Object.keys(result.FailedResourcesMap || {}).length;
|
|
62
|
+
untagged += batch.length - failures;
|
|
63
|
+
failed += failures;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
// Some resource types don't support tagging API — skip
|
|
66
|
+
failed += batch.length;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
log(`TAGGING: Removed [${tagKeysToRemove.join(", ")}] from ${untagged} resources (${failed} skipped)`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolve the ARN of a stack resource.
|
|
75
|
+
* PhysicalResourceId is sometimes the ARN, sometimes just the name/ID.
|
|
76
|
+
*/
|
|
77
|
+
function resolveArn(resource) {
|
|
78
|
+
const physicalId = resource.PhysicalResourceId;
|
|
79
|
+
if (!physicalId) return null;
|
|
80
|
+
|
|
81
|
+
// Already an ARN
|
|
82
|
+
if (physicalId.startsWith("arn:")) return physicalId;
|
|
83
|
+
|
|
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];
|
|
91
|
+
|
|
92
|
+
// Build ARN by resource type
|
|
93
|
+
const type = resource.ResourceType;
|
|
94
|
+
const builders = {
|
|
95
|
+
"AWS::Lambda::Function": () =>
|
|
96
|
+
`arn:${partition}:lambda:${region}:${account}:function:${physicalId}`,
|
|
97
|
+
"AWS::SNS::Topic": () =>
|
|
98
|
+
`arn:${partition}:sns:${region}:${account}:${physicalId}`,
|
|
99
|
+
"AWS::Events::EventBus": () =>
|
|
100
|
+
`arn:${partition}:events:${region}:${account}:event-bus/${physicalId}`,
|
|
101
|
+
"AWS::Logs::LogGroup": () =>
|
|
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}`,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const builder = builders[type];
|
|
119
|
+
return builder ? builder() : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { removeUnwantedTags };
|
package/src/tags.js
CHANGED
|
@@ -3,15 +3,19 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Tag building module.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
6
|
+
* Auto-generates datamart:* tags only (no legacy PascalCase):
|
|
7
|
+
* - datamart:environment (from deployment stage)
|
|
8
|
+
* - datamart:resource (from CloudFormation LogicalID)
|
|
9
|
+
*
|
|
10
|
+
* Legacy tags (Stage, Resource) removed in v3.1.0 per tagging policy v2.1.
|
|
10
11
|
*/
|
|
11
12
|
|
|
13
|
+
// Tags injected by SFW that should be removed post-deploy
|
|
14
|
+
const TAGS_TO_REMOVE = ["STAGE"];
|
|
15
|
+
|
|
12
16
|
/**
|
|
13
17
|
* Build list-based tags [{Key, Value}] for CloudFormation resources.
|
|
14
|
-
* Merges stackTags + auto-generated tags.
|
|
18
|
+
* Merges stackTags + auto-generated datamart:* tags.
|
|
15
19
|
*/
|
|
16
20
|
function buildListTags(stackTags, stage, logicalId) {
|
|
17
21
|
const tags = [];
|
|
@@ -25,11 +29,9 @@ function buildListTags(stackTags, stage, logicalId) {
|
|
|
25
29
|
}
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
// Auto-generated
|
|
32
|
+
// Auto-generated datamart:* tags only
|
|
29
33
|
const autoTags = [
|
|
30
|
-
{ Key: "Stage", Value: stage },
|
|
31
34
|
{ Key: "datamart:environment", Value: stage },
|
|
32
|
-
{ Key: "Resource", Value: logicalId },
|
|
33
35
|
{ Key: "datamart:resource", Value: logicalId },
|
|
34
36
|
];
|
|
35
37
|
|
|
@@ -57,10 +59,8 @@ function buildDictTags(stackTags, stage, logicalId) {
|
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
// Auto-generated
|
|
61
|
-
if (!tags["Stage"]) tags["Stage"] = stage;
|
|
62
|
+
// Auto-generated datamart:* tags only — don't overwrite user-provided
|
|
62
63
|
if (!tags["datamart:environment"]) tags["datamart:environment"] = stage;
|
|
63
|
-
if (!tags["Resource"]) tags["Resource"] = logicalId;
|
|
64
64
|
if (!tags["datamart:resource"]) tags["datamart:resource"] = logicalId;
|
|
65
65
|
|
|
66
66
|
return tags;
|
|
@@ -75,4 +75,4 @@ function excludeAwsTags(tags) {
|
|
|
75
75
|
);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
module.exports = { buildListTags, buildDictTags, excludeAwsTags };
|
|
78
|
+
module.exports = { buildListTags, buildDictTags, excludeAwsTags, TAGS_TO_REMOVE };
|