@intentius/chant-lexicon-azure 0.124.30 → 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.
- package/dist/actions/appservice.js +7 -0
- package/dist/actions/compute.js +9 -0
- package/dist/actions/container.js +14 -0
- package/dist/actions/identity.js +11 -0
- package/dist/actions/index.js +11 -0
- package/dist/actions/keyvault.js +12 -0
- package/dist/actions/network.js +9 -0
- package/dist/actions/sql.js +9 -0
- package/dist/actions/storage.js +15 -0
- package/dist/api/read-client.js +146 -0
- package/dist/codegen/docs-cli.js +2 -0
- package/dist/codegen/docs.js +83 -0
- package/dist/codegen/generate-cli.js +11 -0
- package/dist/codegen/generate-lexicon.js +77 -0
- package/dist/codegen/generate-typescript.js +163 -0
- package/dist/codegen/generate.js +106 -0
- package/dist/codegen/naming.js +139 -0
- package/dist/codegen/package.js +64 -0
- package/dist/composites/aks-cluster.js +68 -0
- package/dist/composites/app-service.js +62 -0
- package/dist/composites/application-gateway.js +107 -0
- package/dist/composites/catalog.js +856 -0
- package/dist/composites/container-instance.js +36 -0
- package/dist/composites/container-registry.js +62 -0
- package/dist/composites/cosmos-database.js +56 -0
- package/dist/composites/from-arm.js +47 -0
- package/dist/composites/function-app.js +57 -0
- package/dist/composites/index.js +16 -0
- package/dist/composites/keyvault.js +69 -0
- package/dist/composites/landing-zone.js +119 -0
- package/dist/composites/private-endpoint.js +61 -0
- package/dist/composites/redis-cache.js +21 -0
- package/dist/composites/service-bus-pipeline.js +54 -0
- package/dist/composites/sql-database.js +62 -0
- package/dist/composites/storage-account.js +59 -0
- package/dist/composites/vm-linux.js +139 -0
- package/dist/composites/vnet-default.js +102 -0
- package/dist/coverage.js +28 -0
- package/dist/deep-observe.js +356 -0
- package/dist/default-tags.js +50 -0
- package/dist/deploy-scopes.d.ts.map +1 -1
- package/dist/deploy-scopes.js +157 -0
- package/dist/describe-resources.js +187 -0
- package/dist/detect.js +17 -0
- package/dist/export-resources.js +108 -0
- package/dist/generated/index.js +16598 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/governance.js +144 -0
- package/dist/import/generator.js +140 -0
- package/dist/import/live-export.js +39 -0
- package/dist/import/parser.js +192 -0
- package/dist/index.js +44 -0
- package/dist/integrity.json +2 -2
- package/dist/intrinsics.js +179 -0
- package/dist/lint/audit-catalog.js +36 -0
- package/dist/lint/audit-lineage.js +71 -0
- package/dist/lint/post-synth/arm-refs.js +94 -0
- package/dist/lint/post-synth/azr010.js +45 -0
- package/dist/lint/post-synth/azr011.js +43 -0
- package/dist/lint/post-synth/azr012.js +41 -0
- package/dist/lint/post-synth/azr013.js +50 -0
- package/dist/lint/post-synth/azr014.js +49 -0
- package/dist/lint/post-synth/azr015.js +66 -0
- package/dist/lint/post-synth/azr016.js +37 -0
- package/dist/lint/post-synth/azr017.js +37 -0
- package/dist/lint/post-synth/azr018.js +42 -0
- package/dist/lint/post-synth/azr019.js +41 -0
- package/dist/lint/post-synth/azr020.js +37 -0
- package/dist/lint/post-synth/azr021.js +36 -0
- package/dist/lint/post-synth/azr022.js +39 -0
- package/dist/lint/post-synth/azr023.js +39 -0
- package/dist/lint/post-synth/azr024.js +39 -0
- package/dist/lint/post-synth/azr025.js +37 -0
- package/dist/lint/post-synth/azr026.js +38 -0
- package/dist/lint/post-synth/azr027.js +37 -0
- package/dist/lint/post-synth/azr028.js +37 -0
- package/dist/lint/post-synth/azr029.js +38 -0
- package/dist/lint/post-synth/azr030.js +44 -0
- package/dist/lint/post-synth/azr031.js +36 -0
- package/dist/lint/post-synth/azr032.js +53 -0
- package/dist/lint/post-synth/azr033.js +35 -0
- package/dist/lint/post-synth/index.js +50 -0
- package/dist/lint/rules/hardcoded-location.js +82 -0
- package/dist/lint/rules/index.js +6 -0
- package/dist/lint/rules/nsg-wildcard.js +44 -0
- package/dist/lint/rules/storage-https.js +62 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +18 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +44 -0
- package/dist/manifest.json +1 -1
- package/dist/op/activities/az-apply.js +563 -0
- package/dist/op/activities/azure.js +48 -0
- package/dist/op/activities/floci-az.js +37 -0
- package/dist/op/activities/index.js +10 -0
- package/dist/op/builders.js +70 -0
- package/dist/ownership.js +13 -0
- package/dist/package-cli.js +21 -0
- package/dist/parameter.js +13 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +472 -0
- package/dist/pseudo.js +40 -0
- package/dist/serializer.js +457 -0
- package/dist/spec/api-versions.js +130 -0
- package/dist/spec/fetch.js +196 -0
- package/dist/spec/parse.js +308 -0
- package/dist/taggable.js +34 -0
- package/dist/validate-cli.js +16 -0
- package/dist/validate.js +53 -0
- package/package.json +8 -8
- package/src/codegen/package.ts +2 -2
- package/src/deploy-scopes.ts +2 -1
- package/src/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
- package/src/plugin.ts +3 -2
- package/src/serializer.ts +2 -2
- 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;
|
|
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
|
+
}
|