@intentius/chant-lexicon-azure 0.124.31 → 0.124.33

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 (144) hide show
  1. package/dist/actions/appservice.js +7 -0
  2. package/dist/actions/compute.js +9 -0
  3. package/dist/actions/container.js +14 -0
  4. package/dist/actions/identity.js +11 -0
  5. package/dist/actions/index.js +11 -0
  6. package/dist/actions/keyvault.js +12 -0
  7. package/dist/actions/network.js +9 -0
  8. package/dist/actions/sql.js +9 -0
  9. package/dist/actions/storage.js +15 -0
  10. package/dist/api/read-client.js +146 -0
  11. package/dist/codegen/docs-cli.js +2 -0
  12. package/dist/codegen/docs.js +83 -0
  13. package/dist/codegen/generate-cli.js +11 -0
  14. package/dist/codegen/generate-lexicon.js +77 -0
  15. package/dist/codegen/generate-typescript.js +163 -0
  16. package/dist/codegen/generate.js +106 -0
  17. package/dist/codegen/naming.js +139 -0
  18. package/dist/codegen/package.js +64 -0
  19. package/dist/composites/aks-cluster.js +68 -0
  20. package/dist/composites/app-service.js +62 -0
  21. package/dist/composites/application-gateway.js +107 -0
  22. package/dist/composites/catalog.js +856 -0
  23. package/dist/composites/container-instance.js +36 -0
  24. package/dist/composites/container-registry.js +62 -0
  25. package/dist/composites/cosmos-database.js +56 -0
  26. package/dist/composites/from-arm.js +47 -0
  27. package/dist/composites/function-app.js +57 -0
  28. package/dist/composites/index.js +16 -0
  29. package/dist/composites/keyvault.js +69 -0
  30. package/dist/composites/landing-zone.js +119 -0
  31. package/dist/composites/private-endpoint.js +61 -0
  32. package/dist/composites/redis-cache.js +21 -0
  33. package/dist/composites/service-bus-pipeline.js +54 -0
  34. package/dist/composites/sql-database.js +62 -0
  35. package/dist/composites/storage-account.js +59 -0
  36. package/dist/composites/vm-linux.js +139 -0
  37. package/dist/composites/vnet-default.js +102 -0
  38. package/dist/coverage.js +28 -0
  39. package/dist/deep-observe.js +356 -0
  40. package/dist/default-tags.js +50 -0
  41. package/dist/deploy-scopes.d.ts.map +1 -1
  42. package/dist/deploy-scopes.js +157 -0
  43. package/dist/describe-resources.js +187 -0
  44. package/dist/detect.js +17 -0
  45. package/dist/export-resources.js +108 -0
  46. package/dist/generated/index.d.ts.map +1 -1
  47. package/dist/generated/index.js +16598 -0
  48. package/dist/generated/runtime.js +4 -0
  49. package/dist/governance.js +144 -0
  50. package/dist/health.d.ts +42 -0
  51. package/dist/health.d.ts.map +1 -0
  52. package/dist/health.js +137 -0
  53. package/dist/import/generator.js +140 -0
  54. package/dist/import/live-export.js +39 -0
  55. package/dist/import/parser.js +192 -0
  56. package/dist/index.js +44 -0
  57. package/dist/integrity.json +5 -5
  58. package/dist/intrinsics.js +179 -0
  59. package/dist/lint/audit-catalog.js +36 -0
  60. package/dist/lint/audit-lineage.js +71 -0
  61. package/dist/lint/post-synth/arm-refs.d.ts +1 -0
  62. package/dist/lint/post-synth/arm-refs.d.ts.map +1 -1
  63. package/dist/lint/post-synth/arm-refs.js +96 -0
  64. package/dist/lint/post-synth/azr010.js +45 -0
  65. package/dist/lint/post-synth/azr011.js +43 -0
  66. package/dist/lint/post-synth/azr012.js +41 -0
  67. package/dist/lint/post-synth/azr013.js +50 -0
  68. package/dist/lint/post-synth/azr014.js +49 -0
  69. package/dist/lint/post-synth/azr015.js +66 -0
  70. package/dist/lint/post-synth/azr016.js +37 -0
  71. package/dist/lint/post-synth/azr017.js +37 -0
  72. package/dist/lint/post-synth/azr018.js +42 -0
  73. package/dist/lint/post-synth/azr019.js +41 -0
  74. package/dist/lint/post-synth/azr020.js +37 -0
  75. package/dist/lint/post-synth/azr021.js +36 -0
  76. package/dist/lint/post-synth/azr022.js +39 -0
  77. package/dist/lint/post-synth/azr023.js +39 -0
  78. package/dist/lint/post-synth/azr024.js +39 -0
  79. package/dist/lint/post-synth/azr025.js +37 -0
  80. package/dist/lint/post-synth/azr026.js +38 -0
  81. package/dist/lint/post-synth/azr027.js +37 -0
  82. package/dist/lint/post-synth/azr028.js +37 -0
  83. package/dist/lint/post-synth/azr029.js +38 -0
  84. package/dist/lint/post-synth/azr030.js +44 -0
  85. package/dist/lint/post-synth/azr031.js +36 -0
  86. package/dist/lint/post-synth/azr032.js +53 -0
  87. package/dist/lint/post-synth/azr033.js +35 -0
  88. package/dist/lint/post-synth/index.js +50 -0
  89. package/dist/lint/rules/hardcoded-location.js +82 -0
  90. package/dist/lint/rules/index.js +6 -0
  91. package/dist/lint/rules/nsg-wildcard.js +44 -0
  92. package/dist/lint/rules/storage-https.js +62 -0
  93. package/dist/lsp/completions.d.ts.map +1 -1
  94. package/dist/lsp/completions.js +18 -0
  95. package/dist/lsp/hover.d.ts.map +1 -1
  96. package/dist/lsp/hover.js +44 -0
  97. package/dist/manifest.json +1 -1
  98. package/dist/meta.json +6 -1
  99. package/dist/okf/types/containerApps.md +5 -0
  100. package/dist/op/activities/az-apply.js +563 -0
  101. package/dist/op/activities/azure.js +48 -0
  102. package/dist/op/activities/floci-az.js +37 -0
  103. package/dist/op/activities/index.js +10 -0
  104. package/dist/op/builders.js +70 -0
  105. package/dist/ownership.js +13 -0
  106. package/dist/package-cli.js +21 -0
  107. package/dist/parameter.js +13 -0
  108. package/dist/plugin.d.ts.map +1 -1
  109. package/dist/plugin.js +478 -0
  110. package/dist/pseudo.js +40 -0
  111. package/dist/rules/arm-refs.ts +5 -3
  112. package/dist/serializer.d.ts +7 -0
  113. package/dist/serializer.d.ts.map +1 -1
  114. package/dist/serializer.js +543 -0
  115. package/dist/spec/api-versions.js +130 -0
  116. package/dist/spec/fetch.js +196 -0
  117. package/dist/spec/parse.d.ts.map +1 -1
  118. package/dist/spec/parse.js +318 -0
  119. package/dist/taggable.js +34 -0
  120. package/dist/types/index.d.ts +5 -0
  121. package/dist/validate-cli.js +16 -0
  122. package/dist/validate.js +53 -0
  123. package/package.json +8 -8
  124. package/src/attributes.test.ts +17 -0
  125. package/src/codegen/generate-typescript.ts +1 -1
  126. package/src/codegen/generate.ts +1 -1
  127. package/src/codegen/package.ts +2 -2
  128. package/src/deploy-scopes.ts +2 -1
  129. package/src/generated/index.d.ts +5 -0
  130. package/src/generated/index.ts +1 -1
  131. package/src/generated/lexicon-azure.json +6 -1
  132. package/src/health.test.ts +66 -0
  133. package/src/health.ts +162 -0
  134. package/src/lint/post-synth/arm-refs.ts +5 -3
  135. package/src/lint/post-synth/post-synth.test.ts +1 -1
  136. package/src/lsp/completions.ts +2 -1
  137. package/src/lsp/hover.ts +2 -1
  138. package/src/plugin.ts +11 -2
  139. package/src/serializer-names.test.ts +83 -0
  140. package/src/serializer-outputs.test.ts +65 -0
  141. package/src/serializer.ts +112 -30
  142. package/src/spec/parse.test.ts +15 -0
  143. package/src/spec/parse.ts +10 -0
  144. package/src/taggable.ts +2 -2
