@intentius/chant-lexicon-azure 0.124.34 → 0.124.36
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/api/read-client.d.ts.map +1 -1
- package/dist/api/read-client.js +2 -1
- package/dist/arm-type.d.ts +21 -0
- package/dist/arm-type.d.ts.map +1 -0
- package/dist/arm-type.js +31 -0
- package/dist/deep-observe.d.ts +9 -0
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/deep-observe.js +42 -0
- package/dist/graph-edges.d.ts +21 -0
- package/dist/graph-edges.d.ts.map +1 -0
- package/dist/graph-edges.js +71 -0
- package/dist/integrity.json +3 -3
- package/dist/lint/post-synth/arm-refs.d.ts +2 -0
- package/dist/lint/post-synth/arm-refs.d.ts.map +1 -1
- package/dist/lint/post-synth/arm-refs.js +6 -1
- package/dist/manifest.json +1 -1
- package/dist/op/activities/az-apply.d.ts +22 -2
- package/dist/op/activities/az-apply.d.ts.map +1 -1
- package/dist/op/activities/az-apply.js +49 -17
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +3 -0
- package/dist/rules/arm-refs.ts +6 -1
- package/dist/serializer.d.ts +9 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/serializer.js +20 -6
- package/package.json +2 -2
- package/src/api/read-client.ts +2 -1
- package/src/arm-type.test.ts +47 -0
- package/src/arm-type.ts +31 -0
- package/src/deep-observe.test.ts +56 -1
- package/src/deep-observe.ts +41 -0
- package/src/graph-edges.test.ts +94 -0
- package/src/graph-edges.ts +72 -0
- package/src/lint/post-synth/arm-refs.ts +6 -1
- package/src/op/activities/az-apply.test.ts +81 -0
- package/src/op/activities/az-apply.ts +55 -13
- package/src/plugin.ts +3 -0
- package/src/serializer.ts +21 -6
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import { armResourcePath } from "../../arm-type";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { hasOwnershipMarker, OWNERSHIP_MANAGED_BY_VALUE } from "@intentius/chant/ownership";
|
|
4
5
|
import { AZURE_TAG_OWNERSHIP_KEYS } from "../../ownership";
|
|
@@ -25,6 +26,11 @@ export interface ArmResource {
|
|
|
25
26
|
sku?: unknown;
|
|
26
27
|
kind?: unknown;
|
|
27
28
|
tags?: Record<string, string>;
|
|
29
|
+
identity?: unknown;
|
|
30
|
+
zones?: unknown;
|
|
31
|
+
plan?: unknown;
|
|
32
|
+
extendedLocation?: unknown;
|
|
33
|
+
managedBy?: unknown;
|
|
28
34
|
dependsOn?: unknown;
|
|
29
35
|
}
|
|
30
36
|
|
|
@@ -64,6 +70,29 @@ export interface ArmEvalCtx {
|
|
|
64
70
|
signal?: AbortSignal;
|
|
65
71
|
}
|
|
66
72
|
|
|
73
|
+
/** The ARM resource id of a resource in the context's group. */
|
|
74
|
+
export function armResourceIdOf(ctx: Pick<ArmEvalCtx, "subscriptionId" | "resourceGroup">, type: string, name: string): string {
|
|
75
|
+
return `/subscriptions/${ctx.subscriptionId}/resourceGroups/${ctx.resourceGroup}/providers/${armResourcePath(type, name)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Record an applied resource's response so `reference()` resolves it by name
|
|
80
|
+
* and by resource id (#4243). Ids are case-insensitive in ARM, so they are
|
|
81
|
+
* stored lowercased, under both the id chant derives and the one the
|
|
82
|
+
* response reports.
|
|
83
|
+
*/
|
|
84
|
+
export function rememberDeployed(ctx: ArmEvalCtx, type: string, name: string, body: unknown): void {
|
|
85
|
+
ctx.deployed.set(name, body);
|
|
86
|
+
ctx.deployed.set(armResourceIdOf(ctx, type, name).toLowerCase(), body);
|
|
87
|
+
const id = (body as { id?: unknown } | null)?.id;
|
|
88
|
+
if (typeof id === "string") ctx.deployed.set(id.toLowerCase(), body);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** An applied resource by the name or the resource id a `reference()` names. */
|
|
92
|
+
export function lookupDeployed(ctx: ArmEvalCtx, ref: string): unknown {
|
|
93
|
+
return ctx.deployed.get(ref) ?? (ref.startsWith("/") ? ctx.deployed.get(ref.toLowerCase()) : undefined);
|
|
94
|
+
}
|
|
95
|
+
|
|
67
96
|
/** Evaluate an ARM expression string (`"[...]"`); a plain string is returned as-is. */
|
|
68
97
|
export async function evalArmString(s: string, ctx: ArmEvalCtx): Promise<unknown> {
|
|
69
98
|
if (!(s.startsWith("[") && s.endsWith("]"))) return s;
|
|
@@ -71,13 +100,17 @@ export async function evalArmString(s: string, ctx: ArmEvalCtx): Promise<unknown
|
|
|
71
100
|
return new ArmExpr(s.slice(1, -1), ctx).parse();
|
|
72
101
|
}
|
|
73
102
|
|
|
74
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* Recursively evaluate every string in a value against the ARM context,
|
|
105
|
+
* object keys included: ARM evaluates a property name too, and
|
|
106
|
+
* `identity.userAssignedIdentities` is keyed by `[resourceId(...)]` (#4231).
|
|
107
|
+
*/
|
|
75
108
|
export async function evalArm(value: unknown, ctx: ArmEvalCtx): Promise<unknown> {
|
|
76
109
|
if (typeof value === "string") return evalArmString(value, ctx);
|
|
77
110
|
if (Array.isArray(value)) return Promise.all(value.map((v) => evalArm(v, ctx)));
|
|
78
111
|
if (value && typeof value === "object") {
|
|
79
112
|
const out: Record<string, unknown> = {};
|
|
80
|
-
for (const [k, v] of Object.entries(value)) out[k] = await evalArm(v, ctx);
|
|
113
|
+
for (const [k, v] of Object.entries(value)) out[String(await evalArmString(k, ctx))] = await evalArm(v, ctx);
|
|
81
114
|
return out;
|
|
82
115
|
}
|
|
83
116
|
return value;
|
|
@@ -151,10 +184,13 @@ class ArmExpr {
|
|
|
151
184
|
return { subscriptionId: this.ctx.subscriptionId, id: `/subscriptions/${this.ctx.subscriptionId}` };
|
|
152
185
|
case "resourceId":
|
|
153
186
|
// resourceId('Microsoft.X/y', 'name'[, 'child']) → the resource-id path.
|
|
154
|
-
return
|
|
187
|
+
return armResourceIdOf(this.ctx, String(args[0]), args.slice(1).map(String).join("/"));
|
|
155
188
|
case "reference": {
|
|
156
|
-
// reference('name'
|
|
157
|
-
|
|
189
|
+
// reference('name' | resourceId(...)[, apiVersion[, 'Full']]) → the
|
|
190
|
+
// runtime `properties` of an already-applied resource, or its whole
|
|
191
|
+
// body with 'Full' (#4243).
|
|
192
|
+
const dep = lookupDeployed(this.ctx, String(args[0]));
|
|
193
|
+
if (String(args[2] ?? "").toLowerCase() === "full") return dep;
|
|
158
194
|
return (dep as { properties?: unknown } | undefined)?.properties ?? dep;
|
|
159
195
|
}
|
|
160
196
|
case "listKeys": {
|
|
@@ -260,17 +296,23 @@ export function orderArmResources(resources: ArmResource[]): ArmResource[] {
|
|
|
260
296
|
/** The ARM resource-ID PUT URL for a resource (name expression evaluated). */
|
|
261
297
|
export async function armResourceUrl(resource: ArmResource, ctx: ArmEvalCtx): Promise<string> {
|
|
262
298
|
const name = await evalArmString(resource.name, ctx);
|
|
263
|
-
return `${ctx.base}/subscriptions/${ctx.subscriptionId}/resourceGroups/${ctx.resourceGroup}/providers/${resource.type
|
|
299
|
+
return `${ctx.base}/subscriptions/${ctx.subscriptionId}/resourceGroups/${ctx.resourceGroup}/providers/${armResourcePath(resource.type, String(name))}?api-version=${resource.apiVersion}`;
|
|
264
300
|
}
|
|
265
301
|
|
|
266
|
-
/**
|
|
302
|
+
/**
|
|
303
|
+
* The top-level ARM resource fields a PUT takes beside `location` (#4231). A
|
|
304
|
+
* field missing here is dropped from the body, which is how a declared
|
|
305
|
+
* `identity` came back absent.
|
|
306
|
+
*/
|
|
307
|
+
const ARM_BODY_FIELDS = ["properties", "sku", "kind", "tags", "identity", "zones", "plan", "extendedLocation", "managedBy"] as const;
|
|
308
|
+
|
|
309
|
+
/** The ARM resource PUT body: `location` and {@link ARM_BODY_FIELDS}, expressions evaluated. */
|
|
267
310
|
export async function armResourceBody(resource: ArmResource, ctx: ArmEvalCtx): Promise<Record<string, unknown>> {
|
|
268
311
|
const body: Record<string, unknown> = {};
|
|
269
312
|
if (resource.location) body.location = await evalArmString(resource.location, ctx);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (resource.tags !== undefined) body.tags = await evalArm(resource.tags, ctx);
|
|
313
|
+
for (const field of ARM_BODY_FIELDS) {
|
|
314
|
+
if (resource[field] !== undefined) body[field] = await evalArm(resource[field], ctx);
|
|
315
|
+
}
|
|
274
316
|
return body;
|
|
275
317
|
}
|
|
276
318
|
|
|
@@ -346,7 +388,7 @@ export async function azApply(
|
|
|
346
388
|
}
|
|
347
389
|
// Capture the applied resource so later reference()/dependents resolve.
|
|
348
390
|
try {
|
|
349
|
-
ctx.
|
|
391
|
+
rememberDeployed(ctx, resource.type, name, JSON.parse(res.text));
|
|
350
392
|
} catch {
|
|
351
393
|
// non-JSON response — reference() to this resource resolves to undefined
|
|
352
394
|
}
|
|
@@ -454,7 +496,7 @@ export async function deleteArmResource(
|
|
|
454
496
|
http: AzHttp = defaultHttp,
|
|
455
497
|
signal?: AbortSignal,
|
|
456
498
|
): Promise<{ type: string; name: string; deleted: boolean }> {
|
|
457
|
-
const url = `${ctx.base}/subscriptions/${ctx.subscriptionId}/resourceGroups/${ctx.resourceGroup}/providers/${type
|
|
499
|
+
const url = `${ctx.base}/subscriptions/${ctx.subscriptionId}/resourceGroups/${ctx.resourceGroup}/providers/${armResourcePath(type, name)}?api-version=${apiVersion}`;
|
|
458
500
|
const res = await http("DELETE", url, undefined, signal);
|
|
459
501
|
if (res.status === 404) return { type, name, deleted: false };
|
|
460
502
|
if (res.status >= 300) throw new Error(`${type} ${name} delete failed (${res.status}): ${res.text}`);
|
package/src/plugin.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { join, dirname } from "path";
|
|
|
15
15
|
import { fileURLToPath } from "url";
|
|
16
16
|
import { moduleSourceDir } from "@intentius/chant/module-source";
|
|
17
17
|
import { azureSerializer } from "./serializer";
|
|
18
|
+
import { azureGraphEdges } from "./graph-edges";
|
|
18
19
|
import { hardcodedLocationRule } from "./lint/rules/hardcoded-location";
|
|
19
20
|
import { storageHttpsRule } from "./lint/rules/storage-https";
|
|
20
21
|
import { nsgWildcardRule } from "./lint/rules/nsg-wildcard";
|
|
@@ -37,6 +38,8 @@ export const azurePlugin: LexiconPlugin = {
|
|
|
37
38
|
emulator: FLOCI_AZ_EMULATOR,
|
|
38
39
|
auditCatalog: () => azureAuditCatalog,
|
|
39
40
|
serializer: azureSerializer,
|
|
41
|
+
// References written as ARM expressions (resourceId, reference, listKeys) as graph edges (#4232).
|
|
42
|
+
graphEdges: (entities) => azureGraphEdges(entities),
|
|
40
43
|
|
|
41
44
|
lintRules(): LintRule[] {
|
|
42
45
|
return [hardcodedLocationRule, storageHttpsRule, nsgWildcardRule];
|
package/src/serializer.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - parameters, outputs sections
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { armType } from "./arm-type";
|
|
13
14
|
import type { Declarable, CoreParameter } from "@intentius/chant/declarable";
|
|
14
15
|
import { isPropertyDeclarable, isResourceDeclarable } from "@intentius/chant/declarable";
|
|
15
16
|
import type { Serializer, SerializerResult, SerializeContext } from "@intentius/chant/serializer";
|
|
@@ -194,7 +195,7 @@ function entityNamed(entityNames: Map<Declarable, string>, name: string): Declar
|
|
|
194
195
|
function armResourceId(entityNames: Map<Declarable, string>, name: string): string {
|
|
195
196
|
const entity = entityNamed(entityNames, name);
|
|
196
197
|
const { args } = armNameArgs(entity, name);
|
|
197
|
-
return entity ? `resourceId('${entity.entityType}', ${args})` : `resourceId(${args})`;
|
|
198
|
+
return entity ? `resourceId('${armType(entity.entityType)}', ${args})` : `resourceId(${args})`;
|
|
198
199
|
}
|
|
199
200
|
|
|
200
201
|
/**
|
|
@@ -259,10 +260,24 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
259
260
|
return proto === Object.prototype || proto === null;
|
|
260
261
|
}
|
|
261
262
|
|
|
262
|
-
|
|
263
|
+
/** A prop value as the template writes it: references become bracket expressions. */
|
|
264
|
+
export function toArmValue(value: unknown, entityNames: Map<Declarable, string>): unknown {
|
|
263
265
|
return walkValue(value, entityNames, armVisitor(entityNames));
|
|
264
266
|
}
|
|
265
267
|
|
|
268
|
+
/**
|
|
269
|
+
* The name a resource takes in the template: its `name` prop, or the entity
|
|
270
|
+
* key when it sets none. `findArmResourceRefs` reports references by this
|
|
271
|
+
* name, so the dependsOn inference below and the graph edges
|
|
272
|
+
* (./graph-edges.ts) both resolve a reference through it.
|
|
273
|
+
*/
|
|
274
|
+
export function armResourceName(entity: Declarable, key: string, entityNames: Map<Declarable, string>): unknown {
|
|
275
|
+
const props = isResourceDeclarable(entity) && typeof entity.props === "object" && entity.props !== null
|
|
276
|
+
? entity.props as Record<string, unknown>
|
|
277
|
+
: undefined;
|
|
278
|
+
return props?.name !== undefined ? toArmValue(props.name, entityNames) : key;
|
|
279
|
+
}
|
|
280
|
+
|
|
266
281
|
/**
|
|
267
282
|
* Serialize entities into an ARM template object.
|
|
268
283
|
*/
|
|
@@ -291,7 +306,7 @@ function serializeToTemplate(
|
|
|
291
306
|
} else if (isChildProject(entity)) {
|
|
292
307
|
emittedTypes.push("Microsoft.Resources/deployments");
|
|
293
308
|
} else if (!isPropertyDeclarable(entity)) {
|
|
294
|
-
emittedTypes.push(entity.entityType);
|
|
309
|
+
emittedTypes.push(armType(entity.entityType));
|
|
295
310
|
}
|
|
296
311
|
}
|
|
297
312
|
const templateScope = pinnedScope ?? resolveTemplateScope(emittedTypes);
|
|
@@ -369,9 +384,9 @@ function serializeToTemplate(
|
|
|
369
384
|
const apiVersion = (typeof attrs0?.apiVersion === "string") ? attrs0.apiVersion : getApiVersion(resourceType);
|
|
370
385
|
|
|
371
386
|
const resource: ArmResource = {
|
|
372
|
-
type: resourceType,
|
|
387
|
+
type: armType(resourceType),
|
|
373
388
|
apiVersion,
|
|
374
|
-
name: name,
|
|
389
|
+
name: armResourceName(entity, name, entityNames) as string,
|
|
375
390
|
};
|
|
376
391
|
|
|
377
392
|
// Extract all props
|
|
@@ -386,7 +401,7 @@ function serializeToTemplate(
|
|
|
386
401
|
// Hoist to resource level
|
|
387
402
|
(resource as unknown as Record<string, unknown>)[key] = toArmValue(value, entityNames);
|
|
388
403
|
} else if (key === "name") {
|
|
389
|
-
|
|
404
|
+
// Set above by armResourceName.
|
|
390
405
|
} else if (key === "properties" && isPlainObject(value)) {
|
|
391
406
|
// The generated types take ARM's own `properties` object, so
|
|
392
407
|
// merge it, not nest it (#4183).
|