UNPKG

serverless-tag-resources

Version:

Datamart: Tag all AWS resources with dual legacy + datamart:* tag support

130 lines (114 loc) 4.39 kB
"use strict"; const { ResourceGroupsTaggingAPIClient, UntagResourcesCommand, } = require("@aws-sdk/client-resource-groups-tagging-api"); const { CloudFormationClient, DescribeStacksCommand, DescribeStackResourcesCommand, } = require("@aws-sdk/client-cloudformation"); const { getClient } = require("../aws-clients"); const { SKIP_TYPES } = require("../resource-classifier"); /** * Remove unwanted tags from the stack itself and all its resources. * Uses Resource Groups Tagging API (UntagResources) which works on * both CF stacks and individual resources. * * No UpdateStack — only tag removal, zero infrastructure changes. */ async function removeUnwantedTags(config, stackName, tagKeysToRemove, log) { const cfnClient = getClient(CloudFormationClient, config); const taggingClient = getClient(ResourceGroupsTaggingAPIClient, config); const arns = []; // 1. Get stack ARN const stackResult = await cfnClient.send( new DescribeStacksCommand({ StackName: stackName }) ); const stackArn = stackResult.Stacks?.[0]?.StackId; if (stackArn) arns.push(stackArn); // 2. Get resource ARNs (deduplicated, excluding types that don't support tags) const seen = new Set(); const resourceResult = await cfnClient.send( new DescribeStackResourcesCommand({ StackName: stackName }) ); for (const resource of resourceResult.StackResources || []) { if (SKIP_TYPES.has(resource.ResourceType)) continue; const arn = resolveArn(resource); if (arn && !seen.has(arn)) { seen.add(arn); arns.push(arn); } } if (arns.length === 0) return; // UntagResources accepts max 20 ARNs per call const batchSize = 20; let untagged = 0; let skipped = 0; for (let i = 0; i < arns.length; i += batchSize) { const batch = arns.slice(i, i + batchSize); try { const result = await taggingClient.send( new UntagResourcesCommand({ ResourceARNList: batch, TagKeys: tagKeysToRemove, }) ); const failedMap = result.FailedResourcesMap || {}; const failures = Object.keys(failedMap).length; untagged += batch.length - failures; skipped += failures; } catch (err) { skipped += batch.length; } } log(`TAGGING: Removed [${tagKeysToRemove.join(", ")}] from ${untagged} resources (${skipped} skipped)`); } /** * Resolve the ARN of a stack resource. * PhysicalResourceId is sometimes the ARN, sometimes just the name/ID. */ function resolveArn(resource) { const physicalId = resource.PhysicalResourceId; if (!physicalId) return null; // Already an ARN if (physicalId.startsWith("arn:")) return physicalId; // Extract account from stack ARN const stackId = resource.StackId || ""; const parts = stackId.split(":"); if (parts.length < 5) return null; const partition = parts[1]; const region = parts[3]; const account = parts[4]; // Build ARN by resource type const type = resource.ResourceType; const builders = { "AWS::Lambda::Function": () => `arn:${partition}:lambda:${region}:${account}:function:${physicalId}`, "AWS::SNS::Topic": () => `arn:${partition}:sns:${region}:${account}:${physicalId}`, "AWS::Events::EventBus": () => `arn:${partition}:events:${region}:${account}:event-bus/${physicalId}`, "AWS::Events::Rule": () => `arn:${partition}:events:${region}:${account}:rule/${physicalId}`, "AWS::Logs::LogGroup": () => `arn:${partition}:logs:${region}:${account}:log-group:${physicalId}`, "AWS::IAM::Role": () => `arn:${partition}:iam::${account}:role/${physicalId}`, "AWS::S3::Bucket": () => `arn:${partition}:s3:::${physicalId}`, "AWS::SSM::Parameter": () => `arn:${partition}:ssm:${region}:${account}:parameter${physicalId.startsWith("/") ? "" : "/"}${physicalId}`, "AWS::KMS::Key": () => `arn:${partition}:kms:${region}:${account}:key/${physicalId}`, "AWS::CodeBuild::Project": () => `arn:${partition}:codebuild:${region}:${account}:project/${physicalId}`, "AWS::ApiGateway::RestApi": () => `arn:${partition}:apigateway:${region}::/restapis/${physicalId}`, "AWS::WAFv2::WebACL": () => `arn:${partition}:wafv2:${region}:${account}:regional/webacl/${physicalId}`, }; const builder = builders[type]; return builder ? builder() : null; } module.exports = { removeUnwantedTags };