@@ -0,0 +1,59 @@
1
+ /**
2
+ * StorageAccountSecure composite — Storage Account with security hardening.
3
+ *
4
+ * A higher-level construct that creates a Storage Account with
5
+ * HTTPS-only, encryption at rest, default-deny network rules, and TLS 1.2.
6
+ */
7
+ import { Composite, mergeDefaults } from "@intentius/chant";
8
+ import { StorageAccount } from "../generated/index.js";
9
+ /**
10
+ * Create a StorageAccountSecure composite — returns a Storage Account
11
+ * resource with security best practices.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { StorageAccountSecure } from "@intentius/chant-lexicon-azure";
16
+ *
17
+ * const { storageAccount } = StorageAccountSecure({
18
+ * name: "myappstorage01",
19
+ * sku: "Standard_GRS",
20
+ * tags: { environment: "production" },
21
+ * });
22
+ *
23
+ * export { storageAccount };
24
+ * ```
25
+ */
26
+ export const StorageAccountSecure = Composite((props) => {
27
+ const { name, location = "[resourceGroup().location]", sku = "Standard_LRS", tags = {}, defaults, } = props;
28
+ const storageAccount = new StorageAccount(mergeDefaults({
29
+ name,
30
+ location,
31
+ tags: {
32
+ "managed-by": "chant",
33
+ ...tags,
34
+ },
35
+ sku: {
36
+ name: sku,
37
+ },
38
+ kind: "StorageV2",
39
+ supportsHttpsTrafficOnly: true,
40
+ minimumTlsVersion: "TLS1_2",
41
+ allowBlobPublicAccess: false,
42
+ encryption: {
43
+ services: {
44
+ blob: { enabled: true, keyType: "Account" },
45
+ file: { enabled: true, keyType: "Account" },
46
+ table: { enabled: true, keyType: "Account" },
47
+ queue: { enabled: true, keyType: "Account" },
48
+ },
49
+ keySource: "Microsoft.Storage",
50
+ },
51
+ networkAcls: {
52
+ bypass: "AzureServices",
53
+ defaultAction: "Deny",
54
+ ipRules: [],
55
+ virtualNetworkRules: [],
56
+ },
57
+ }, defaults?.storageAccount), { apiVersion: "2023-01-01" });
58
+ return { storageAccount };
59
+ }, "StorageAccountSecure");
@@ -0,0 +1,139 @@
1
+ /**
2
+ * VmLinux composite — Linux Virtual Machine + NIC + NSG + optional Public IP.
3
+ *
4
+ * A higher-level construct for deploying a Linux VM with SSH access,
5
+ * managed disk, and a Network Security Group.
6
+ */
7
+ import { Composite, mergeDefaults } from "@intentius/chant";
8
+ import { VirtualMachine, NetworkInterface, NetworkSecurityGroup, PublicIPAddress, } from "../generated/index.js";
9
+ /**
10
+ * Create a VmLinux composite — returns a Linux VM, NIC, NSG,
11
+ * and optional Public IP.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { VmLinux } from "@intentius/chant-lexicon-azure";
16
+ *
17
+ * const { virtualMachine, nic, nsg, publicIpAddress } = VmLinux({
18
+ * name: "my-vm",
19
+ * vmSize: "Standard_B2s",
20
+ * adminUsername: "azureuser",
21
+ * sshPublicKey: "ssh-rsa AAAA...",
22
+ * subnetId: "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'my-vnet', 'subnet-1')]",
23
+ * publicIp: true,
24
+ * });
25
+ *
26
+ * export { virtualMachine, nic, nsg, publicIpAddress };
27
+ * ```
28
+ */
29
+ export const VmLinux = Composite((props) => {
30
+ const { name, vmSize, adminUsername, sshPublicKey, subnetId, location = "[resourceGroup().location]", publicIp = false, tags = {}, defaults, } = props;
31
+ const commonTags = {
32
+ "managed-by": "chant",
33
+ ...tags,
34
+ };
35
+ const nsgName = `${name}-nsg`;
36
+ const nicName = `${name}-nic`;
37
+ const pipName = `${name}-pip`;
38
+ const nsg = new NetworkSecurityGroup(mergeDefaults({
39
+ name: nsgName,
40
+ location,
41
+ tags: commonTags,
42
+ securityRules: [
43
+ {
44
+ name: "AllowSSH",
45
+ properties: {
46
+ priority: 1000,
47
+ direction: "Inbound",
48
+ access: "Allow",
49
+ protocol: "Tcp",
50
+ sourcePortRange: "*",
51
+ destinationPortRange: "22",
52
+ sourceAddressPrefix: "*",
53
+ destinationAddressPrefix: "*",
54
+ },
55
+ },
56
+ ],
57
+ }, defaults?.nsg), { apiVersion: "2023-05-01" });
58
+ const ipConfiguration = {
59
+ name: "ipconfig1",
60
+ properties: {
61
+ privateIPAllocationMethod: "Dynamic",
62
+ subnet: {
63
+ id: subnetId,
64
+ },
65
+ ...(publicIp && {
66
+ publicIPAddress: {
67
+ id: `[resourceId('Microsoft.Network/publicIPAddresses', '${pipName}')]`,
68
+ },
69
+ }),
70
+ },
71
+ };
72
+ const nic = new NetworkInterface(mergeDefaults({
73
+ name: nicName,
74
+ location,
75
+ tags: commonTags,
76
+ ipConfigurations: [ipConfiguration],
77
+ networkSecurityGroup: {
78
+ id: `[resourceId('Microsoft.Network/networkSecurityGroups', '${nsgName}')]`,
79
+ },
80
+ }, defaults?.nic), { apiVersion: "2023-05-01" });
81
+ const virtualMachine = new VirtualMachine(mergeDefaults({
82
+ name,
83
+ location,
84
+ tags: commonTags,
85
+ hardwareProfile: {
86
+ vmSize,
87
+ },
88
+ osProfile: {
89
+ computerName: name,
90
+ adminUsername,
91
+ linuxConfiguration: {
92
+ disablePasswordAuthentication: true,
93
+ ssh: {
94
+ publicKeys: [
95
+ {
96
+ path: `/home/${adminUsername}/.ssh/authorized_keys`,
97
+ keyData: sshPublicKey,
98
+ },
99
+ ],
100
+ },
101
+ },
102
+ },
103
+ storageProfile: {
104
+ imageReference: {
105
+ publisher: "Canonical",
106
+ offer: "0001-com-ubuntu-server-jammy",
107
+ sku: "22_04-lts-gen2",
108
+ version: "latest",
109
+ },
110
+ osDisk: {
111
+ createOption: "FromImage",
112
+ managedDisk: {
113
+ storageAccountType: "Premium_LRS",
114
+ },
115
+ },
116
+ },
117
+ networkProfile: {
118
+ networkInterfaces: [
119
+ {
120
+ id: `[resourceId('Microsoft.Network/networkInterfaces', '${nicName}')]`,
121
+ },
122
+ ],
123
+ },
124
+ }, defaults?.virtualMachine), { apiVersion: "2023-07-01" });
125
+ const result = { virtualMachine, nic, nsg };
126
+ if (publicIp) {
127
+ result.publicIpAddress = new PublicIPAddress(mergeDefaults({
128
+ name: pipName,
129
+ location,
130
+ tags: commonTags,
131
+ sku: {
132
+ name: "Standard",
133
+ },
134
+ publicIPAllocationMethod: "Static",
135
+ publicIPAddressVersion: "IPv4",
136
+ }, defaults?.publicIpAddress), { apiVersion: "2023-05-01" });
137
+ }
138
+ return result;
139
+ }, "VmLinux");
@@ -0,0 +1,102 @@
1
+ /**
2
+ * VnetDefault composite — Virtual Network + Subnets + NSG + Route Table.
3
+ *
4
+ * A higher-level construct for a standard Azure virtual network with
5
+ * two subnets, a Network Security Group, and a Route Table.
6
+ */
7
+ import { Composite, mergeDefaults } from "@intentius/chant";
8
+ import { VirtualNetwork, Subnet, NetworkSecurityGroup, RouteTable, } from "../generated/index.js";
9
+ /**
10
+ * Create a VnetDefault composite — returns a Virtual Network,
11
+ * two Subnets, an NSG, and a Route Table.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { VnetDefault } from "@intentius/chant-lexicon-azure";
16
+ *
17
+ * const { virtualNetwork, subnet1, subnet2, nsg, routeTable } = VnetDefault({
18
+ * name: "my-vnet",
19
+ * addressPrefix: "10.0.0.0/16",
20
+ * subnetPrefixes: ["10.0.1.0/24", "10.0.2.0/24"],
21
+ * });
22
+ *
23
+ * export { virtualNetwork, subnet1, subnet2, nsg, routeTable };
24
+ * ```
25
+ */
26
+ export const VnetDefault = Composite((props) => {
27
+ const { name, addressPrefix = "10.0.0.0/16", subnetPrefixes = ["10.0.1.0/24", "10.0.2.0/24"], location = "[resourceGroup().location]", tags = {}, defaults, } = props;
28
+ const commonTags = {
29
+ "managed-by": "chant",
30
+ ...tags,
31
+ };
32
+ const nsgName = `${name}-nsg`;
33
+ const routeTableName = `${name}-rt`;
34
+ const nsg = new NetworkSecurityGroup(mergeDefaults({
35
+ name: nsgName,
36
+ location,
37
+ tags: commonTags,
38
+ securityRules: [],
39
+ }, defaults?.nsg), { apiVersion: "2023-05-01" });
40
+ const routeTable = new RouteTable(mergeDefaults({
41
+ name: routeTableName,
42
+ location,
43
+ tags: commonTags,
44
+ disableBgpRoutePropagation: false,
45
+ routes: [],
46
+ }, defaults?.routeTable), { apiVersion: "2023-05-01" });
47
+ const subnet1 = new Subnet(mergeDefaults({
48
+ name: `${name}/subnet-1`,
49
+ addressPrefix: subnetPrefixes[0],
50
+ networkSecurityGroup: {
51
+ id: `[resourceId('Microsoft.Network/networkSecurityGroups', '${nsgName}')]`,
52
+ },
53
+ routeTable: {
54
+ id: `[resourceId('Microsoft.Network/routeTables', '${routeTableName}')]`,
55
+ },
56
+ }, defaults?.subnet1), { apiVersion: "2023-05-01" });
57
+ const subnet2 = new Subnet(mergeDefaults({
58
+ name: `${name}/subnet-2`,
59
+ addressPrefix: subnetPrefixes[1],
60
+ networkSecurityGroup: {
61
+ id: `[resourceId('Microsoft.Network/networkSecurityGroups', '${nsgName}')]`,
62
+ },
63
+ routeTable: {
64
+ id: `[resourceId('Microsoft.Network/routeTables', '${routeTableName}')]`,
65
+ },
66
+ }, defaults?.subnet2), { apiVersion: "2023-05-01" });
67
+ const virtualNetwork = new VirtualNetwork(mergeDefaults({
68
+ name,
69
+ location,
70
+ tags: commonTags,
71
+ addressSpace: {
72
+ addressPrefixes: [addressPrefix],
73
+ },
74
+ subnets: [
75
+ {
76
+ name: "subnet-1",
77
+ properties: {
78
+ addressPrefix: subnetPrefixes[0],
79
+ networkSecurityGroup: {
80
+ id: `[resourceId('Microsoft.Network/networkSecurityGroups', '${nsgName}')]`,
81
+ },
82
+ routeTable: {
83
+ id: `[resourceId('Microsoft.Network/routeTables', '${routeTableName}')]`,
84
+ },
85
+ },
86
+ },
87
+ {
88
+ name: "subnet-2",
89
+ properties: {
90
+ addressPrefix: subnetPrefixes[1],
91
+ networkSecurityGroup: {
92
+ id: `[resourceId('Microsoft.Network/networkSecurityGroups', '${nsgName}')]`,
93
+ },
94
+ routeTable: {
95
+ id: `[resourceId('Microsoft.Network/routeTables', '${routeTableName}')]`,
96
+ },
97
+ },
98
+ },
99
+ ],
100
+ }, defaults?.virtualNetwork), { apiVersion: "2023-05-01" });
101
+ return { virtualNetwork, subnet1, subnet2, nsg, routeTable };
102
+ }, "VnetDefault");
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Azure lexicon coverage analysis.
3
+ */
4
+ import { computeCoverage, formatSummary, formatVerbose, checkThresholds, } from "@intentius/chant/codegen/coverage";
5
+ import { readFileSync } from "fs";
6
+ import { join, dirname } from "path";
7
+ import { fileURLToPath } from "url";
8
+ export { computeCoverage, overallPct, formatSummary, formatVerbose, checkThresholds, } from "@intentius/chant/codegen/coverage";
9
+ export async function analyzeAzureCoverage(opts) {
10
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
11
+ const lexiconPath = join(pkgDir, "src", "generated", "lexicon-azure.json");
12
+ const content = readFileSync(lexiconPath, "utf-8");
13
+ const report = computeCoverage(content);
14
+ if (opts?.verbose) {
15
+ console.log(formatVerbose(report));
16
+ }
17
+ else {
18
+ console.log(formatSummary(report));
19
+ }
20
+ if (typeof opts?.minOverall === "number") {
21
+ const result = checkThresholds(report, { minOverallPct: opts.minOverall });
22
+ if (!result.ok) {
23
+ for (const v of result.violations)
24
+ console.error(` FAIL: ${v}`);
25
+ throw new Error("Coverage below threshold");
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,356 @@
1
+ /**
2
+ * Azure deep observation (#1086) — the azure row of the deep-observe contract
3
+ * (#1014).
4
+ *
5
+ * `az resource show --resource-group <env> --name <name> --resource-type
6
+ * <type> -o json` already returns the full ARM resource — the same call the
7
+ * thin path (./describe-resources.ts) makes, which keeps `provisioningState`
8
+ * (as `status`) plus `location`/`tags`. The depth is free; the work is
9
+ * entirely normalization, per the issue.
10
+ *
11
+ * ## Why the live tree is flattened, not passed through raw
12
+ *
13
+ * Unlike AWS (whose declared props mirror CloudFormation's vocabulary 1:1, so
14
+ * the Cloud Control payload normalizes and diffs as-is), chant's Azure
15
+ * resource classes do not mirror the ARM response shape verbatim: codegen
16
+ * (`spec/parse.ts`'s `RESOURCE_LEVEL_FIELDS` pass) flattens ARM's `properties`
17
+ * wrapper onto the constructor's top level, so a storage account's declared
18
+ * `minimumTlsVersion` is a sibling of `location`/`tags`, not nested under a
19
+ * `properties` key. Passing the raw `az resource show` response through
20
+ * unflattened would make `properties.minimumTlsVersion` a path with no
21
+ * declared counterpart, ever — the same permanent-noise problem AWS avoids by
22
+ * having a vocabulary match to begin with. So this reader flattens
23
+ * `properties.*` up to the same top level chant's generated classes use,
24
+ * alongside the resource-level fields (`name`, `location`, `sku`, `kind`,
25
+ * `identity`, `tags`, `zones`, `plan`) ARM returns as siblings of `properties`.
26
+ *
27
+ * `id`, `type`, `etag` and `systemData` are the one thing that's *not*
28
+ * flattened in — they have no declared counterpart of any kind (chant's
29
+ * codegen skips `type`/`apiVersion` explicitly, and `id`/`etag`/`systemData`
30
+ * are pure ARM-envelope bookkeeping), so they are simply never read into the
31
+ * tree, the same way `type`/`physicalId` live outside `properties` on {@link
32
+ * DeepResourceObservation} rather than inside it.
33
+ *
34
+ * ## Scope
35
+ *
36
+ * Same as the thin path: any top-level ARM type (`Microsoft.<provider>/<kind>`,
37
+ * exactly one `/`) is readable; a nested compound type
38
+ * (`Microsoft.Storage/storageAccounts/blobServices`) is `unsupported-kind`,
39
+ * because `az resource show` does not accept a compound type name either.
40
+ * Deferred, not fixed, here — same as the issue's own "worth fixing here or
41
+ * explicitly deferring" — widening it is additive and needs no contract change.
42
+ *
43
+ * ## Nothing here talks to real Azure on its own terms
44
+ *
45
+ * ARM over the applier's own transport (`./api/read-client.ts`), exactly like
46
+ * the thin path (#1212) — no CLI, no ARM SDK, no ambient token. The payload is
47
+ * the same ARM JSON `az resource show` was relaying, so the normalization below
48
+ * is untouched by the move.
49
+ */
50
+ import { boundedConcurrently } from "@intentius/chant/observation";
51
+ import { getResource, isNotFound } from "./api/read-client.js";
52
+ import { LEGACY_OWNERSHIP_TAG_KEY } from "./op/activities/az-apply.js";
53
+ import { deepObservation, normalizeDeepProperties } from "@intentius/chant/deep-observation";
54
+ import { OWNERSHIP_MANAGED_BY_VALUE } from "@intentius/chant/ownership";
55
+ import { classifyArmFailure, isTopLevelType } from "./describe-resources.js";
56
+ import { isBracketExpression } from "./lint/post-synth/arm-refs.js";
57
+ import { AZURE_TAG_OWNERSHIP_KEYS } from "./ownership.js";
58
+ /**
59
+ * Reshape one `az resource show` response into chant's own flat declared
60
+ * vocabulary: the resource-level fields ARM returns as siblings of
61
+ * `properties`, plus every `properties.*` field spread onto that same top
62
+ * level (see the module doc). `id`/`type`/`etag`/`systemData` are envelope
63
+ * bookkeeping with no declared counterpart and are never read in at all.
64
+ *
65
+ * `queriedName` is the declared name the GET was addressed by. A child
66
+ * resource is declared as `vnet/subnet` while ARM answers with the leaf
67
+ * (`name: "subnet"`) — the same identity in two spellings, since the URL that
68
+ * returned 200 is the declared name. The leaf is folded back to the declared
69
+ * spelling; a name that is not the leaf of the query is kept as returned.
70
+ */
71
+ function buildLiveProperties(obj, queriedName) {
72
+ const out = {};
73
+ if (obj.name !== undefined) {
74
+ out.name =
75
+ queriedName !== undefined && queriedName !== obj.name && queriedName.endsWith(`/${obj.name}`)
76
+ ? queriedName
77
+ : obj.name;
78
+ }
79
+ if (obj.location !== undefined)
80
+ out.location = obj.location;
81
+ if (obj.sku !== undefined)
82
+ out.sku = obj.sku;
83
+ if (obj.kind !== undefined)
84
+ out.kind = obj.kind;
85
+ if (obj.identity !== undefined)
86
+ out.identity = obj.identity;
87
+ if (obj.tags !== undefined)
88
+ out.tags = obj.tags;
89
+ if (obj.zones !== undefined)
90
+ out.zones = obj.zones;
91
+ if (obj.plan !== undefined)
92
+ out.plan = obj.plan;
93
+ if (obj.properties && typeof obj.properties === "object") {
94
+ for (const [k, v] of Object.entries(obj.properties)) {
95
+ if (v !== undefined)
96
+ out[k] = v;
97
+ }
98
+ }
99
+ return out;
100
+ }
101
+ // ── Noise rules (#1086) ──────────────────────────────────────────────────────
102
+ /**
103
+ * Server-populated wherever they appear — matched on the final path segment,
104
+ * the same technique AWS's `AWS_READ_ONLY_NAMES` uses, because ARM repeats
105
+ * `provisioningState` at every resource-properties depth and a per-type list
106
+ * of full paths would be a maintenance trap. Deliberately excludes ambiguous
107
+ * names like `id`/`name`: a subnet's `properties.routeTable.id` is a declared
108
+ * cross-reference, and pruning it is how a normalization pass starts hiding
109
+ * real drift. (The resource's *own* envelope `id` is handled separately —
110
+ * see the module doc — precisely so this list never has to touch `id` at all.)
111
+ */
112
+ export const AZURE_READ_ONLY_NAMES = new Set([
113
+ "provisioningState",
114
+ "resourceGuid",
115
+ "principalId",
116
+ "tenantId",
117
+ // The resource's own envelope etag never reaches the tree (see the module
118
+ // doc), but ARM also stamps one on every *nested* child — each security rule,
119
+ // each subnet — and nobody declares those.
120
+ "etag",
121
+ ]);
122
+ /**
123
+ * Server-injected boilerplate no chant user ever declares nor could
124
+ * meaningfully override — Azure stamps every network security group with
125
+ * these regardless of what was requested, so unlike `AZURE_READ_ONLY_NAMES`
126
+ * this is dropped outright rather than compared.
127
+ */
128
+ const AZURE_UNCONDITIONAL_PRUNE_NAMES = new Set(["defaultSecurityRules"]);
129
+ /**
130
+ * Server-computed surfaces ARM fills in on its own (#1214, the AKS row of the
131
+ * CC round-trip): a managed cluster always comes back with `fqdn`,
132
+ * `currentKubernetesVersion` and `nodeResourceGroup` whether or not the
133
+ * declaration said anything about them. Unlike `AZURE_READ_ONLY_NAMES` these
134
+ * are counterpart-gated rather than pruned outright: `nodeResourceGroup` IS
135
+ * declarable at create time, so a declared value is still compared — only the
136
+ * purely server-filled appearance is noise. Sparse and evidence-based, like
137
+ * `AZURE_SERVICE_DEFAULTS`.
138
+ */
139
+ export const AZURE_SERVER_COMPUTED_NAMES = new Set([
140
+ "currentKubernetesVersion",
141
+ "fqdn",
142
+ "nodeResourceGroup",
143
+ ]);
144
+ /**
145
+ * ARM service defaults, per type, as index-erased property paths. Subtracted
146
+ * only where source never declared the property — cdk-real-drift's default
147
+ * subtraction, same as AWS's `AWS_SERVICE_DEFAULTS`. Sparse and evidence-based
148
+ * rather than exhaustive across 1900+ ARM types: widening this table is
149
+ * additive and needs no contract change.
150
+ */
151
+ export const AZURE_SERVICE_DEFAULTS = {
152
+ "Microsoft.Storage/storageAccounts": {
153
+ minimumTlsVersion: "TLS1_2",
154
+ },
155
+ };
156
+ /** Arrays known to be ARM sets (server order is not authoring order), keyed by well-known ARM naming conventions. */
157
+ const AZURE_SET_ARRAY_NAMES = new Set([
158
+ "securityRules",
159
+ "subnets",
160
+ "ipRules",
161
+ "virtualNetworkRules",
162
+ "addressPrefixes",
163
+ "dnsServers",
164
+ "zones",
165
+ ]);
166
+ /**
167
+ * chant's own ownership marker, as tag paths (#1213 — the azure edition of
168
+ * AWS's #1301 correction). The serializer stamps `chant-stack`/`chant-env` from
169
+ * project config and `azApply` stamps the managed-by pair on every resource it
170
+ * PUTs, so a managed estate always carries them live while the declared
171
+ * *properties* the diff compares do not — chant reading its own signature back
172
+ * as drift. Counterpart-gated at the call site, so a source that declares one
173
+ * of these tags itself is still compared.
174
+ */
175
+ const AZURE_OWNERSHIP_TAG_PATTERNS = new Set([
176
+ `tags.${AZURE_TAG_OWNERSHIP_KEYS.stack}`,
177
+ `tags.${AZURE_TAG_OWNERSHIP_KEYS.env}`,
178
+ ]);
179
+ /**
180
+ * The managed-by pair is gated on the value as well: both keys always carry
181
+ * `"chant"` when chant wrote them, so `managed-by: terraform` appearing out of
182
+ * band still surfaces as the drift it is.
183
+ */
184
+ const AZURE_MANAGED_BY_TAG_PATTERNS = new Set([
185
+ `tags.${AZURE_TAG_OWNERSHIP_KEYS.managedBy}`,
186
+ `tags.${LEGACY_OWNERSHIP_TAG_KEY}`,
187
+ ]);
188
+ function isRecord(value) {
189
+ return typeof value === "object" && value !== null && !Array.isArray(value);
190
+ }
191
+ function isEmptyRecord(value) {
192
+ return isRecord(value) && Object.keys(value).length === 0;
193
+ }
194
+ /** Stable JSON with sorted keys — the fallback ordering key for a set-like array without a `name` field. */
195
+ function canonicalJson(value) {
196
+ return (JSON.stringify(value, (_k, v) => v && typeof v === "object" && !Array.isArray(v)
197
+ ? Object.fromEntries(Object.entries(v).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
198
+ : v) ?? "");
199
+ }
200
+ /** The final segment of an index-erased pattern (`properties.networkAcls.ipRules[]` → `ipRules`). */
201
+ function lastSegment(pattern) {
202
+ const withoutIndex = pattern.replace(/\[\]$/, "");
203
+ const dot = withoutIndex.lastIndexOf(".");
204
+ return dot === -1 ? withoutIndex : withoutIndex.slice(dot + 1);
205
+ }
206
+ /**
207
+ * The azure lexicon's noise rules. Three classes, same as AWS: server-
208
+ * populated fields (by name, unconditional), controller-injected boilerplate
209
+ * (by name, unconditional), and ARM service defaults (gated on
210
+ * `counterpart === "absent"`) — plus tag-map defaults, which AWS has no
211
+ * equivalent for since its `Tags` is an array rather than a plain object (an
212
+ * ARM tag map's key order is already canonicalized unconditionally by core,
213
+ * since it is a plain object; only the *default empty map* needs a rule here).
214
+ */
215
+ export const azureDeepNormalizationHooks = {
216
+ prune(node) {
217
+ const name = lastSegment(node.pattern);
218
+ if (AZURE_READ_ONLY_NAMES.has(name) || AZURE_UNCONDITIONAL_PRUNE_NAMES.has(name))
219
+ return true;
220
+ if (node.side !== "live" || node.counterpart !== "absent")
221
+ return false;
222
+ // ARM always returns *some* tag map; an empty one when nobody declared any
223
+ // tag at all is the default, not a finding.
224
+ if (node.pattern === "tags" && isEmptyRecord(node.value))
225
+ return true;
226
+ // A live `location` nobody declared is the effective placement, not drift:
227
+ // a child resource (a subnet) has no location property of its own to
228
+ // declare, and every top-level resource that CAN take one is declared with
229
+ // one (or with `[resourceGroup().location]`, which is a declared
230
+ // counterpart). Resource-level only — `pattern` is the exact top-level
231
+ // path, so a nested `.location` inside a declared property is untouched.
232
+ if (node.pattern === "location")
233
+ return true;
234
+ // A server-computed surface nobody declared is ARM doing its job, not
235
+ // drift (see the set's doc for why this is counterpart-gated).
236
+ if (AZURE_SERVER_COMPUTED_NAMES.has(name))
237
+ return true;
238
+ // chant's own ownership marker is not drift (see the sets' docs).
239
+ if (AZURE_OWNERSHIP_TAG_PATTERNS.has(node.pattern))
240
+ return true;
241
+ if (AZURE_MANAGED_BY_TAG_PATTERNS.has(node.pattern) && node.value === OWNERSHIP_MANAGED_BY_VALUE)
242
+ return true;
243
+ // ARM stamps a self-`id` on every element of a nested child collection —
244
+ // each security rule, each inline subnet. Only the element's *own* id
245
+ // (`securityRules[].id`, directly on the element): a declared
246
+ // cross-reference like `virtualNetworkRules[].id` has a counterpart and is
247
+ // still compared, and object-valued references (`routeTable.id`) never
248
+ // match the array-element shape.
249
+ if (node.pattern.endsWith("[].id"))
250
+ return true;
251
+ const defaults = AZURE_SERVICE_DEFAULTS[node.entityType];
252
+ if (!defaults || !Object.prototype.hasOwnProperty.call(defaults, node.pattern))
253
+ return false;
254
+ return canonicalJson(defaults[node.pattern]) === canonicalJson(node.value);
255
+ },
256
+ /**
257
+ * An ARM bracket-expression string (`"[resourceId(...)]"`) is an unevaluated
258
+ * reference: the applier resolves it at deploy time, so the live side holds
259
+ * the evaluated value and comparing the two is comparing a formula to its
260
+ * result. Collapsed to UNRESOLVED on the declared side only — a live payload
261
+ * cannot carry an unevaluated expression, and `[[`-escaped strings are ARM's
262
+ * own literal-`[` spelling, not expressions.
263
+ */
264
+ unresolved(node) {
265
+ return (node.side === "declared" &&
266
+ isBracketExpression(node.value) &&
267
+ !node.value.startsWith("[["));
268
+ },
269
+ /**
270
+ * The key doubles as a path segment (`securityRules[#allow-ssh]`), so a
271
+ * `name` field is the element's own identity where ARM gives one — which it
272
+ * does for essentially every nested rule/subnet/range collection — and
273
+ * canonical JSON only as a fallback for primitive-valued sets
274
+ * (`addressPrefixes`, `dnsServers`).
275
+ */
276
+ orderKey(element) {
277
+ const name = lastSegment(element.pattern);
278
+ if (!AZURE_SET_ARRAY_NAMES.has(name))
279
+ return undefined;
280
+ const el = element.element;
281
+ if (isRecord(el)) {
282
+ const key = typeof el.name === "string" ? el.name : undefined;
283
+ return key ?? canonicalJson(el);
284
+ }
285
+ return typeof el === "string" ? el : canonicalJson(el);
286
+ },
287
+ };
288
+ /**
289
+ * Read the live property tree for each declared entity via `az resource
290
+ * show`. One call per entity, same as the thin path — ARM has no bulk
291
+ * "describe everything in this group" call with per-resource depth the way
292
+ * CloudFormation's `describe-stack-resources` does, so there is no cheap
293
+ * list-then-describe split to make here the way AWS's own reader does.
294
+ */
295
+ export async function observeResourcesDeepAzure(options) {
296
+ const resources = {};
297
+ const unobserved = {};
298
+ const readable = [];
299
+ const client = {
300
+ resourceGroup: options.environment,
301
+ ...(process.env.AZURE_ENDPOINT_URL ? { endpoint: process.env.AZURE_ENDPOINT_URL } : {}),
302
+ ...(process.env.AZURE_SUBSCRIPTION_ID ? { subscriptionId: process.env.AZURE_SUBSCRIPTION_ID } : {}),
303
+ ...(options.http ? { http: options.http } : {}),
304
+ };
305
+ for (const [entityName, { entityType, props }] of options.entities) {
306
+ if (!entityType.startsWith("Microsoft.")) {
307
+ unobserved[entityName] = {
308
+ type: entityType,
309
+ reason: "unsupported-kind",
310
+ detail: "not an ARM resource type (expected Microsoft.<provider>/<kind>)",
311
+ };
312
+ continue;
313
+ }
314
+ if (!isTopLevelType(entityType)) {
315
+ unobserved[entityName] = {
316
+ type: entityType,
317
+ reason: "unsupported-kind",
318
+ detail: "a nested ARM type needs a different read path; chant never queried this resource",
319
+ };
320
+ continue;
321
+ }
322
+ const name = props.name;
323
+ if (!name) {
324
+ unobserved[entityName] = {
325
+ type: entityType,
326
+ reason: "read-failed",
327
+ detail: "declared entity has no name to query by",
328
+ };
329
+ continue;
330
+ }
331
+ readable.push({ entityName, entityType, name });
332
+ }
333
+ await boundedConcurrently(readable, async ({ entityName, entityType, name }) => {
334
+ try {
335
+ const obj = (await getResource(client, entityType, name));
336
+ resources[entityName] = {
337
+ type: entityType,
338
+ physicalId: obj.id,
339
+ properties: normalizeDeepProperties(buildLiveProperties(obj, name), {
340
+ entityType,
341
+ side: "live",
342
+ hooks: azureDeepNormalizationHooks,
343
+ }),
344
+ };
345
+ }
346
+ catch (err) {
347
+ // Not-found leaves the entity out (absence, same as the thin path).
348
+ // Auth/binding/other failures are holes so they can't become creates.
349
+ if (isNotFound(err))
350
+ return;
351
+ const outcome = classifyArmFailure(err);
352
+ unobserved[entityName] = { type: entityType, reason: outcome.reason, detail: outcome.detail };
353
+ }
354
+ });
355
+ return deepObservation(resources, unobserved);
356
+ }