@intentius/chant-lexicon-azure 0.39.0 → 0.41.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/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/op/activities/az-apply.d.ts +96 -10
- 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/package.json +2 -2
- package/src/op/activities/az-apply.test.ts +203 -5
- package/src/op/activities/az-apply.ts +161 -15
- package/src/op/activities/index.ts +1 -0
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "7a3fb1cb00d88162dfce29698e3f3ce481d64c21d5103ae2c471b92d9470b371",
|
|
5
5
|
"meta.json": "051fb4ee43f7de48a36f1abc59c8ff5207c3b75aa28f13db6440b9f5b0337605",
|
|
6
6
|
"types/index.d.ts": "617a6de433bd92a126170a7f5ae903d6d7aa83fbb0b57169d3450b5a13e72e0d",
|
|
7
7
|
"rules/hardcoded-location.ts": "a9b1d1cec93f2ca9c5206641f53fc9621cefcfd2c00b3ab89c194b30a75fa949",
|
|
@@ -33,5 +33,5 @@
|
|
|
33
33
|
"skills/chant-azure-patterns.md": "1a3bacfac612826b77332d2692a59195638db9f1b5e8ea2eda7579621d25f603",
|
|
34
34
|
"skills/chant-azure-aks.md": "2d4e0098c1a22b54ffadd410564d9df0f3a04cb4eb7c6261f20ae33106e7aca8"
|
|
35
35
|
},
|
|
36
|
-
"composite": "
|
|
36
|
+
"composite": "579545b4d3b7b8be7eae56689bbdcd3f4289c3580805f527b5aa93a6882e8023"
|
|
37
37
|
}
|
package/dist/manifest.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ApplyResult } from "@intentius/chant/apply";
|
|
1
2
|
/** One ARM resource from a `deploymentTemplate.json` `resources[]`. */
|
|
2
3
|
export interface ArmResource {
|
|
3
4
|
type: string;
|
|
@@ -82,10 +83,34 @@ export declare function azApply(args: AzApplyArgs, signal?: AbortSignal, http?:
|
|
|
82
83
|
name: string;
|
|
83
84
|
deleted: boolean;
|
|
84
85
|
}>;
|
|
86
|
+
/** Owned, undeclared resources the prune could not delete (#1457). Empty when
|
|
87
|
+
* `prune` is off, and empty on a clean prune. */
|
|
88
|
+
notPrunable: AzNotPrunable[];
|
|
85
89
|
}>;
|
|
86
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* The ownership tags azApply stamps on every resource it applies.
|
|
92
|
+
*
|
|
93
|
+
* Stamps the declared channel's key. The legacy key goes on too, so a resource
|
|
94
|
+
* applied by this chant is still recognised by an older one mid-rollout; it is
|
|
95
|
+
* transitional and can be dropped once no deployed estate predates #1446.
|
|
96
|
+
*
|
|
97
|
+
* Only the managed-by marker, not stack/env: this applier has no
|
|
98
|
+
* {@link OwnershipMarker} to hand — the serializer stamps those from project
|
|
99
|
+
* config. Same shape the fly serializer uses when no ownership context is set.
|
|
100
|
+
*/
|
|
87
101
|
export declare function chantOwnershipTags(): Record<string, string>;
|
|
88
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Whether a resource's tags mark it chant-owned.
|
|
104
|
+
*
|
|
105
|
+
* Resolves through core's `hasOwnershipMarker` against the lexicon's declared
|
|
106
|
+
* channel (#1446), so this predicate and the serializer's stamp can no longer
|
|
107
|
+
* drift — the arrangement the fly lexicon already had.
|
|
108
|
+
*
|
|
109
|
+
* The legacy key is accepted as well, and the union is deliberate: it is
|
|
110
|
+
* strictly more than this predicate recognised before, so nothing that was
|
|
111
|
+
* prunable stops being prunable, and resources the serializer marked become
|
|
112
|
+
* prunable for the first time.
|
|
113
|
+
*/
|
|
89
114
|
export declare function isChantOwned(tags: Record<string, string> | null | undefined): boolean;
|
|
90
115
|
/** One resource from the ARM resource-group listing. */
|
|
91
116
|
export interface ArmListItem {
|
|
@@ -103,17 +128,78 @@ export declare function deleteArmResource(type: string, name: string, apiVersion
|
|
|
103
128
|
deleted: boolean;
|
|
104
129
|
}>;
|
|
105
130
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
131
|
+
* Normalize an azApply/azDelete result into core's apply envelope (#1446).
|
|
132
|
+
*
|
|
133
|
+
* The lexicon keeps its own shape — ARM `type`, per-resource applied/pruned —
|
|
134
|
+
* and this projects it onto the shared tri-state so a caller can read any
|
|
135
|
+
* applier's result the same way.
|
|
111
136
|
*/
|
|
112
|
-
export declare function
|
|
137
|
+
export declare function toApplyResult(result: {
|
|
138
|
+
applied?: Array<{
|
|
139
|
+
type: string;
|
|
140
|
+
name: string;
|
|
141
|
+
}>;
|
|
142
|
+
pruned?: Array<{
|
|
143
|
+
type: string;
|
|
144
|
+
name: string;
|
|
145
|
+
deleted: boolean;
|
|
146
|
+
}>;
|
|
147
|
+
deleted?: Array<{
|
|
148
|
+
type: string;
|
|
149
|
+
name: string;
|
|
150
|
+
deleted: boolean;
|
|
151
|
+
}>;
|
|
152
|
+
notPrunable?: AzNotPrunable[];
|
|
153
|
+
}): ApplyResult;
|
|
154
|
+
/** A chant-owned live resource an owned-only prune could not delete, and why (#1457). */
|
|
155
|
+
export interface AzNotPrunable {
|
|
113
156
|
type: string;
|
|
114
157
|
name: string;
|
|
115
|
-
|
|
116
|
-
|
|
158
|
+
/**
|
|
159
|
+
* `no-api-version` — the resource is owned and undeclared, but its type is
|
|
160
|
+
* absent from the current template, which is the only place an `apiVersion`
|
|
161
|
+
* comes from. `deleteArmResource` cannot build a URL without one.
|
|
162
|
+
*/
|
|
163
|
+
reason: "no-api-version";
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Owned-only prune: for each resource type present in the template, delete the
|
|
167
|
+
* chant-owned live resources of that type whose (evaluated) name is not in the
|
|
168
|
+
* template. Foreign (non-chant) resources are never touched.
|
|
169
|
+
*
|
|
170
|
+
* ## The type-left-the-template hole (#1457)
|
|
171
|
+
*
|
|
172
|
+
* `byType` is keyed off the resources the template CURRENTLY declares, and the
|
|
173
|
+
* `!entry` guard skipped any live resource of a type the template no longer
|
|
174
|
+
* mentions — before `isChantOwned` was ever consulted.
|
|
175
|
+
*
|
|
176
|
+
* So: declare one storage account, apply, then delete it from source. The
|
|
177
|
+
* template now has zero resources of that type, `byType` has no entry, and the
|
|
178
|
+
* live account is skipped. It is owned, it is undeclared, and prune would never
|
|
179
|
+
* touch it — on that run or any future one. **Prune the last resource of a type
|
|
180
|
+
* and you created a permanent orphan.** Prune one of several and it works,
|
|
181
|
+
* which is why it survived testing.
|
|
182
|
+
*
|
|
183
|
+
* The guard cannot simply be removed: `deleteArmResource` needs an
|
|
184
|
+
* `apiVersion`, and the template is the only source of one — ARM's
|
|
185
|
+
* resource-group listing does not return it per resource. So the skip stays,
|
|
186
|
+
* and is now REPORTED rather than silent: a permanent invisible orphan becomes
|
|
187
|
+
* a permanent visible one, which is the difference between a bug and a known
|
|
188
|
+
* limitation. Resolving it properly wants a per-type apiVersion source (the
|
|
189
|
+
* provider metadata endpoint, or a lexicon-side map).
|
|
190
|
+
*
|
|
191
|
+
* This matters more since #1448, which routed `ApplyOp`'s `arm` target through
|
|
192
|
+
* `azApply` — before that, the composite reached `--mode Complete` and never
|
|
193
|
+
* came here at all.
|
|
194
|
+
*/
|
|
195
|
+
export declare function pruneArmOrphans(desired: ArmResource[], ctx: ArmEvalCtx, http?: AzHttp, signal?: AbortSignal): Promise<{
|
|
196
|
+
pruned: Array<{
|
|
197
|
+
type: string;
|
|
198
|
+
name: string;
|
|
199
|
+
deleted: boolean;
|
|
200
|
+
}>;
|
|
201
|
+
notPrunable: AzNotPrunable[];
|
|
202
|
+
}>;
|
|
117
203
|
/**
|
|
118
204
|
* The inverse of {@link azApply} — read a built ARM template and delete the
|
|
119
205
|
* resources it declares, in reverse dependency order (a referrer goes before the
|
|
@@ -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, deleteArmResource, listGroupResources, chantOwnershipTags, isChantOwned, evalArm, evalArmString, armResourceUrl, armResourceBody, armDependencies, orderArmResources, } from "./az-apply.js";
|
|
10
|
+
export { azApply, azDelete, pruneArmOrphans, 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,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,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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-azure",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.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.41.0",
|
|
78
78
|
"typescript": "^5.9.3"
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { writeFileSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { describeApplyConformance } from "@intentius/chant-test-utils";
|
|
2
4
|
import {
|
|
3
5
|
evalArmString,
|
|
4
6
|
evalArm,
|
|
@@ -9,6 +11,7 @@ import {
|
|
|
9
11
|
azApply,
|
|
10
12
|
azDelete,
|
|
11
13
|
pruneArmOrphans,
|
|
14
|
+
toApplyResult,
|
|
12
15
|
deleteArmResource,
|
|
13
16
|
listGroupResources,
|
|
14
17
|
chantOwnershipTags,
|
|
@@ -200,10 +203,30 @@ describe("azApply flow (#707)", () => {
|
|
|
200
203
|
});
|
|
201
204
|
|
|
202
205
|
describe("ownership helpers (#azure-prune)", () => {
|
|
203
|
-
|
|
204
|
-
|
|
206
|
+
// #1446: the applier used a bare `managed-by` while the lexicon's declared
|
|
207
|
+
// channel (AZURE_TAG_OWNERSHIP_KEYS, what the serializer stamps and
|
|
208
|
+
// live-export filters on) is `chant-managed-by`. Both are ordinary ARM tag
|
|
209
|
+
// keys on the same surface, so nothing forced the difference — and it meant a
|
|
210
|
+
// resource carrying only the serializer's marker read as foreign here.
|
|
211
|
+
test("stamps the lexicon's declared channel, plus the legacy key", () => {
|
|
212
|
+
expect(chantOwnershipTags()).toEqual({
|
|
213
|
+
"chant-managed-by": "chant",
|
|
214
|
+
"managed-by": "chant",
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("recognises the declared channel — the marker the serializer stamps", () => {
|
|
219
|
+
expect(isChantOwned({ "chant-managed-by": "chant" })).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("still recognises the legacy key, so nothing already deployed is orphaned", () => {
|
|
205
223
|
expect(isChantOwned({ "managed-by": "chant" })).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("a foreign or unmarked resource is still foreign", () => {
|
|
206
227
|
expect(isChantOwned({ "managed-by": "someone-else" })).toBe(false);
|
|
228
|
+
expect(isChantOwned({ "chant-managed-by": "terraform" })).toBe(false);
|
|
229
|
+
expect(isChantOwned({})).toBe(false);
|
|
207
230
|
expect(isChantOwned(undefined)).toBe(false);
|
|
208
231
|
});
|
|
209
232
|
});
|
|
@@ -263,10 +286,16 @@ describe("pruneArmOrphans (#azure-prune)", () => {
|
|
|
263
286
|
if (method === "DELETE") deletes.push(url);
|
|
264
287
|
return { status: 200, text: method === "GET" ? JSON.stringify(live) : "" };
|
|
265
288
|
};
|
|
266
|
-
const pruned = await pruneArmOrphans(desired, ctx(), http);
|
|
289
|
+
const { pruned, notPrunable } = await pruneArmOrphans(desired, ctx(), http);
|
|
267
290
|
expect(pruned).toEqual([{ type: "Microsoft.Storage/storageAccounts", name: "orphan1", deleted: true }]);
|
|
268
291
|
expect(deletes).toHaveLength(1);
|
|
269
292
|
expect(deletes[0]).toContain("/storageAccounts/orphan1?api-version=2023-01-01"); // apiVersion from the template
|
|
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" },
|
|
298
|
+
]);
|
|
270
299
|
});
|
|
271
300
|
|
|
272
301
|
// #1448 routes ApplyOp's `arm` target here, so this is now the delete scope for
|
|
@@ -285,9 +314,12 @@ describe("pruneArmOrphans (#azure-prune)", () => {
|
|
|
285
314
|
if (method === "DELETE") deletes.push(url);
|
|
286
315
|
return { status: 200, text: method === "GET" ? JSON.stringify(live) : "" };
|
|
287
316
|
};
|
|
288
|
-
const pruned = await pruneArmOrphans(desired, ctx(), http);
|
|
317
|
+
const { pruned, notPrunable } = await pruneArmOrphans(desired, ctx(), http);
|
|
289
318
|
expect(deletes).toEqual([]);
|
|
290
319
|
expect(pruned).toEqual([]);
|
|
320
|
+
// Foreign resources are not chant's orphans, so they are not reported as
|
|
321
|
+
// ones chant failed to prune either — ownership is checked first.
|
|
322
|
+
expect(notPrunable).toEqual([]);
|
|
291
323
|
});
|
|
292
324
|
|
|
293
325
|
// The caveat that was a comment on OWNERSHIP_TAG_KEY: floci-az drops resource
|
|
@@ -305,7 +337,7 @@ describe("pruneArmOrphans (#azure-prune)", () => {
|
|
|
305
337
|
if (method === "DELETE") deletes.push(url);
|
|
306
338
|
return { status: 200, text: method === "GET" ? JSON.stringify(live) : "" };
|
|
307
339
|
};
|
|
308
|
-
expect(await pruneArmOrphans(desired, ctx(), http)).toEqual([]);
|
|
340
|
+
expect(await pruneArmOrphans(desired, ctx(), http)).toEqual({ pruned: [], notPrunable: [] });
|
|
309
341
|
expect(deletes).toEqual([]);
|
|
310
342
|
});
|
|
311
343
|
});
|
|
@@ -369,3 +401,169 @@ describe("azDelete (#azure-prune)", () => {
|
|
|
369
401
|
expect(deletes[1]).toContain("/serverfarms/plan1");
|
|
370
402
|
});
|
|
371
403
|
});
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* #1457 — the permanent-orphan case, stated as its own scenario rather than a
|
|
407
|
+
* clause in another test.
|
|
408
|
+
*
|
|
409
|
+
* Prune the LAST resource of a type and the template stops mentioning that type
|
|
410
|
+
* at all. `byType` then has no entry, and the `!entry` guard skipped the live
|
|
411
|
+
* resource before ownership was ever checked. It is owned, undeclared, and was
|
|
412
|
+
* unreachable by prune forever. Prune one of SEVERAL and it works, which is why
|
|
413
|
+
* this survived testing.
|
|
414
|
+
*/
|
|
415
|
+
describe("pruneArmOrphans: a type that left the template (#1457)", () => {
|
|
416
|
+
const ctx = (): ArmEvalCtx => ({
|
|
417
|
+
subscriptionId: "sub",
|
|
418
|
+
resourceGroup: "rg",
|
|
419
|
+
location: "eastus",
|
|
420
|
+
deployed: new Map(),
|
|
421
|
+
http: async () => ({ status: 200, text: "" }),
|
|
422
|
+
base: "http://x",
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const liveHttp = (value: unknown[], deletes: string[]): AzHttp => async (method, url) => {
|
|
426
|
+
if (method === "DELETE") deletes.push(url);
|
|
427
|
+
return { status: 200, text: method === "GET" ? JSON.stringify({ value }) : "" };
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
test("an owned orphan whose type is gone is reported, not silently skipped", async () => {
|
|
431
|
+
// The template now declares a DIFFERENT type entirely — the storage account
|
|
432
|
+
// was the last of its kind and has been removed from source.
|
|
433
|
+
const desired: ArmResource[] = [
|
|
434
|
+
{ type: "Microsoft.Web/sites", apiVersion: "2022-03-01", name: "site" },
|
|
435
|
+
];
|
|
436
|
+
const deletes: string[] = [];
|
|
437
|
+
const http = liveHttp(
|
|
438
|
+
[{ id: "/1", name: "leftover", type: "Microsoft.Storage/storageAccounts", tags: { "managed-by": "chant" } }],
|
|
439
|
+
deletes,
|
|
440
|
+
);
|
|
441
|
+
const { pruned, notPrunable } = await pruneArmOrphans(desired, ctx(), http);
|
|
442
|
+
expect(pruned).toEqual([]);
|
|
443
|
+
expect(notPrunable).toEqual([
|
|
444
|
+
{ type: "Microsoft.Storage/storageAccounts", name: "leftover", reason: "no-api-version" },
|
|
445
|
+
]);
|
|
446
|
+
// Still not deleted — the apiVersion genuinely is not available. The fix is
|
|
447
|
+
// that it is now visible rather than that it is now deleted.
|
|
448
|
+
expect(deletes).toEqual([]);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("pruning one of several of a type still works — the case that hid this", async () => {
|
|
452
|
+
const desired: ArmResource[] = [
|
|
453
|
+
{ type: "Microsoft.Storage/storageAccounts", apiVersion: "2023-01-01", name: "keep" },
|
|
454
|
+
];
|
|
455
|
+
const deletes: string[] = [];
|
|
456
|
+
const http = liveHttp(
|
|
457
|
+
[
|
|
458
|
+
{ id: "/1", name: "keep", type: "Microsoft.Storage/storageAccounts", tags: { "managed-by": "chant" } },
|
|
459
|
+
{ id: "/2", name: "gone", type: "Microsoft.Storage/storageAccounts", tags: { "managed-by": "chant" } },
|
|
460
|
+
],
|
|
461
|
+
deletes,
|
|
462
|
+
);
|
|
463
|
+
const { pruned, notPrunable } = await pruneArmOrphans(desired, ctx(), http);
|
|
464
|
+
expect(pruned).toEqual([{ type: "Microsoft.Storage/storageAccounts", name: "gone", deleted: true }]);
|
|
465
|
+
expect(notPrunable).toEqual([]);
|
|
466
|
+
expect(deletes).toHaveLength(1);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test("a foreign resource of a departed type is not reported as chant's problem", async () => {
|
|
470
|
+
const desired: ArmResource[] = [
|
|
471
|
+
{ type: "Microsoft.Web/sites", apiVersion: "2022-03-01", name: "site" },
|
|
472
|
+
];
|
|
473
|
+
const deletes: string[] = [];
|
|
474
|
+
const http = liveHttp(
|
|
475
|
+
[{ id: "/1", name: "theirs", type: "Microsoft.Storage/storageAccounts", tags: { "managed-by": "terraform" } }],
|
|
476
|
+
deletes,
|
|
477
|
+
);
|
|
478
|
+
const { pruned, notPrunable } = await pruneArmOrphans(desired, ctx(), http);
|
|
479
|
+
expect(pruned).toEqual([]);
|
|
480
|
+
expect(notPrunable).toEqual([]);
|
|
481
|
+
expect(deletes).toEqual([]);
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* The shared apply-contract suite (#1446), run against azure's own mocked
|
|
487
|
+
* transport. Assertion 4 — the owned-only prune, asserted on the transport — is
|
|
488
|
+
* the one that matters most here: it is what #1448 violated, and what the
|
|
489
|
+
* two-key split found in #1446 made unreliable even after #1448.
|
|
490
|
+
*/
|
|
491
|
+
describeApplyConformance({
|
|
492
|
+
lexicon: "azure",
|
|
493
|
+
scenarios: [
|
|
494
|
+
{
|
|
495
|
+
name: "a template of two resources",
|
|
496
|
+
plan: [
|
|
497
|
+
{ kind: "Microsoft.Storage/storageAccounts", name: "one" },
|
|
498
|
+
{ kind: "Microsoft.Storage/storageAccounts", name: "two" },
|
|
499
|
+
],
|
|
500
|
+
run: async () => {
|
|
501
|
+
const path = `/tmp/chant-1446-az-${process.pid}.json`;
|
|
502
|
+
writeFileSync(
|
|
503
|
+
path,
|
|
504
|
+
JSON.stringify({
|
|
505
|
+
resources: [
|
|
506
|
+
{ type: "Microsoft.Storage/storageAccounts", apiVersion: "2023-01-01", name: "one" },
|
|
507
|
+
{ type: "Microsoft.Storage/storageAccounts", apiVersion: "2023-01-01", name: "two" },
|
|
508
|
+
],
|
|
509
|
+
}),
|
|
510
|
+
);
|
|
511
|
+
try {
|
|
512
|
+
const http: AzHttp = async () => ({ status: 200, text: "{}" });
|
|
513
|
+
return toApplyResult(
|
|
514
|
+
await azApply({ templatePath: path, resourceGroup: "rg", endpoint: "http://x" }, undefined, http),
|
|
515
|
+
);
|
|
516
|
+
} finally {
|
|
517
|
+
unlinkSync(path);
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
expectApplied: [
|
|
521
|
+
"Microsoft.Storage/storageAccounts/one",
|
|
522
|
+
"Microsoft.Storage/storageAccounts/two",
|
|
523
|
+
],
|
|
524
|
+
},
|
|
525
|
+
],
|
|
526
|
+
pruneScenarios: [
|
|
527
|
+
{
|
|
528
|
+
name: "an owned orphan beside a foreign resource in the same group",
|
|
529
|
+
ownedOrphan: "orphan",
|
|
530
|
+
foreign: "foreign",
|
|
531
|
+
run: async () => {
|
|
532
|
+
const deletes: string[] = [];
|
|
533
|
+
const live = {
|
|
534
|
+
value: [
|
|
535
|
+
{ id: "/1", name: "keep", type: "Microsoft.Storage/storageAccounts", tags: { "chant-managed-by": "chant" } },
|
|
536
|
+
{ id: "/2", name: "orphan", type: "Microsoft.Storage/storageAccounts", tags: { "chant-managed-by": "chant" } },
|
|
537
|
+
{ id: "/3", name: "foreign", type: "Microsoft.Storage/storageAccounts", tags: { "managed-by": "terraform" } },
|
|
538
|
+
],
|
|
539
|
+
};
|
|
540
|
+
const path = `/tmp/chant-1446-az-prune-${process.pid}.json`;
|
|
541
|
+
writeFileSync(
|
|
542
|
+
path,
|
|
543
|
+
JSON.stringify({
|
|
544
|
+
resources: [{ type: "Microsoft.Storage/storageAccounts", apiVersion: "2023-01-01", name: "keep" }],
|
|
545
|
+
}),
|
|
546
|
+
);
|
|
547
|
+
try {
|
|
548
|
+
const http: AzHttp = async (method, url) => {
|
|
549
|
+
if (method === "DELETE") deletes.push(url);
|
|
550
|
+
return {
|
|
551
|
+
status: 200,
|
|
552
|
+
text: method === "GET" && url.includes("/resources?") ? JSON.stringify(live) : "{}",
|
|
553
|
+
};
|
|
554
|
+
};
|
|
555
|
+
const result = toApplyResult(
|
|
556
|
+
await azApply(
|
|
557
|
+
{ templatePath: path, resourceGroup: "rg", endpoint: "http://x", prune: true },
|
|
558
|
+
undefined,
|
|
559
|
+
http,
|
|
560
|
+
),
|
|
561
|
+
);
|
|
562
|
+
return { result, deletes };
|
|
563
|
+
} finally {
|
|
564
|
+
unlinkSync(path);
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
],
|
|
569
|
+
});
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { safeHeartbeat } from "@intentius/chant/op";
|
|
4
|
+
import { hasOwnershipMarker, OWNERSHIP_MANAGED_BY_VALUE } from "@intentius/chant/ownership";
|
|
5
|
+
import { AZURE_TAG_OWNERSHIP_KEYS } from "../../ownership";
|
|
6
|
+
import {
|
|
7
|
+
applyResult,
|
|
8
|
+
type ApplyResult,
|
|
9
|
+
type AppliedResource,
|
|
10
|
+
type NotAttemptedResource,
|
|
11
|
+
} from "@intentius/chant/apply";
|
|
4
12
|
|
|
5
13
|
// The floci-az default local subscription (from its ARM management-plane mock).
|
|
6
14
|
const DEFAULT_SUBSCRIPTION = "00000000-0000-0000-0000-000000000001";
|
|
@@ -296,7 +304,13 @@ export async function azApply(
|
|
|
296
304
|
args: AzApplyArgs,
|
|
297
305
|
signal?: AbortSignal,
|
|
298
306
|
http: AzHttp = defaultHttp,
|
|
299
|
-
): Promise<{
|
|
307
|
+
): Promise<{
|
|
308
|
+
applied: Array<{ type: string; name: string }>;
|
|
309
|
+
pruned: Array<{ type: string; name: string; deleted: boolean }>;
|
|
310
|
+
/** Owned, undeclared resources the prune could not delete (#1457). Empty when
|
|
311
|
+
* `prune` is off, and empty on a clean prune. */
|
|
312
|
+
notPrunable: AzNotPrunable[];
|
|
313
|
+
}> {
|
|
300
314
|
const base = (args.endpoint ?? DEFAULT_ENDPOINT).replace(/\/$/, "");
|
|
301
315
|
const ctx: ArmEvalCtx = {
|
|
302
316
|
subscriptionId: args.subscriptionId ?? DEFAULT_SUBSCRIPTION,
|
|
@@ -340,8 +354,10 @@ export async function azApply(
|
|
|
340
354
|
applied.push({ type: resource.type, name });
|
|
341
355
|
}
|
|
342
356
|
|
|
343
|
-
const
|
|
344
|
-
|
|
357
|
+
const prune = args.prune
|
|
358
|
+
? await pruneArmOrphans(resources, ctx, http, signal)
|
|
359
|
+
: { pruned: [], notPrunable: [] };
|
|
360
|
+
return { applied, pruned: prune.pruned, notPrunable: prune.notPrunable };
|
|
345
361
|
}
|
|
346
362
|
|
|
347
363
|
// ── Ownership + prune + delete ────────────────────────────────────────────────
|
|
@@ -351,17 +367,62 @@ export async function azApply(
|
|
|
351
367
|
// touched. Real Azure persists resource tags; note that floci-az currently drops
|
|
352
368
|
// them, so owned-only prune only takes effect against real Azure (the delete
|
|
353
369
|
// mechanics themselves work against either — see azDelete).
|
|
354
|
-
const OWNERSHIP_TAG_KEY = "managed-by";
|
|
355
|
-
const OWNERSHIP_TAG_VALUE = "chant";
|
|
356
370
|
|
|
357
|
-
/**
|
|
371
|
+
/**
|
|
372
|
+
* The key this applier used before #1446, and still recognises.
|
|
373
|
+
*
|
|
374
|
+
* The lexicon declares its marker channel in `../../ownership.ts`
|
|
375
|
+
* (`chant-managed-by`) — the key the serializer stamps, the plugin registers as
|
|
376
|
+
* `ownershipChannel`, and `import/live-export.ts` filters on. This applier used
|
|
377
|
+
* a bare `managed-by` instead, so the two never agreed.
|
|
378
|
+
*
|
|
379
|
+
* That is not cosmetic. Both are ordinary ARM tag keys on the same surface, so
|
|
380
|
+
* nothing forced the difference — and it meant a resource carrying only the
|
|
381
|
+
* SERIALIZER's marker was invisible to this prune. Anything deployed by a path
|
|
382
|
+
* other than `azApply` reads as foreign and is never pruned, including
|
|
383
|
+
* everything `ApplyOp` deployed before #1448 routed it here from
|
|
384
|
+
* `az deployment --mode Complete`.
|
|
385
|
+
*
|
|
386
|
+
* (GCP has the same-looking split for a real reason: its serializer stamps a
|
|
387
|
+
* Config Connector object, where `app.kubernetes.io/managed-by` is a legal
|
|
388
|
+
* Kubernetes label, while `gcpApply` writes GCP REST labels, where it is not.
|
|
389
|
+
* Two surfaces, two key vocabularies. Azure has one surface.)
|
|
390
|
+
*/
|
|
391
|
+
const LEGACY_OWNERSHIP_TAG_KEY = "managed-by";
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* The ownership tags azApply stamps on every resource it applies.
|
|
395
|
+
*
|
|
396
|
+
* Stamps the declared channel's key. The legacy key goes on too, so a resource
|
|
397
|
+
* applied by this chant is still recognised by an older one mid-rollout; it is
|
|
398
|
+
* transitional and can be dropped once no deployed estate predates #1446.
|
|
399
|
+
*
|
|
400
|
+
* Only the managed-by marker, not stack/env: this applier has no
|
|
401
|
+
* {@link OwnershipMarker} to hand — the serializer stamps those from project
|
|
402
|
+
* config. Same shape the fly serializer uses when no ownership context is set.
|
|
403
|
+
*/
|
|
358
404
|
export function chantOwnershipTags(): Record<string, string> {
|
|
359
|
-
return {
|
|
405
|
+
return {
|
|
406
|
+
[AZURE_TAG_OWNERSHIP_KEYS.managedBy]: OWNERSHIP_MANAGED_BY_VALUE,
|
|
407
|
+
[LEGACY_OWNERSHIP_TAG_KEY]: OWNERSHIP_MANAGED_BY_VALUE,
|
|
408
|
+
};
|
|
360
409
|
}
|
|
361
410
|
|
|
362
|
-
/**
|
|
411
|
+
/**
|
|
412
|
+
* Whether a resource's tags mark it chant-owned.
|
|
413
|
+
*
|
|
414
|
+
* Resolves through core's `hasOwnershipMarker` against the lexicon's declared
|
|
415
|
+
* channel (#1446), so this predicate and the serializer's stamp can no longer
|
|
416
|
+
* drift — the arrangement the fly lexicon already had.
|
|
417
|
+
*
|
|
418
|
+
* The legacy key is accepted as well, and the union is deliberate: it is
|
|
419
|
+
* strictly more than this predicate recognised before, so nothing that was
|
|
420
|
+
* prunable stops being prunable, and resources the serializer marked become
|
|
421
|
+
* prunable for the first time.
|
|
422
|
+
*/
|
|
363
423
|
export function isChantOwned(tags: Record<string, string> | null | undefined): boolean {
|
|
364
|
-
|
|
424
|
+
if (hasOwnershipMarker(tags ?? undefined, AZURE_TAG_OWNERSHIP_KEYS)) return true;
|
|
425
|
+
return tags?.[LEGACY_OWNERSHIP_TAG_KEY] === OWNERSHIP_MANAGED_BY_VALUE;
|
|
365
426
|
}
|
|
366
427
|
|
|
367
428
|
/** One resource from the ARM resource-group listing. */
|
|
@@ -400,19 +461,92 @@ export async function deleteArmResource(
|
|
|
400
461
|
return { type, name, deleted: true };
|
|
401
462
|
}
|
|
402
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Normalize an azApply/azDelete result into core's apply envelope (#1446).
|
|
466
|
+
*
|
|
467
|
+
* The lexicon keeps its own shape — ARM `type`, per-resource applied/pruned —
|
|
468
|
+
* and this projects it onto the shared tri-state so a caller can read any
|
|
469
|
+
* applier's result the same way.
|
|
470
|
+
*/
|
|
471
|
+
export function toApplyResult(result: {
|
|
472
|
+
applied?: Array<{ type: string; name: string }>;
|
|
473
|
+
pruned?: Array<{ type: string; name: string; deleted: boolean }>;
|
|
474
|
+
deleted?: Array<{ type: string; name: string; deleted: boolean }>;
|
|
475
|
+
notPrunable?: AzNotPrunable[];
|
|
476
|
+
}): ApplyResult {
|
|
477
|
+
const applied: AppliedResource[] = (result.applied ?? []).map((a) => ({
|
|
478
|
+
kind: a.type,
|
|
479
|
+
name: a.name,
|
|
480
|
+
// ARM PUT is a create-or-update with no distinguishing response, so this
|
|
481
|
+
// applier cannot tell the two apart. `updated` is the honest verdict for a
|
|
482
|
+
// call that converged without claiming which it did.
|
|
483
|
+
action: "updated",
|
|
484
|
+
}));
|
|
485
|
+
const notAttempted: NotAttemptedResource[] = (result.notPrunable ?? []).map((n) => ({
|
|
486
|
+
kind: n.type,
|
|
487
|
+
name: n.name,
|
|
488
|
+
reason: "not-prunable" as const,
|
|
489
|
+
detail: n.reason,
|
|
490
|
+
}));
|
|
491
|
+
const pruned = (result.pruned ?? result.deleted ?? []).map((p) => ({
|
|
492
|
+
kind: p.type,
|
|
493
|
+
name: p.name,
|
|
494
|
+
deleted: p.deleted,
|
|
495
|
+
}));
|
|
496
|
+
return applyResult(applied, pruned, notAttempted);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** A chant-owned live resource an owned-only prune could not delete, and why (#1457). */
|
|
500
|
+
export interface AzNotPrunable {
|
|
501
|
+
type: string;
|
|
502
|
+
name: string;
|
|
503
|
+
/**
|
|
504
|
+
* `no-api-version` — the resource is owned and undeclared, but its type is
|
|
505
|
+
* absent from the current template, which is the only place an `apiVersion`
|
|
506
|
+
* comes from. `deleteArmResource` cannot build a URL without one.
|
|
507
|
+
*/
|
|
508
|
+
reason: "no-api-version";
|
|
509
|
+
}
|
|
510
|
+
|
|
403
511
|
/**
|
|
404
512
|
* Owned-only prune: for each resource type present in the template, delete the
|
|
405
513
|
* chant-owned live resources of that type whose (evaluated) name is not in the
|
|
406
|
-
* template.
|
|
407
|
-
*
|
|
408
|
-
*
|
|
514
|
+
* template. Foreign (non-chant) resources are never touched.
|
|
515
|
+
*
|
|
516
|
+
* ## The type-left-the-template hole (#1457)
|
|
517
|
+
*
|
|
518
|
+
* `byType` is keyed off the resources the template CURRENTLY declares, and the
|
|
519
|
+
* `!entry` guard skipped any live resource of a type the template no longer
|
|
520
|
+
* mentions — before `isChantOwned` was ever consulted.
|
|
521
|
+
*
|
|
522
|
+
* So: declare one storage account, apply, then delete it from source. The
|
|
523
|
+
* template now has zero resources of that type, `byType` has no entry, and the
|
|
524
|
+
* live account is skipped. It is owned, it is undeclared, and prune would never
|
|
525
|
+
* touch it — on that run or any future one. **Prune the last resource of a type
|
|
526
|
+
* and you created a permanent orphan.** Prune one of several and it works,
|
|
527
|
+
* which is why it survived testing.
|
|
528
|
+
*
|
|
529
|
+
* The guard cannot simply be removed: `deleteArmResource` needs an
|
|
530
|
+
* `apiVersion`, and the template is the only source of one — ARM's
|
|
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).
|
|
536
|
+
*
|
|
537
|
+
* This matters more since #1448, which routed `ApplyOp`'s `arm` target through
|
|
538
|
+
* `azApply` — before that, the composite reached `--mode Complete` and never
|
|
539
|
+
* came here at all.
|
|
409
540
|
*/
|
|
410
541
|
export async function pruneArmOrphans(
|
|
411
542
|
desired: ArmResource[],
|
|
412
543
|
ctx: ArmEvalCtx,
|
|
413
544
|
http: AzHttp = defaultHttp,
|
|
414
545
|
signal?: AbortSignal,
|
|
415
|
-
): Promise<
|
|
546
|
+
): Promise<{
|
|
547
|
+
pruned: Array<{ type: string; name: string; deleted: boolean }>;
|
|
548
|
+
notPrunable: AzNotPrunable[];
|
|
549
|
+
}> {
|
|
416
550
|
const byType = new Map<string, { keep: Set<string>; apiVersion: string }>();
|
|
417
551
|
for (const r of desired) {
|
|
418
552
|
const name = String(await evalArmString(r.name, ctx));
|
|
@@ -422,15 +556,27 @@ export async function pruneArmOrphans(
|
|
|
422
556
|
}
|
|
423
557
|
|
|
424
558
|
const pruned: Array<{ type: string; name: string; deleted: boolean }> = [];
|
|
559
|
+
const notPrunable: AzNotPrunable[] = [];
|
|
425
560
|
for (const item of await listGroupResources(ctx, http, signal)) {
|
|
561
|
+
// Ownership first, so a foreign resource never reaches the report either —
|
|
562
|
+
// it is not an orphan chant failed to prune, it is simply not chant's.
|
|
563
|
+
if (!isChantOwned(item.tags)) continue;
|
|
426
564
|
const entry = byType.get(item.type);
|
|
427
|
-
if (!entry
|
|
565
|
+
if (!entry) {
|
|
566
|
+
console.log(
|
|
567
|
+
`prune: ${item.type}/${item.name} is chant-owned and undeclared, but its type is absent ` +
|
|
568
|
+
`from the template so no apiVersion is available — not deleted`,
|
|
569
|
+
);
|
|
570
|
+
notPrunable.push({ type: item.type, name: item.name, reason: "no-api-version" });
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (entry.keep.has(item.name)) continue;
|
|
428
574
|
safeHeartbeat({ step: "azPrune", type: item.type, name: item.name });
|
|
429
575
|
const result = await deleteArmResource(item.type, item.name, entry.apiVersion, ctx, http, signal);
|
|
430
576
|
console.log(`pruned: ${item.type}/${item.name} (${ctx.base})`);
|
|
431
577
|
pruned.push(result);
|
|
432
578
|
}
|
|
433
|
-
return pruned;
|
|
579
|
+
return { pruned, notPrunable };
|
|
434
580
|
}
|
|
435
581
|
|
|
436
582
|
/**
|