serverless-tag-resources 3.0.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "serverless-tag-resources",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
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:datamartcl/dm-serverless-tag-resources-plugin.git"
17
+ "url": "git+ssh://git@bitbucket.org/datamartcl/dm-serverless-tag-resources-plugin.git"
18
18
  },
19
19
  "keywords": [
20
20
  "serverless",
@@ -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 };
@@ -5,8 +5,9 @@ const {
5
5
  DescribeStackResourcesCommand,
6
6
  } = require("@aws-sdk/client-cloudformation");
7
7
  const { getClient } = require("./aws-clients");
8
- const { buildListTags, buildDictTags } = require("./tags");
8
+ const { buildListTags, buildDictTags, TAGS_TO_REMOVE } = require("./tags");
9
9
  const { needsPostDeployTagging, DICT_BASED_TYPES, API_ONLY_TYPES, RELATED_TYPES } = require("./resource-classifier");
10
+ const { removeTagsFromStackResources } = require("./post-deploy/untag");
10
11
 
11
12
  const { tagSSMParameter } = require("./post-deploy/ssm");
12
13
  const { tagPinpointApp } = require("./post-deploy/pinpoint");
@@ -90,6 +91,12 @@ async function updateTagsPostDeploy(config, stackName, stackTags, stage, partiti
90
91
  log(`TAGGING: ERROR post-deploy ${type} ${logicalId}: ${err.message}`);
91
92
  }
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
+ }
93
100
  }
94
101
 
95
102
  module.exports = { updateTagsPostDeploy };
package/src/tags.js CHANGED
@@ -3,15 +3,19 @@
3
3
  /**
4
4
  * Tag building module.
5
5
  *
6
- * Produces both legacy (PascalCase) and new datamart:* tags
7
- * during the transition period. Auto-generates:
8
- * - Stage / datamart:environment (from deployment stage)
9
- * - Resource / datamart:resource (from CloudFormation LogicalID)
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. Does not overwrite existing keys.
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 pairs (legacy + datamart)
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 pairs (legacy + datamart) — don't overwrite user-provided
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 };