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.
- package/README.md +106 -20
- package/index.js +60 -575
- package/package.json +23 -8
- package/src/aws-clients.js +59 -0
- package/src/post-deploy/apigatewayv2.js +42 -0
- package/src/post-deploy/ec2-related.js +78 -0
- package/src/post-deploy/firehose.js +20 -0
- package/src/post-deploy/pinpoint.js +25 -0
- package/src/post-deploy/rds.js +16 -0
- package/src/post-deploy/ssm.js +16 -0
- package/src/post-deploy/untag.js +97 -0
- package/src/post-deploy-tagger.js +102 -0
- package/src/resource-classifier.js +381 -0
- package/src/tags.js +78 -0
- package/src/template-tagger.js +90 -0
- package/src/validation.js +76 -0
- package/awsCloudFormation.js +0 -20
- package/awsSSM.js +0 -22
- package/bitbucket-pipelines.yml +0 -17
|
@@ -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 };
|
package/awsCloudFormation.js
DELETED
|
@@ -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
|
-
}
|
package/bitbucket-pipelines.yml
DELETED
|
@@ -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
|