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.
@@ -0,0 +1,381 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Resource classifier.
5
+ *
6
+ * Determines how each CloudFormation resource type should be tagged:
7
+ * - 'list' → Tags: [{Key, Value}] in template
8
+ * - 'dict' → Tags: {key: value} in template + post-deploy API
9
+ * - 'api-only' → Tagged only post-deploy via AWS API (not in template)
10
+ * - 'skip' → Cannot be tagged
11
+ */
12
+
13
+ // Resources that use dict-based tag format in CloudFormation templates
14
+ const DICT_BASED_TYPES = new Set([
15
+ "AWS::SSM::Parameter",
16
+ "AWS::Pinpoint::App",
17
+ "AWS::ApiGatewayV2::Api",
18
+ "AWS::ApiGatewayV2::Stage",
19
+ "AWS::ApiGatewayV2::DomainName",
20
+ "AWS::ApiGatewayV2::VpcLink",
21
+ "AWS::Glue::Job",
22
+ "AWS::Glue::Crawler",
23
+ "AWS::Glue::DevEndpoint",
24
+ "AWS::Glue::MLTransform",
25
+ "AWS::Glue::Trigger",
26
+ "AWS::Glue::Workflow",
27
+ "AWS::Batch::JobDefinition",
28
+ "AWS::Batch::ComputeEnvironment",
29
+ "AWS::Batch::JobQueue",
30
+ "AWS::Batch::SchedulingPolicy",
31
+ ]);
32
+
33
+ // Resources tagged only via API post-deploy (not in template)
34
+ const API_ONLY_TYPES = new Set([
35
+ "AWS::RDS::DBCluster",
36
+ "AWS::KinesisFirehose::DeliveryStream",
37
+ ]);
38
+
39
+ // Resources whose related resources also get tagged (volumes, ENIs, etc.)
40
+ const RELATED_TYPES = new Set([
41
+ "AWS::EC2::Instance",
42
+ ]);
43
+
44
+ // Common resource types known to support list-based tags [{Key, Value}].
45
+ // Types here won't generate a warning. Types NOT in any list will be
46
+ // tagged as list-based but with a warning (unclassified).
47
+ const LIST_BASED_TYPES = new Set([
48
+ // Lambda
49
+ "AWS::Lambda::Function",
50
+ // S3
51
+ "AWS::S3::Bucket",
52
+ // DynamoDB
53
+ "AWS::DynamoDB::Table",
54
+ "AWS::DynamoDB::GlobalTable",
55
+ // SNS / SQS
56
+ "AWS::SNS::Topic",
57
+ "AWS::SQS::Queue",
58
+ // API Gateway v1
59
+ "AWS::ApiGateway::RestApi",
60
+ "AWS::ApiGateway::Stage",
61
+ "AWS::ApiGateway::UsagePlan",
62
+ "AWS::ApiGateway::DomainName",
63
+ "AWS::ApiGateway::VpcLink",
64
+ "AWS::ApiGateway::ClientCertificate",
65
+ // CloudWatch
66
+ "AWS::Logs::LogGroup",
67
+ // IAM
68
+ "AWS::IAM::Role",
69
+ "AWS::IAM::User",
70
+ // EC2 / VPC
71
+ "AWS::EC2::Instance",
72
+ "AWS::EC2::SecurityGroup",
73
+ "AWS::EC2::Subnet",
74
+ "AWS::EC2::VPC",
75
+ "AWS::EC2::InternetGateway",
76
+ "AWS::EC2::NatGateway",
77
+ "AWS::EC2::RouteTable",
78
+ "AWS::EC2::NetworkInterface",
79
+ "AWS::EC2::Volume",
80
+ "AWS::EC2::EIP",
81
+ "AWS::EC2::TransitGateway",
82
+ "AWS::EC2::TransitGatewayAttachment",
83
+ // ECS
84
+ "AWS::ECS::Cluster",
85
+ "AWS::ECS::Service",
86
+ "AWS::ECS::TaskDefinition",
87
+ // ELB
88
+ "AWS::ElasticLoadBalancingV2::LoadBalancer",
89
+ "AWS::ElasticLoadBalancingV2::TargetGroup",
90
+ // RDS
91
+ "AWS::RDS::DBInstance",
92
+ "AWS::RDS::DBSubnetGroup",
93
+ "AWS::RDS::DBParameterGroup",
94
+ "AWS::RDS::DBClusterParameterGroup",
95
+ "AWS::RDS::DBProxy",
96
+ "AWS::RDS::EventSubscription",
97
+ // ElastiCache
98
+ "AWS::ElastiCache::CacheCluster",
99
+ "AWS::ElastiCache::ReplicationGroup",
100
+ "AWS::ElastiCache::SubnetGroup",
101
+ "AWS::ElastiCache::ParameterGroup",
102
+ // CloudFront
103
+ "AWS::CloudFront::Distribution",
104
+ // KMS
105
+ "AWS::KMS::Key",
106
+ // Secrets Manager
107
+ "AWS::SecretsManager::Secret",
108
+ // Step Functions
109
+ "AWS::StepFunctions::StateMachine",
110
+ "AWS::StepFunctions::Activity",
111
+ // CodeBuild / CodePipeline
112
+ "AWS::CodeBuild::Project",
113
+ "AWS::CodePipeline::Pipeline",
114
+ // Kinesis
115
+ "AWS::Kinesis::Stream",
116
+ // OpenSearch
117
+ "AWS::OpenSearchService::Domain",
118
+ "AWS::Elasticsearch::Domain",
119
+ // WAF
120
+ "AWS::WAFv2::WebACL",
121
+ "AWS::WAFv2::IPSet",
122
+ "AWS::WAFv2::RegexPatternSet",
123
+ "AWS::WAFv2::RuleGroup",
124
+ // AppSync
125
+ "AWS::AppSync::GraphQLApi",
126
+ // Cognito (taggable ones)
127
+ // EventBridge
128
+ "AWS::Events::EventBus",
129
+ // Redshift
130
+ "AWS::Redshift::Cluster",
131
+ "AWS::Redshift::ClusterSubnetGroup",
132
+ "AWS::Redshift::ClusterParameterGroup",
133
+ // ECR
134
+ "AWS::ECR::Repository",
135
+ // GlobalAccelerator
136
+ "AWS::GlobalAccelerator::Accelerator",
137
+ // ACM
138
+ "AWS::CertificateManager::Certificate",
139
+ // SSM
140
+ "AWS::SSM::Document",
141
+ "AWS::SSM::MaintenanceWindow",
142
+ "AWS::SSM::PatchBaseline",
143
+ ]);
144
+
145
+ // Resource types that do NOT support tagging.
146
+ // Using a Set for O(1) lookups. Kept as skip-list because CF adds new
147
+ // taggable types regularly and we want them tagged by default.
148
+ const SKIP_TYPES = new Set([
149
+ // Lambda
150
+ "AWS::Lambda::Version",
151
+ "AWS::Lambda::EventSourceMapping",
152
+ "AWS::Lambda::LayerVersion",
153
+ "AWS::Lambda::EventInvokeConfig",
154
+ "AWS::Lambda::Alias",
155
+ "AWS::Lambda::Permission",
156
+ "AWS::Lambda::LayerVersionPermission",
157
+ "AWS::Lambda::Url",
158
+ // CloudWatch Logs
159
+ "AWS::Logs::LogStream",
160
+ "AWS::Logs::Destination",
161
+ "AWS::Logs::MetricFilter",
162
+ "AWS::Logs::QueryDefinition",
163
+ "AWS::Logs::ResourcePolicy",
164
+ "AWS::Logs::SubscriptionFilter",
165
+ // API Gateway v1
166
+ "AWS::ApiGateway::Account",
167
+ "AWS::ApiGateway::ApiKey",
168
+ "AWS::ApiGateway::Method",
169
+ "AWS::ApiGateway::Deployment",
170
+ "AWS::ApiGateway::UsagePlanKey",
171
+ "AWS::ApiGateway::BasePathMapping",
172
+ "AWS::ApiGateway::Resource",
173
+ "AWS::ApiGateway::Model",
174
+ "AWS::ApiGateway::RequestValidator",
175
+ "AWS::ApiGateway::GatewayResponse",
176
+ "AWS::ApiGateway::Authorizer",
177
+ // API Gateway v2
178
+ "AWS::ApiGatewayV2::Integration",
179
+ "AWS::ApiGatewayV2::Route",
180
+ "AWS::ApiGatewayV2::ApiMapping",
181
+ "AWS::ApiGatewayV2::ApiGatewayManagedOverrides",
182
+ "AWS::ApiGatewayV2::Authorizer",
183
+ "AWS::ApiGatewayV2::Deployment",
184
+ "AWS::ApiGatewayV2::IntegrationResponse",
185
+ "AWS::ApiGatewayV2::Model",
186
+ "AWS::ApiGatewayV2::RouteResponse",
187
+ // AppSync
188
+ "AWS::AppSync::DataSource",
189
+ "AWS::AppSync::ApiKey",
190
+ "AWS::AppSync::ApiCache",
191
+ "AWS::AppSync::DomainName",
192
+ "AWS::AppSync::DomainNameApiAssociation",
193
+ "AWS::AppSync::FunctionConfiguration",
194
+ "AWS::AppSync::GraphQLSchema",
195
+ "AWS::AppSync::Resolver",
196
+ // AutoScaling
197
+ "AWS::AutoScaling::AutoScalingGroup",
198
+ // Backup
199
+ "AWS::Backup::BackupVault",
200
+ "AWS::Backup::BackupSelection",
201
+ "AWS::Backup::BackupPlan",
202
+ // CodeDeploy
203
+ "AWS::CodeDeploy::Application",
204
+ "AWS::CodeDeploy::DeploymentConfig",
205
+ // Cognito
206
+ "AWS::Cognito::IdentityPool",
207
+ "AWS::Cognito::IdentityPoolRoleAttachment",
208
+ "AWS::Cognito::UserPool",
209
+ "AWS::Cognito::UserPoolDomain",
210
+ "AWS::Cognito::UserPoolClient",
211
+ "AWS::Cognito::UserPoolGroup",
212
+ "AWS::Cognito::UserPoolUser",
213
+ "AWS::Cognito::UserPoolUserToGroupAttachment",
214
+ "AWS::Cognito::UserPoolIdentityProvider",
215
+ "AWS::Cognito::UserPoolResourceServer",
216
+ // CloudWatch
217
+ "AWS::CloudWatch::Alarm",
218
+ "AWS::CloudWatch::Dashboard",
219
+ // CloudFront
220
+ "AWS::CloudFront::CloudFrontOriginAccessIdentity",
221
+ "AWS::CloudFront::OriginAccessControl",
222
+ "AWS::CloudFront::OriginRequestPolicy",
223
+ "AWS::CloudFront::Function",
224
+ "AWS::CloudFront::ResponseHeadersPolicy",
225
+ "AWS::CloudFront::CachePolicy",
226
+ // Elastic Beanstalk
227
+ "AWS::ElasticBeanstalk::ApplicationVersion",
228
+ "AWS::ElasticBeanstalk::ConfigurationTemplate",
229
+ // ELB
230
+ "AWS::ElasticLoadBalancingV2::Listener",
231
+ "AWS::ElasticLoadBalancingV2::ListenerRule",
232
+ // ECS
233
+ "AWS::ECS::ClusterCapacityProviderAssociations",
234
+ "AWS::ECS::PrimaryTaskSet",
235
+ // EC2
236
+ "AWS::EC2::SecurityGroupEgress",
237
+ "AWS::EC2::SecurityGroupIngress",
238
+ "AWS::EC2::LaunchTemplate",
239
+ "AWS::EC2::VPCGatewayAttachment",
240
+ "AWS::EC2::Route",
241
+ "AWS::EC2::SubnetRouteTableAssociation",
242
+ "AWS::EC2::VPCDHCPOptionsAssociation",
243
+ "AWS::EC2::VPCEndpoint",
244
+ "AWS::EC2::TransitGatewayRoute",
245
+ "AWS::EC2::TransitGatewayRouteTableAssociation",
246
+ "AWS::EC2::TransitGatewayRouteTablePropagation",
247
+ "AWS::EC2::IPAMAllocation",
248
+ "AWS::EC2::IPAMPoolCidr",
249
+ // Events
250
+ "AWS::Events::Rule",
251
+ "AWS::Events::EventBus",
252
+ "AWS::Events::EventBusPolicy",
253
+ "AWS::Events::Connection",
254
+ "AWS::Events::ApiDestination",
255
+ "AWS::Events::Endpoint",
256
+ "AWS::Events::Archive",
257
+ // EFS
258
+ "AWS::EFS::FileSystem",
259
+ "AWS::EFS::MountTarget",
260
+ "AWS::EFS::AccessPoint",
261
+ // GlobalAccelerator
262
+ "AWS::GlobalAccelerator::Listener",
263
+ "AWS::GlobalAccelerator::EndpointGroup",
264
+ // Glue
265
+ "AWS::Glue::Database",
266
+ "AWS::Glue::Classifier",
267
+ "AWS::Glue::Connection",
268
+ "AWS::Glue::DataCatalogEncryptionSettings",
269
+ "AWS::Glue::Partition",
270
+ "AWS::Glue::SchemaVersion",
271
+ "AWS::Glue::SchemaVersionMetadata",
272
+ "AWS::Glue::SecurityConfiguration",
273
+ "AWS::Glue::Table",
274
+ // Grafana
275
+ "AWS::Grafana::Workspace",
276
+ // KMS
277
+ "AWS::KMS::Alias",
278
+ // Route53
279
+ "AWS::Route53::HostedZone",
280
+ "AWS::Route53::RecordSet",
281
+ "AWS::Route53::RecordSetGroup",
282
+ "AWS::Route53::HealthCheck",
283
+ // RDS
284
+ "AWS::RDS::DBProxyTargetGroup",
285
+ // S3
286
+ "AWS::S3::AccessPoint",
287
+ "AWS::S3::MultiRegionAccessPoint",
288
+ "AWS::S3::MultiRegionAccessPointPolicy",
289
+ "AWS::S3::BucketPolicy",
290
+ // SES
291
+ "AWS::SES::ReceiptRuleSet",
292
+ "AWS::SES::ReceiptRule",
293
+ "AWS::SES::ConfigurationSet",
294
+ "AWS::SES::ConfigurationSetEventDestination",
295
+ "AWS::SES::ReceiptFilter",
296
+ "AWS::SES::Template",
297
+ // SNS / SQS
298
+ "AWS::SNS::Subscription",
299
+ "AWS::SNS::TopicPolicy",
300
+ "AWS::SQS::QueuePolicy",
301
+ // SSM
302
+ "AWS::SSM::ResourceDataSync",
303
+ // Secrets Manager
304
+ "AWS::SecretsManager::SecretTargetAttachment",
305
+ "AWS::SecretsManager::RotationSchedule",
306
+ "AWS::SecretsManager::ResourcePolicy",
307
+ // IAM
308
+ "AWS::IAM::Policy",
309
+ "AWS::IAM::AccessKey",
310
+ "AWS::IAM::UserToGroupAddition",
311
+ "AWS::IAM::ServiceLinkedRole",
312
+ "AWS::IAM::ManagedPolicy",
313
+ "AWS::IAM::InstanceProfile",
314
+ "AWS::IAM::Group",
315
+ "AWS::IAM::RolePolicy",
316
+ // Application AutoScaling
317
+ "AWS::ApplicationAutoScaling::ScalableTarget",
318
+ "AWS::ApplicationAutoScaling::ScalingPolicy",
319
+ // WAF
320
+ "AWS::WAFv2::WebACLAssociation",
321
+ "AWS::WAFv2::LoggingConfiguration",
322
+ // OpenSearch Serverless
323
+ "AWS::OpenSearchServerless::AccessPolicy",
324
+ "AWS::OpenSearchServerless::SecurityPolicy",
325
+ "AWS::OpenSearchServerless::VpcEndpoint",
326
+ // Pinpoint
327
+ "AWS::PinpointEmail::ConfigurationSetEventDestination",
328
+ "AWS::Pinpoint::ADMChannel",
329
+ "AWS::Pinpoint::APNSChannel",
330
+ "AWS::Pinpoint::APNSSandboxChannel",
331
+ "AWS::Pinpoint::APNSVoipChannel",
332
+ "AWS::Pinpoint::APNSVoipSandboxChannel",
333
+ "AWS::Pinpoint::ApplicationSettings",
334
+ "AWS::Pinpoint::BaiduChannel",
335
+ "AWS::Pinpoint::EmailChannel",
336
+ "AWS::Pinpoint::EventStream",
337
+ "AWS::Pinpoint::GCMChannel",
338
+ "AWS::Pinpoint::SMSChannel",
339
+ "AWS::Pinpoint::VoiceChannel",
340
+ // Others
341
+ "AWS::Pipes::Pipe",
342
+ "AWS::QLDB::Ledger",
343
+ "AWS::Scheduler::Schedule",
344
+ "AWS::Athena::NamedQuery",
345
+ ]);
346
+
347
+ /**
348
+ * Classify a CloudFormation resource type.
349
+ *
350
+ * @param {string} resourceType - e.g. "AWS::Lambda::Function"
351
+ * @returns {'list'|'dict'|'api-only'|'related'|'skip'}
352
+ */
353
+ function classifyResource(resourceType) {
354
+ if (!resourceType) return "skip";
355
+
356
+ // Custom resources are never tagged
357
+ if (resourceType.toLowerCase().startsWith("custom::")) return "skip";
358
+
359
+ // Check specific classifications first
360
+ if (DICT_BASED_TYPES.has(resourceType)) return "dict";
361
+ if (API_ONLY_TYPES.has(resourceType)) return "api-only";
362
+ if (RELATED_TYPES.has(resourceType)) return "related";
363
+ if (SKIP_TYPES.has(resourceType)) return "skip";
364
+ if (LIST_BASED_TYPES.has(resourceType)) return "list";
365
+
366
+ // Unknown type: attempt list-based tagging with warning
367
+ return "unclassified";
368
+ }
369
+
370
+ /**
371
+ * Check if a resource type needs post-deploy API tagging.
372
+ */
373
+ function needsPostDeployTagging(resourceType) {
374
+ return (
375
+ DICT_BASED_TYPES.has(resourceType) ||
376
+ API_ONLY_TYPES.has(resourceType) ||
377
+ RELATED_TYPES.has(resourceType)
378
+ );
379
+ }
380
+
381
+ module.exports = { classifyResource, needsPostDeployTagging, LIST_BASED_TYPES, DICT_BASED_TYPES, API_ONLY_TYPES, RELATED_TYPES, SKIP_TYPES };
package/src/tags.js ADDED
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Tag building module.
5
+ *
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.
11
+ */
12
+
13
+ // Tags injected by SFW that should be removed post-deploy
14
+ const TAGS_TO_REMOVE = ["STAGE"];
15
+
16
+ /**
17
+ * Build list-based tags [{Key, Value}] for CloudFormation resources.
18
+ * Merges stackTags + auto-generated datamart:* tags.
19
+ */
20
+ function buildListTags(stackTags, stage, logicalId) {
21
+ const tags = [];
22
+ const seen = new Set();
23
+
24
+ // User-provided stackTags
25
+ if (stackTags && typeof stackTags === "object") {
26
+ for (const [key, value] of Object.entries(stackTags)) {
27
+ tags.push({ Key: key, Value: String(value) });
28
+ seen.add(key.toLowerCase());
29
+ }
30
+ }
31
+
32
+ // Auto-generated datamart:* tags only
33
+ const autoTags = [
34
+ { Key: "datamart:environment", Value: stage },
35
+ { Key: "datamart:resource", Value: logicalId },
36
+ ];
37
+
38
+ for (const tag of autoTags) {
39
+ if (!seen.has(tag.Key.toLowerCase())) {
40
+ tags.push(tag);
41
+ seen.add(tag.Key.toLowerCase());
42
+ }
43
+ }
44
+
45
+ return tags;
46
+ }
47
+
48
+ /**
49
+ * Build dict-based tags {key: value} for resources that use map format
50
+ * (SSM Parameter, API Gateway V2, Glue, Batch, etc.)
51
+ */
52
+ function buildDictTags(stackTags, stage, logicalId) {
53
+ const tags = {};
54
+
55
+ // User-provided stackTags
56
+ if (stackTags && typeof stackTags === "object") {
57
+ for (const [key, value] of Object.entries(stackTags)) {
58
+ tags[key] = String(value);
59
+ }
60
+ }
61
+
62
+ // Auto-generated datamart:* tags only — don't overwrite user-provided
63
+ if (!tags["datamart:environment"]) tags["datamart:environment"] = stage;
64
+ if (!tags["datamart:resource"]) tags["datamart:resource"] = logicalId;
65
+
66
+ return tags;
67
+ }
68
+
69
+ /**
70
+ * Filter out aws: prefixed tags (reserved by AWS, cannot be set by user).
71
+ */
72
+ function excludeAwsTags(tags) {
73
+ return tags.filter(
74
+ (tag) => !tag.Key.toLowerCase().startsWith("aws:")
75
+ );
76
+ }
77
+
78
+ module.exports = { buildListTags, buildDictTags, excludeAwsTags, TAGS_TO_REMOVE };
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+
3
+ const { buildListTags, buildDictTags } = require("./tags");
4
+ const { classifyResource } = require("./resource-classifier");
5
+
6
+ /**
7
+ * Template Tagger — Hook: before:package:finalize
8
+ *
9
+ * Mutates the compiled CloudFormation template to inject tags
10
+ * into resource Properties before deployment.
11
+ */
12
+
13
+ /**
14
+ * Tag all resources in a CloudFormation resources object.
15
+ *
16
+ * @param {object} resources - { LogicalId: { Type, Properties, ... } }
17
+ * @param {object} stackTags - from provider.stackTags
18
+ * @param {string} stage - deployment stage
19
+ * @param {Function} log - logging function
20
+ */
21
+ function tagResources(resources, stackTags, stage, log) {
22
+ if (!resources) return;
23
+
24
+ for (const [logicalId, resource] of Object.entries(resources)) {
25
+ const resourceType = resource.Type;
26
+ if (!resourceType) continue;
27
+
28
+ const classification = classifyResource(resourceType);
29
+
30
+ if (classification === "skip") continue;
31
+
32
+ if (!resource.Properties) {
33
+ log(`TAGGING: skipping ${resourceType} (${logicalId}) — no Properties`);
34
+ continue;
35
+ }
36
+
37
+ switch (classification) {
38
+ case "list":
39
+ tagListBased(resource, stackTags, stage, logicalId);
40
+ break;
41
+ case "dict":
42
+ tagDictBased(resource, stackTags, stage, logicalId);
43
+ break;
44
+ case "unclassified":
45
+ log(`TAGGING: WARNING — unclassified resource type ${resourceType} (${logicalId}). Attempting list-based tagging. If deploy fails, add to skipTypes.`);
46
+ tagListBased(resource, stackTags, stage, logicalId);
47
+ break;
48
+ case "api-only":
49
+ case "related":
50
+ // These are tagged post-deploy via API, not in the template
51
+ break;
52
+ }
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Apply list-based tags [{Key, Value}] to a resource.
58
+ * Merges with existing tags without overwriting.
59
+ */
60
+ function tagListBased(resource, stackTags, stage, logicalId) {
61
+ const newTags = buildListTags(stackTags, stage, logicalId);
62
+ const existing = resource.Properties.Tags || [];
63
+
64
+ // Build set of existing keys (case-insensitive)
65
+ const existingKeys = new Set(
66
+ existing.map((t) => (t.Key || "").toLowerCase())
67
+ );
68
+
69
+ // Only add tags that don't already exist
70
+ const merged = [
71
+ ...existing,
72
+ ...newTags.filter((t) => !existingKeys.has(t.Key.toLowerCase())),
73
+ ];
74
+
75
+ resource.Properties.Tags = merged;
76
+ }
77
+
78
+ /**
79
+ * Apply dict-based tags {key: value} to a resource.
80
+ * Merges with existing tags without overwriting.
81
+ */
82
+ function tagDictBased(resource, stackTags, stage, logicalId) {
83
+ const newTags = buildDictTags(stackTags, stage, logicalId);
84
+ const existing = resource.Properties.Tags || {};
85
+
86
+ // Merge: existing keys take priority
87
+ resource.Properties.Tags = { ...newTags, ...existing };
88
+ }
89
+
90
+ module.exports = { tagResources, tagListBased, tagDictBased };
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Tag validation module.
5
+ *
6
+ * When enabled (custom.datamart.validation: true), validates that
7
+ * required datamart:* tags are present in stackTags and values
8
+ * match the allowed domains.
9
+ */
10
+
11
+ const REQUIRED_TAGS = [
12
+ "datamart:cost-center",
13
+ "datamart:finops-scope",
14
+ "datamart:environment",
15
+ "datamart:data-classification",
16
+ "datamart:criticality",
17
+ "datamart:team",
18
+ ];
19
+
20
+ const ALLOWED_VALUES = {
21
+ "datamart:finops-scope": [
22
+ "runtime", "compliance", "security", "scraping", "ai", "devtools", "infrastructure",
23
+ ],
24
+ "datamart:environment": [
25
+ "prod", "homo", "qa", "dev", "sandbox", "dr",
26
+ ],
27
+ "datamart:data-classification": [
28
+ "public", "internal", "confidential", "restricted",
29
+ ],
30
+ "datamart:criticality": [
31
+ "critical", "high", "medium", "low",
32
+ ],
33
+ "datamart:cost-center": [
34
+ "datamart", "getdata", "connect", "vizdata", "legalbase", "facesign",
35
+ "keyshield", "lendbot", "jscipher", "atlas", "openfinance", "payments", "operations",
36
+ ],
37
+ };
38
+
39
+ /**
40
+ * Validate stackTags against the datamart tag strategy.
41
+ *
42
+ * @param {object} stackTags - from provider.stackTags
43
+ * @param {string} stage - deployment stage (used to auto-set datamart:environment)
44
+ * @returns {{ valid: boolean, errors: string[] }}
45
+ */
46
+ function validateTags(stackTags, stage) {
47
+ const errors = [];
48
+ const tags = stackTags || {};
49
+
50
+ // Build effective tags (including auto-generated ones)
51
+ const effective = { ...tags };
52
+ if (!effective["datamart:environment"]) {
53
+ effective["datamart:environment"] = stage;
54
+ }
55
+
56
+ // Check required tags
57
+ for (const required of REQUIRED_TAGS) {
58
+ if (!effective[required]) {
59
+ errors.push(`Missing required tag: ${required}`);
60
+ }
61
+ }
62
+
63
+ // Check allowed values
64
+ for (const [tag, allowed] of Object.entries(ALLOWED_VALUES)) {
65
+ const value = effective[tag];
66
+ if (value && !allowed.includes(value)) {
67
+ errors.push(
68
+ `Invalid value for ${tag}: "${value}". Allowed: ${allowed.join(", ")}`
69
+ );
70
+ }
71
+ }
72
+
73
+ return { valid: errors.length === 0, errors };
74
+ }
75
+
76
+ module.exports = { validateTags, REQUIRED_TAGS, ALLOWED_VALUES };
@@ -1,20 +0,0 @@
1
- const aws = require('aws-sdk')
2
-
3
- module.exports = class CloudformationClient {
4
- constructor(options = {}) {
5
- this.CloudFormation = new aws.CloudFormation(options)
6
- }
7
-
8
- async describeStackResources(stackName){
9
- let params = {
10
- StackName: stackName
11
- }
12
- try {
13
- let stackResources = await this.CloudFormation.describeStackResources(params).promise()
14
- return stackResources
15
- } catch (error) {
16
- console.log(`CloudformationClient::describeStackResources An error has occurred: ${JSON.stringify(error)}`)
17
- return false
18
- }
19
- }
20
- }
package/awsSSM.js DELETED
@@ -1,22 +0,0 @@
1
- const aws = require('aws-sdk')
2
-
3
- module.exports = class SSMClient {
4
- constructor(options = {}) {
5
- this.SSM = new aws.SSM(options)
6
- }
7
-
8
- async addTagsToResource(pResourceId, pResourceType, pTags){
9
- let params = {
10
- ResourceId: pResourceId,
11
- ResourceType: pResourceType,
12
- Tags: pTags
13
- }
14
- try {
15
- let addUpdateTags = await this.SSM.addTagsToResource(params).promise()
16
- return addUpdateTags
17
- } catch (error) {
18
- console.log(`SSMClient::addTagsToResource An error has occurred: ${JSON.stringify(error)}`)
19
- return false
20
- }
21
- }
22
- }
@@ -1,17 +0,0 @@
1
- image: node:lts
2
-
3
- pipelines:
4
-
5
- branches:
6
- master:
7
- - step:
8
- name: Deploy New Version
9
- deployment: prod
10
- caches:
11
- - node
12
- script:
13
- - export TAG_VERSION=$(git describe --tags `git rev-list --tags --max-count=1`)
14
- # - npm install -g npm-cli-login
15
- # - npm-cli-login
16
- - npm version $TAG_VERSION
17
- - npm publish