@intentius/chant-lexicon-azure 0.124.32 → 0.124.34
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/generate-typescript.js +1 -1
- package/dist/codegen/generate.js +1 -1
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/generated/index.js +1 -1
- package/dist/health.d.ts +42 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +137 -0
- package/dist/integrity.json +5 -5
- package/dist/lint/post-synth/arm-refs.d.ts +1 -0
- package/dist/lint/post-synth/arm-refs.d.ts.map +1 -1
- package/dist/lint/post-synth/arm-refs.js +5 -3
- package/dist/manifest.json +1 -1
- package/dist/meta.json +6 -1
- package/dist/okf/types/containerApps.md +5 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +6 -0
- package/dist/rules/arm-refs.ts +5 -3
- package/dist/serializer.d.ts +7 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/serializer.js +115 -29
- package/dist/spec/parse.d.ts.map +1 -1
- package/dist/spec/parse.js +10 -0
- package/dist/types/index.d.ts +5 -0
- package/package.json +2 -2
- package/src/attributes.test.ts +17 -0
- package/src/codegen/generate-typescript.ts +1 -1
- package/src/codegen/generate.ts +1 -1
- package/src/generated/index.d.ts +5 -0
- package/src/generated/index.ts +1 -1
- package/src/generated/lexicon-azure.json +6 -1
- package/src/health.test.ts +66 -0
- package/src/health.ts +162 -0
- package/src/lint/post-synth/arm-refs.ts +5 -3
- package/src/lint/post-synth/post-synth.test.ts +1 -1
- package/src/plugin.ts +8 -0
- package/src/serializer-names.test.ts +83 -0
- package/src/serializer-outputs.test.ts +65 -0
- package/src/serializer.ts +110 -28
- package/src/spec/parse.test.ts +15 -0
- package/src/spec/parse.ts +10 -0
package/src/serializer.ts
CHANGED
|
@@ -170,20 +170,52 @@ export function getApiVersion(resourceType: string): string {
|
|
|
170
170
|
return "2023-01-01";
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
/**
|
|
174
|
+
* The ARM name of a chant export, as the arguments an ARM function takes
|
|
175
|
+
* (#4183). A resource's `name` prop is its ARM name, and the export name is
|
|
176
|
+
* used only when it has none. A literal name is quoted, one argument per
|
|
177
|
+
* segment of a child's `parent/child` name; a bracket expression such as
|
|
178
|
+
* `[parameters('n')]` is passed unwrapped.
|
|
179
|
+
*/
|
|
180
|
+
function armNameArgs(entity: Declarable | undefined, exportName: string): { args: string; literal: string | null } {
|
|
181
|
+
const props = entity && isResourceDeclarable(entity) && typeof entity.props === "object" && entity.props !== null
|
|
182
|
+
? entity.props as Record<string, unknown>
|
|
183
|
+
: undefined;
|
|
184
|
+
const name = typeof props?.name === "string" ? props.name : exportName;
|
|
185
|
+
if (name.startsWith("[") && name.endsWith("]")) return { args: name.slice(1, -1), literal: null };
|
|
186
|
+
return { args: name.split("/").map((seg) => `'${seg}'`).join(", "), literal: name };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function entityNamed(entityNames: Map<Declarable, string>, name: string): Declarable | undefined {
|
|
190
|
+
return [...entityNames.entries()].find(([, n]) => n === name)?.[0];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** `resourceId(...)` for an export: the type and its ARM name. */
|
|
194
|
+
function armResourceId(entityNames: Map<Declarable, string>, name: string): string {
|
|
195
|
+
const entity = entityNamed(entityNames, name);
|
|
196
|
+
const { args } = armNameArgs(entity, name);
|
|
197
|
+
return entity ? `resourceId('${entity.entityType}', ${args})` : `resourceId(${args})`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* An attribute of an export. `id` is `resourceId()`, since `reference()`
|
|
202
|
+
* returns the resource's properties and they have no `id`; anything else
|
|
203
|
+
* reads `reference()` by the ARM name.
|
|
204
|
+
*/
|
|
205
|
+
export function armAttrExpression(entityNames: Map<Declarable, string>, name: string, attr: string): string {
|
|
206
|
+
if (attr === "id") return `[${armResourceId(entityNames, name)}]`;
|
|
207
|
+
const { args, literal } = armNameArgs(entityNamed(entityNames, name), name);
|
|
208
|
+
const target = literal !== null && !literal.includes("/") ? args : armResourceId(entityNames, name);
|
|
209
|
+
return `[reference(${target}).${attr}]`;
|
|
210
|
+
}
|
|
211
|
+
|
|
173
212
|
/**
|
|
174
213
|
* ARM-specific visitor for the generic serializer walker.
|
|
175
214
|
*/
|
|
176
215
|
function armVisitor(entityNames: Map<Declarable, string>): SerializerVisitor {
|
|
177
216
|
return {
|
|
178
|
-
attrRef: (name, attr) =>
|
|
179
|
-
resourceRef: (name) => {
|
|
180
|
-
// Look up the resource type from entityNames
|
|
181
|
-
const entity = [...entityNames.entries()].find(([, n]) => n === name)?.[0];
|
|
182
|
-
if (entity) {
|
|
183
|
-
return `[resourceId('${entity.entityType}', '${name}')]`;
|
|
184
|
-
}
|
|
185
|
-
return `[resourceId('${name}')]`;
|
|
186
|
-
},
|
|
217
|
+
attrRef: (name, attr) => armAttrExpression(entityNames, name, attr),
|
|
218
|
+
resourceRef: (name) => `[${armResourceId(entityNames, name)}]`,
|
|
187
219
|
propertyDeclarable: (entity, walk) => {
|
|
188
220
|
if (!isResourceDeclarable(entity) || typeof entity.props !== "object" || entity.props === null) {
|
|
189
221
|
return undefined;
|
|
@@ -203,6 +235,30 @@ function armVisitor(entityNames: Map<Declarable, string>): SerializerVisitor {
|
|
|
203
235
|
/**
|
|
204
236
|
* Convert a value to ARM-compatible JSON using the generic walker.
|
|
205
237
|
*/
|
|
238
|
+
/** An ARM output for a literal, or for an azure intrinsic's `[...]` expression. */
|
|
239
|
+
function armOutputValue(value: unknown): { type: string; value: unknown } | undefined {
|
|
240
|
+
if (typeof value === "string") return { type: "string", value };
|
|
241
|
+
if (typeof value === "number" && Number.isInteger(value)) return { type: "int", value };
|
|
242
|
+
if (typeof value === "boolean") return { type: "bool", value };
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function isIntrinsicLike(value: unknown): value is { toJSON(): unknown } {
|
|
247
|
+
return typeof value === "object" && value !== null && typeof (value as { toJSON?: unknown }).toJSON === "function";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function isCloudFormationShaped(value: unknown): boolean {
|
|
251
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
252
|
+
const keys = Object.keys(value);
|
|
253
|
+
return keys.length === 1 && (keys[0] === "Ref" || keys[0].startsWith("Fn::"));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
257
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
258
|
+
const proto = Object.getPrototypeOf(value);
|
|
259
|
+
return proto === Object.prototype || proto === null;
|
|
260
|
+
}
|
|
261
|
+
|
|
206
262
|
function toArmValue(value: unknown, entityNames: Map<Declarable, string>): unknown {
|
|
207
263
|
return walkValue(value, entityNames, armVisitor(entityNames));
|
|
208
264
|
}
|
|
@@ -214,6 +270,7 @@ function serializeToTemplate(
|
|
|
214
270
|
entities: Map<string, Declarable>,
|
|
215
271
|
outputs?: LexiconOutput[],
|
|
216
272
|
ownership?: OwnershipMarker,
|
|
273
|
+
warnings: string[] = [],
|
|
217
274
|
): ArmTemplate {
|
|
218
275
|
// Resolve the deployment scope from the resource types being emitted
|
|
219
276
|
// (#1545): a template of tenant-only resources (management groups, policy
|
|
@@ -330,6 +387,12 @@ function serializeToTemplate(
|
|
|
330
387
|
(resource as unknown as Record<string, unknown>)[key] = toArmValue(value, entityNames);
|
|
331
388
|
} else if (key === "name") {
|
|
332
389
|
resource.name = toArmValue(value, entityNames) as string;
|
|
390
|
+
} else if (key === "properties" && isPlainObject(value)) {
|
|
391
|
+
// The generated types take ARM's own `properties` object, so
|
|
392
|
+
// merge it, not nest it (#4183).
|
|
393
|
+
for (const [inner, innerValue] of Object.entries(value)) {
|
|
394
|
+
if (innerValue !== undefined) armProperties[inner] = toArmValue(innerValue, entityNames);
|
|
395
|
+
}
|
|
333
396
|
} else {
|
|
334
397
|
armProperties[key] = toArmValue(value, entityNames);
|
|
335
398
|
}
|
|
@@ -356,14 +419,9 @@ function serializeToTemplate(
|
|
|
356
419
|
if (attrs?.DependsOn !== undefined) {
|
|
357
420
|
const resolved = resolveDependsOn(attrs.DependsOn, entityNames, name);
|
|
358
421
|
if (resolved.length > 0) {
|
|
359
|
-
resource.dependsOn = resolved.map((dep) =>
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (depEntity) {
|
|
363
|
-
return `[resourceId('${depEntity.entityType}', '${dep}')]`;
|
|
364
|
-
}
|
|
365
|
-
return dep;
|
|
366
|
-
});
|
|
422
|
+
resource.dependsOn = resolved.map((dep) =>
|
|
423
|
+
entityNamed(entityNames, dep) ? `[${armResourceId(entityNames, dep)}]` : dep,
|
|
424
|
+
);
|
|
367
425
|
}
|
|
368
426
|
}
|
|
369
427
|
|
|
@@ -424,7 +482,7 @@ function serializeToTemplate(
|
|
|
424
482
|
// Find the resource to build the resourceId expression
|
|
425
483
|
const depResource = template.resources.find((r) => r.name === refName);
|
|
426
484
|
if (depResource) {
|
|
427
|
-
const depExpr = `[resourceId('${depResource.type}', '${
|
|
485
|
+
const depExpr = `[resourceId('${depResource.type}', ${refName.split("/").map((seg) => `'${seg}'`).join(", ")})]`;
|
|
428
486
|
if (!existing.has(depExpr)) {
|
|
429
487
|
existing.add(depExpr);
|
|
430
488
|
}
|
|
@@ -442,17 +500,20 @@ function serializeToTemplate(
|
|
|
442
500
|
if (!template.outputs) template.outputs = {};
|
|
443
501
|
const stackOutput = entity as StackOutput;
|
|
444
502
|
// `unknown` so isAttrRefLike narrows cleanly across the workspace/published
|
|
445
|
-
// type boundary.
|
|
446
|
-
// reference form, so skip it here (only aws/CFN emits those).
|
|
503
|
+
// type boundary.
|
|
447
504
|
const ref: unknown = stackOutput.sourceRef;
|
|
448
505
|
if (isAttrRefLike(ref)) {
|
|
449
506
|
const logicalName = ref.getLogicalName();
|
|
450
507
|
if (logicalName) {
|
|
451
508
|
template.outputs[name] = {
|
|
452
509
|
type: "string",
|
|
453
|
-
value:
|
|
510
|
+
value: armAttrExpression(entityNames, logicalName, ref.attribute),
|
|
454
511
|
};
|
|
455
512
|
}
|
|
513
|
+
} else {
|
|
514
|
+
const value = armOutputValue(isIntrinsicLike(ref) ? ref.toJSON() : ref);
|
|
515
|
+
if (value) template.outputs[name] = value;
|
|
516
|
+
else warnings.push(`azure: stack output "${name}" is not written: its value has no ARM form.`);
|
|
456
517
|
}
|
|
457
518
|
}
|
|
458
519
|
}
|
|
@@ -461,11 +522,27 @@ function serializeToTemplate(
|
|
|
461
522
|
if (outputs && outputs.length > 0) {
|
|
462
523
|
template.outputs = template.outputs ?? {};
|
|
463
524
|
for (const output of outputs) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
525
|
+
if (output.sourceAttribute !== null) {
|
|
526
|
+
if (output.sourceEntity) {
|
|
527
|
+
template.outputs[output.outputName] = {
|
|
528
|
+
type: "string",
|
|
529
|
+
value: armAttrExpression(entityNames, output.sourceEntity, output.sourceAttribute),
|
|
530
|
+
};
|
|
531
|
+
} else {
|
|
532
|
+
warnings.push(`azure: output "${output.outputName}" is not written: its resource has no name in this build.`);
|
|
533
|
+
}
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
// A literal or an intrinsic (#4181). It has no source lexicon, so every
|
|
537
|
+
// lexicon receives it; a CloudFormation-shaped value is aws's to write.
|
|
538
|
+
const raw = output.getOutputValue();
|
|
539
|
+
const value = armOutputValue(raw);
|
|
540
|
+
if (value) template.outputs[output.outputName] = value;
|
|
541
|
+
else if (!isCloudFormationShaped(raw)) {
|
|
542
|
+
warnings.push(`azure: output "${output.outputName}" is not written: its value has no ARM form.`);
|
|
543
|
+
}
|
|
468
544
|
}
|
|
545
|
+
if (Object.keys(template.outputs).length === 0) delete template.outputs;
|
|
469
546
|
}
|
|
470
547
|
|
|
471
548
|
return template;
|
|
@@ -519,13 +596,18 @@ export const azureSerializer: Serializer = {
|
|
|
519
596
|
}
|
|
520
597
|
}
|
|
521
598
|
|
|
522
|
-
const
|
|
599
|
+
const warnings: string[] = [];
|
|
600
|
+
const template = serializeToTemplate(entities, outputs, context?.ownership, warnings);
|
|
523
601
|
const primary = JSON.stringify(template, null, 2);
|
|
524
602
|
|
|
525
|
-
if (!hasChildProjects) {
|
|
603
|
+
if (!hasChildProjects && warnings.length === 0) {
|
|
526
604
|
return primary;
|
|
527
605
|
}
|
|
528
606
|
|
|
529
|
-
return {
|
|
607
|
+
return {
|
|
608
|
+
primary,
|
|
609
|
+
...(hasChildProjects ? { files: allFiles } : {}),
|
|
610
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
611
|
+
};
|
|
530
612
|
},
|
|
531
613
|
};
|
package/src/spec/parse.test.ts
CHANGED
|
@@ -68,6 +68,21 @@ describe("parseArmSchema", () => {
|
|
|
68
68
|
expect(result.resource.attributes.find((a) => a.name === "id")).toBeTruthy();
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
it("gives a Container App its hostname attributes (#4179)", () => {
|
|
72
|
+
const schema = {
|
|
73
|
+
resourceType: "Microsoft.App/containerApps",
|
|
74
|
+
apiVersion: "2024-03-01",
|
|
75
|
+
provider: "Microsoft.App",
|
|
76
|
+
resourceName: "containerApps",
|
|
77
|
+
resourceDefinition: { properties: {} },
|
|
78
|
+
definitions: {},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const names = parseArmSchema(JSON.stringify(schema)).resource.attributes.map((a) => a.name);
|
|
82
|
+
expect(names).toContain("configuration.ingress.fqdn");
|
|
83
|
+
expect(names).toContain("latestRevisionFqdn");
|
|
84
|
+
});
|
|
85
|
+
|
|
71
86
|
it("parses definitions into property types and enums", () => {
|
|
72
87
|
const schema = {
|
|
73
88
|
resourceType: "Microsoft.Test/resources",
|
package/src/spec/parse.ts
CHANGED
|
@@ -168,6 +168,16 @@ const CURATED_ATTRIBUTES: Record<string, Array<{ name: string; tsType: string }>
|
|
|
168
168
|
{ name: "id", tsType: "string" },
|
|
169
169
|
{ name: "provisioningState", tsType: "string" },
|
|
170
170
|
],
|
|
171
|
+
// The app's hostname, for DNS in front of it (#4179). Dotted names read a
|
|
172
|
+
// nested field of reference()'s properties, as AWS's Endpoint.Address does.
|
|
173
|
+
"Microsoft.App/containerApps": [
|
|
174
|
+
{ name: "id", tsType: "string" },
|
|
175
|
+
{ name: "configuration.ingress.fqdn", tsType: "string" },
|
|
176
|
+
{ name: "latestRevisionFqdn", tsType: "string" },
|
|
177
|
+
{ name: "latestRevisionName", tsType: "string" },
|
|
178
|
+
{ name: "outboundIpAddresses", tsType: "string[]" },
|
|
179
|
+
{ name: "provisioningState", tsType: "string" },
|
|
180
|
+
],
|
|
171
181
|
};
|
|
172
182
|
|
|
173
183
|
/**
|