@intentius/chant 0.52.2 → 0.53.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.
Files changed (125) hide show
  1. package/dist/cdk/advise.d.ts +29 -0
  2. package/dist/cdk/advise.d.ts.map +1 -0
  3. package/dist/cdk/assembly.d.ts +38 -0
  4. package/dist/cdk/assembly.d.ts.map +1 -0
  5. package/dist/cdk/graph.d.ts +68 -0
  6. package/dist/cdk/graph.d.ts.map +1 -0
  7. package/dist/cdk/tier-map.d.ts +44 -0
  8. package/dist/cdk/tier-map.d.ts.map +1 -0
  9. package/dist/cdk/types.d.ts +114 -0
  10. package/dist/cdk/types.d.ts.map +1 -0
  11. package/dist/cli/commands/carve-apply.d.ts.map +1 -1
  12. package/dist/cli/commands/carve-bridge.d.ts.map +1 -1
  13. package/dist/cli/commands/carve-emit.d.ts.map +1 -1
  14. package/dist/cli/commands/carve.d.ts +48 -6
  15. package/dist/cli/commands/carve.d.ts.map +1 -1
  16. package/dist/cli/handlers/carve-emit.d.ts.map +1 -1
  17. package/dist/cli/handlers/carve.d.ts +5 -4
  18. package/dist/cli/handlers/carve.d.ts.map +1 -1
  19. package/dist/cli/handlers/lifecycle.d.ts +10 -0
  20. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  21. package/dist/cli/handlers/op-progress.d.ts +37 -4
  22. package/dist/cli/handlers/op-progress.d.ts.map +1 -1
  23. package/dist/cli/handlers/operator.d.ts +61 -1
  24. package/dist/cli/handlers/operator.d.ts.map +1 -1
  25. package/dist/cli/handlers/run.d.ts.map +1 -1
  26. package/dist/cli/main.d.ts.map +1 -1
  27. package/dist/cli/registry.d.ts +9 -1
  28. package/dist/cli/registry.d.ts.map +1 -1
  29. package/dist/identity.d.ts +196 -0
  30. package/dist/identity.d.ts.map +1 -0
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/lexicon.d.ts +41 -0
  34. package/dist/lexicon.d.ts.map +1 -1
  35. package/dist/lifecycle/converge-ledger.d.ts +81 -1
  36. package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
  37. package/dist/lifecycle/gate-ledger.d.ts +43 -1
  38. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  39. package/dist/terraform/adopt-state.d.ts +17 -66
  40. package/dist/terraform/adopt-state.d.ts.map +1 -1
  41. package/dist/terraform/aws-resources.d.ts +1 -1
  42. package/dist/terraform/carve-provider.d.ts +142 -0
  43. package/dist/terraform/carve-provider.d.ts.map +1 -0
  44. package/dist/terraform/carve.d.ts +36 -3
  45. package/dist/terraform/carve.d.ts.map +1 -1
  46. package/dist/terraform/emit-source.d.ts +25 -0
  47. package/dist/terraform/emit-source.d.ts.map +1 -0
  48. package/dist/terraform/graduate.d.ts +11 -1
  49. package/dist/terraform/graduate.d.ts.map +1 -1
  50. package/dist/terraform/providers/aws.d.ts +19 -0
  51. package/dist/terraform/providers/aws.d.ts.map +1 -0
  52. package/dist/terraform/providers/gcp.d.ts +41 -0
  53. package/dist/terraform/providers/gcp.d.ts.map +1 -0
  54. package/dist/terraform/providers/index.d.ts +15 -0
  55. package/dist/terraform/providers/index.d.ts.map +1 -0
  56. package/dist/terraform/providers/kubernetes.d.ts +29 -0
  57. package/dist/terraform/providers/kubernetes.d.ts.map +1 -0
  58. package/dist/terraform/score.d.ts +70 -4
  59. package/dist/terraform/score.d.ts.map +1 -1
  60. package/dist/terraform/tier-map.d.ts +30 -35
  61. package/dist/terraform/tier-map.d.ts.map +1 -1
  62. package/dist/terraform/types.d.ts +6 -0
  63. package/dist/terraform/types.d.ts.map +1 -1
  64. package/package.json +1 -1
  65. package/src/cdk/__fixtures__/cdk.out/AppStack.template.json +171 -0
  66. package/src/cdk/__fixtures__/cdk.out/DataStack.template.json +90 -0
  67. package/src/cdk/__fixtures__/cdk.out/cdk.out +1 -0
  68. package/src/cdk/__fixtures__/cdk.out/manifest.json +30 -0
  69. package/src/cdk/__fixtures__/cdk.out/tree.json +201 -0
  70. package/src/cdk/__fixtures__/cdk.out-dummy/LookupStack.template.json +29 -0
  71. package/src/cdk/__fixtures__/cdk.out-dummy/manifest.json +26 -0
  72. package/src/cdk/advise.test.ts +208 -0
  73. package/src/cdk/advise.ts +44 -0
  74. package/src/cdk/assembly.ts +133 -0
  75. package/src/cdk/graph.test.ts +206 -0
  76. package/src/cdk/graph.ts +525 -0
  77. package/src/cdk/tier-map.ts +71 -0
  78. package/src/cdk/types.ts +115 -0
  79. package/src/cli/commands/carve-apply.ts +20 -4
  80. package/src/cli/commands/carve-bridge.ts +9 -5
  81. package/src/cli/commands/carve-emit-k8s.test.ts +262 -0
  82. package/src/cli/commands/carve-emit-provider.test.ts +207 -0
  83. package/src/cli/commands/carve-emit.ts +28 -24
  84. package/src/cli/commands/carve.ts +139 -36
  85. package/src/cli/handlers/carve-emit.ts +8 -3
  86. package/src/cli/handlers/carve.ts +8 -7
  87. package/src/cli/handlers/lifecycle.test.ts +187 -1
  88. package/src/cli/handlers/lifecycle.ts +125 -1
  89. package/src/cli/handlers/op-progress.test.ts +67 -1
  90. package/src/cli/handlers/op-progress.ts +70 -14
  91. package/src/cli/handlers/operator.test.ts +302 -1
  92. package/src/cli/handlers/operator.ts +193 -5
  93. package/src/cli/handlers/run.ts +11 -13
  94. package/src/cli/main.test.ts +19 -0
  95. package/src/cli/main.ts +38 -10
  96. package/src/cli/registry.ts +9 -1
  97. package/src/identity.test.ts +199 -0
  98. package/src/identity.ts +346 -0
  99. package/src/index.ts +1 -0
  100. package/src/lexicon.ts +53 -0
  101. package/src/lifecycle/converge-ledger.test.ts +110 -0
  102. package/src/lifecycle/converge-ledger.ts +110 -2
  103. package/src/lifecycle/gate-ledger.test.ts +81 -1
  104. package/src/lifecycle/gate-ledger.ts +64 -1
  105. package/src/terraform/__fixtures__/gcp-estate/main.tf +60 -0
  106. package/src/terraform/adopt-state.test.ts +131 -0
  107. package/src/terraform/adopt-state.ts +19 -167
  108. package/src/terraform/aws-resources.test.ts +38 -22
  109. package/src/terraform/aws-resources.ts +1 -1
  110. package/src/terraform/carve-provider.test.ts +155 -0
  111. package/src/terraform/carve-provider.ts +237 -0
  112. package/src/terraform/carve.test.ts +55 -1
  113. package/src/terraform/carve.ts +0 -0
  114. package/src/terraform/emit-source.ts +39 -0
  115. package/src/terraform/graduate.test.ts +37 -0
  116. package/src/terraform/graduate.ts +55 -7
  117. package/src/terraform/graph.ts +3 -3
  118. package/src/terraform/providers/aws.ts +169 -0
  119. package/src/terraform/providers/gcp.test.ts +228 -0
  120. package/src/terraform/providers/gcp.ts +329 -0
  121. package/src/terraform/providers/index.ts +21 -0
  122. package/src/terraform/providers/kubernetes.ts +224 -0
  123. package/src/terraform/score.ts +111 -25
  124. package/src/terraform/tier-map.ts +45 -107
  125. package/src/terraform/types.ts +6 -0
