@intentius/chant 0.41.20 → 0.44.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/dist/build.d.ts +15 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/carve-apply.d.ts +9 -0
- package/dist/cli/commands/carve-apply.d.ts.map +1 -1
- package/dist/cli/commands/carve-bridge.d.ts +7 -0
- package/dist/cli/commands/carve-bridge.d.ts.map +1 -1
- package/dist/cli/commands/carve-emit.d.ts +7 -0
- package/dist/cli/commands/carve-emit.d.ts.map +1 -1
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/handlers/carve-apply.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/explain.d.ts +11 -0
- package/dist/cli/handlers/explain.d.ts.map +1 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +8 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +2 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-rule-scanning.d.ts +11 -0
- package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
- package/dist/codegen/okf-lexicon.d.ts +36 -0
- package/dist/codegen/okf-lexicon.d.ts.map +1 -0
- package/dist/codegen/package.d.ts +2 -1
- package/dist/codegen/package.d.ts.map +1 -1
- package/dist/components/deploy-units.d.ts.map +1 -1
- package/dist/components/verbs/sbom.d.ts +1 -1
- package/dist/components/verbs/sbom.d.ts.map +1 -1
- package/dist/components/verbs/vuln-gate.d.ts +15 -4
- package/dist/components/verbs/vuln-gate.d.ts.map +1 -1
- package/dist/components/verbs/vuln-scan.d.ts +18 -6
- package/dist/components/verbs/vuln-scan.d.ts.map +1 -1
- package/dist/config.d.ts +12 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +13 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/lexicon.d.ts +47 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/okf.d.ts +57 -0
- package/dist/okf.d.ts.map +1 -0
- package/dist/terraform/__fixtures__/build-graph.d.ts +13 -0
- package/dist/terraform/__fixtures__/build-graph.d.ts.map +1 -0
- package/dist/terraform/adopt-state.d.ts +33 -1
- package/dist/terraform/adopt-state.d.ts.map +1 -1
- package/dist/terraform/aws-resources.d.ts.map +1 -1
- package/dist/terraform/bridge.d.ts +7 -1
- package/dist/terraform/bridge.d.ts.map +1 -1
- package/dist/terraform/carve.d.ts +12 -0
- package/dist/terraform/carve.d.ts.map +1 -1
- package/dist/terraform/excise.d.ts +31 -0
- package/dist/terraform/excise.d.ts.map +1 -0
- package/dist/terraform/graduate.d.ts +12 -0
- package/dist/terraform/graduate.d.ts.map +1 -1
- package/dist/terraform/graph.d.ts +35 -5
- package/dist/terraform/graph.d.ts.map +1 -1
- package/dist/terraform/manifest.d.ts +89 -0
- package/dist/terraform/manifest.d.ts.map +1 -0
- package/dist/terraform/parse.d.ts +9 -4
- package/dist/terraform/parse.d.ts.map +1 -1
- package/dist/terraform/tier-map.d.ts +4 -3
- package/dist/terraform/tier-map.d.ts.map +1 -1
- package/dist/terraform/types.d.ts +7 -0
- package/dist/terraform/types.d.ts.map +1 -1
- package/dist/terraform/unified-diff.d.ts +11 -0
- package/dist/terraform/unified-diff.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/__snapshots__/okf.test.ts.snap +381 -0
- package/src/audit/rules-doc.ts +1 -1
- package/src/build.test.ts +95 -0
- package/src/build.ts +46 -1
- package/src/cli/commands/build.ts +9 -1
- package/src/cli/commands/carve-apply.test.ts +94 -2
- package/src/cli/commands/carve-apply.ts +62 -11
- package/src/cli/commands/carve-bridge.test.ts +82 -4
- package/src/cli/commands/carve-bridge.ts +79 -11
- package/src/cli/commands/carve-emit-state.test.ts +110 -1
- package/src/cli/commands/carve-emit.ts +184 -8
- package/src/cli/commands/init.ts +1 -1
- package/src/cli/handlers/carve-apply.ts +1 -0
- package/src/cli/handlers/components.ts +4 -2
- package/src/cli/handlers/explain.test.ts +93 -0
- package/src/cli/handlers/explain.ts +60 -0
- package/src/cli/handlers/lifecycle.ts +16 -9
- package/src/cli/main.ts +21 -5
- package/src/cli/mcp/server.test.ts +17 -0
- package/src/cli/mcp/tools/explain.ts +15 -3
- package/src/cli/plugins.ts +17 -0
- package/src/cli/registry.ts +2 -0
- package/src/codegen/__snapshots__/okf-lexicon.test.ts.snap +170 -0
- package/src/codegen/docs-rule-scanning.ts +35 -14
- package/src/codegen/okf-lexicon.test.ts +248 -0
- package/src/codegen/okf-lexicon.ts +303 -0
- package/src/codegen/package.ts +29 -4
- package/src/components/config-defaults.test.ts +30 -0
- package/src/components/deploy-units.test.ts +13 -0
- package/src/components/deploy-units.ts +5 -0
- package/src/components/verbs/__fixtures__/grype-with-kev-epss.json +1 -0
- package/src/components/verbs/__fixtures__/trivy-with-kev-epss.json +631 -0
- package/src/components/verbs/exploitability-roundtrip.test.ts +190 -0
- package/src/components/verbs/sbom.ts +1 -1
- package/src/components/verbs/vuln-gate.test.ts +144 -1
- package/src/components/verbs/vuln-gate.ts +61 -10
- package/src/components/verbs/vuln-scan.test.ts +94 -0
- package/src/components/verbs/vuln-scan.ts +44 -9
- package/src/config.test.ts +21 -0
- package/src/config.ts +18 -0
- package/src/deep-observation.test.ts +19 -0
- package/src/deep-observation.ts +32 -13
- package/src/lexicon.ts +50 -0
- package/src/lifecycle/observe.test.ts +18 -0
- package/src/lifecycle/observe.ts +10 -3
- package/src/lifecycle/snapshot.ts +5 -4
- package/src/okf.test.ts +169 -0
- package/src/okf.ts +308 -0
- package/src/terraform/__fixtures__/build-graph.ts +25 -0
- package/src/terraform/adopt-state.test.ts +34 -1
- package/src/terraform/adopt-state.ts +69 -2
- package/src/terraform/aws-resources.test.ts +52 -2
- package/src/terraform/aws-resources.ts +95 -4
- package/src/terraform/bridge.test.ts +38 -14
- package/src/terraform/bridge.ts +48 -14
- package/src/terraform/carve.test.ts +9 -7
- package/src/terraform/carve.ts +0 -0
- package/src/terraform/excise.test.ts +92 -0
- package/src/terraform/excise.ts +139 -0
- package/src/terraform/graduate.test.ts +73 -6
- package/src/terraform/graduate.ts +49 -0
- package/src/terraform/graph.test.ts +73 -10
- package/src/terraform/graph.ts +124 -47
- package/src/terraform/manifest.test.ts +105 -0
- package/src/terraform/manifest.ts +162 -0
- package/src/terraform/parse.test.ts +39 -1
- package/src/terraform/parse.ts +36 -9
- package/src/terraform/score.test.ts +8 -8
- package/src/terraform/state.test.ts +4 -4
- package/src/terraform/tier-map.ts +60 -8
- package/src/terraform/types.ts +7 -0
- package/src/terraform/unified-diff.test.ts +77 -0
- package/src/terraform/unified-diff.ts +146 -0
|
@@ -120,7 +120,7 @@ export const AWS_CARVE_TYPES: AwsCarveType[] = [
|
|
|
120
120
|
fields: { name: "AutoScalingGroupName", min_size: "MinSize", max_size: "MaxSize", desired_capacity: "DesiredCapacity", vpc_zone_identifier: "VPCZoneIdentifier", health_check_type: "HealthCheckType" } },
|
|
121
121
|
{ tfType: "aws_ecs_cluster", tier: 1, nativeType: "AWS::ECS::Cluster", ctor: "EcsCluster", identityAttr: "name",
|
|
122
122
|
fields: { name: "ClusterName" }, tags: true },
|
|
123
|
-
{ tfType: "aws_ecs_task_definition", tier: 3, nativeType: "AWS::ECS::TaskDefinition", ctor: "TaskDefinition",
|
|
123
|
+
{ tfType: "aws_ecs_task_definition", tier: 3, nativeType: "AWS::ECS::TaskDefinition", ctor: "TaskDefinition", identityAttr: "family",
|
|
124
124
|
fields: { family: "Family", cpu: "Cpu", memory: "Memory", network_mode: "NetworkMode", execution_role_arn: "ExecutionRoleArn", task_role_arn: "TaskRoleArn" }, tags: true },
|
|
125
125
|
{ tfType: "aws_ebs_volume", tier: 1, nativeType: "AWS::EC2::Volume", ctor: "EC2Volume",
|
|
126
126
|
fields: { availability_zone: "AvailabilityZone", size: "Size", type: "VolumeType", encrypted: "Encrypted", iops: "Iops" }, tags: true },
|
|
@@ -140,13 +140,13 @@ export const AWS_CARVE_TYPES: AwsCarveType[] = [
|
|
|
140
140
|
fields: { identifier: "DBInstanceIdentifier", engine: "Engine", engine_version: "EngineVersion", instance_class: "DBInstanceClass", allocated_storage: "AllocatedStorage", db_name: "DBName", multi_az: "MultiAZ", storage_encrypted: "StorageEncrypted" }, tags: true },
|
|
141
141
|
{ tfType: "aws_db_subnet_group", tier: 1, nativeType: "AWS::RDS::DBSubnetGroup", ctor: "RDSDBSubnetGroup", identityAttr: "name",
|
|
142
142
|
fields: { name: "DBSubnetGroupName", description: "DBSubnetGroupDescription", subnet_ids: "SubnetIds" }, tags: true },
|
|
143
|
-
{ tfType: "aws_elasticache_cluster", tier: 2, nativeType: "AWS::ElastiCache::CacheCluster", ctor: "CacheCluster",
|
|
144
|
-
fields: { engine: "Engine", node_type: "CacheNodeType", num_cache_nodes: "NumCacheNodes", engine_version: "EngineVersion" }, tags: true },
|
|
143
|
+
{ tfType: "aws_elasticache_cluster", tier: 2, nativeType: "AWS::ElastiCache::CacheCluster", ctor: "CacheCluster", identityAttr: "cluster_id",
|
|
144
|
+
fields: { cluster_id: "ClusterName", engine: "Engine", node_type: "CacheNodeType", num_cache_nodes: "NumCacheNodes", engine_version: "EngineVersion" }, tags: true },
|
|
145
145
|
{ tfType: "aws_elasticache_replication_group", tier: 2, nativeType: "AWS::ElastiCache::ReplicationGroup", ctor: "ReplicationGroup", identityAttr: "replication_group_id",
|
|
146
146
|
fields: { replication_group_id: "ReplicationGroupId", description: "ReplicationGroupDescription", node_type: "CacheNodeType", engine: "Engine" }, tags: true },
|
|
147
147
|
|
|
148
148
|
// ── DNS, CDN & API ──
|
|
149
|
-
{ tfType: "aws_route53_record", tier: 2, nativeType: "AWS::Route53::RecordSet", ctor: "RecordSet",
|
|
149
|
+
{ tfType: "aws_route53_record", tier: 2, nativeType: "AWS::Route53::RecordSet", ctor: "RecordSet", identityAttr: "name",
|
|
150
150
|
fields: { name: "Name", type: "Type", ttl: "TTL", zone_id: "HostedZoneId", records: "ResourceRecords" } },
|
|
151
151
|
{ tfType: "aws_api_gateway_rest_api", tier: 2, nativeType: "AWS::ApiGateway::RestApi", ctor: "RestApi", identityAttr: "name",
|
|
152
152
|
fields: { name: "Name", description: "Description" }, tags: true },
|
|
@@ -168,6 +168,97 @@ export const AWS_CARVE_TYPES: AwsCarveType[] = [
|
|
|
168
168
|
fields: { protocol: "Protocol", endpoint: "Endpoint", topic_arn: "TopicArn" } },
|
|
169
169
|
{ tfType: "aws_sfn_state_machine", tier: 3, nativeType: "AWS::StepFunctions::StateMachine", ctor: "StateMachine", identityAttr: "name",
|
|
170
170
|
fields: { name: "StateMachineName", role_arn: "RoleArn", type: "StateMachineType" }, tags: true },
|
|
171
|
+
{ tfType: "aws_sqs_queue_policy", tier: 2, nativeType: "AWS::SQS::QueuePolicy", ctor: "QueuePolicy",
|
|
172
|
+
fields: { policy: json("PolicyDocument"), queue_url: { prop: "Queues", transform: (v) => (v === undefined ? v : [v]) } } },
|
|
173
|
+
{ tfType: "aws_sns_topic_policy", tier: 2, nativeType: "AWS::SNS::TopicPolicy", ctor: "TopicPolicy",
|
|
174
|
+
fields: { policy: json("PolicyDocument"), arn: { prop: "Topics", transform: (v) => (v === undefined ? v : [v]) } } },
|
|
175
|
+
|
|
176
|
+
// ── Streaming ──
|
|
177
|
+
{ tfType: "aws_kinesis_stream", tier: 1, nativeType: "AWS::Kinesis::Stream", ctor: "KinesisStream", identityAttr: "name",
|
|
178
|
+
fields: { name: "Name", shard_count: "ShardCount", retention_period: "RetentionPeriodHours" }, tags: true },
|
|
179
|
+
{ tfType: "aws_kinesis_firehose_delivery_stream", tier: 3, nativeType: "AWS::KinesisFirehose::DeliveryStream", ctor: "DeliveryStream", identityAttr: "name",
|
|
180
|
+
fields: { name: "DeliveryStreamName" }, tags: true },
|
|
181
|
+
|
|
182
|
+
// ── EKS ──
|
|
183
|
+
{ tfType: "aws_eks_cluster", tier: 2, nativeType: "AWS::EKS::Cluster", ctor: "EKSCluster", identityAttr: "name",
|
|
184
|
+
fields: { name: "Name", role_arn: "RoleArn", version: "Version" }, tags: true },
|
|
185
|
+
{ tfType: "aws_eks_node_group", tier: 2, nativeType: "AWS::EKS::Nodegroup", ctor: "Nodegroup", identityAttr: "node_group_name",
|
|
186
|
+
fields: { cluster_name: "ClusterName", node_group_name: "NodegroupName", node_role_arn: "NodeRole", subnet_ids: "Subnets",
|
|
187
|
+
instance_types: "InstanceTypes", ami_type: "AmiType", capacity_type: "CapacityType", disk_size: "DiskSize" } },
|
|
188
|
+
|
|
189
|
+
// ── Lambda periphery ──
|
|
190
|
+
{ tfType: "aws_lambda_permission", tier: 1, nativeType: "AWS::Lambda::Permission", ctor: "Permission",
|
|
191
|
+
fields: { action: "Action", function_name: "FunctionName", principal: "Principal", source_arn: "SourceArn", source_account: "SourceAccount" } },
|
|
192
|
+
{ tfType: "aws_lambda_event_source_mapping", tier: 2, nativeType: "AWS::Lambda::EventSourceMapping", ctor: "EventSourceMapping",
|
|
193
|
+
fields: { event_source_arn: "EventSourceArn", function_name: "FunctionName", batch_size: "BatchSize", enabled: "Enabled",
|
|
194
|
+
starting_position: "StartingPosition", maximum_batching_window_in_seconds: "MaximumBatchingWindowInSeconds" } },
|
|
195
|
+
{ tfType: "aws_lambda_alias", tier: 1, nativeType: "AWS::Lambda::Alias", ctor: "LambdaAlias", identityAttr: "name",
|
|
196
|
+
fields: { name: "Name", function_name: "FunctionName", function_version: "FunctionVersion", description: "Description" } },
|
|
197
|
+
{ tfType: "aws_lambda_layer_version", tier: 2, nativeType: "AWS::Lambda::LayerVersion", ctor: "LayerVersion", identityAttr: "layer_name",
|
|
198
|
+
fields: { layer_name: "LayerName", description: "Description", compatible_runtimes: "CompatibleRuntimes",
|
|
199
|
+
compatible_architectures: "CompatibleArchitectures", license_info: "LicenseInfo" } },
|
|
200
|
+
|
|
201
|
+
// ── Networking (endpoints, routes, peering, flow logs) ──
|
|
202
|
+
{ tfType: "aws_vpc_endpoint", tier: 2, nativeType: "AWS::EC2::VPCEndpoint", ctor: "VPCEndpoint",
|
|
203
|
+
fields: { vpc_id: "VpcId", service_name: "ServiceName", vpc_endpoint_type: "VpcEndpointType", route_table_ids: "RouteTableIds",
|
|
204
|
+
subnet_ids: "SubnetIds", security_group_ids: "SecurityGroupIds", private_dns_enabled: "PrivateDnsEnabled", policy: json("PolicyDocument") }, tags: true },
|
|
205
|
+
{ tfType: "aws_route_table_association", tier: 2, nativeType: "AWS::EC2::SubnetRouteTableAssociation", ctor: "SubnetRouteTableAssociation",
|
|
206
|
+
fields: { subnet_id: "SubnetId", route_table_id: "RouteTableId" } },
|
|
207
|
+
{ tfType: "aws_route", tier: 2, nativeType: "AWS::EC2::Route", ctor: "EC2Route",
|
|
208
|
+
fields: { route_table_id: "RouteTableId", destination_cidr_block: "DestinationCidrBlock", destination_ipv6_cidr_block: "DestinationIpv6CidrBlock",
|
|
209
|
+
gateway_id: "GatewayId", nat_gateway_id: "NatGatewayId", transit_gateway_id: "TransitGatewayId",
|
|
210
|
+
vpc_peering_connection_id: "VpcPeeringConnectionId", network_interface_id: "NetworkInterfaceId" } },
|
|
211
|
+
{ tfType: "aws_egress_only_internet_gateway", tier: 1, nativeType: "AWS::EC2::EgressOnlyInternetGateway", ctor: "EgressOnlyInternetGateway",
|
|
212
|
+
fields: { vpc_id: "VpcId" } },
|
|
213
|
+
{ tfType: "aws_vpc_peering_connection", tier: 2, nativeType: "AWS::EC2::VPCPeeringConnection", ctor: "VPCPeeringConnection",
|
|
214
|
+
fields: { vpc_id: "VpcId", peer_vpc_id: "PeerVpcId", peer_owner_id: "PeerOwnerId", peer_region: "PeerRegion" }, tags: true },
|
|
215
|
+
{ tfType: "aws_flow_log", tier: 2, nativeType: "AWS::EC2::FlowLog", ctor: "FlowLog",
|
|
216
|
+
fields: { traffic_type: "TrafficType", log_destination: "LogDestination", log_destination_type: "LogDestinationType",
|
|
217
|
+
iam_role_arn: "DeliverLogsPermissionArn", log_group_name: "LogGroupName", max_aggregation_interval: "MaxAggregationInterval" }, tags: true },
|
|
218
|
+
{ tfType: "aws_key_pair", tier: 1, nativeType: "AWS::EC2::KeyPair", ctor: "KeyPair", identityAttr: "key_name",
|
|
219
|
+
fields: { key_name: "KeyName", public_key: "PublicKeyMaterial" }, tags: true },
|
|
220
|
+
|
|
221
|
+
// ── Audit & observability ──
|
|
222
|
+
{ tfType: "aws_cloudtrail", tier: 2, nativeType: "AWS::CloudTrail::Trail", ctor: "Trail", identityAttr: "name",
|
|
223
|
+
fields: { name: "TrailName", s3_bucket_name: "S3BucketName", s3_key_prefix: "S3KeyPrefix",
|
|
224
|
+
include_global_service_events: "IncludeGlobalServiceEvents", is_multi_region_trail: "IsMultiRegionTrail",
|
|
225
|
+
enable_logging: "IsLogging", enable_log_file_validation: "EnableLogFileValidation", kms_key_id: "KMSKeyId",
|
|
226
|
+
sns_topic_name: "SnsTopicName", cloud_watch_logs_group_arn: "CloudWatchLogsLogGroupArn", cloud_watch_logs_role_arn: "CloudWatchLogsRoleArn" }, tags: true },
|
|
227
|
+
{ tfType: "aws_cloudwatch_dashboard", tier: 1, nativeType: "AWS::CloudWatch::Dashboard", ctor: "CwDashboard", identityAttr: "dashboard_name",
|
|
228
|
+
fields: { dashboard_name: "DashboardName", dashboard_body: "DashboardBody" } },
|
|
229
|
+
|
|
230
|
+
// ── Databases & cache (groups, clusters) ──
|
|
231
|
+
{ tfType: "aws_elasticache_subnet_group", tier: 1, nativeType: "AWS::ElastiCache::SubnetGroup", ctor: "EcSubnetGroup", identityAttr: "name",
|
|
232
|
+
fields: { name: "CacheSubnetGroupName", description: "Description", subnet_ids: "SubnetIds" }, tags: true },
|
|
233
|
+
{ tfType: "aws_db_parameter_group", tier: 2, nativeType: "AWS::RDS::DBParameterGroup", ctor: "RDSDBParameterGroup", identityAttr: "name",
|
|
234
|
+
fields: { name: "DBParameterGroupName", family: "Family", description: "Description" }, tags: true },
|
|
235
|
+
{ tfType: "aws_rds_cluster", tier: 2, nativeType: "AWS::RDS::DBCluster", ctor: "DbCluster", identityAttr: "cluster_identifier",
|
|
236
|
+
fields: { cluster_identifier: "DBClusterIdentifier", engine: "Engine", engine_version: "EngineVersion", database_name: "DatabaseName",
|
|
237
|
+
master_username: "MasterUsername", backup_retention_period: "BackupRetentionPeriod", preferred_backup_window: "PreferredBackupWindow",
|
|
238
|
+
storage_encrypted: "StorageEncrypted", kms_key_id: "KmsKeyId", port: "Port" }, tags: true },
|
|
239
|
+
|
|
240
|
+
// ── Load balancing & API stages ──
|
|
241
|
+
{ tfType: "aws_lb_listener_rule", tier: 3, nativeType: "AWS::ElasticLoadBalancingV2::ListenerRule", ctor: "ListenerRule",
|
|
242
|
+
fields: { listener_arn: "ListenerArn", priority: "Priority" } },
|
|
243
|
+
{ tfType: "aws_api_gateway_stage", tier: 2, nativeType: "AWS::ApiGateway::Stage", ctor: "ApigwStage", identityAttr: "stage_name",
|
|
244
|
+
fields: { rest_api_id: "RestApiId", stage_name: "StageName", deployment_id: "DeploymentId", description: "Description" }, tags: true },
|
|
245
|
+
{ tfType: "aws_api_gateway_deployment", tier: 2, nativeType: "AWS::ApiGateway::Deployment", ctor: "ApigwDeployment",
|
|
246
|
+
fields: { rest_api_id: "RestApiId", description: "Description" } },
|
|
247
|
+
{ tfType: "aws_apigatewayv2_stage", tier: 2, nativeType: "AWS::ApiGatewayV2::Stage", ctor: "Apigwv2Stage", identityAttr: "name",
|
|
248
|
+
fields: { api_id: "ApiId", name: "StageName", auto_deploy: "AutoDeploy", description: "Description" } },
|
|
249
|
+
|
|
250
|
+
// ── Identity & scaling ──
|
|
251
|
+
{ tfType: "aws_cognito_user_pool", tier: 3, nativeType: "AWS::Cognito::UserPool", ctor: "UserPool", identityAttr: "name",
|
|
252
|
+
fields: { name: "UserPoolName", mfa_configuration: "MfaConfiguration", deletion_protection: "DeletionProtection" } },
|
|
253
|
+
{ tfType: "aws_appautoscaling_target", tier: 2, nativeType: "AWS::ApplicationAutoScaling::ScalableTarget", ctor: "ScalableTarget",
|
|
254
|
+
fields: { max_capacity: "MaxCapacity", min_capacity: "MinCapacity", resource_id: "ResourceId",
|
|
255
|
+
scalable_dimension: "ScalableDimension", service_namespace: "ServiceNamespace", role_arn: "RoleARN" } },
|
|
256
|
+
|
|
257
|
+
// ── EFS periphery ──
|
|
258
|
+
{ tfType: "aws_efs_mount_target", tier: 1, nativeType: "AWS::EFS::MountTarget", ctor: "EFSMountTarget",
|
|
259
|
+
fields: { file_system_id: "FileSystemId", subnet_id: "SubnetId", ip_address: "IpAddress", security_groups: "SecurityGroups" } },
|
|
260
|
+
{ tfType: "aws_efs_access_point", tier: 2, nativeType: "AWS::EFS::AccessPoint", ctor: "EFSAccessPoint",
|
|
261
|
+
fields: { file_system_id: "FileSystemId" } },
|
|
171
262
|
];
|
|
172
263
|
|
|
173
264
|
const BY_TYPE = new Map(AWS_CARVE_TYPES.map((t) => [t.tfType, t]));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { buildFixtureGraph } from "./__fixtures__/build-graph";
|
|
3
3
|
import { boundaryReport } from "./carve";
|
|
4
4
|
import { generateBridge, type CarvedIdentity } from "./bridge";
|
|
5
5
|
import type { Hcl2JsonTree } from "./types";
|
|
@@ -41,7 +41,7 @@ const identities = new Map<string, CarvedIdentity>([
|
|
|
41
41
|
|
|
42
42
|
describe("generateBridge — inbound (data-source rewrite)", () => {
|
|
43
43
|
test("adds a data source for the carved bucket", () => {
|
|
44
|
-
const report = boundaryReport(
|
|
44
|
+
const report = boundaryReport(buildFixtureGraph(workedExample), "aws_s3_bucket.assets")!;
|
|
45
45
|
const plan = generateBridge(report, [{ path: "api.tf", content: API_TF }], identities);
|
|
46
46
|
|
|
47
47
|
expect(plan.dataSources).toHaveLength(1);
|
|
@@ -52,7 +52,7 @@ describe("generateBridge — inbound (data-source rewrite)", () => {
|
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
test("rewrites survivor references to the data source", () => {
|
|
55
|
-
const report = boundaryReport(
|
|
55
|
+
const report = boundaryReport(buildFixtureGraph(workedExample), "aws_s3_bucket.assets")!;
|
|
56
56
|
const plan = generateBridge(report, [{ path: "api.tf", content: API_TF }], identities);
|
|
57
57
|
|
|
58
58
|
const rewrite = plan.rewrites.find((r) => r.path === "api.tf")!;
|
|
@@ -64,7 +64,7 @@ describe("generateBridge — inbound (data-source rewrite)", () => {
|
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
test("does not double-prefix an already-rewritten reference (idempotent)", () => {
|
|
67
|
-
const report = boundaryReport(
|
|
67
|
+
const report = boundaryReport(buildFixtureGraph(workedExample), "aws_s3_bucket.assets")!;
|
|
68
68
|
const already = API_TF.replace(/aws_s3_bucket\.assets/g, "data.aws_s3_bucket.assets");
|
|
69
69
|
const plan = generateBridge(report, [{ path: "api.tf", content: already }], identities);
|
|
70
70
|
const rewrite = plan.rewrites.find((r) => r.path === "api.tf")!;
|
|
@@ -72,19 +72,39 @@ describe("generateBridge — inbound (data-source rewrite)", () => {
|
|
|
72
72
|
expect(rewrite.rewritten).not.toContain("data.data.aws_s3_bucket");
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
test("
|
|
76
|
-
const report = boundaryReport(
|
|
77
|
-
const decl = `resource "aws_s3_bucket" "assets" {\n bucket = "myapp-assets-prod"\n}\n`;
|
|
75
|
+
test("excises the carved resource's own declaration (#998 — the data source replaces it)", () => {
|
|
76
|
+
const report = boundaryReport(buildFixtureGraph(workedExample), "aws_s3_bucket.assets")!;
|
|
77
|
+
const decl = `resource "aws_s3_bucket" "assets" {\n bucket = "myapp-assets-prod"\n}\n\nresource "aws_sns_topic" "alerts" {\n name = "a"\n}\n`;
|
|
78
78
|
const plan = generateBridge(report, [{ path: "bucket.tf", content: decl }], identities);
|
|
79
79
|
const rewrite = plan.rewrites.find((r) => r.path === "bucket.tf")!;
|
|
80
|
-
// `
|
|
81
|
-
//
|
|
82
|
-
expect(rewrite.
|
|
83
|
-
expect(rewrite.
|
|
80
|
+
// After `terraform state rm`, a block left behind would re-create the
|
|
81
|
+
// resource on the next apply — so the bridge removes it.
|
|
82
|
+
expect(rewrite.changed).toBe(true);
|
|
83
|
+
expect(rewrite.excised).toEqual(["aws_s3_bucket.assets"]);
|
|
84
|
+
expect(rewrite.rewritten).not.toContain('resource "aws_s3_bucket" "assets"');
|
|
85
|
+
expect(rewrite.rewritten).toContain('resource "aws_sns_topic" "alerts"');
|
|
86
|
+
expect(plan.excised).toEqual(["aws_s3_bucket.assets"]);
|
|
87
|
+
expect(plan.runbook).toContain("removes the carved block(s): aws_s3_bucket.assets");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("a folded sub-resource's block is excised along with its parent", () => {
|
|
91
|
+
const withVersioning: Hcl2JsonTree = {
|
|
92
|
+
resource: {
|
|
93
|
+
...workedExample.resource,
|
|
94
|
+
aws_s3_bucket_versioning: {
|
|
95
|
+
assets: [{ bucket: "${aws_s3_bucket.assets.id}", versioning_configuration: { status: "Enabled" } }],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
const report = boundaryReport(buildFixtureGraph(withVersioning), "aws_s3_bucket.assets")!;
|
|
100
|
+
const decl = `resource "aws_s3_bucket" "assets" {\n bucket = "b"\n}\n\nresource "aws_s3_bucket_versioning" "assets" {\n bucket = aws_s3_bucket.assets.id\n}\n`;
|
|
101
|
+
const plan = generateBridge(report, [{ path: "bucket.tf", content: decl }], identities);
|
|
102
|
+
expect(plan.excised).toEqual(["aws_s3_bucket.assets", "aws_s3_bucket_versioning.assets"]);
|
|
103
|
+
expect(plan.rewrites[0].rewritten.trim()).toBe("");
|
|
84
104
|
});
|
|
85
105
|
|
|
86
106
|
test("unknown identity emits a TODO data source instead of a wrong value", () => {
|
|
87
|
-
const report = boundaryReport(
|
|
107
|
+
const report = boundaryReport(buildFixtureGraph(workedExample), "aws_s3_bucket.assets")!;
|
|
88
108
|
const plan = generateBridge(report, [{ path: "api.tf", content: API_TF }], new Map());
|
|
89
109
|
expect(plan.dataSources[0].hcl).toContain("# TODO");
|
|
90
110
|
});
|
|
@@ -98,17 +118,21 @@ describe("generateBridge — outbound (deferred inputs)", () => {
|
|
|
98
118
|
aws_sqs_queue: { dlq: [{ name: "d", redrive: "${aws_sns_topic.alerts.arn}" }] },
|
|
99
119
|
},
|
|
100
120
|
};
|
|
101
|
-
const report = boundaryReport(
|
|
121
|
+
const report = boundaryReport(buildFixtureGraph(tree), "aws_sqs_queue.dlq")!;
|
|
102
122
|
const plan = generateBridge(report, [], new Map());
|
|
103
123
|
expect(plan.dataSources).toEqual([]); // no inbound → no survivor patch
|
|
104
124
|
expect(plan.deferredInputs).toHaveLength(1);
|
|
105
125
|
expect(plan.deferredInputs[0]).toMatchObject({ survivor: "aws_sns_topic.alerts", carved: "aws_sqs_queue.dlq" });
|
|
126
|
+
// The deferred input names the build param emit declares for it (#998).
|
|
127
|
+
expect(plan.deferredInputs[0].params).toEqual(["redrive"]);
|
|
128
|
+
expect(plan.deferredInputs[0].note).toContain("build param `redrive`");
|
|
129
|
+
expect(plan.deferredInputs[0].note).toContain("--param");
|
|
106
130
|
});
|
|
107
131
|
});
|
|
108
132
|
|
|
109
133
|
describe("generateBridge — runbook", () => {
|
|
110
134
|
test("is reversible and observe-first, with the state-rm and import steps", () => {
|
|
111
|
-
const report = boundaryReport(
|
|
135
|
+
const report = boundaryReport(buildFixtureGraph(workedExample), "aws_s3_bucket.assets")!;
|
|
112
136
|
const plan = generateBridge(report, [{ path: "api.tf", content: API_TF }], identities);
|
|
113
137
|
expect(plan.runbook).toContain("terraform state rm aws_s3_bucket.assets");
|
|
114
138
|
expect(plan.runbook).toContain("does NOT destroy");
|
package/src/terraform/bridge.ts
CHANGED
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* is reversible via `terraform import`.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import type
|
|
22
|
+
import { deferredParamName, type CarveReport } from "./carve";
|
|
23
|
+
import { exciseResourceBlocks, type ExciseTarget } from "./excise";
|
|
23
24
|
|
|
24
25
|
export interface CarvedIdentity {
|
|
25
26
|
/** The HCL identity attribute, e.g. `bucket` or `name`. */
|
|
@@ -41,12 +42,16 @@ export interface FileRewrite {
|
|
|
41
42
|
original: string;
|
|
42
43
|
rewritten: string;
|
|
43
44
|
changed: boolean;
|
|
45
|
+
/** Carved addresses whose own `resource` block was removed from this file (#998). */
|
|
46
|
+
excised: string[];
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
export interface DeferredInput {
|
|
47
50
|
survivor: string;
|
|
48
51
|
carved: string;
|
|
49
52
|
attrs: string[];
|
|
53
|
+
/** Build-parameter name(s) the input becomes in the emitted project (#998). */
|
|
54
|
+
params: string[];
|
|
50
55
|
note: string;
|
|
51
56
|
}
|
|
52
57
|
|
|
@@ -55,6 +60,8 @@ export interface BridgePlan {
|
|
|
55
60
|
dataSources: DataSourceBlock[];
|
|
56
61
|
rewrites: FileRewrite[];
|
|
57
62
|
deferredInputs: DeferredInput[];
|
|
63
|
+
/** Every carved address whose own block the rewrites remove, across files. */
|
|
64
|
+
excised: string[];
|
|
58
65
|
runbook: string;
|
|
59
66
|
}
|
|
60
67
|
|
|
@@ -99,33 +106,56 @@ export function generateBridge(
|
|
|
99
106
|
hcl: dataSourceHcl(type, name, identities.get(address) ?? {}),
|
|
100
107
|
}));
|
|
101
108
|
|
|
102
|
-
//
|
|
109
|
+
// Excise the carve set's own blocks (#998): after `terraform state rm`, a
|
|
110
|
+
// block left behind would re-create the resource on the next apply. Then
|
|
111
|
+
// rewrite every remaining survivor reference into a data ref.
|
|
112
|
+
const exciseTargets: ExciseTarget[] = report.carveSet
|
|
113
|
+
.filter((m) => !m.address.startsWith("module."))
|
|
114
|
+
.map((m) => {
|
|
115
|
+
const [type, ...rest] = m.address.split(".");
|
|
116
|
+
return { address: m.address, type, name: rest.join(".") };
|
|
117
|
+
});
|
|
103
118
|
const rewrites: FileRewrite[] = files.map(({ path, content }) => {
|
|
104
|
-
|
|
119
|
+
const excision = exciseResourceBlocks(content, exciseTargets);
|
|
120
|
+
let rewritten = excision.content;
|
|
105
121
|
for (const address of carvedWithInbound.keys()) {
|
|
106
122
|
rewritten = rewritten.replace(referenceRegex(address), `data.${address}`);
|
|
107
123
|
}
|
|
108
|
-
return { path, original: content, rewritten, changed: rewritten !== content };
|
|
124
|
+
return { path, original: content, rewritten, changed: rewritten !== content, excised: excision.excised };
|
|
109
125
|
});
|
|
126
|
+
const excised = rewrites.flatMap((r) => r.excised);
|
|
110
127
|
|
|
111
|
-
const deferredInputs: DeferredInput[] = report.outbound.map((e) =>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
128
|
+
const deferredInputs: DeferredInput[] = report.outbound.map((e) => {
|
|
129
|
+
const params = (e.via ?? []).map(deferredParamName);
|
|
130
|
+
const wiring = params.length
|
|
131
|
+
? `emit declares it as build param ${params.map((p) => `\`${p}\``).join(", ")} — override with --param at build`
|
|
132
|
+
: "supply this as a deploy-time input to the carved component at apply";
|
|
133
|
+
return {
|
|
134
|
+
survivor: e.survivor,
|
|
135
|
+
carved: e.carved,
|
|
136
|
+
attrs: e.attrs,
|
|
137
|
+
params,
|
|
138
|
+
note: `${e.carved} reads ${e.survivor}.${e.attrs.join(", ")} — ${wiring}.`,
|
|
139
|
+
};
|
|
140
|
+
});
|
|
117
141
|
|
|
118
142
|
return {
|
|
119
143
|
target: report.target,
|
|
120
144
|
dataSources,
|
|
121
145
|
rewrites,
|
|
122
146
|
deferredInputs,
|
|
123
|
-
|
|
147
|
+
excised,
|
|
148
|
+
runbook: buildRunbook(report, dataSources, deferredInputs, excised),
|
|
124
149
|
};
|
|
125
150
|
}
|
|
126
151
|
|
|
127
152
|
/** The reversible, observe-first handoff runbook (#197). */
|
|
128
|
-
function buildRunbook(
|
|
153
|
+
function buildRunbook(
|
|
154
|
+
report: CarveReport,
|
|
155
|
+
dataSources: DataSourceBlock[],
|
|
156
|
+
deferred: DeferredInput[],
|
|
157
|
+
excised: string[],
|
|
158
|
+
): string {
|
|
129
159
|
const carvedAddrs = report.carveSet.map((m) => m.address);
|
|
130
160
|
const L: string[] = [];
|
|
131
161
|
L.push(`# Carve-out: ${report.target} → chant [observe-first, reversible]`);
|
|
@@ -140,10 +170,14 @@ function buildRunbook(report: CarveReport, dataSources: DataSourceBlock[], defer
|
|
|
140
170
|
L.push("");
|
|
141
171
|
L.push("## 3. Confirm no destroy, then patch the survivors");
|
|
142
172
|
L.push(" terraform plan # expect 0 to destroy; refs to the carved resource now error");
|
|
173
|
+
if (excised.length) {
|
|
174
|
+
L.push(` # the generated bridge patch removes the carved block(s): ${excised.join(", ")}`);
|
|
175
|
+
L.push(" # (without this, the next apply would re-create what state rm released)");
|
|
176
|
+
}
|
|
143
177
|
if (dataSources.length) {
|
|
144
|
-
L.push(" #
|
|
178
|
+
L.push(" # it also adds these data sources and rewires references:");
|
|
145
179
|
for (const ds of dataSources) L.push(` # data.${ds.type}.${ds.name} (for ${ds.address})`);
|
|
146
|
-
} else {
|
|
180
|
+
} else if (!excised.length) {
|
|
147
181
|
L.push(" # no inbound edges — no survivor patch needed.");
|
|
148
182
|
}
|
|
149
183
|
L.push(" terraform plan # expect: in-place updates to the survivors only");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { buildFixtureGraph } from "./__fixtures__/build-graph";
|
|
3
3
|
import { resolveCarveSet, boundaryReport } from "./carve";
|
|
4
4
|
import type { Hcl2JsonTree } from "./types";
|
|
5
5
|
|
|
@@ -27,7 +27,7 @@ const workedExample: Hcl2JsonTree = {
|
|
|
27
27
|
|
|
28
28
|
describe("resolveCarveSet", () => {
|
|
29
29
|
test("selected node plus its folded sub-resources", () => {
|
|
30
|
-
const set = resolveCarveSet(
|
|
30
|
+
const set = resolveCarveSet(buildFixtureGraph(workedExample), "aws_s3_bucket.assets");
|
|
31
31
|
expect(set.map((m) => m.address).sort()).toEqual([
|
|
32
32
|
"aws_s3_bucket.assets",
|
|
33
33
|
"aws_s3_bucket_versioning.assets",
|
|
@@ -37,13 +37,13 @@ describe("resolveCarveSet", () => {
|
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
test("empty for an unknown address", () => {
|
|
40
|
-
expect(resolveCarveSet(
|
|
40
|
+
expect(resolveCarveSet(buildFixtureGraph(workedExample), "aws_s3_bucket.nope")).toEqual([]);
|
|
41
41
|
});
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
describe("boundaryReport", () => {
|
|
45
45
|
test("classifies the Lambda inbound edge; folds the versioning internal edge away", () => {
|
|
46
|
-
const report = boundaryReport(
|
|
46
|
+
const report = boundaryReport(buildFixtureGraph(workedExample), "aws_s3_bucket.assets")!;
|
|
47
47
|
expect(report.target).toBe("aws_s3_bucket.assets");
|
|
48
48
|
expect(report.peelability).toBe(88);
|
|
49
49
|
expect(report.reversible).toBe(true);
|
|
@@ -56,6 +56,7 @@ describe("boundaryReport", () => {
|
|
|
56
56
|
survivor: "aws_lambda_function.api",
|
|
57
57
|
carved: "aws_s3_bucket.assets",
|
|
58
58
|
attrs: ["arn", "bucket"],
|
|
59
|
+
via: ["environment"],
|
|
59
60
|
bridge: "tf-data-source",
|
|
60
61
|
required: "immediately",
|
|
61
62
|
},
|
|
@@ -71,7 +72,7 @@ describe("boundaryReport", () => {
|
|
|
71
72
|
},
|
|
72
73
|
};
|
|
73
74
|
// Carve the queue: it depends on the topic (a survivor) → outbound/deferred.
|
|
74
|
-
const report = boundaryReport(
|
|
75
|
+
const report = boundaryReport(buildFixtureGraph(tree), "aws_sqs_queue.dlq")!;
|
|
75
76
|
expect(report.inbound).toEqual([]);
|
|
76
77
|
expect(report.outbound).toEqual([
|
|
77
78
|
{
|
|
@@ -79,6 +80,7 @@ describe("boundaryReport", () => {
|
|
|
79
80
|
survivor: "aws_sns_topic.alerts",
|
|
80
81
|
carved: "aws_sqs_queue.dlq",
|
|
81
82
|
attrs: ["arn"],
|
|
83
|
+
via: ["redrive"],
|
|
82
84
|
bridge: "deferred-input",
|
|
83
85
|
required: "at-apply",
|
|
84
86
|
},
|
|
@@ -87,11 +89,11 @@ describe("boundaryReport", () => {
|
|
|
87
89
|
|
|
88
90
|
test("unsupported type is flagged in diagnostics", () => {
|
|
89
91
|
const tree: Hcl2JsonTree = { resource: { random_pet: { n: [{ length: 2 }] } } };
|
|
90
|
-
const report = boundaryReport(
|
|
92
|
+
const report = boundaryReport(buildFixtureGraph(tree), "random_pet.n")!;
|
|
91
93
|
expect(report.diagnostics.join(" ")).toMatch(/no known native mapping/i);
|
|
92
94
|
});
|
|
93
95
|
|
|
94
96
|
test("null for an unknown target", () => {
|
|
95
|
-
expect(boundaryReport(
|
|
97
|
+
expect(boundaryReport(buildFixtureGraph(workedExample), "aws_s3_bucket.nope")).toBeNull();
|
|
96
98
|
});
|
|
97
99
|
});
|
package/src/terraform/carve.ts
CHANGED
|
Binary file
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { exciseResourceBlocks, type ExciseTarget } from "./excise";
|
|
3
|
+
|
|
4
|
+
const bucket: ExciseTarget = { address: "aws_s3_bucket.assets", type: "aws_s3_bucket", name: "assets" };
|
|
5
|
+
const versioning: ExciseTarget = {
|
|
6
|
+
address: "aws_s3_bucket_versioning.assets",
|
|
7
|
+
type: "aws_s3_bucket_versioning",
|
|
8
|
+
name: "assets",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe("exciseResourceBlocks", () => {
|
|
12
|
+
test("removes the target block, leaves the rest byte-for-byte", () => {
|
|
13
|
+
const tf = `# the bucket
|
|
14
|
+
resource "aws_s3_bucket" "assets" {
|
|
15
|
+
bucket = "myapp-assets-prod"
|
|
16
|
+
tags = { Team = "web" }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
resource "aws_lambda_function" "api" {
|
|
20
|
+
function_name = "myapp-api"
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
const r = exciseResourceBlocks(tf, [bucket]);
|
|
24
|
+
expect(r.excised).toEqual(["aws_s3_bucket.assets"]);
|
|
25
|
+
expect(r.content).toBe(`# the bucket
|
|
26
|
+
resource "aws_lambda_function" "api" {
|
|
27
|
+
function_name = "myapp-api"
|
|
28
|
+
}
|
|
29
|
+
`);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("removes several targets (a folded sub-resource travels with its parent)", () => {
|
|
33
|
+
const tf = `resource "aws_s3_bucket" "assets" {
|
|
34
|
+
bucket = "b"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
resource "aws_s3_bucket_versioning" "assets" {
|
|
38
|
+
bucket = aws_s3_bucket.assets.id
|
|
39
|
+
versioning_configuration {
|
|
40
|
+
status = "Enabled"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
resource "aws_sns_topic" "alerts" {
|
|
45
|
+
name = "a"
|
|
46
|
+
}
|
|
47
|
+
`;
|
|
48
|
+
const r = exciseResourceBlocks(tf, [bucket, versioning]);
|
|
49
|
+
expect(r.excised).toEqual(["aws_s3_bucket.assets", "aws_s3_bucket_versioning.assets"]);
|
|
50
|
+
expect(r.content).toBe(`resource "aws_sns_topic" "alerts" {\n name = "a"\n}\n`);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("a same-name block of a different type stays", () => {
|
|
54
|
+
const tf = `resource "aws_s3_bucket" "assets" { bucket = "b" }
|
|
55
|
+
resource "aws_s3_bucket_versioning" "assets" { bucket = "b" }
|
|
56
|
+
`;
|
|
57
|
+
const r = exciseResourceBlocks(tf, [bucket]);
|
|
58
|
+
expect(r.excised).toEqual(["aws_s3_bucket.assets"]);
|
|
59
|
+
expect(r.content).toContain('resource "aws_s3_bucket_versioning" "assets"');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("braces inside strings, interpolations, comments, and heredocs do not fool the scanner", () => {
|
|
63
|
+
const tf = `resource "aws_s3_bucket" "assets" {
|
|
64
|
+
bucket = "b"
|
|
65
|
+
# a } in a comment
|
|
66
|
+
policy = jsonencode({ Statement = [{ Effect = "Allow" }] })
|
|
67
|
+
note = "literal } and \${var.m["k}"]} too"
|
|
68
|
+
user_data = <<EOF
|
|
69
|
+
if true; then echo "}"; fi
|
|
70
|
+
EOF
|
|
71
|
+
}
|
|
72
|
+
resource "aws_sns_topic" "alerts" { name = "a" }
|
|
73
|
+
`;
|
|
74
|
+
const r = exciseResourceBlocks(tf, [bucket]);
|
|
75
|
+
expect(r.excised).toEqual(["aws_s3_bucket.assets"]);
|
|
76
|
+
expect(r.content).toBe(`resource "aws_sns_topic" "alerts" { name = "a" }\n`);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("no target present → the file comes back unchanged", () => {
|
|
80
|
+
const tf = `resource "aws_sns_topic" "alerts" { name = "a" }\n`;
|
|
81
|
+
const r = exciseResourceBlocks(tf, [bucket]);
|
|
82
|
+
expect(r.excised).toEqual([]);
|
|
83
|
+
expect(r.content).toBe(tf);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("an unbalanced block is left alone rather than corrupted", () => {
|
|
87
|
+
const tf = `resource "aws_s3_bucket" "assets" {\n bucket = "b"\n`;
|
|
88
|
+
const r = exciseResourceBlocks(tf, [bucket]);
|
|
89
|
+
expect(r.excised).toEqual([]);
|
|
90
|
+
expect(r.content).toBe(tf);
|
|
91
|
+
});
|
|
92
|
+
});
|