@intentius/chant-lexicon-azure 0.44.13 → 0.45.0
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/codegen/docs.d.ts.map +1 -1
- package/dist/deploy-scopes.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/op/activities/az-apply.d.ts +37 -23
- package/dist/op/activities/az-apply.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +1 -1
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/serializer.d.ts +14 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/codegen/docs.ts +0 -16
- package/src/deploy-scopes.ts +19 -5
- package/src/op/activities/az-apply.test.ts +187 -21
- package/src/op/activities/az-apply.ts +104 -29
- package/src/op/activities/index.ts +1 -0
- package/src/serializer-api-versions.test.ts +94 -0
- package/src/serializer.ts +69 -32
- package/src/validate.ts +6 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/codegen/docs.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/codegen/docs.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAwEH;;GAEG;AACH,wBAAsB,YAAY,CAAC,IAAI,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAM9E"}
|
|
@@ -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;
|
|
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"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "4609a8f5c9124e4231b979bb8d752c0e30548aee93dab288629af7cc575d4ac3",
|
|
5
5
|
"meta.json": "760a8d2e359be71f29a990339cb075701fe668e6275dfa923b9d112474ba660a",
|
|
6
6
|
"types/index.d.ts": "c5a8a3e107b07a11ac992fa7d6d136fe1a71c99a89c57921b3e4bc86361adb3f",
|
|
7
7
|
"rules/hardcoded-location.ts": "a9b1d1cec93f2ca9c5206641f53fc9621cefcfd2c00b3ab89c194b30a75fa949",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"skills/chant-azure-patterns.md": "1a3bacfac612826b77332d2692a59195638db9f1b5e8ea2eda7579621d25f603",
|
|
38
38
|
"skills/chant-azure-aks.md": "2d4e0098c1a22b54ffadd410564d9df0f3a04cb4eb7c6261f20ae33106e7aca8"
|
|
39
39
|
},
|
|
40
|
-
"composite": "
|
|
40
|
+
"composite": "b01efebfbcfbced31dbbb52f7f3ae3b656ba2a0ff41be474bbe9c27aad9961bf"
|
|
41
41
|
}
|
package/dist/manifest.json
CHANGED
|
@@ -177,37 +177,51 @@ export interface AzNotPrunable {
|
|
|
177
177
|
type: string;
|
|
178
178
|
name: string;
|
|
179
179
|
/**
|
|
180
|
-
* `no-api-version` — the resource is owned and undeclared, but
|
|
181
|
-
*
|
|
182
|
-
*
|
|
180
|
+
* `no-api-version` — the resource is owned and undeclared, but no apiVersion
|
|
181
|
+
* could be found for its type: the current template does not declare the
|
|
182
|
+
* type, the lexicon's pinned registry has no entry for it, and ARM's
|
|
183
|
+
* provider metadata listed no apiVersion for it (or the call failed).
|
|
184
|
+
* `deleteArmResource` cannot build a URL without one.
|
|
183
185
|
*/
|
|
184
186
|
reason: "no-api-version";
|
|
185
187
|
}
|
|
186
188
|
/**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
189
|
+
* Resolve an apiVersion for a live resource type from ARM's provider metadata:
|
|
190
|
+
* `GET /subscriptions/{sub}/providers/{namespace}?api-version=2021-04-01`
|
|
191
|
+
* returns `resourceTypes[].apiVersions[]`. Picks the newest stable version,
|
|
192
|
+
* falling back to the newest preview when no stable one exists. One call per
|
|
193
|
+
* namespace per prune run; the result is memoised in `cache` so a group with
|
|
194
|
+
* many orphans of one provider costs one round trip. Returns `undefined` when
|
|
195
|
+
* the endpoint is unavailable (floci-az does not implement it) or the type is
|
|
196
|
+
* not listed.
|
|
197
|
+
*/
|
|
198
|
+
export declare function providerApiVersion(resourceType: string, ctx: ArmEvalCtx, http?: AzHttp, signal?: AbortSignal, cache?: Map<string, Map<string, string>>): Promise<string | undefined>;
|
|
199
|
+
/**
|
|
200
|
+
* Owned-only prune: delete every chant-owned live resource in the group whose
|
|
201
|
+
* (evaluated) name is not declared in the template. Foreign (non-chant)
|
|
202
|
+
* resources are never touched.
|
|
203
|
+
*
|
|
204
|
+
* ## Where the apiVersion comes from (#1457, #1472)
|
|
190
205
|
*
|
|
191
|
-
*
|
|
206
|
+
* `deleteArmResource` needs an `apiVersion`, and ARM's resource-group listing
|
|
207
|
+
* does not return one per resource. The template is the obvious source, but it
|
|
208
|
+
* only covers types it still declares: declare one storage account, apply,
|
|
209
|
+
* delete it from source, and the template has zero resources of that type.
|
|
210
|
+
* Keying the delete scope off the template alone made the last resource of a
|
|
211
|
+
* type a permanent orphan (#1457 made that a reported skip rather than a
|
|
212
|
+
* silent one).
|
|
192
213
|
*
|
|
193
|
-
*
|
|
194
|
-
* `!entry` guard skipped any live resource of a type the template no longer
|
|
195
|
-
* mentions — before `isChantOwned` was ever consulted.
|
|
214
|
+
* So the apiVersion is resolved in order:
|
|
196
215
|
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
216
|
+
* 1. the template, when it still declares the type;
|
|
217
|
+
* 2. the lexicon's pinned per-provider registry (`lookupApiVersion`, the same
|
|
218
|
+
* registry the serializer stamps into the template) — deterministic, no
|
|
219
|
+
* extra call, covers every type the lexicon can emit;
|
|
220
|
+
* 3. ARM's provider metadata (`providerApiVersion`) — covers a type the
|
|
221
|
+
* lexicon has never heard of, at one extra call per namespace per run.
|
|
203
222
|
*
|
|
204
|
-
*
|
|
205
|
-
* `
|
|
206
|
-
* resource-group listing does not return it per resource. So the skip stays,
|
|
207
|
-
* and is now REPORTED rather than silent: a permanent invisible orphan becomes
|
|
208
|
-
* a permanent visible one, which is the difference between a bug and a known
|
|
209
|
-
* limitation. Resolving it properly wants a per-type apiVersion source (the
|
|
210
|
-
* provider metadata endpoint, or a lexicon-side map).
|
|
223
|
+
* Only when all three come up empty is the resource reported as
|
|
224
|
+
* `notPrunable: no-api-version`.
|
|
211
225
|
*
|
|
212
226
|
* This matters more since #1448, which routed `ApplyOp`'s `arm` target through
|
|
213
227
|
* `azApply` — before that, the composite reached `--mode Complete` and never
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"az-apply.d.ts","sourceRoot":"","sources":["../../../src/op/activities/az-apply.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"az-apply.d.ts","sourceRoot":"","sources":["../../../src/op/activities/az-apply.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,6 +7,6 @@ export { azGroupEnsure, azGroupDelete, azGroupEnsureCommand, azGroupDeleteComman
|
|
|
7
7
|
export type { AzGroupEnsureArgs, AzGroupDeleteArgs } from "./azure.js";
|
|
8
8
|
export { flociAzUp, flociAzDown, flociAzRunCommand, flociAzRmCommand, flociAzExistsCommand, flociAzHealthUrl, flociAzEndpoint, } from "./floci-az.js";
|
|
9
9
|
export type { FlociAzUpArgs, FlociAzDownArgs } from "./floci-az.js";
|
|
10
|
-
export { azApply, azDelete, pruneArmOrphans, toApplyResult, deleteArmResource, listGroupResources, chantOwnershipTags, isChantOwned, evalArm, evalArmString, armResourceUrl, armResourceBody, armDependencies, orderArmResources, } from "./az-apply.js";
|
|
10
|
+
export { azApply, azDelete, pruneArmOrphans, providerApiVersion, toApplyResult, deleteArmResource, listGroupResources, chantOwnershipTags, isChantOwned, evalArm, evalArmString, armResourceUrl, armResourceBody, armDependencies, orderArmResources, } from "./az-apply.js";
|
|
11
11
|
export type { AzApplyArgs, ArmEvalCtx, ArmResource, ArmListItem, AzHttp } from "./az-apply.js";
|
|
12
12
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/op/activities/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AACnG,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAIpE,OAAO,EACL,SAAS,EACT,WAAW,EACX,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEjE,OAAO,EACL,OAAO,EACP,QAAQ,EACR,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,YAAY,EACZ,OAAO,EACP,aAAa,EACb,cAAc,EACd,eAAe,EACf,eAAe,EACf,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/op/activities/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AACnG,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAIpE,OAAO,EACL,SAAS,EACT,WAAW,EACX,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEjE,OAAO,EACL,OAAO,EACP,QAAQ,EACR,eAAe,EACf,kBAAkB,EAClB,aAAa,EACb,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,YAAY,EACZ,OAAO,EACP,aAAa,EACb,cAAc,EACd,eAAe,EACf,eAAe,EACf,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/serializer.d.ts
CHANGED
|
@@ -10,6 +10,20 @@
|
|
|
10
10
|
* - parameters, outputs sections
|
|
11
11
|
*/
|
|
12
12
|
import type { Serializer } from "@intentius/chant/serializer";
|
|
13
|
+
export declare function apiVersionRegistryCandidates(dir?: string): string[];
|
|
14
|
+
export declare function loadApiVersions(candidates?: string[]): Map<string, string>;
|
|
15
|
+
/** Test hook: drop the cached registry so the next lookup reloads it. */
|
|
16
|
+
export declare function resetApiVersionCache(): void;
|
|
17
|
+
/**
|
|
18
|
+
* Registry lookup without the serializer's fallback: the pinned apiVersion for
|
|
19
|
+
* `resourceType`, or `undefined` when neither the overrides nor the generated
|
|
20
|
+
* registry know the type. ARM resource types are case-insensitive and the live
|
|
21
|
+
* listing does not always echo the casing the registry uses, so the match is
|
|
22
|
+
* case-insensitive too. Used by prune (#1472), which needs an apiVersion for a
|
|
23
|
+
* type the current template no longer declares.
|
|
24
|
+
*/
|
|
25
|
+
export declare function lookupApiVersion(resourceType: string): string | undefined;
|
|
26
|
+
export declare function getApiVersion(resourceType: string): string;
|
|
13
27
|
/**
|
|
14
28
|
* Azure ARM template serializer implementation.
|
|
15
29
|
*/
|
package/dist/serializer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAsC,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAsC,MAAM,6BAA6B,CAAC;AA8ElG,wBAAgB,4BAA4B,CAAC,GAAG,GAAE,MAAqB,GAAG,MAAM,EAAE,CAKjF;AAED,wBAAgB,eAAe,CAAC,UAAU,GAAE,MAAM,EAAmC,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAwB1G;AAcD,yEAAyE;AACzE,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAWzE;AAED,wBAAgB,aAAa,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAO1D;AA2TD;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,UA0C7B,CAAC"}
|
package/dist/validate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAGlG,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AA2BvF,wBAAsB,QAAQ,CAAC,IAAI,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,cAAc,CAAC,
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAGlG,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AA2BvF,wBAAsB,QAAQ,CAAC,IAAI,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAsBpF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-azure",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
4
4
|
"description": "Azure lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"typescript": "^5.9.3"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"@intentius/chant": "^0.
|
|
77
|
+
"@intentius/chant": "^0.45.0",
|
|
78
78
|
"typescript": "^5.9.3"
|
|
79
79
|
}
|
|
80
80
|
}
|
package/src/codegen/docs.ts
CHANGED
|
@@ -75,22 +75,6 @@ const docsConfig: DocsConfig = {
|
|
|
75
75
|
// `lint-rules` below is the rules documentation; the generated `rules` table
|
|
76
76
|
// would duplicate it and land in no sidebar (#1312). aws, k8s, gitlab and
|
|
77
77
|
// github already suppress it for the same reason.
|
|
78
|
-
// These pages are hand-written files under docs/src/content/docs/. The
|
|
79
|
-
// pipeline preserves pages it did not emit, but Starlight has no
|
|
80
|
-
// auto-discovery, so without an entry here they are reachable only by URL —
|
|
81
|
-
// which is what had happened to all ten of them.
|
|
82
|
-
sidebarExtra: [
|
|
83
|
-
{ label: "Getting Started", slug: "getting-started" },
|
|
84
|
-
{ label: "Resources", slug: "resources" },
|
|
85
|
-
{ label: "Parameters & Outputs", slug: "parameters-outputs" },
|
|
86
|
-
{ label: "Composites", slug: "composites" },
|
|
87
|
-
{ label: "Linked Templates", slug: "linked-templates" },
|
|
88
|
-
{ label: "Lint Rules", slug: "lint-rules" },
|
|
89
|
-
{ label: "Importing ARM Templates", slug: "importing" },
|
|
90
|
-
{ label: "Deploying to AKS", slug: "aks-kubernetes" },
|
|
91
|
-
{ label: "Examples", slug: "examples" },
|
|
92
|
-
{ label: "AI Skills", slug: "skills" },
|
|
93
|
-
],
|
|
94
78
|
};
|
|
95
79
|
|
|
96
80
|
/**
|
package/src/deploy-scopes.ts
CHANGED
|
@@ -85,7 +85,20 @@ interface LexiconEntry {
|
|
|
85
85
|
|
|
86
86
|
let _cached: Map<string, DeployScope[]> | undefined;
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Directory of this module, resolved lazily. On workerd `import.meta.url`
|
|
90
|
+
* is undefined and `fileURLToPath` throws, so this must never run at module
|
|
91
|
+
* scope (#1621): the post-synth barrel imports this file and a module-scope
|
|
92
|
+
* throw kills the worker at startup. Returns undefined when unresolvable;
|
|
93
|
+
* callers then take the documented resource-group-only fallback.
|
|
94
|
+
*/
|
|
95
|
+
function moduleDir(): string | undefined {
|
|
96
|
+
try {
|
|
97
|
+
return dirname(fileURLToPath(import.meta.url));
|
|
98
|
+
} catch {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
89
102
|
|
|
90
103
|
/**
|
|
91
104
|
* Load per-resource deployment scopes from the lexicon JSON.
|
|
@@ -98,11 +111,12 @@ export function loadDeployScopes(): Map<string, DeployScope[]> {
|
|
|
98
111
|
const map = new Map<string, DeployScope[]>();
|
|
99
112
|
try {
|
|
100
113
|
let content: string | undefined;
|
|
114
|
+
const dir = moduleDir();
|
|
101
115
|
// Try generated/ (dev) first, then dist/meta.json (installed package)
|
|
102
|
-
for (const candidate of [
|
|
103
|
-
join(
|
|
104
|
-
join(
|
|
105
|
-
]) {
|
|
116
|
+
for (const candidate of dir ? [
|
|
117
|
+
join(dir, "generated", "lexicon-azure.json"),
|
|
118
|
+
join(dir, "..", "dist", "meta.json"),
|
|
119
|
+
] : []) {
|
|
106
120
|
try { content = readFileSync(candidate, "utf-8"); break; } catch {}
|
|
107
121
|
}
|
|
108
122
|
if (content) {
|
|
@@ -19,7 +19,9 @@ import {
|
|
|
19
19
|
type ArmEvalCtx,
|
|
20
20
|
type ArmResource,
|
|
21
21
|
type AzHttp,
|
|
22
|
+
providerApiVersion,
|
|
22
23
|
} from "./az-apply";
|
|
24
|
+
import { lookupApiVersion } from "../../serializer";
|
|
23
25
|
|
|
24
26
|
const noHttp: AzHttp = async () => ({ status: 200, text: "{}" });
|
|
25
27
|
|
|
@@ -278,7 +280,7 @@ describe("pruneArmOrphans (#azure-prune)", () => {
|
|
|
278
280
|
{ id: "/1", name: "keep1", type: "Microsoft.Storage/storageAccounts", tags: { "managed-by": "chant" } }, // in template → keep
|
|
279
281
|
{ id: "/2", name: "orphan1", type: "Microsoft.Storage/storageAccounts", tags: { "managed-by": "chant" } }, // owned, not in template → prune
|
|
280
282
|
{ id: "/3", name: "foreign", type: "Microsoft.Storage/storageAccounts", tags: {} }, // not owned → skip
|
|
281
|
-
{ id: "/4", name: "othertype", type: "Microsoft.Web/sites", tags: { "managed-by": "chant" } }, // type not templated →
|
|
283
|
+
{ id: "/4", name: "othertype", type: "Microsoft.Web/sites", tags: { "managed-by": "chant" } }, // type not templated → registry apiVersion
|
|
282
284
|
],
|
|
283
285
|
};
|
|
284
286
|
const deletes: string[] = [];
|
|
@@ -287,15 +289,16 @@ describe("pruneArmOrphans (#azure-prune)", () => {
|
|
|
287
289
|
return { status: 200, text: method === "GET" ? JSON.stringify(live) : "" };
|
|
288
290
|
};
|
|
289
291
|
const { pruned, notPrunable } = await pruneArmOrphans(desired, ctx(), http);
|
|
290
|
-
expect(pruned).toEqual([
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
// #1457: "othertype" is chant-owned and undeclared, and its TYPE is absent
|
|
294
|
-
// from the template — so there is no apiVersion to delete it with. It used
|
|
295
|
-
// to be dropped by the `!entry` guard in silence; now it is reported.
|
|
296
|
-
expect(notPrunable).toEqual([
|
|
297
|
-
{ type: "Microsoft.Web/sites", name: "othertype", reason: "no-api-version" },
|
|
292
|
+
expect(pruned).toEqual([
|
|
293
|
+
{ type: "Microsoft.Storage/storageAccounts", name: "orphan1", deleted: true },
|
|
294
|
+
{ type: "Microsoft.Web/sites", name: "othertype", deleted: true },
|
|
298
295
|
]);
|
|
296
|
+
expect(deletes).toHaveLength(2);
|
|
297
|
+
expect(deletes[0]).toContain("/storageAccounts/orphan1?api-version=2023-01-01"); // apiVersion from the template
|
|
298
|
+
// #1472: "othertype" is chant-owned and undeclared, and its TYPE is absent
|
|
299
|
+
// from the template — the apiVersion comes from the lexicon registry instead.
|
|
300
|
+
expect(deletes[1]).toContain(`/sites/othertype?api-version=${lookupApiVersion("Microsoft.Web/sites")}`);
|
|
301
|
+
expect(notPrunable).toEqual([]);
|
|
299
302
|
});
|
|
300
303
|
|
|
301
304
|
// #1448 routes ApplyOp's `arm` target here, so this is now the delete scope for
|
|
@@ -412,7 +415,7 @@ describe("azDelete (#azure-prune)", () => {
|
|
|
412
415
|
* unreachable by prune forever. Prune one of SEVERAL and it works, which is why
|
|
413
416
|
* this survived testing.
|
|
414
417
|
*/
|
|
415
|
-
describe("pruneArmOrphans: a type that left the template (#1457)", () => {
|
|
418
|
+
describe("pruneArmOrphans: a type that left the template (#1457, #1472)", () => {
|
|
416
419
|
const ctx = (): ArmEvalCtx => ({
|
|
417
420
|
subscriptionId: "sub",
|
|
418
421
|
resourceGroup: "rg",
|
|
@@ -422,30 +425,193 @@ describe("pruneArmOrphans: a type that left the template (#1457)", () => {
|
|
|
422
425
|
base: "http://x",
|
|
423
426
|
});
|
|
424
427
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
428
|
+
// Only the resource-group listing answers; the provider-metadata endpoint
|
|
429
|
+
// 404s, the way floci-az does.
|
|
430
|
+
const liveHttp = (value: unknown[], deletes: string[], providers: string[] = []): AzHttp =>
|
|
431
|
+
async (method, url) => {
|
|
432
|
+
if (method === "DELETE") deletes.push(url);
|
|
433
|
+
if (method === "GET" && url.includes("/resources?")) return { status: 200, text: JSON.stringify({ value }) };
|
|
434
|
+
if (method === "GET" && url.includes("/providers/")) {
|
|
435
|
+
providers.push(url);
|
|
436
|
+
return { status: 404, text: "" };
|
|
437
|
+
}
|
|
438
|
+
return { status: 200, text: "" };
|
|
439
|
+
};
|
|
429
440
|
|
|
430
|
-
test("an owned orphan whose type is gone is
|
|
441
|
+
test("an owned orphan whose type is gone is deleted with the registry apiVersion", async () => {
|
|
431
442
|
// The template now declares a DIFFERENT type entirely — the storage account
|
|
432
|
-
// was the last of its kind and has been removed from source.
|
|
443
|
+
// was the last of its kind and has been removed from source. #1457 only
|
|
444
|
+
// reported this; the registry the serializer pins from now supplies the
|
|
445
|
+
// apiVersion, so the orphan is actually deleted.
|
|
433
446
|
const desired: ArmResource[] = [
|
|
434
447
|
{ type: "Microsoft.Web/sites", apiVersion: "2022-03-01", name: "site" },
|
|
435
448
|
];
|
|
436
449
|
const deletes: string[] = [];
|
|
450
|
+
const providers: string[] = [];
|
|
437
451
|
const http = liveHttp(
|
|
438
452
|
[{ id: "/1", name: "leftover", type: "Microsoft.Storage/storageAccounts", tags: { "managed-by": "chant" } }],
|
|
439
453
|
deletes,
|
|
454
|
+
providers,
|
|
440
455
|
);
|
|
441
456
|
const { pruned, notPrunable } = await pruneArmOrphans(desired, ctx(), http);
|
|
442
|
-
expect(pruned).toEqual([]);
|
|
443
|
-
expect(notPrunable).toEqual([
|
|
444
|
-
|
|
457
|
+
expect(pruned).toEqual([{ type: "Microsoft.Storage/storageAccounts", name: "leftover", deleted: true }]);
|
|
458
|
+
expect(notPrunable).toEqual([]);
|
|
459
|
+
const registryVersion = lookupApiVersion("Microsoft.Storage/storageAccounts");
|
|
460
|
+
expect(registryVersion).toBeDefined();
|
|
461
|
+
expect(deletes).toEqual([
|
|
462
|
+
`http://x/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/leftover?api-version=${registryVersion}`,
|
|
463
|
+
]);
|
|
464
|
+
// The registry answered, so ARM's provider metadata was never consulted.
|
|
465
|
+
expect(providers).toEqual([]);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("the live listing's type casing need not match the registry's", async () => {
|
|
469
|
+
const desired: ArmResource[] = [];
|
|
470
|
+
const deletes: string[] = [];
|
|
471
|
+
const http = liveHttp(
|
|
472
|
+
[{ id: "/1", name: "leftover", type: "microsoft.storage/storageaccounts", tags: { "managed-by": "chant" } }],
|
|
473
|
+
deletes,
|
|
474
|
+
);
|
|
475
|
+
const { pruned, notPrunable } = await pruneArmOrphans(desired, ctx(), http);
|
|
476
|
+
expect(pruned).toEqual([{ type: "microsoft.storage/storageaccounts", name: "leftover", deleted: true }]);
|
|
477
|
+
expect(notPrunable).toEqual([]);
|
|
478
|
+
expect(deletes[0]).toContain(`?api-version=${lookupApiVersion("Microsoft.Storage/storageAccounts")}`);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("a type the registry does not know falls back to ARM provider metadata, newest stable", async () => {
|
|
482
|
+
const desired: ArmResource[] = [];
|
|
483
|
+
const deletes: string[] = [];
|
|
484
|
+
const gets: string[] = [];
|
|
485
|
+
const http: AzHttp = async (method, url) => {
|
|
486
|
+
if (method === "DELETE") deletes.push(url);
|
|
487
|
+
if (method === "GET") gets.push(url);
|
|
488
|
+
if (method === "GET" && url.includes("/resources?")) {
|
|
489
|
+
return {
|
|
490
|
+
status: 200,
|
|
491
|
+
text: JSON.stringify({
|
|
492
|
+
value: [
|
|
493
|
+
{ id: "/1", name: "a", type: "Microsoft.Example/widgets", tags: { "managed-by": "chant" } },
|
|
494
|
+
{ id: "/2", name: "b", type: "Microsoft.Example/widgets", tags: { "managed-by": "chant" } },
|
|
495
|
+
],
|
|
496
|
+
}),
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
if (method === "GET" && url.includes("/providers/Microsoft.Example?")) {
|
|
500
|
+
return {
|
|
501
|
+
status: 200,
|
|
502
|
+
text: JSON.stringify({
|
|
503
|
+
namespace: "Microsoft.Example",
|
|
504
|
+
resourceTypes: [
|
|
505
|
+
{ resourceType: "widgets", apiVersions: ["2024-05-01-preview", "2023-09-01", "2024-01-01", "2022-01-01"] },
|
|
506
|
+
{ resourceType: "gadgets", apiVersions: ["2020-01-01"] },
|
|
507
|
+
],
|
|
508
|
+
}),
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
return { status: 200, text: "" };
|
|
512
|
+
};
|
|
513
|
+
const { pruned, notPrunable } = await pruneArmOrphans(desired, ctx(), http);
|
|
514
|
+
expect(pruned).toEqual([
|
|
515
|
+
{ type: "Microsoft.Example/widgets", name: "a", deleted: true },
|
|
516
|
+
{ type: "Microsoft.Example/widgets", name: "b", deleted: true },
|
|
445
517
|
]);
|
|
446
|
-
|
|
447
|
-
//
|
|
518
|
+
expect(notPrunable).toEqual([]);
|
|
519
|
+
// Newest STABLE wins over the newer preview.
|
|
520
|
+
expect(deletes).toEqual([
|
|
521
|
+
"http://x/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Example/widgets/a?api-version=2024-01-01",
|
|
522
|
+
"http://x/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Example/widgets/b?api-version=2024-01-01",
|
|
523
|
+
]);
|
|
524
|
+
// One provider-metadata call per namespace per run, not per orphan.
|
|
525
|
+
expect(gets.filter((u) => u.includes("/providers/Microsoft.Example?api-version=2021-04-01"))).toHaveLength(1);
|
|
526
|
+
expect(gets.find((u) => u.includes("/providers/Microsoft.Example?"))).toBe(
|
|
527
|
+
"http://x/subscriptions/sub/providers/Microsoft.Example?api-version=2021-04-01",
|
|
528
|
+
);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("no template, no registry entry, no provider metadata → still reported, never deleted", async () => {
|
|
532
|
+
const desired: ArmResource[] = [];
|
|
533
|
+
const deletes: string[] = [];
|
|
534
|
+
const providers: string[] = [];
|
|
535
|
+
const http = liveHttp(
|
|
536
|
+
[{ id: "/1", name: "mystery", type: "Microsoft.Nowhere/things", tags: { "managed-by": "chant" } }],
|
|
537
|
+
deletes,
|
|
538
|
+
providers,
|
|
539
|
+
);
|
|
540
|
+
const { pruned, notPrunable } = await pruneArmOrphans(desired, ctx(), http);
|
|
541
|
+
expect(pruned).toEqual([]);
|
|
542
|
+
expect(notPrunable).toEqual([{ type: "Microsoft.Nowhere/things", name: "mystery", reason: "no-api-version" }]);
|
|
448
543
|
expect(deletes).toEqual([]);
|
|
544
|
+
// All three sources were tried; the last one is the provider endpoint.
|
|
545
|
+
expect(providers).toEqual(["http://x/subscriptions/sub/providers/Microsoft.Nowhere?api-version=2021-04-01"]);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
test("providerApiVersion: preview-only types, missing types, and failed calls", async () => {
|
|
549
|
+
const cache = new Map<string, Map<string, string>>();
|
|
550
|
+
let calls = 0;
|
|
551
|
+
const http: AzHttp = async () => {
|
|
552
|
+
calls++;
|
|
553
|
+
return {
|
|
554
|
+
status: 200,
|
|
555
|
+
text: JSON.stringify({
|
|
556
|
+
resourceTypes: [{ resourceType: "previewOnly", apiVersions: ["2023-01-01-preview", "2024-01-01-preview"] }],
|
|
557
|
+
}),
|
|
558
|
+
};
|
|
559
|
+
};
|
|
560
|
+
expect(await providerApiVersion("Microsoft.P/previewOnly", ctx(), http, undefined, cache)).toBe("2024-01-01-preview");
|
|
561
|
+
expect(await providerApiVersion("Microsoft.P/absent", ctx(), http, undefined, cache)).toBeUndefined();
|
|
562
|
+
expect(calls).toBe(1);
|
|
563
|
+
expect(await providerApiVersion("notatype", ctx(), http)).toBeUndefined();
|
|
564
|
+
const failing: AzHttp = async () => { throw new Error("network"); };
|
|
565
|
+
expect(await providerApiVersion("Microsoft.P/previewOnly", ctx(), failing)).toBeUndefined();
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
test("azApply end to end: declare one storage account, apply, remove it, prune → deleted", async () => {
|
|
569
|
+
// The Floci-az style fake: PUT records the resource (with the ownership tag
|
|
570
|
+
// chant stamps), the group listing echoes what was recorded, DELETE removes
|
|
571
|
+
// it. The provider-metadata endpoint is absent, as on floci-az.
|
|
572
|
+
const store = new Map<string, { id: string; name: string; type: string; tags?: Record<string, string> }>();
|
|
573
|
+
const deletes: string[] = [];
|
|
574
|
+
const http: AzHttp = async (method, url, body) => {
|
|
575
|
+
const m = url.match(/\/providers\/([^/]+\/[^/]+)\/([^/?]+)\?api-version=([^&]+)$/);
|
|
576
|
+
if (method === "PUT" && m) {
|
|
577
|
+
const tags = (body as { tags?: Record<string, string> }).tags;
|
|
578
|
+
store.set(`${m[1]}/${m[2]}`, { id: `/${m[2]}`, name: m[2], type: m[1], tags });
|
|
579
|
+
return { status: 200, text: JSON.stringify({ properties: {} }) };
|
|
580
|
+
}
|
|
581
|
+
if (method === "DELETE" && m) {
|
|
582
|
+
deletes.push(url);
|
|
583
|
+
const had = store.delete(`${m[1]}/${m[2]}`);
|
|
584
|
+
return { status: had ? 200 : 404, text: "" };
|
|
585
|
+
}
|
|
586
|
+
if (method === "GET" && url.includes("/resources?")) {
|
|
587
|
+
return { status: 200, text: JSON.stringify({ value: [...store.values()] }) };
|
|
588
|
+
}
|
|
589
|
+
return { status: 404, text: "" };
|
|
590
|
+
};
|
|
591
|
+
const path = `/tmp/chant-1472-az-${process.pid}.json`;
|
|
592
|
+
const account = { type: "Microsoft.Storage/storageAccounts", apiVersion: "2023-01-01", name: "lastone" };
|
|
593
|
+
try {
|
|
594
|
+
writeFileSync(path, JSON.stringify({ resources: [account] }));
|
|
595
|
+
const first = await azApply({ templatePath: path, resourceGroup: "rg", endpoint: "http://x", prune: true }, undefined, http);
|
|
596
|
+
expect(first.applied).toEqual([{ type: account.type, name: "lastone" }]);
|
|
597
|
+
expect(store.size).toBe(1);
|
|
598
|
+
|
|
599
|
+
// Remove the only storage account from source: the template now declares
|
|
600
|
+
// no resource of that type at all.
|
|
601
|
+
writeFileSync(path, JSON.stringify({ resources: [] }));
|
|
602
|
+
const second = await azApply({ templatePath: path, resourceGroup: "rg", endpoint: "http://x", prune: true }, undefined, http);
|
|
603
|
+
expect(second.applied).toEqual([]);
|
|
604
|
+
expect(second.pruned).toEqual([{ type: account.type, name: "lastone", deleted: true }]);
|
|
605
|
+
expect(second.notPrunable).toEqual([]);
|
|
606
|
+
expect(store.size).toBe(0);
|
|
607
|
+
expect(deletes).toEqual([
|
|
608
|
+
`http://x/subscriptions/${"00000000-0000-0000-0000-000000000001"}/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/lastone?api-version=${lookupApiVersion(account.type)}`,
|
|
609
|
+
]);
|
|
610
|
+
// Core's envelope agrees: nothing left over that could not be attempted.
|
|
611
|
+
expect(toApplyResult(second).notAttempted ?? []).toEqual([]);
|
|
612
|
+
} finally {
|
|
613
|
+
unlinkSync(path);
|
|
614
|
+
}
|
|
449
615
|
});
|
|
450
616
|
|
|
451
617
|
test("pruning one of several of a type still works — the case that hid this", async () => {
|
|
@@ -3,6 +3,8 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { safeHeartbeat } from "@intentius/chant/op";
|
|
4
4
|
import { hasOwnershipMarker, OWNERSHIP_MANAGED_BY_VALUE } from "@intentius/chant/ownership";
|
|
5
5
|
import { AZURE_TAG_OWNERSHIP_KEYS } from "../../ownership";
|
|
6
|
+
import { lookupApiVersion } from "../../serializer";
|
|
7
|
+
import { compareApiDates } from "../../spec/api-versions";
|
|
6
8
|
import {
|
|
7
9
|
applyResult,
|
|
8
10
|
type ApplyResult,
|
|
@@ -501,38 +503,96 @@ export interface AzNotPrunable {
|
|
|
501
503
|
type: string;
|
|
502
504
|
name: string;
|
|
503
505
|
/**
|
|
504
|
-
* `no-api-version` — the resource is owned and undeclared, but
|
|
505
|
-
*
|
|
506
|
-
*
|
|
506
|
+
* `no-api-version` — the resource is owned and undeclared, but no apiVersion
|
|
507
|
+
* could be found for its type: the current template does not declare the
|
|
508
|
+
* type, the lexicon's pinned registry has no entry for it, and ARM's
|
|
509
|
+
* provider metadata listed no apiVersion for it (or the call failed).
|
|
510
|
+
* `deleteArmResource` cannot build a URL without one.
|
|
507
511
|
*/
|
|
508
512
|
reason: "no-api-version";
|
|
509
513
|
}
|
|
510
514
|
|
|
515
|
+
/** The ARM provider-metadata apiVersion (`GET /subscriptions/{sub}/providers/{ns}`). */
|
|
516
|
+
const PROVIDER_METADATA_API_VERSION = "2021-04-01";
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Resolve an apiVersion for a live resource type from ARM's provider metadata:
|
|
520
|
+
* `GET /subscriptions/{sub}/providers/{namespace}?api-version=2021-04-01`
|
|
521
|
+
* returns `resourceTypes[].apiVersions[]`. Picks the newest stable version,
|
|
522
|
+
* falling back to the newest preview when no stable one exists. One call per
|
|
523
|
+
* namespace per prune run; the result is memoised in `cache` so a group with
|
|
524
|
+
* many orphans of one provider costs one round trip. Returns `undefined` when
|
|
525
|
+
* the endpoint is unavailable (floci-az does not implement it) or the type is
|
|
526
|
+
* not listed.
|
|
527
|
+
*/
|
|
528
|
+
export async function providerApiVersion(
|
|
529
|
+
resourceType: string,
|
|
530
|
+
ctx: ArmEvalCtx,
|
|
531
|
+
http: AzHttp = defaultHttp,
|
|
532
|
+
signal?: AbortSignal,
|
|
533
|
+
cache: Map<string, Map<string, string>> = new Map(),
|
|
534
|
+
): Promise<string | undefined> {
|
|
535
|
+
const slash = resourceType.indexOf("/");
|
|
536
|
+
if (slash < 0) return undefined;
|
|
537
|
+
const namespace = resourceType.slice(0, slash);
|
|
538
|
+
const typeName = resourceType.slice(slash + 1).toLowerCase();
|
|
539
|
+
let byType = cache.get(namespace.toLowerCase());
|
|
540
|
+
if (!byType) {
|
|
541
|
+
byType = new Map();
|
|
542
|
+
cache.set(namespace.toLowerCase(), byType);
|
|
543
|
+
const url = `${ctx.base}/subscriptions/${ctx.subscriptionId}/providers/${namespace}?api-version=${PROVIDER_METADATA_API_VERSION}`;
|
|
544
|
+
let res: { status: number; text: string };
|
|
545
|
+
try {
|
|
546
|
+
res = await http("GET", url, undefined, signal);
|
|
547
|
+
} catch {
|
|
548
|
+
return undefined;
|
|
549
|
+
}
|
|
550
|
+
if (res.status >= 300) return undefined;
|
|
551
|
+
let parsed: { resourceTypes?: Array<{ resourceType?: string; apiVersions?: string[] }> };
|
|
552
|
+
try {
|
|
553
|
+
parsed = JSON.parse(res.text);
|
|
554
|
+
} catch {
|
|
555
|
+
return undefined;
|
|
556
|
+
}
|
|
557
|
+
for (const rt of parsed.resourceTypes ?? []) {
|
|
558
|
+
if (!rt?.resourceType || !Array.isArray(rt.apiVersions) || rt.apiVersions.length === 0) continue;
|
|
559
|
+
const versions = rt.apiVersions.filter((v): v is string => typeof v === "string");
|
|
560
|
+
const stable = versions.filter((v) => !v.endsWith("-preview"));
|
|
561
|
+
const pool = stable.length > 0 ? stable : versions;
|
|
562
|
+
if (pool.length === 0) continue;
|
|
563
|
+
const newest = pool.reduce((a, b) => (compareApiDates(b, a) > 0 ? b : a));
|
|
564
|
+
byType.set(rt.resourceType.toLowerCase(), newest);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return byType.get(typeName);
|
|
568
|
+
}
|
|
569
|
+
|
|
511
570
|
/**
|
|
512
|
-
* Owned-only prune:
|
|
513
|
-
*
|
|
514
|
-
*
|
|
571
|
+
* Owned-only prune: delete every chant-owned live resource in the group whose
|
|
572
|
+
* (evaluated) name is not declared in the template. Foreign (non-chant)
|
|
573
|
+
* resources are never touched.
|
|
574
|
+
*
|
|
575
|
+
* ## Where the apiVersion comes from (#1457, #1472)
|
|
515
576
|
*
|
|
516
|
-
*
|
|
577
|
+
* `deleteArmResource` needs an `apiVersion`, and ARM's resource-group listing
|
|
578
|
+
* does not return one per resource. The template is the obvious source, but it
|
|
579
|
+
* only covers types it still declares: declare one storage account, apply,
|
|
580
|
+
* delete it from source, and the template has zero resources of that type.
|
|
581
|
+
* Keying the delete scope off the template alone made the last resource of a
|
|
582
|
+
* type a permanent orphan (#1457 made that a reported skip rather than a
|
|
583
|
+
* silent one).
|
|
517
584
|
*
|
|
518
|
-
*
|
|
519
|
-
* `!entry` guard skipped any live resource of a type the template no longer
|
|
520
|
-
* mentions — before `isChantOwned` was ever consulted.
|
|
585
|
+
* So the apiVersion is resolved in order:
|
|
521
586
|
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
587
|
+
* 1. the template, when it still declares the type;
|
|
588
|
+
* 2. the lexicon's pinned per-provider registry (`lookupApiVersion`, the same
|
|
589
|
+
* registry the serializer stamps into the template) — deterministic, no
|
|
590
|
+
* extra call, covers every type the lexicon can emit;
|
|
591
|
+
* 3. ARM's provider metadata (`providerApiVersion`) — covers a type the
|
|
592
|
+
* lexicon has never heard of, at one extra call per namespace per run.
|
|
528
593
|
*
|
|
529
|
-
*
|
|
530
|
-
* `
|
|
531
|
-
* resource-group listing does not return it per resource. So the skip stays,
|
|
532
|
-
* and is now REPORTED rather than silent: a permanent invisible orphan becomes
|
|
533
|
-
* a permanent visible one, which is the difference between a bug and a known
|
|
534
|
-
* limitation. Resolving it properly wants a per-type apiVersion source (the
|
|
535
|
-
* provider metadata endpoint, or a lexicon-side map).
|
|
594
|
+
* Only when all three come up empty is the resource reported as
|
|
595
|
+
* `notPrunable: no-api-version`.
|
|
536
596
|
*
|
|
537
597
|
* This matters more since #1448, which routed `ApplyOp`'s `arm` target through
|
|
538
598
|
* `azApply` — before that, the composite reached `--mode Complete` and never
|
|
@@ -555,24 +615,39 @@ export async function pruneArmOrphans(
|
|
|
555
615
|
byType.set(r.type, entry);
|
|
556
616
|
}
|
|
557
617
|
|
|
618
|
+
const providerCache = new Map<string, Map<string, string>>();
|
|
619
|
+
const resolveApiVersion = async (type: string): Promise<string | undefined> => {
|
|
620
|
+
const fromTemplate = byType.get(type)?.apiVersion;
|
|
621
|
+
if (fromTemplate) return fromTemplate;
|
|
622
|
+
let fromRegistry: string | undefined;
|
|
623
|
+
try {
|
|
624
|
+
fromRegistry = lookupApiVersion(type);
|
|
625
|
+
} catch {
|
|
626
|
+
// A missing registry is the serializer's problem to report; prune still
|
|
627
|
+
// has the provider metadata to fall back on.
|
|
628
|
+
}
|
|
629
|
+
if (fromRegistry) return fromRegistry;
|
|
630
|
+
return providerApiVersion(type, ctx, http, signal, providerCache);
|
|
631
|
+
};
|
|
632
|
+
|
|
558
633
|
const pruned: Array<{ type: string; name: string; deleted: boolean }> = [];
|
|
559
634
|
const notPrunable: AzNotPrunable[] = [];
|
|
560
635
|
for (const item of await listGroupResources(ctx, http, signal)) {
|
|
561
636
|
// Ownership first, so a foreign resource never reaches the report either —
|
|
562
637
|
// it is not an orphan chant failed to prune, it is simply not chant's.
|
|
563
638
|
if (!isChantOwned(item.tags)) continue;
|
|
564
|
-
|
|
565
|
-
|
|
639
|
+
if (byType.get(item.type)?.keep.has(item.name)) continue;
|
|
640
|
+
const apiVersion = await resolveApiVersion(item.type);
|
|
641
|
+
if (!apiVersion) {
|
|
566
642
|
console.log(
|
|
567
|
-
`prune: ${item.type}/${item.name} is chant-owned and undeclared, but
|
|
568
|
-
`
|
|
643
|
+
`prune: ${item.type}/${item.name} is chant-owned and undeclared, but no apiVersion is ` +
|
|
644
|
+
`available for its type (not in the template, the lexicon registry, or ARM provider metadata) — not deleted`,
|
|
569
645
|
);
|
|
570
646
|
notPrunable.push({ type: item.type, name: item.name, reason: "no-api-version" });
|
|
571
647
|
continue;
|
|
572
648
|
}
|
|
573
|
-
if (entry.keep.has(item.name)) continue;
|
|
574
649
|
safeHeartbeat({ step: "azPrune", type: item.type, name: item.name });
|
|
575
|
-
const result = await deleteArmResource(item.type, item.name,
|
|
650
|
+
const result = await deleteArmResource(item.type, item.name, apiVersion, ctx, http, signal);
|
|
576
651
|
console.log(`pruned: ${item.type}/${item.name} (${ctx.base})`);
|
|
577
652
|
pruned.push(result);
|
|
578
653
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, writeFileSync, rmSync } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join, dirname } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { execFileSync } from "child_process";
|
|
7
|
+
import { DECLARABLE_MARKER } from "@intentius/chant/declarable";
|
|
8
|
+
import {
|
|
9
|
+
azureSerializer,
|
|
10
|
+
apiVersionRegistryCandidates,
|
|
11
|
+
getApiVersion,
|
|
12
|
+
loadApiVersions,
|
|
13
|
+
resetApiVersionCache,
|
|
14
|
+
} from "./serializer";
|
|
15
|
+
import { PROVIDER_VERSION_OVERRIDES } from "./spec/api-versions";
|
|
16
|
+
|
|
17
|
+
function makeEntity(entityType: string, props: Record<string, unknown> = {}): any {
|
|
18
|
+
return { [DECLARABLE_MARKER]: true, lexicon: "azure", entityType, kind: "resource", props };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// This file runs as ESM (package.json "type": "module"), so `require` is not
|
|
22
|
+
// defined in module scope — the path #1581 broke.
|
|
23
|
+
describe("apiVersion registry under ESM (#1581)", () => {
|
|
24
|
+
afterEach(() => resetApiVersionCache());
|
|
25
|
+
|
|
26
|
+
it("resolves pinned versions in a real ESM process (no require shim)", () => {
|
|
27
|
+
// vitest defines a module-scope `require`, which hid #1581. Run the
|
|
28
|
+
// serializer under plain tsx ESM, where `require` is undefined.
|
|
29
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const script = [
|
|
31
|
+
`import { azureSerializer } from ${JSON.stringify(join(here, "serializer.ts"))};`,
|
|
32
|
+
`import { DECLARABLE_MARKER } from "@intentius/chant/declarable";`,
|
|
33
|
+
`const entities = new Map();`,
|
|
34
|
+
`entities.set("vm", { [DECLARABLE_MARKER]: true, lexicon: "azure", entityType: "Microsoft.Compute/virtualMachines", kind: "resource", props: { name: "vm", location: "eastus" } });`,
|
|
35
|
+
`const t = JSON.parse(azureSerializer.serialize(entities));`,
|
|
36
|
+
`console.log(JSON.stringify({ req: typeof require, apiVersion: t.resources[0].apiVersion }));`,
|
|
37
|
+
].join("\n");
|
|
38
|
+
const dir = mkdtempSync(join(tmpdir(), "azure-esm-"));
|
|
39
|
+
try {
|
|
40
|
+
const file = join(dir, "probe.mts");
|
|
41
|
+
writeFileSync(file, script);
|
|
42
|
+
const out = execFileSync("npx", ["tsx", file], { cwd: here, encoding: "utf-8", timeout: 120_000 });
|
|
43
|
+
const { req, apiVersion } = JSON.parse(out.trim().split("\n").pop()!);
|
|
44
|
+
expect(req).toBe("undefined");
|
|
45
|
+
expect(PROVIDER_VERSION_OVERRIDES["Microsoft.Compute"]).toContain(apiVersion);
|
|
46
|
+
expect(apiVersion).not.toBe("2023-01-01");
|
|
47
|
+
} finally {
|
|
48
|
+
rmSync(dir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("loads the registry via import.meta.url and uses pinned versions", () => {
|
|
53
|
+
const versions = loadApiVersions();
|
|
54
|
+
expect(versions.size).toBeGreaterThan(100);
|
|
55
|
+
|
|
56
|
+
// The pinned Microsoft.Compute version (spec/api-versions.ts) must reach
|
|
57
|
+
// the serializer, not the uniform 2023-01-01 fallback.
|
|
58
|
+
const computePins = PROVIDER_VERSION_OVERRIDES["Microsoft.Compute"];
|
|
59
|
+
expect(computePins?.length).toBeGreaterThan(0);
|
|
60
|
+
expect(computePins).toContain(versions.get("Microsoft.Compute/virtualMachines"));
|
|
61
|
+
|
|
62
|
+
const entities = new Map<string, any>();
|
|
63
|
+
entities.set("vm", makeEntity("Microsoft.Compute/virtualMachines", { name: "vm", location: "eastus" }));
|
|
64
|
+
entities.set("sa", makeEntity("Microsoft.Storage/storageAccounts", { name: "sa", location: "eastus" }));
|
|
65
|
+
const template = JSON.parse(azureSerializer.serialize(entities) as string);
|
|
66
|
+
const byType = Object.fromEntries(template.resources.map((r: any) => [r.type, r.apiVersion]));
|
|
67
|
+
expect(computePins).toContain(byType["Microsoft.Compute/virtualMachines"]);
|
|
68
|
+
expect(byType["Microsoft.Storage/storageAccounts"]).toBe(versions.get("Microsoft.Storage/storageAccounts"));
|
|
69
|
+
expect(byType["Microsoft.Storage/storageAccounts"]).not.toBe("2023-01-01");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("throws loudly when no registry can be found", () => {
|
|
73
|
+
const dir = mkdtempSync(join(tmpdir(), "azure-apiv-"));
|
|
74
|
+
try {
|
|
75
|
+
expect(() => loadApiVersions(apiVersionRegistryCandidates(dir))).toThrow(/apiVersion registry not found/);
|
|
76
|
+
} finally {
|
|
77
|
+
rmSync(dir, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("throws on an empty registry rather than falling back uniformly", () => {
|
|
82
|
+
const dir = mkdtempSync(join(tmpdir(), "azure-apiv-"));
|
|
83
|
+
try {
|
|
84
|
+
writeFileSync(join(dir, "meta.json"), "{}");
|
|
85
|
+
expect(() => loadApiVersions([join(dir, "meta.json")])).toThrow(/registry is empty/);
|
|
86
|
+
} finally {
|
|
87
|
+
rmSync(dir, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("keeps explicit overrides ahead of the registry", () => {
|
|
92
|
+
expect(getApiVersion("Microsoft.Authorization/roleAssignments")).toBe("2022-04-01");
|
|
93
|
+
});
|
|
94
|
+
});
|
package/src/serializer.ts
CHANGED
|
@@ -18,6 +18,9 @@ import { AZURE_TAG_OWNERSHIP_KEYS } from "./ownership";
|
|
|
18
18
|
import type { LexiconOutput } from "@intentius/chant/lexicon-output";
|
|
19
19
|
import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
|
|
20
20
|
import { isChildProject, type ChildProjectInstance } from "@intentius/chant/child-project";
|
|
21
|
+
import { existsSync, readFileSync } from "fs";
|
|
22
|
+
import { dirname, join } from "path";
|
|
23
|
+
import { fileURLToPath } from "url";
|
|
21
24
|
import { isStackOutput, type StackOutput } from "@intentius/chant/stack-output";
|
|
22
25
|
import { isAttrRefLike } from "@intentius/chant/utils";
|
|
23
26
|
import { resolveDependsOn } from "@intentius/chant/resource-attributes";
|
|
@@ -74,42 +77,48 @@ const RESOURCE_LEVEL_FIELDS = new Set([
|
|
|
74
77
|
|
|
75
78
|
/**
|
|
76
79
|
* Load apiVersion for a resource type from the lexicon registry.
|
|
80
|
+
*
|
|
81
|
+
* Reads the generated registry (src/generated/lexicon-azure.json in a dev
|
|
82
|
+
* checkout, dist/meta.json in the installed package). Resolves the package
|
|
83
|
+
* directory from import.meta.url so it works under pure ESM as well as
|
|
84
|
+
* tsx/vitest — the old `require()`-based resolver threw under ESM and the
|
|
85
|
+
* throw was swallowed, so every resource emitted the fallback apiVersion
|
|
86
|
+
* (#1581). A missing registry is now an error: a uniform fallback apiVersion
|
|
87
|
+
* silently bypasses the per-provider pins in spec/api-versions.ts.
|
|
77
88
|
*/
|
|
78
89
|
function resolveDir(): string {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return dirname(require.resolve("./serializer"));
|
|
90
|
+
return dirname(fileURLToPath(import.meta.url));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function apiVersionRegistryCandidates(dir: string = resolveDir()): string[] {
|
|
94
|
+
return [
|
|
95
|
+
join(dir, "generated", "lexicon-azure.json"),
|
|
96
|
+
join(dir, "..", "dist", "meta.json"),
|
|
97
|
+
];
|
|
88
98
|
}
|
|
89
99
|
|
|
90
|
-
function loadApiVersions(): Map<string, string> {
|
|
100
|
+
export function loadApiVersions(candidates: string[] = apiVersionRegistryCandidates()): Map<string, string> {
|
|
91
101
|
const map = new Map<string, string>();
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
join(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
map.set(entry.resourceType, entry.apiVersion);
|
|
109
|
-
}
|
|
102
|
+
let content: string | undefined;
|
|
103
|
+
for (const candidate of candidates) {
|
|
104
|
+
if (!existsSync(candidate)) continue;
|
|
105
|
+
content = readFileSync(candidate, "utf-8");
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
if (content === undefined) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`azure: apiVersion registry not found (tried ${candidates.join(", ")}). ` +
|
|
111
|
+
"Run `npm run generate` (dev) or reinstall the package (dist/meta.json).",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const data = JSON.parse(content) as Record<string, { resourceType: string; apiVersion?: string }>;
|
|
115
|
+
for (const entry of Object.values(data)) {
|
|
116
|
+
if (entry.apiVersion && entry.resourceType) {
|
|
117
|
+
map.set(entry.resourceType, entry.apiVersion);
|
|
110
118
|
}
|
|
111
|
-
}
|
|
112
|
-
|
|
119
|
+
}
|
|
120
|
+
if (map.size === 0) {
|
|
121
|
+
throw new Error("azure: apiVersion registry is empty — regenerate with `npm run generate`");
|
|
113
122
|
}
|
|
114
123
|
return map;
|
|
115
124
|
}
|
|
@@ -126,11 +135,39 @@ const API_VERSION_OVERRIDES = new Map<string, string>([
|
|
|
126
135
|
|
|
127
136
|
let _apiVersions: Map<string, string> | undefined;
|
|
128
137
|
|
|
129
|
-
|
|
138
|
+
/** Test hook: drop the cached registry so the next lookup reloads it. */
|
|
139
|
+
export function resetApiVersionCache(): void {
|
|
140
|
+
_apiVersions = undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Registry lookup without the serializer's fallback: the pinned apiVersion for
|
|
145
|
+
* `resourceType`, or `undefined` when neither the overrides nor the generated
|
|
146
|
+
* registry know the type. ARM resource types are case-insensitive and the live
|
|
147
|
+
* listing does not always echo the casing the registry uses, so the match is
|
|
148
|
+
* case-insensitive too. Used by prune (#1472), which needs an apiVersion for a
|
|
149
|
+
* type the current template no longer declares.
|
|
150
|
+
*/
|
|
151
|
+
export function lookupApiVersion(resourceType: string): string | undefined {
|
|
130
152
|
const override = API_VERSION_OVERRIDES.get(resourceType);
|
|
131
153
|
if (override) return override;
|
|
132
154
|
if (!_apiVersions) _apiVersions = loadApiVersions();
|
|
133
|
-
|
|
155
|
+
const exact = _apiVersions.get(resourceType);
|
|
156
|
+
if (exact) return exact;
|
|
157
|
+
const wanted = resourceType.toLowerCase();
|
|
158
|
+
for (const [type, version] of _apiVersions) {
|
|
159
|
+
if (type.toLowerCase() === wanted) return version;
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function getApiVersion(resourceType: string): string {
|
|
165
|
+
const version = lookupApiVersion(resourceType);
|
|
166
|
+
if (version) return version;
|
|
167
|
+
console.warn(
|
|
168
|
+
`azure: no pinned apiVersion for ${resourceType} in the lexicon registry; falling back to 2023-01-01`,
|
|
169
|
+
);
|
|
170
|
+
return "2023-01-01";
|
|
134
171
|
}
|
|
135
172
|
|
|
136
173
|
/**
|
package/src/validate.ts
CHANGED
|
@@ -44,6 +44,12 @@ export async function validate(opts?: { basePath?: string }): Promise<ValidateRe
|
|
|
44
44
|
// after bounding (#438), so match required names by substring.
|
|
45
45
|
requiredNamesMatchSubstring: true,
|
|
46
46
|
basePath,
|
|
47
|
+
// chant #1475 — azure generates from a commit sha of
|
|
48
|
+
// azure-resource-manager-schemas (#1144), so a fresh generate is
|
|
49
|
+
// deterministic. Gate the surface on every validate: the #1144 pin left
|
|
50
|
+
// the snapshot 483 entries behind until #1572 re-baselined it, because
|
|
51
|
+
// nothing compared the two on a PR.
|
|
52
|
+
checkSurfaceSnapshot: "always",
|
|
47
53
|
coverageThresholds: {
|
|
48
54
|
minPropertyPct: 1,
|
|
49
55
|
minLifecyclePct: 1,
|