@intentius/chant-lexicon-azure 0.124.31 → 0.124.32

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 (117) 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.js +16598 -0
  47. package/dist/generated/runtime.js +4 -0
  48. package/dist/governance.js +144 -0
  49. package/dist/import/generator.js +140 -0
  50. package/dist/import/live-export.js +39 -0
  51. package/dist/import/parser.js +192 -0
  52. package/dist/index.js +44 -0
  53. package/dist/integrity.json +2 -2
  54. package/dist/intrinsics.js +179 -0
  55. package/dist/lint/audit-catalog.js +36 -0
  56. package/dist/lint/audit-lineage.js +71 -0
  57. package/dist/lint/post-synth/arm-refs.js +94 -0
  58. package/dist/lint/post-synth/azr010.js +45 -0
  59. package/dist/lint/post-synth/azr011.js +43 -0
  60. package/dist/lint/post-synth/azr012.js +41 -0
  61. package/dist/lint/post-synth/azr013.js +50 -0
  62. package/dist/lint/post-synth/azr014.js +49 -0
  63. package/dist/lint/post-synth/azr015.js +66 -0
  64. package/dist/lint/post-synth/azr016.js +37 -0
  65. package/dist/lint/post-synth/azr017.js +37 -0
  66. package/dist/lint/post-synth/azr018.js +42 -0
  67. package/dist/lint/post-synth/azr019.js +41 -0
  68. package/dist/lint/post-synth/azr020.js +37 -0
  69. package/dist/lint/post-synth/azr021.js +36 -0
  70. package/dist/lint/post-synth/azr022.js +39 -0
  71. package/dist/lint/post-synth/azr023.js +39 -0
  72. package/dist/lint/post-synth/azr024.js +39 -0
  73. package/dist/lint/post-synth/azr025.js +37 -0
  74. package/dist/lint/post-synth/azr026.js +38 -0
  75. package/dist/lint/post-synth/azr027.js +37 -0
  76. package/dist/lint/post-synth/azr028.js +37 -0
  77. package/dist/lint/post-synth/azr029.js +38 -0
  78. package/dist/lint/post-synth/azr030.js +44 -0
  79. package/dist/lint/post-synth/azr031.js +36 -0
  80. package/dist/lint/post-synth/azr032.js +53 -0
  81. package/dist/lint/post-synth/azr033.js +35 -0
  82. package/dist/lint/post-synth/index.js +50 -0
  83. package/dist/lint/rules/hardcoded-location.js +82 -0
  84. package/dist/lint/rules/index.js +6 -0
  85. package/dist/lint/rules/nsg-wildcard.js +44 -0
  86. package/dist/lint/rules/storage-https.js +62 -0
  87. package/dist/lsp/completions.d.ts.map +1 -1
  88. package/dist/lsp/completions.js +18 -0
  89. package/dist/lsp/hover.d.ts.map +1 -1
  90. package/dist/lsp/hover.js +44 -0
  91. package/dist/manifest.json +1 -1
  92. package/dist/op/activities/az-apply.js +563 -0
  93. package/dist/op/activities/azure.js +48 -0
  94. package/dist/op/activities/floci-az.js +37 -0
  95. package/dist/op/activities/index.js +10 -0
  96. package/dist/op/builders.js +70 -0
  97. package/dist/ownership.js +13 -0
  98. package/dist/package-cli.js +21 -0
  99. package/dist/parameter.js +13 -0
  100. package/dist/plugin.d.ts.map +1 -1
  101. package/dist/plugin.js +472 -0
  102. package/dist/pseudo.js +40 -0
  103. package/dist/serializer.js +457 -0
  104. package/dist/spec/api-versions.js +130 -0
  105. package/dist/spec/fetch.js +196 -0
  106. package/dist/spec/parse.js +308 -0
  107. package/dist/taggable.js +34 -0
  108. package/dist/validate-cli.js +16 -0
  109. package/dist/validate.js +53 -0
  110. package/package.json +8 -8
  111. package/src/codegen/package.ts +2 -2
  112. package/src/deploy-scopes.ts +2 -1
  113. package/src/lsp/completions.ts +2 -1
  114. package/src/lsp/hover.ts +2 -1
  115. package/src/plugin.ts +3 -2
  116. package/src/serializer.ts +2 -2
  117. package/src/taggable.ts +2 -2
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Default Tags — declares project-wide tags for all taggable Azure resources.
3
+ *
4
+ * When a project exports a `defaultTags(...)` declaration, the serializer
5
+ * automatically injects those tags into every taggable resource at synthesis
6
+ * time. Explicit tags on individual resources take precedence.
7
+ */
8
+ import { DECLARABLE_MARKER } from "@intentius/chant/declarable";
9
+ /**
10
+ * Marker symbol for default tags identification.
11
+ */
12
+ export const DEFAULT_TAGS_MARKER = Symbol.for("chant.azure.defaultTags");
13
+ /**
14
+ * Type guard for DefaultTags.
15
+ */
16
+ export function isDefaultTags(value) {
17
+ return (typeof value === "object" &&
18
+ value !== null &&
19
+ DEFAULT_TAGS_MARKER in value &&
20
+ value[DEFAULT_TAGS_MARKER] === true);
21
+ }
22
+ /**
23
+ * Declare project-wide default tags for all taggable Azure resources.
24
+ *
25
+ * Tags are injected at synthesis time into every resource that supports
26
+ * tagging. If a resource has an explicit tag with the same key, the
27
+ * explicit value wins.
28
+ *
29
+ * @param tags - Array of { key, value } tag entries
30
+ * @returns A DefaultTags Declarable
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { defaultTags, Azure } from "@intentius/chant-lexicon-azure";
35
+ *
36
+ * export const tags = defaultTags([
37
+ * { key: "Project", value: "my-app" },
38
+ * { key: "Environment", value: "production" },
39
+ * ]);
40
+ * ```
41
+ */
42
+ export function defaultTags(tags) {
43
+ return {
44
+ [DEFAULT_TAGS_MARKER]: true,
45
+ [DECLARABLE_MARKER]: true,
46
+ lexicon: "azure",
47
+ entityType: "chant:azure:defaultTags",
48
+ tags,
49
+ };
50
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"deploy-scopes.d.ts","sourceRoot":"","sources":["../src/deploy-scopes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAAE,iBAAiB,EAAE,KAAK,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAEjF,MAAM,MAAM,WAAW,GAAG,eAAe,GAAG,cAAc,GAAG,iBAAiB,GAAG,QAAQ,CAAC;AAE1F,6DAA6D;AAC7D,eAAO,MAAM,uBAAuB,eAA4C,CAAC;AAEjF;;;GAGG;AACH,MAAM,WAAW,eAAgB,SAAQ,UAAU;IACjD,QAAQ,CAAC,CAAC,uBAAuB,CAAC,EAAE,IAAI,CAAC;IACzC,QAAQ,CAAC,CAAC,iBAAiB,CAAC,EAAE,IAAI,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,6BAA6B,CAAC;IACnD,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;CAC7B;AAED,sCAAsC;AACtC,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,eAAe,CAO1E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,WAAW,GAAG,eAAe,CAQnE;AAED,iDAAiD;AACjD,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAKxD,CAAC;AA4BF;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC,CA4B7D;AAED,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,WAAW,EAAE,CAEnE;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,WAAW,CAiBjF;AAED,+DAA+D;AAC/D,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,OAAO,GAAG,WAAW,CAOnE"}
1
+ {"version":3,"file":"deploy-scopes.d.ts","sourceRoot":"","sources":["../src/deploy-scopes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,EAAE,iBAAiB,EAAE,KAAK,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAEjF,MAAM,MAAM,WAAW,GAAG,eAAe,GAAG,cAAc,GAAG,iBAAiB,GAAG,QAAQ,CAAC;AAE1F,6DAA6D;AAC7D,eAAO,MAAM,uBAAuB,eAA4C,CAAC;AAEjF;;;GAGG;AACH,MAAM,WAAW,eAAgB,SAAQ,UAAU;IACjD,QAAQ,CAAC,CAAC,uBAAuB,CAAC,EAAE,IAAI,CAAC;IACzC,QAAQ,CAAC,CAAC,iBAAiB,CAAC,EAAE,IAAI,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,6BAA6B,CAAC;IACnD,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;CAC7B;AAED,sCAAsC;AACtC,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,eAAe,CAO1E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,WAAW,GAAG,eAAe,CAQnE;AAED,iDAAiD;AACjD,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAKxD,CAAC;AA4BF;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC,CA4B7D;AAED,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,WAAW,EAAE,CAEnE;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,WAAW,CAiBjF;AAED,+DAA+D;AAC/D,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,OAAO,GAAG,WAAW,CAOnE"}
@@ -0,0 +1,157 @@
1
+ /**
2
+ * ARM deployment-scope metadata (#1545).
3
+ *
4
+ * The generated lexicon records `deployScopes` for resources that exist
5
+ * somewhere other than plain resource-group scope (management groups,
6
+ * subscription aliases, policy definitions). This module loads that map and
7
+ * resolves which deployment scope a template targets, shared by the
8
+ * serializer (picks the template $schema) and AZR030 (flags resources
9
+ * emitted at a scope their schema does not define).
10
+ *
11
+ * Lazy-loaded and cached for the lifetime of the process.
12
+ */
13
+ import { readFileSync } from "fs";
14
+ import { join } from "path";
15
+ import { moduleSourceDir } from "@intentius/chant/module-source";
16
+ import { DECLARABLE_MARKER } from "@intentius/chant/declarable";
17
+ /** Marker symbol for deployment-scope pin identification. */
18
+ export const DEPLOYMENT_SCOPE_MARKER = Symbol.for("chant.azure.deploymentScope");
19
+ /** Type guard for DeploymentScope. */
20
+ export function isDeploymentScope(value) {
21
+ return (typeof value === "object" &&
22
+ value !== null &&
23
+ DEPLOYMENT_SCOPE_MARKER in value &&
24
+ value[DEPLOYMENT_SCOPE_MARKER] === true);
25
+ }
26
+ /**
27
+ * Pin the project's deployment scope. Without a pin the serializer picks
28
+ * the scope every resource supports that is closest to a resource group,
29
+ * which cannot tell a management-group policy project from a subscription
30
+ * one — policy definitions and assignments deploy at both. One pin per
31
+ * project; AZR030 flags resources whose schema does not define the pinned
32
+ * scope.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * export const scope = deploymentScope("managementGroup");
37
+ * ```
38
+ */
39
+ export function deploymentScope(scope) {
40
+ return {
41
+ [DEPLOYMENT_SCOPE_MARKER]: true,
42
+ [DECLARABLE_MARKER]: true,
43
+ lexicon: "azure",
44
+ entityType: "chant:azure:deploymentScope",
45
+ scope,
46
+ };
47
+ }
48
+ /** Template $schema URL per deployment scope. */
49
+ export const TEMPLATE_SCHEMAS = {
50
+ resourceGroup: "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
51
+ subscription: "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
52
+ managementGroup: "https://schema.management.azure.com/schemas/2019-08-01/managementGroupDeploymentTemplate.json#",
53
+ tenant: "https://schema.management.azure.com/schemas/2019-08-01/tenantDeploymentTemplate.json#",
54
+ };
55
+ const RESOURCE_GROUP_ONLY = ["resourceGroup"];
56
+ let _cached;
57
+ /**
58
+ * Directory of this module, resolved lazily. On workerd `import.meta.url`
59
+ * is undefined and `fileURLToPath` throws, so this must never run at module
60
+ * scope (#1621): the post-synth barrel imports this file and a module-scope
61
+ * throw kills the worker at startup. Returns undefined when unresolvable;
62
+ * callers then take the documented resource-group-only fallback.
63
+ */
64
+ function moduleDir() {
65
+ try {
66
+ return moduleSourceDir(import.meta.url);
67
+ }
68
+ catch {
69
+ return undefined;
70
+ }
71
+ }
72
+ /**
73
+ * Load per-resource deployment scopes from the lexicon JSON.
74
+ * Only resources with a non-default scope set appear; everything else is
75
+ * plain resource-group scope. Result is cached after first call.
76
+ */
77
+ export function loadDeployScopes() {
78
+ if (_cached)
79
+ return _cached;
80
+ const map = new Map();
81
+ try {
82
+ let content;
83
+ const dir = moduleDir();
84
+ // Try generated/ (dev) first, then dist/meta.json (installed package)
85
+ for (const candidate of dir ? [
86
+ join(dir, "generated", "lexicon-azure.json"),
87
+ join(dir, "..", "dist", "meta.json"),
88
+ ] : []) {
89
+ try {
90
+ content = readFileSync(candidate, "utf-8");
91
+ break;
92
+ }
93
+ catch { }
94
+ }
95
+ if (content) {
96
+ const data = JSON.parse(content);
97
+ for (const entry of Object.values(data)) {
98
+ if (entry.kind === "resource" && entry.resourceType && entry.deployScopes?.length) {
99
+ map.set(entry.resourceType, entry.deployScopes);
100
+ }
101
+ }
102
+ }
103
+ }
104
+ catch {
105
+ // Lexicon not available — every resource reads as resource-group scope
106
+ }
107
+ _cached = map;
108
+ return map;
109
+ }
110
+ /** Deployment scopes for one ARM resource type (defaults to resource group). */
111
+ export function deployScopesFor(resourceType) {
112
+ return loadDeployScopes().get(resourceType) ?? RESOURCE_GROUP_ONLY;
113
+ }
114
+ /**
115
+ * Resolve the deployment scope for a template containing the given resource
116
+ * types: the scopes every resource supports, intersected.
117
+ *
118
+ * Resource-group scope wins whenever the intersection allows it (the
119
+ * pre-#1545 behavior for every template that could already be emitted).
120
+ * Otherwise the scope closest to a resource group is chosen. An empty
121
+ * intersection has no valid single-template answer; resource-group scope is
122
+ * returned and AZR030 reports the resources that cannot deploy there.
123
+ */
124
+ export function resolveTemplateScope(resourceTypes) {
125
+ let candidates;
126
+ for (const type of resourceTypes) {
127
+ const scopes = deployScopesFor(type);
128
+ if (!candidates) {
129
+ candidates = new Set(scopes);
130
+ }
131
+ else {
132
+ for (const scope of [...candidates]) {
133
+ if (!scopes.includes(scope))
134
+ candidates.delete(scope);
135
+ }
136
+ }
137
+ }
138
+ if (!candidates || candidates.size === 0)
139
+ return "resourceGroup";
140
+ for (const scope of ["resourceGroup", "subscription", "managementGroup", "tenant"]) {
141
+ if (candidates.has(scope))
142
+ return scope;
143
+ }
144
+ return "resourceGroup";
145
+ }
146
+ /** Map a template $schema URL back to its deployment scope. */
147
+ export function scopeForTemplateSchema(schema) {
148
+ if (typeof schema === "string") {
149
+ if (schema.includes("subscriptionDeploymentTemplate"))
150
+ return "subscription";
151
+ if (schema.includes("managementGroupDeploymentTemplate"))
152
+ return "managementGroup";
153
+ if (schema.includes("tenantDeploymentTemplate"))
154
+ return "tenant";
155
+ }
156
+ return "resourceGroup";
157
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Live introspection of an Azure resource group over ARM (#1212).
3
+ *
4
+ * For each declared Azure entity, GETs
5
+ * {endpoint}/subscriptions/{sub}/resourceGroups/{env}/providers/{type}/{name}
6
+ *
7
+ * on the applier's own transport (`./api/read-client.ts`, which is
8
+ * `az-apply.ts`'s client pointed at the read side) rather than shelling
9
+ * `az resource show`. The payload is the same ARM JSON either way — the CLI was
10
+ * only ever relaying it — so this is transport, not translation: no CLI to
11
+ * spawn, reads that run concurrently, failures carrying ARM's own error code,
12
+ * and an emulator override that reaches floci-az the same way every other
13
+ * lexicon's does.
14
+ *
15
+ * The response maps to a ResourceMetadata entry keyed by chant entity name
16
+ * (using props.name from #39's entity-prop pass-through). The environment
17
+ * argument is treated as the Azure resource group name.
18
+ *
19
+ * Resource-not-found is an absence — `state diff --live` then reports it as
20
+ * missing. Nested resource types (e.g.
21
+ * `Microsoft.Storage/storageAccounts/blobServices`) are NOT-OBSERVED
22
+ * (`unsupported-kind`, #1089): `az resource show` doesn't accept a compound
23
+ * type, so chant never asks, and a blob service that already exists must not
24
+ * come back as a proposed `create`. Auth failures and unreachable subscriptions
25
+ * are holes for the same reason.
26
+ */
27
+ import { boundedConcurrently, observation } from "@intentius/chant/observation";
28
+ import { readOwnership } from "@intentius/chant/ownership";
29
+ import { AZURE_TAG_OWNERSHIP_KEYS } from "./ownership.js";
30
+ import { AzureReadError, getResource, isNotFound } from "./api/read-client.js";
31
+ function pruneUndefined(obj) {
32
+ const out = {};
33
+ for (const [k, v] of Object.entries(obj)) {
34
+ if (v !== undefined)
35
+ out[k] = v;
36
+ }
37
+ return out;
38
+ }
39
+ /**
40
+ * Top-level ARM type — i.e. exactly one `/` separating provider from kind.
41
+ * Nested types like `Microsoft.Storage/storageAccounts/blobServices` need a
42
+ * different query path that this implementation doesn't yet support.
43
+ *
44
+ * Exported so the deep reader (./deep-observe.ts, #1086) applies the exact
45
+ * same scope limit — a nested type is `unsupported-kind` at both depths, not
46
+ * silently deeper on one and not the other.
47
+ */
48
+ export function isTopLevelType(entityType) {
49
+ const slashCount = (entityType.match(/\//g) ?? []).length;
50
+ return slashCount === 1;
51
+ }
52
+ /**
53
+ * Classify an `az resource show` failure (#1089). Only the CLI's explicit
54
+ * not-found establishes absence; everything else — an expired login, a
55
+ * subscription that can't be resolved, a network failure — proves nothing.
56
+ */
57
+ export function classifyAzFailure(err) {
58
+ const raw = typeof err === "object" && err !== null && typeof err.stderr === "string"
59
+ ? (err.stderr.trim() || String(err.message ?? err))
60
+ : String(err?.message ?? err);
61
+ const lower = raw.toLowerCase();
62
+ const detail = (raw.split("\n").find((l) => l.trim().length > 0) ?? raw).trim().slice(0, 200);
63
+ if (lower.includes("resourcenotfound") ||
64
+ lower.includes("was not found") ||
65
+ lower.includes("could not be found") ||
66
+ lower.includes("does not exist")) {
67
+ return { absent: true };
68
+ }
69
+ if (lower.includes("please run 'az login'") ||
70
+ lower.includes("az login") ||
71
+ lower.includes("authenticationfailed") ||
72
+ lower.includes("expired") ||
73
+ lower.includes("authorizationfailed") ||
74
+ lower.includes("forbidden")) {
75
+ return { absent: false, reason: "no-credentials", detail };
76
+ }
77
+ if (lower.includes("subscriptionnotfound") ||
78
+ lower.includes("no subscription") ||
79
+ lower.includes("please run 'az account set'")) {
80
+ return { absent: false, reason: "no-binding", detail };
81
+ }
82
+ return { absent: false, reason: "read-failed", detail };
83
+ }
84
+ /**
85
+ * Classify an ARM failure off its own error code (#1212).
86
+ *
87
+ * The CLI classifier above matched on prose because stderr was all it had.
88
+ * ARM sends `{ error: { code, message } }`, so the code is the signal and the
89
+ * message is only for the human — the same distinction the AWS read client
90
+ * makes. Kept beside `classifyAzFailure` rather than replacing it: the CLI
91
+ * path still exists for a signed read against real ARM.
92
+ */
93
+ export function classifyArmFailure(err) {
94
+ const code = err instanceof AzureReadError ? (err.code ?? "") : "";
95
+ const message = err instanceof Error ? err.message : String(err);
96
+ const detail = (code ? `${code}: ${message}` : message).slice(0, 200);
97
+ const both = `${code} ${message}`.toLowerCase();
98
+ if (both.includes("authenticationfailed") ||
99
+ both.includes("authorizationfailed") ||
100
+ both.includes("expired") ||
101
+ both.includes("forbidden") ||
102
+ (err instanceof AzureReadError && (err.status === 401 || err.status === 403))) {
103
+ return { reason: "no-credentials", detail };
104
+ }
105
+ if (both.includes("subscriptionnotfound") || both.includes("resourcegroupnotfound")) {
106
+ return { reason: "no-binding", detail };
107
+ }
108
+ return { reason: "read-failed", detail };
109
+ }
110
+ export async function describeResources(options) {
111
+ const result = {};
112
+ const unobserved = {};
113
+ const skippedNested = [];
114
+ const readable = [];
115
+ // The environment is the resource group, as it has always been on this path.
116
+ const client = {
117
+ resourceGroup: options.environment,
118
+ ...(process.env.AZURE_ENDPOINT_URL ? { endpoint: process.env.AZURE_ENDPOINT_URL } : {}),
119
+ ...(process.env.AZURE_SUBSCRIPTION_ID ? { subscriptionId: process.env.AZURE_SUBSCRIPTION_ID } : {}),
120
+ ...(options.http ? { http: options.http } : {}),
121
+ };
122
+ for (const [entityName, { entityType, props }] of options.entities) {
123
+ if (!entityType.startsWith("Microsoft.")) {
124
+ // Not an ARM resource type, so there is no ARM URL to GET.
125
+ // Unobserved rather than skipped: a silent skip reads as absence (#1089).
126
+ unobserved[entityName] = {
127
+ type: entityType,
128
+ reason: "unsupported-kind",
129
+ detail: "not an ARM resource type (expected Microsoft.<provider>/<kind>)",
130
+ };
131
+ continue;
132
+ }
133
+ if (!isTopLevelType(entityType)) {
134
+ skippedNested.push(entityName);
135
+ unobserved[entityName] = {
136
+ type: entityType,
137
+ reason: "unsupported-kind",
138
+ detail: "a nested ARM type needs a different read path; chant never queried this resource",
139
+ };
140
+ continue;
141
+ }
142
+ const name = props.name;
143
+ if (!name) {
144
+ unobserved[entityName] = {
145
+ type: entityType,
146
+ reason: "read-failed",
147
+ detail: "declared entity has no name to query by",
148
+ };
149
+ continue;
150
+ }
151
+ readable.push({ entityName, entityType, name });
152
+ }
153
+ // Concurrent, where the CLI path was one spawn after another (#1201/#1212).
154
+ await boundedConcurrently(readable, async ({ entityName, entityType, name }) => {
155
+ try {
156
+ const body = await getResource(client, entityType, name);
157
+ result[entityName] = {
158
+ type: entityType,
159
+ physicalId: body.id,
160
+ status: body.properties?.provisioningState ?? "PRESENT",
161
+ // Marker identity (#1222): the ARM envelope carries the resource's
162
+ // tags, so the stamped stack/env identity is readable here even though
163
+ // this path declares no ownership channel (the verdict stays out —
164
+ // floci-az drops tags, so a verdict here would misreport under the
165
+ // emulator; the identity, when the tags are present, is verbatim).
166
+ marker: readOwnership(body.tags, AZURE_TAG_OWNERSHIP_KEYS),
167
+ attributes: pruneUndefined({
168
+ location: body.location,
169
+ tags: body.tags,
170
+ }),
171
+ };
172
+ }
173
+ catch (err) {
174
+ // Not-found leaves the entity out (absence). Auth/binding/other failures
175
+ // are recorded as holes so they can't become creates (#1089).
176
+ if (isNotFound(err))
177
+ return;
178
+ const outcome = classifyArmFailure(err);
179
+ unobserved[entityName] = { type: entityType, reason: outcome.reason, detail: outcome.detail };
180
+ }
181
+ });
182
+ if (skippedNested.length > 0) {
183
+ // eslint-disable-next-line no-console
184
+ console.warn(`[azure] ${skippedNested.length} nested-type entity(ies) reported as unobserved (not absent) — az resource show doesn't accept compound types: ${skippedNested.slice(0, 5).join(", ")}${skippedNested.length > 5 ? ", ..." : ""}`);
185
+ }
186
+ return observation(result, unobserved);
187
+ }
package/dist/detect.js ADDED
@@ -0,0 +1,17 @@
1
+ /** Edge-safe template detection for azure (ARM) — see #426. */
2
+ export function detectTemplate(content) {
3
+ if (typeof content !== "string")
4
+ return false;
5
+ try {
6
+ const parsed = JSON.parse(content);
7
+ return (typeof parsed === "object" &&
8
+ parsed !== null &&
9
+ "$schema" in parsed &&
10
+ typeof parsed.$schema === "string" &&
11
+ parsed.$schema.includes("deploymentTemplate") &&
12
+ Array.isArray(parsed.resources));
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Live export for the Azure lexicon — implements LexiconPlugin.exportResources()
3
+ * so `chant import --from <azure-env>` regenerates Azure resources as chant
4
+ * TypeScript.
5
+ *
6
+ * The environment argument is the Azure resource group. Two transports, same
7
+ * IR:
8
+ *
9
+ * - With `AZURE_ENDPOINT_URL` set (the emulator lane, and the var azure's own
10
+ * observers already read on every live call), export rides the applier's ARM
11
+ * transport (#1212's swap, extended to the cloud→code direction for #1214):
12
+ * list the group's resources, GET each one whole, scrub the server-managed
13
+ * surface back to the declared shape, and hand the result to the same
14
+ * `ArmParser` the static-import path uses. Unsigned, exactly like `azApply`
15
+ * — which is why this path is gated on the ambient var rather than replacing
16
+ * the CLI: real ARM wants a bearer token.
17
+ * - Without it, the signed path: `az group export` via the Azure CLI,
18
+ * unchanged.
19
+ *
20
+ * Selector/ownership filtering and IR-building are pure in
21
+ * `./import/live-export`; all I/O lives here.
22
+ */
23
+ import { exec } from "node:child_process";
24
+ import { promisify } from "node:util";
25
+ import { parseExportedTemplate } from "./import/live-export.js";
26
+ import { getResourceById, listResources } from "./api/read-client.js";
27
+ import { AZURE_READ_ONLY_NAMES, AZURE_SERVER_COMPUTED_NAMES } from "./deep-observe.js";
28
+ const execAsync = promisify(exec);
29
+ /** Generic ARM api-version, same as the read client's — adequate for every top-level type chant declares. */
30
+ const EXPORT_API_VERSION = "2021-04-01";
31
+ /**
32
+ * Envelope bookkeeping ARM stamps on every body — never part of the declared
33
+ * shape at any depth. `id` is NOT here: a nested `routeTable.id` is a declared
34
+ * cross-reference; only an array element's self-`id` is scrubbed (below), the
35
+ * same two rules deep observation applies.
36
+ */
37
+ const ENVELOPE_KEYS = new Set(["etag", "systemData"]);
38
+ /**
39
+ * Scrub a live ARM body back to the declared shape (the contract's default
40
+ * fidelity): server-populated names, server-computed surfaces, NSG default
41
+ * rules and nested envelope bookkeeping all drop; everything a user would
42
+ * have authored stays.
43
+ */
44
+ function scrub(value) {
45
+ if (Array.isArray(value)) {
46
+ return value.map((el) => {
47
+ if (el !== null && typeof el === "object" && !Array.isArray(el)) {
48
+ const { id: _selfId, ...rest } = el;
49
+ return scrub(rest);
50
+ }
51
+ return scrub(el);
52
+ });
53
+ }
54
+ if (value !== null && typeof value === "object") {
55
+ const out = {};
56
+ for (const [k, v] of Object.entries(value)) {
57
+ if (AZURE_READ_ONLY_NAMES.has(k) || AZURE_SERVER_COMPUTED_NAMES.has(k))
58
+ continue;
59
+ if (ENVELOPE_KEYS.has(k) || k === "defaultSecurityRules")
60
+ continue;
61
+ out[k] = scrub(v);
62
+ }
63
+ return out;
64
+ }
65
+ return value;
66
+ }
67
+ /** One live body → one ARM-template resource, the shape `ArmParser` reads. */
68
+ function toTemplateResource(body, verbatim) {
69
+ const { id: _id, type, name, ...rest } = body;
70
+ const content = verbatim ? rest : scrub(rest);
71
+ return { type, apiVersion: EXPORT_API_VERSION, name, ...content };
72
+ }
73
+ async function exportViaArm(endpoint, options) {
74
+ const client = { endpoint, resourceGroup: options.environment };
75
+ const listed = await listResources(client);
76
+ const resources = [];
77
+ for (const entry of listed) {
78
+ if (typeof entry.id !== "string")
79
+ continue;
80
+ const body = await getResourceById(client, entry.id);
81
+ resources.push(toTemplateResource(body, options.verbatim === true));
82
+ }
83
+ const template = {
84
+ $schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
85
+ contentVersion: "1.0.0.0",
86
+ resources,
87
+ };
88
+ return parseExportedTemplate(JSON.stringify(template), options.selector, options.owned);
89
+ }
90
+ export async function exportResources(options) {
91
+ const endpoint = process.env.AZURE_ENDPOINT_URL;
92
+ if (endpoint)
93
+ return exportViaArm(endpoint, options);
94
+ const cmd = [
95
+ "az", "group", "export",
96
+ "--resource-group", options.environment,
97
+ "--output", "json",
98
+ ].join(" ");
99
+ let stdout;
100
+ try {
101
+ ({ stdout } = await execAsync(cmd));
102
+ }
103
+ catch (err) {
104
+ const stderr = err.stderr ?? err.message;
105
+ throw new Error(`Failed to export resource group "${options.environment}": ${stderr}`);
106
+ }
107
+ return parseExportedTemplate(stdout, options.selector, options.owned);
108
+ }