@@ -0,0 +1,169 @@
1
+ /**
2
+ * The AWS carve provider (#2016) — the seam's implementation over the AWS
3
+ * carve-out table (`../aws-resources.ts`), which stays the single source of
4
+ * truth for which AWS Terraform types chant can carve and how.
5
+ *
6
+ * chant AWS constructors take CloudFormation PascalCase properties (e.g.
7
+ * `new Bucket({ BucketName })`), while Terraform state uses the provider's
8
+ * snake_case attributes (`bucket`, `tags`). Attributes without a mapping are
9
+ * preserved in a reference comment, never dropped.
10
+ *
11
+ * Deferred outbound inputs (#998): an attribute whose value the Terraform
12
+ * source read from a survivor (`vpc_id = aws_vpc.main.id`) is not a fact of
13
+ * the carved resource — it is a deploy-time input. Those props are emitted as
14
+ * `params.<name>` references instead of frozen literals, with the state's
15
+ * resolved value as the declared default (see `carve-emit.ts`'s scaffold).
16
+ */
17
+
18
+ import {
19
+ AWS_CARVE_TYPES,
20
+ AWS_LEXICON_IMPORT,
21
+ awsCarveType,
22
+ applyAwsMapper,
23
+ applyAwsFold,
24
+ unmappedFoldAttrs,
25
+ } from "../aws-resources";
26
+ import { PARAMS_IMPORT, ParamRef, renderObject } from "../emit-source";
27
+ import type { AdoptedSource, CarveProvider, DeferredParam, FoldedContribution, TierInfo } from "../carve-provider";
28
+ import type { StateResource } from "../state";
29
+
30
+ /**
31
+ * TF "sub-resource" types that inline into a parent resource rather than
32
+ * standing alone (TF splits config CloudFormation keeps in one resource).
33
+ * A sub-resource that shares its parent's name is folded into the parent's
34
+ * carve set: it is not ranked on its own, and its edge to the parent does not
35
+ * count as inbound boundary work — inlining it is free.
36
+ */
37
+ const AWS_FOLDS_INTO: Record<string, string> = {
38
+ aws_s3_bucket_versioning: "aws_s3_bucket",
39
+ aws_s3_bucket_acl: "aws_s3_bucket",
40
+ aws_s3_bucket_policy: "aws_s3_bucket",
41
+ aws_s3_bucket_public_access_block: "aws_s3_bucket",
42
+ aws_s3_bucket_server_side_encryption_configuration: "aws_s3_bucket",
43
+ aws_s3_bucket_lifecycle_configuration: "aws_s3_bucket",
44
+ aws_s3_bucket_website_configuration: "aws_s3_bucket",
45
+ aws_s3_bucket_cors_configuration: "aws_s3_bucket",
46
+ aws_s3_bucket_logging: "aws_s3_bucket",
47
+ aws_s3_bucket_ownership_controls: "aws_s3_bucket",
48
+ aws_s3_bucket_notification: "aws_s3_bucket",
49
+ aws_s3_bucket_accelerate_configuration: "aws_s3_bucket",
50
+ aws_s3_bucket_request_payment_configuration: "aws_s3_bucket",
51
+ aws_s3_bucket_intelligent_tiering_configuration: "aws_s3_bucket",
52
+ aws_ecr_lifecycle_policy: "aws_ecr_repository",
53
+ aws_ecr_repository_policy: "aws_ecr_repository",
54
+ };
55
+
56
+ const AWS_TIERS: Record<string, TierInfo> = Object.fromEntries(
57
+ AWS_CARVE_TYPES.map((t) => [t.tfType, { tier: t.tier, mapsTo: t.nativeType }]),
58
+ );
59
+
60
+ const AWS_IDENTITY_ATTRS: Record<string, string> = Object.fromEntries(
61
+ AWS_CARVE_TYPES.filter((t) => t.identityAttr).map((t) => [t.tfType, t.identityAttr!]),
62
+ );
63
+
64
+ /**
65
+ * Render chant source for a state-adopted resource. Emits the native
66
+ * constructor with mapped properties, plus a reference comment listing the
67
+ * Terraform attributes that were not mapped so nothing is silently dropped.
68
+ *
69
+ * A mapped attribute named by a `DeferredParam` renders as a `params.<name>`
70
+ * reference (a real chant build parameter) instead of the state literal —
71
+ * the value came from a survivor, so it stays overridable per build.
72
+ *
73
+ * `folded` carries the carve set's sub-resources (`aws_s3_bucket_versioning`
74
+ * and friends), read from the same state file. Their mappable attributes join
75
+ * the parent's props (#1637) — a fold that only announced itself and left the
76
+ * emitted resource without the versioning or public-access block the Terraform
77
+ * declared was a silent loss of configuration. A sub-resource's setting wins
78
+ * over the parent's own legacy in-state block: it is the one the config
79
+ * actually declares.
80
+ */
81
+ function adoptAwsFromState(
82
+ resource: StateResource,
83
+ params: DeferredParam[],
84
+ folded: StateResource[],
85
+ ): AdoptedSource | null {
86
+ const entry = awsCarveType(resource.type);
87
+ if (!entry) return null;
88
+
89
+ const { props, mappedKeys } = applyAwsMapper(entry, resource.attributes);
90
+
91
+ const contributions: FoldedContribution[] = [];
92
+ const foldedUnmapped: Record<string, Record<string, unknown>> = {};
93
+ for (const sub of folded) {
94
+ const address = `${sub.type}.${sub.name}`;
95
+ const fold = applyAwsFold(sub.type, sub.attributes);
96
+ // No fold mapping for this sub-resource type: it still carves with the
97
+ // parent, so report its attributes rather than dropping them on the floor.
98
+ const rest = fold ? fold.unmapped : unmappedFoldAttrs(sub.attributes);
99
+ if (fold) Object.assign(props, fold.props);
100
+ if (Object.keys(rest).length) foldedUnmapped[address] = rest;
101
+ contributions.push({ address, props: Object.keys(fold?.props ?? {}) });
102
+ }
103
+
104
+ // Substitute deferred inputs: only plain (untransformed) field mappings can
105
+ // carry a parameter reference — a transform ran against the literal at emit
106
+ // time and cannot re-run at build. Everything else keeps the state literal.
107
+ const parameterized: string[] = [];
108
+ for (const param of params) {
109
+ const spec = entry.fields[param.tfAttr];
110
+ if (typeof spec !== "string" || !(spec in props)) continue;
111
+ const type = typeof param.default === "number" ? "number" : typeof param.default === "boolean" ? "boolean" : "string";
112
+ props[spec] = new ParamRef(param.name, type);
113
+ parameterized.push(param.name);
114
+ }
115
+
116
+ const unmapped: Record<string, unknown> = {};
117
+ for (const [k, v] of Object.entries(resource.attributes)) {
118
+ if (!mappedKeys.includes(k)) unmapped[k] = v;
119
+ }
120
+ // A folded sub-resource's leftovers are keyed by its address, so the comment
121
+ // says which block a stray attribute came from.
122
+ for (const [address, attrs] of Object.entries(foldedUnmapped)) unmapped[address] = attrs;
123
+
124
+ const L: string[] = [];
125
+ L.push(`// Adopted from Terraform state: ${resource.type}.${resource.name} -> ${entry.nativeType}`);
126
+ L.push(`// Properties mapped from Terraform attributes (CloudFormation PascalCase).`);
127
+ for (const c of contributions) {
128
+ const into = c.props.length ? c.props.join(", ") : "nothing mappable — see the reference comment below";
129
+ L.push(`// Folded in ${c.address} -> ${into}`);
130
+ }
131
+ L.push(`import { ${entry.ctor} } from "${AWS_LEXICON_IMPORT}";`);
132
+ if (parameterized.length) {
133
+ L.push(`// Deferred deploy-time input(s) — declared in chant.config.ts's buildParams.`);
134
+ L.push(`import { params } from "${PARAMS_IMPORT}";`);
135
+ }
136
+ L.push("");
137
+ L.push(`export const ${resource.name} = new ${entry.ctor}(${renderObject(props, 2)});`);
138
+ if (Object.keys(unmapped).length) {
139
+ L.push("");
140
+ L.push("/* Unmapped Terraform attributes (reconcile to native props before building):");
141
+ L.push(JSON.stringify(unmapped, null, 2));
142
+ L.push("*/");
143
+ }
144
+
145
+ return {
146
+ fileName: `${resource.name}.ts`,
147
+ content: L.join("\n") + "\n",
148
+ mapped: Object.keys(props).length > 0,
149
+ nativeType: entry.nativeType,
150
+ parameterized,
151
+ folded: contributions,
152
+ };
153
+ }
154
+
155
+ export const awsCarveProvider: CarveProvider = {
156
+ name: "aws",
157
+ tfTypePrefixes: ["aws_"],
158
+ lexicon: "aws",
159
+ tiers: AWS_TIERS,
160
+ identityAttrs: AWS_IDENTITY_ATTRS,
161
+ foldsInto: AWS_FOLDS_INTO,
162
+ // Every ranked AWS type is emittable: advise and emit derive from the one
163
+ // table, so there is no advise↔emit cliff to fall off.
164
+ emitTypes: AWS_CARVE_TYPES.map((t) => t.tfType),
165
+ adopt: adoptAwsFromState,
166
+ // The live import filters a CloudFormation stack's template, so the selector
167
+ // is the CFN type the carve table already records.
168
+ liveSelectorType: (tfType) => awsCarveType(tfType)?.nativeType,
169
+ };
@@ -0,0 +1,228 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { GCP_IDENTITY_ATTRS, GCP_TIERS, gcpCarveProvider } from "./gcp";
6
+ import { resolveCarveProvider } from "../carve-provider";
7
+ import { canBridge, canCarveEmit, carveEmitTypes, identityAttrOf, foldParentOf, tierMap } from "../tier-map";
8
+ import { canAdoptFromState } from "../adopt-state";
9
+ import { carveAdvise } from "../../cli/commands/carve";
10
+ import { loadHcl2json } from "../parse";
11
+
12
+ /**
13
+ * The GCP carve provider (#2017). Two things are pinned here: that every
14
+ * `mapsTo` names a Config Connector type the gcp lexicon actually defines, and
15
+ * that coverage cannot silently shrink.
16
+ */
17
+ const repoRoot = fileURLToPath(new URL("../../../../../", import.meta.url));
18
+ const gcpMetaPath = join(repoRoot, "lexicons", "gcp", "dist", "meta.json");
19
+
20
+ interface MetaEntry {
21
+ kind: string;
22
+ resourceType: string;
23
+ }
24
+
25
+ /**
26
+ * Every Config Connector type the gcp lexicon defines, read from the bundle the
27
+ * lexicon's own codegen writes. A `mapsTo` checked against a hand-copied list
28
+ * would only prove the list agrees with itself.
29
+ */
30
+ function lexiconResourceTypes(): Set<string> {
31
+ if (!existsSync(gcpMetaPath)) {
32
+ throw new Error(
33
+ `lexicons/gcp/dist/meta.json is missing, so no mapsTo can be verified. Build it with \`just regen\` or \`npm run bundle -w lexicons/gcp\`.`,
34
+ );
35
+ }
36
+ const meta = JSON.parse(readFileSync(gcpMetaPath, "utf-8")) as Record<string, MetaEntry>;
37
+ return new Set(
38
+ Object.values(meta)
39
+ .filter((entry) => entry?.kind === "resource" && typeof entry.resourceType === "string")
40
+ .map((entry) => entry.resourceType),
41
+ );
42
+ }
43
+
44
+ const googleTierTypes = Object.keys(tierMap())
45
+ .filter((t) => t.startsWith("google_"))
46
+ .sort();
47
+
48
+ describe("GCP carve coverage (#2017)", () => {
49
+ test("every mapsTo is a Config Connector type the gcp lexicon defines", () => {
50
+ // A tier entry pointing at a type chant cannot build is worse than no
51
+ // entry: the report promises a carve target that does not exist.
52
+ const real = lexiconResourceTypes();
53
+ const dangling = Object.entries(GCP_TIERS)
54
+ .filter(([, info]) => !real.has(info.mapsTo))
55
+ .map(([tfType, info]) => `${tfType} -> ${info.mapsTo}`);
56
+ expect(dangling).toEqual([]);
57
+ });
58
+
59
+ test("coverage cannot shrink (no cliff)", () => {
60
+ // The floors are the guard: an entry removed, or a whole family dropped in
61
+ // a refactor, fails here instead of quietly narrowing what advise ranks.
62
+ expect(googleTierTypes.length).toBeGreaterThanOrEqual(200);
63
+ expect(Object.keys(GCP_IDENTITY_ATTRS).length).toBeGreaterThanOrEqual(170);
64
+
65
+ // The tier map's google_ half is exactly this provider's table — nothing
66
+ // ranks google types from anywhere else.
67
+ expect(googleTierTypes).toEqual(Object.keys(GCP_TIERS).sort());
68
+ for (const tfType of googleTierTypes) {
69
+ expect(resolveCarveProvider(tfType)).toBe(gcpCarveProvider);
70
+ }
71
+ });
72
+
73
+ test("covers the common carve targets across families", () => {
74
+ for (const tfType of [
75
+ // storage, compute, networking
76
+ "google_storage_bucket", "google_compute_network", "google_compute_subnetwork",
77
+ "google_compute_firewall", "google_compute_router", "google_compute_router_nat",
78
+ "google_compute_address", "google_compute_instance", "google_compute_disk",
79
+ "google_compute_backend_service", "google_compute_url_map", "google_compute_forwarding_rule",
80
+ // GKE and fleet
81
+ "google_container_cluster", "google_container_node_pool", "google_gke_hub_membership",
82
+ // identity
83
+ "google_service_account", "google_project_iam_custom_role", "google_project_iam_member",
84
+ "google_iam_workload_identity_pool", "google_iam_workload_identity_pool_provider",
85
+ // keys, secrets
86
+ "google_kms_key_ring", "google_kms_crypto_key", "google_secret_manager_secret",
87
+ // data
88
+ "google_sql_database_instance", "google_bigquery_dataset", "google_bigquery_table",
89
+ "google_spanner_instance", "google_bigtable_instance", "google_redis_instance",
90
+ "google_filestore_instance", "google_dataproc_cluster", "google_composer_environment",
91
+ // messaging, serverless
92
+ "google_pubsub_topic", "google_pubsub_subscription", "google_cloud_run_v2_service",
93
+ "google_cloudfunctions2_function", "google_cloud_scheduler_job", "google_workflows_workflow",
94
+ // dns, observability
95
+ "google_dns_managed_zone", "google_dns_record_set", "google_logging_project_sink",
96
+ "google_logging_metric", "google_monitoring_alert_policy",
97
+ // build, project, security
98
+ "google_artifact_registry_repository", "google_cloudbuild_trigger", "google_project",
99
+ "google_project_service", "google_privateca_ca_pool", "google_certificate_manager_certificate",
100
+ ]) {
101
+ expect(GCP_TIERS[tfType]).toBeDefined();
102
+ }
103
+ });
104
+
105
+ test("tiers reflect the reshaping the CNRM constructor demands", () => {
106
+ expect(GCP_TIERS.google_storage_bucket).toEqual({ tier: 1, mapsTo: "GCP::Storage::Bucket" });
107
+ expect(GCP_TIERS.google_pubsub_topic).toEqual({ tier: 1, mapsTo: "GCP::Pubsub::Topic" });
108
+ expect(GCP_TIERS.google_compute_network).toEqual({ tier: 1, mapsTo: "GCP::Compute::Network" });
109
+ // A sibling reference becomes a *Ref object, so the subnetwork reshapes.
110
+ expect(GCP_TIERS.google_compute_subnetwork.tier).toBe(2);
111
+ // Node pools inline into the cluster; the instance settles under `settings`.
112
+ expect(GCP_TIERS.google_container_cluster.tier).toBe(3);
113
+ expect(GCP_TIERS.google_sql_database_instance.tier).toBe(3);
114
+ // Every per-resource IAM membership lands on the one generic policy member.
115
+ for (const tfType of [
116
+ "google_project_iam_member",
117
+ "google_folder_iam_member",
118
+ "google_storage_bucket_iam_member",
119
+ "google_service_account_iam_member",
120
+ "google_kms_crypto_key_iam_member",
121
+ ]) {
122
+ expect(GCP_TIERS[tfType]).toEqual({ tier: 3, mapsTo: "GCP::Iam::PolicyMember" });
123
+ }
124
+ // Two Terraform types for one CNRM type is fine; the reverse is not checked.
125
+ expect(GCP_TIERS.google_compute_global_address.mapsTo).toBe(GCP_TIERS.google_compute_address.mapsTo);
126
+ });
127
+
128
+ test("identity attributes name the HCL attribute holding the physical name", () => {
129
+ expect(identityAttrOf("google_storage_bucket")).toBe("name");
130
+ expect(identityAttrOf("google_service_account")).toBe("account_id");
131
+ expect(identityAttrOf("google_bigquery_dataset")).toBe("dataset_id");
132
+ expect(identityAttrOf("google_bigquery_table")).toBe("table_id");
133
+ expect(identityAttrOf("google_secret_manager_secret")).toBe("secret_id");
134
+ expect(identityAttrOf("google_artifact_registry_repository")).toBe("repository_id");
135
+ expect(identityAttrOf("google_project")).toBe("project_id");
136
+ expect(identityAttrOf("google_project_service")).toBe("service");
137
+ expect(identityAttrOf("google_project_iam_custom_role")).toBe("role_id");
138
+ expect(identityAttrOf("google_iam_workload_identity_pool")).toBe("workload_identity_pool_id");
139
+ expect(identityAttrOf("google_monitoring_alert_policy")).toBe("display_name");
140
+ expect(identityAttrOf("google_tags_tag_key")).toBe("short_name");
141
+ expect(identityAttrOf("google_essential_contacts_contact")).toBe("email");
142
+
143
+ // A binding has no name of its own, so it declares none and the graph
144
+ // falls back to the Terraform logical name.
145
+ expect(identityAttrOf("google_project_iam_member")).toBeUndefined();
146
+
147
+ // Every identity entry is for a type the table ranks.
148
+ for (const tfType of Object.keys(GCP_IDENTITY_ATTRS)) {
149
+ expect(GCP_TIERS[tfType]).toBeDefined();
150
+ }
151
+ // No dotted paths, so `carve bridge` can render a flat data-source body.
152
+ for (const tfType of googleTierTypes) expect(canBridge(tfType)).toBe(true);
153
+ });
154
+
155
+ test("advise ranks GCP and emit refuses it on both paths", () => {
156
+ // Advise-only: the provider declares no emitTypes and no adopt, so the
157
+ // shared emit gate says no for every ranked type rather than accepting on
158
+ // --env what --state would reject (#2015).
159
+ expect(gcpCarveProvider.emitTypes).toBeUndefined();
160
+ expect(gcpCarveProvider.adopt).toBeUndefined();
161
+ expect(gcpCarveProvider.liveSelectorType).toBeUndefined();
162
+ for (const tfType of googleTierTypes) {
163
+ expect(canCarveEmit(tfType)).toBe(false);
164
+ expect(canAdoptFromState(tfType)).toBe(false);
165
+ }
166
+ expect(carveEmitTypes().filter((t) => t.startsWith("google_"))).toEqual([]);
167
+ });
168
+
169
+ test("nothing folds: google and CNRM granularity match", () => {
170
+ expect(gcpCarveProvider.foldsInto).toBeUndefined();
171
+ for (const tfType of googleTierTypes) expect(foldParentOf(tfType)).toBeUndefined();
172
+ });
173
+ });
174
+
175
+ /**
176
+ * The advisor over a google-provider estate, end to end through the registry.
177
+ * Skips cleanly when the optional wasm HCL parser is absent, like the AWS
178
+ * sample-estate test.
179
+ */
180
+ let parserAvailable = false;
181
+ try {
182
+ await loadHcl2json();
183
+ parserAvailable = true;
184
+ } catch {
185
+ parserAvailable = false;
186
+ }
187
+
188
+ const ESTATE = fileURLToPath(new URL("../__fixtures__/gcp-estate", import.meta.url));
189
+
190
+ describe("carve advise against the GCP sample estate (#2017)", () => {
191
+ test("bands a google estate", async () => {
192
+ if (!parserAvailable) return;
193
+ const r = await carveAdvise({ from: ESTATE });
194
+ expect(r.ok).toBe(true);
195
+ const byAddr = Object.fromEntries((r.results ?? []).map((x) => [x.address, x]));
196
+ expect(Object.keys(byAddr)).toHaveLength(10);
197
+
198
+ // Clean leaves: a tier-1 bucket with no edges, and a service account whose
199
+ // identity is account_id.
200
+ expect(byAddr["google_storage_bucket.assets"].score).toBe(100);
201
+ expect(byAddr["google_storage_bucket.assets"].band).toBe("clean leaf");
202
+ expect(byAddr["google_service_account.runner"].score).toBe(100);
203
+
204
+ // The graph read each physical name through the provider's identity attr,
205
+ // which is account_id for a service account and name for the bucket.
206
+ const identityOf = (address: string) => r.graph?.nodes.find((n) => n.address === address)?.identity;
207
+ expect(identityOf("google_service_account.runner")).toBe("myapp-runner");
208
+ expect(identityOf("google_storage_bucket.assets")).toBe("myapp-assets-prod");
209
+
210
+ // Tier 1 with one inbound subscription.
211
+ expect(byAddr["google_pubsub_topic.events"].score).toBe(88);
212
+ // Tier 2 with one outbound edge to the topic.
213
+ expect(byAddr["google_pubsub_subscription.worker"].score).toBe(81);
214
+
215
+ // The hub: four survivors read the network, so carving it is real work.
216
+ expect(byAddr["google_compute_network.main"].score).toBe(52);
217
+ expect(byAddr["google_compute_network.main"].band).toBe("carvable w/ edits");
218
+ expect(byAddr["google_compute_network.main"].breakdown.inbound).toBe(4);
219
+
220
+ // Tier 3 with two outbound edges.
221
+ expect(byAddr["google_container_cluster.primary"].breakdown.tier).toBe(3);
222
+ expect(byAddr["google_container_cluster.primary"].score).toBe(62);
223
+
224
+ // Unsupported provider still scores 0 beside a ranked google estate.
225
+ expect(byAddr["random_pet.suffix"].score).toBe(0);
226
+ expect(byAddr["random_pet.suffix"].band).toBe("leave in Terraform");
227
+ });
228
+ });