@intentius/chant-lexicon-azure 0.124.31 → 0.124.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/appservice.js +7 -0
- package/dist/actions/compute.js +9 -0
- package/dist/actions/container.js +14 -0
- package/dist/actions/identity.js +11 -0
- package/dist/actions/index.js +11 -0
- package/dist/actions/keyvault.js +12 -0
- package/dist/actions/network.js +9 -0
- package/dist/actions/sql.js +9 -0
- package/dist/actions/storage.js +15 -0
- package/dist/api/read-client.js +146 -0
- package/dist/codegen/docs-cli.js +2 -0
- package/dist/codegen/docs.js +83 -0
- package/dist/codegen/generate-cli.js +11 -0
- package/dist/codegen/generate-lexicon.js +77 -0
- package/dist/codegen/generate-typescript.js +163 -0
- package/dist/codegen/generate.js +106 -0
- package/dist/codegen/naming.js +139 -0
- package/dist/codegen/package.js +64 -0
- package/dist/composites/aks-cluster.js +68 -0
- package/dist/composites/app-service.js +62 -0
- package/dist/composites/application-gateway.js +107 -0
- package/dist/composites/catalog.js +856 -0
- package/dist/composites/container-instance.js +36 -0
- package/dist/composites/container-registry.js +62 -0
- package/dist/composites/cosmos-database.js +56 -0
- package/dist/composites/from-arm.js +47 -0
- package/dist/composites/function-app.js +57 -0
- package/dist/composites/index.js +16 -0
- package/dist/composites/keyvault.js +69 -0
- package/dist/composites/landing-zone.js +119 -0
- package/dist/composites/private-endpoint.js +61 -0
- package/dist/composites/redis-cache.js +21 -0
- package/dist/composites/service-bus-pipeline.js +54 -0
- package/dist/composites/sql-database.js +62 -0
- package/dist/composites/storage-account.js +59 -0
- package/dist/composites/vm-linux.js +139 -0
- package/dist/composites/vnet-default.js +102 -0
- package/dist/coverage.js +28 -0
- package/dist/deep-observe.js +356 -0
- package/dist/default-tags.js +50 -0
- package/dist/deploy-scopes.d.ts.map +1 -1
- package/dist/deploy-scopes.js +157 -0
- package/dist/describe-resources.js +187 -0
- package/dist/detect.js +17 -0
- package/dist/export-resources.js +108 -0
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/generated/index.js +16598 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/governance.js +144 -0
- package/dist/health.d.ts +42 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +137 -0
- package/dist/import/generator.js +140 -0
- package/dist/import/live-export.js +39 -0
- package/dist/import/parser.js +192 -0
- package/dist/index.js +44 -0
- package/dist/integrity.json +5 -5
- package/dist/intrinsics.js +179 -0
- package/dist/lint/audit-catalog.js +36 -0
- package/dist/lint/audit-lineage.js +71 -0
- package/dist/lint/post-synth/arm-refs.d.ts +1 -0
- package/dist/lint/post-synth/arm-refs.d.ts.map +1 -1
- package/dist/lint/post-synth/arm-refs.js +96 -0
- package/dist/lint/post-synth/azr010.js +45 -0
- package/dist/lint/post-synth/azr011.js +43 -0
- package/dist/lint/post-synth/azr012.js +41 -0
- package/dist/lint/post-synth/azr013.js +50 -0
- package/dist/lint/post-synth/azr014.js +49 -0
- package/dist/lint/post-synth/azr015.js +66 -0
- package/dist/lint/post-synth/azr016.js +37 -0
- package/dist/lint/post-synth/azr017.js +37 -0
- package/dist/lint/post-synth/azr018.js +42 -0
- package/dist/lint/post-synth/azr019.js +41 -0
- package/dist/lint/post-synth/azr020.js +37 -0
- package/dist/lint/post-synth/azr021.js +36 -0
- package/dist/lint/post-synth/azr022.js +39 -0
- package/dist/lint/post-synth/azr023.js +39 -0
- package/dist/lint/post-synth/azr024.js +39 -0
- package/dist/lint/post-synth/azr025.js +37 -0
- package/dist/lint/post-synth/azr026.js +38 -0
- package/dist/lint/post-synth/azr027.js +37 -0
- package/dist/lint/post-synth/azr028.js +37 -0
- package/dist/lint/post-synth/azr029.js +38 -0
- package/dist/lint/post-synth/azr030.js +44 -0
- package/dist/lint/post-synth/azr031.js +36 -0
- package/dist/lint/post-synth/azr032.js +53 -0
- package/dist/lint/post-synth/azr033.js +35 -0
- package/dist/lint/post-synth/index.js +50 -0
- package/dist/lint/rules/hardcoded-location.js +82 -0
- package/dist/lint/rules/index.js +6 -0
- package/dist/lint/rules/nsg-wildcard.js +44 -0
- package/dist/lint/rules/storage-https.js +62 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +18 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +44 -0
- package/dist/manifest.json +1 -1
- package/dist/meta.json +6 -1
- package/dist/okf/types/containerApps.md +5 -0
- package/dist/op/activities/az-apply.js +563 -0
- package/dist/op/activities/azure.js +48 -0
- package/dist/op/activities/floci-az.js +37 -0
- package/dist/op/activities/index.js +10 -0
- package/dist/op/builders.js +70 -0
- package/dist/ownership.js +13 -0
- package/dist/package-cli.js +21 -0
- package/dist/parameter.js +13 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +478 -0
- package/dist/pseudo.js +40 -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 +543 -0
- package/dist/spec/api-versions.js +130 -0
- package/dist/spec/fetch.js +196 -0
- package/dist/spec/parse.d.ts.map +1 -1
- package/dist/spec/parse.js +318 -0
- package/dist/taggable.js +34 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/validate-cli.js +16 -0
- package/dist/validate.js +53 -0
- package/package.json +8 -8
- package/src/attributes.test.ts +17 -0
- package/src/codegen/generate-typescript.ts +1 -1
- package/src/codegen/generate.ts +1 -1
- package/src/codegen/package.ts +2 -2
- package/src/deploy-scopes.ts +2 -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/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
- package/src/plugin.ts +11 -2
- package/src/serializer-names.test.ts +83 -0
- package/src/serializer-outputs.test.ts +65 -0
- package/src/serializer.ts +112 -30
- package/src/spec/parse.test.ts +15 -0
- package/src/spec/parse.ts +10 -0
- package/src/taggable.ts +2 -2
package/dist/serializer.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* - ARM bracket expression references
|
|
10
10
|
* - parameters, outputs sections
|
|
11
11
|
*/
|
|
12
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
12
13
|
import type { Serializer } from "@intentius/chant/serializer";
|
|
13
14
|
export declare function apiVersionRegistryCandidates(dir?: string): string[];
|
|
14
15
|
export declare function loadApiVersions(candidates?: string[]): Map<string, string>;
|
|
@@ -24,6 +25,12 @@ export declare function resetApiVersionCache(): void;
|
|
|
24
25
|
*/
|
|
25
26
|
export declare function lookupApiVersion(resourceType: string): string | undefined;
|
|
26
27
|
export declare function getApiVersion(resourceType: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* An attribute of an export. `id` is `resourceId()`, since `reference()`
|
|
30
|
+
* returns the resource's properties and they have no `id`; anything else
|
|
31
|
+
* reads `reference()` by the ARM name.
|
|
32
|
+
*/
|
|
33
|
+
export declare function armAttrExpression(entityNames: Map<Declarable, string>, name: string, attr: string): string;
|
|
27
34
|
/**
|
|
28
35
|
* Azure ARM template serializer implementation.
|
|
29
36
|
*/
|
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;
|
|
1
|
+
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAiB,MAAM,6BAA6B,CAAC;AAE7E,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;AA6BD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAK1G;AAiWD;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,UA+C7B,CAAC"}
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure Resource Manager template serializer.
|
|
3
|
+
*
|
|
4
|
+
* Converts Declarable entities into ARM template JSON with:
|
|
5
|
+
* - resources[] array (not keyed by logical name like CloudFormation)
|
|
6
|
+
* - apiVersion per resource (from lexicon registry)
|
|
7
|
+
* - Resource-level fields (sku, kind, identity, tags, zones, plan, location)
|
|
8
|
+
* hoisted from properties
|
|
9
|
+
* - ARM bracket expression references
|
|
10
|
+
* - parameters, outputs sections
|
|
11
|
+
*/
|
|
12
|
+
import { isPropertyDeclarable, isResourceDeclarable } from "@intentius/chant/declarable";
|
|
13
|
+
import { ownershipEntries } from "@intentius/chant/ownership";
|
|
14
|
+
import { AZURE_TAG_OWNERSHIP_KEYS } from "./ownership.js";
|
|
15
|
+
import { walkValue } from "@intentius/chant/serializer-walker";
|
|
16
|
+
import { isChildProject } from "@intentius/chant/child-project";
|
|
17
|
+
import { existsSync, readFileSync } from "fs";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
import { moduleSourceDir } from "@intentius/chant/module-source";
|
|
20
|
+
import { isStackOutput } from "@intentius/chant/stack-output";
|
|
21
|
+
import { isAttrRefLike } from "@intentius/chant/utils";
|
|
22
|
+
import { resolveDependsOn } from "@intentius/chant/resource-attributes";
|
|
23
|
+
import { isDefaultTags } from "./default-tags.js";
|
|
24
|
+
import { loadTaggableResources } from "./taggable.js";
|
|
25
|
+
import { findArmResourceRefs } from "./lint/post-synth/arm-refs.js";
|
|
26
|
+
import { isDeploymentScope, resolveTemplateScope, TEMPLATE_SCHEMAS } from "./deploy-scopes.js";
|
|
27
|
+
/** Check if a declarable is a CoreParameter */
|
|
28
|
+
function isCoreParameter(entity) {
|
|
29
|
+
return "parameterType" in entity;
|
|
30
|
+
}
|
|
31
|
+
/** Resource-level fields that get hoisted from properties to resource level. */
|
|
32
|
+
const RESOURCE_LEVEL_FIELDS = new Set([
|
|
33
|
+
"location", "tags", "sku", "kind", "identity", "zones", "plan",
|
|
34
|
+
]);
|
|
35
|
+
/**
|
|
36
|
+
* Load apiVersion for a resource type from the lexicon registry.
|
|
37
|
+
*
|
|
38
|
+
* Reads the generated registry (src/generated/lexicon-azure.json in a dev
|
|
39
|
+
* checkout, dist/meta.json in the installed package). Resolves the package
|
|
40
|
+
* directory from import.meta.url so it works under pure ESM as well as
|
|
41
|
+
* tsx/vitest — the old `require()`-based resolver threw under ESM and the
|
|
42
|
+
* throw was swallowed, so every resource emitted the fallback apiVersion
|
|
43
|
+
* (#1581). A missing registry is now an error: a uniform fallback apiVersion
|
|
44
|
+
* silently bypasses the per-provider pins in spec/api-versions.ts.
|
|
45
|
+
*/
|
|
46
|
+
function resolveDir() {
|
|
47
|
+
return moduleSourceDir(import.meta.url);
|
|
48
|
+
}
|
|
49
|
+
export function apiVersionRegistryCandidates(dir = resolveDir()) {
|
|
50
|
+
return [
|
|
51
|
+
join(dir, "generated", "lexicon-azure.json"),
|
|
52
|
+
join(dir, "..", "dist", "meta.json"),
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
export function loadApiVersions(candidates = apiVersionRegistryCandidates()) {
|
|
56
|
+
const map = new Map();
|
|
57
|
+
let content;
|
|
58
|
+
for (const candidate of candidates) {
|
|
59
|
+
if (!existsSync(candidate))
|
|
60
|
+
continue;
|
|
61
|
+
content = readFileSync(candidate, "utf-8");
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
if (content === undefined) {
|
|
65
|
+
throw new Error(`azure: apiVersion registry not found (tried ${candidates.join(", ")}). ` +
|
|
66
|
+
"Run `npm run generate` (dev) or reinstall the package (dist/meta.json).");
|
|
67
|
+
}
|
|
68
|
+
const data = JSON.parse(content);
|
|
69
|
+
for (const entry of Object.values(data)) {
|
|
70
|
+
if (entry.apiVersion && entry.resourceType) {
|
|
71
|
+
map.set(entry.resourceType, entry.apiVersion);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (map.size === 0) {
|
|
75
|
+
throw new Error("azure: apiVersion registry is empty — regenerate with `npm run generate`");
|
|
76
|
+
}
|
|
77
|
+
return map;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Override stale apiVersions from the generated registry.
|
|
81
|
+
* These are resource types where the registry apiVersion is known to be
|
|
82
|
+
* too old for modern ARM features (e.g. DataActions on role assignments).
|
|
83
|
+
*/
|
|
84
|
+
const API_VERSION_OVERRIDES = new Map([
|
|
85
|
+
["Microsoft.Authorization/roleAssignments", "2022-04-01"],
|
|
86
|
+
["Microsoft.Authorization/roleDefinitions", "2022-04-01"],
|
|
87
|
+
]);
|
|
88
|
+
let _apiVersions;
|
|
89
|
+
/** Test hook: drop the cached registry so the next lookup reloads it. */
|
|
90
|
+
export function resetApiVersionCache() {
|
|
91
|
+
_apiVersions = undefined;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Registry lookup without the serializer's fallback: the pinned apiVersion for
|
|
95
|
+
* `resourceType`, or `undefined` when neither the overrides nor the generated
|
|
96
|
+
* registry know the type. ARM resource types are case-insensitive and the live
|
|
97
|
+
* listing does not always echo the casing the registry uses, so the match is
|
|
98
|
+
* case-insensitive too. Used by prune (#1472), which needs an apiVersion for a
|
|
99
|
+
* type the current template no longer declares.
|
|
100
|
+
*/
|
|
101
|
+
export function lookupApiVersion(resourceType) {
|
|
102
|
+
const override = API_VERSION_OVERRIDES.get(resourceType);
|
|
103
|
+
if (override)
|
|
104
|
+
return override;
|
|
105
|
+
if (!_apiVersions)
|
|
106
|
+
_apiVersions = loadApiVersions();
|
|
107
|
+
const exact = _apiVersions.get(resourceType);
|
|
108
|
+
if (exact)
|
|
109
|
+
return exact;
|
|
110
|
+
const wanted = resourceType.toLowerCase();
|
|
111
|
+
for (const [type, version] of _apiVersions) {
|
|
112
|
+
if (type.toLowerCase() === wanted)
|
|
113
|
+
return version;
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
export function getApiVersion(resourceType) {
|
|
118
|
+
const version = lookupApiVersion(resourceType);
|
|
119
|
+
if (version)
|
|
120
|
+
return version;
|
|
121
|
+
console.warn(`azure: no pinned apiVersion for ${resourceType} in the lexicon registry; falling back to 2023-01-01`);
|
|
122
|
+
return "2023-01-01";
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The ARM name of a chant export, as the arguments an ARM function takes
|
|
126
|
+
* (#4183). A resource's `name` prop is its ARM name, and the export name is
|
|
127
|
+
* used only when it has none. A literal name is quoted, one argument per
|
|
128
|
+
* segment of a child's `parent/child` name; a bracket expression such as
|
|
129
|
+
* `[parameters('n')]` is passed unwrapped.
|
|
130
|
+
*/
|
|
131
|
+
function armNameArgs(entity, exportName) {
|
|
132
|
+
const props = entity && isResourceDeclarable(entity) && typeof entity.props === "object" && entity.props !== null
|
|
133
|
+
? entity.props
|
|
134
|
+
: undefined;
|
|
135
|
+
const name = typeof props?.name === "string" ? props.name : exportName;
|
|
136
|
+
if (name.startsWith("[") && name.endsWith("]"))
|
|
137
|
+
return { args: name.slice(1, -1), literal: null };
|
|
138
|
+
return { args: name.split("/").map((seg) => `'${seg}'`).join(", "), literal: name };
|
|
139
|
+
}
|
|
140
|
+
function entityNamed(entityNames, name) {
|
|
141
|
+
return [...entityNames.entries()].find(([, n]) => n === name)?.[0];
|
|
142
|
+
}
|
|
143
|
+
/** `resourceId(...)` for an export: the type and its ARM name. */
|
|
144
|
+
function armResourceId(entityNames, name) {
|
|
145
|
+
const entity = entityNamed(entityNames, name);
|
|
146
|
+
const { args } = armNameArgs(entity, name);
|
|
147
|
+
return entity ? `resourceId('${entity.entityType}', ${args})` : `resourceId(${args})`;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* An attribute of an export. `id` is `resourceId()`, since `reference()`
|
|
151
|
+
* returns the resource's properties and they have no `id`; anything else
|
|
152
|
+
* reads `reference()` by the ARM name.
|
|
153
|
+
*/
|
|
154
|
+
export function armAttrExpression(entityNames, name, attr) {
|
|
155
|
+
if (attr === "id")
|
|
156
|
+
return `[${armResourceId(entityNames, name)}]`;
|
|
157
|
+
const { args, literal } = armNameArgs(entityNamed(entityNames, name), name);
|
|
158
|
+
const target = literal !== null && !literal.includes("/") ? args : armResourceId(entityNames, name);
|
|
159
|
+
return `[reference(${target}).${attr}]`;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* ARM-specific visitor for the generic serializer walker.
|
|
163
|
+
*/
|
|
164
|
+
function armVisitor(entityNames) {
|
|
165
|
+
return {
|
|
166
|
+
attrRef: (name, attr) => armAttrExpression(entityNames, name, attr),
|
|
167
|
+
resourceRef: (name) => `[${armResourceId(entityNames, name)}]`,
|
|
168
|
+
propertyDeclarable: (entity, walk) => {
|
|
169
|
+
if (!isResourceDeclarable(entity) || typeof entity.props !== "object" || entity.props === null) {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
const props = entity.props;
|
|
173
|
+
const armProps = {};
|
|
174
|
+
for (const [key, value] of Object.entries(props)) {
|
|
175
|
+
if (value !== undefined) {
|
|
176
|
+
armProps[key] = walk(value);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return Object.keys(armProps).length > 0 ? armProps : undefined;
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Convert a value to ARM-compatible JSON using the generic walker.
|
|
185
|
+
*/
|
|
186
|
+
/** An ARM output for a literal, or for an azure intrinsic's `[...]` expression. */
|
|
187
|
+
function armOutputValue(value) {
|
|
188
|
+
if (typeof value === "string")
|
|
189
|
+
return { type: "string", value };
|
|
190
|
+
if (typeof value === "number" && Number.isInteger(value))
|
|
191
|
+
return { type: "int", value };
|
|
192
|
+
if (typeof value === "boolean")
|
|
193
|
+
return { type: "bool", value };
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
function isIntrinsicLike(value) {
|
|
197
|
+
return typeof value === "object" && value !== null && typeof value.toJSON === "function";
|
|
198
|
+
}
|
|
199
|
+
function isCloudFormationShaped(value) {
|
|
200
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
201
|
+
return false;
|
|
202
|
+
const keys = Object.keys(value);
|
|
203
|
+
return keys.length === 1 && (keys[0] === "Ref" || keys[0].startsWith("Fn::"));
|
|
204
|
+
}
|
|
205
|
+
function isPlainObject(value) {
|
|
206
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
207
|
+
return false;
|
|
208
|
+
const proto = Object.getPrototypeOf(value);
|
|
209
|
+
return proto === Object.prototype || proto === null;
|
|
210
|
+
}
|
|
211
|
+
function toArmValue(value, entityNames) {
|
|
212
|
+
return walkValue(value, entityNames, armVisitor(entityNames));
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Serialize entities into an ARM template object.
|
|
216
|
+
*/
|
|
217
|
+
function serializeToTemplate(entities, outputs, ownership, warnings = []) {
|
|
218
|
+
// Resolve the deployment scope from the resource types being emitted
|
|
219
|
+
// (#1545): a template of tenant-only resources (management groups, policy
|
|
220
|
+
// definitions at management-group scope, ...) must carry the matching
|
|
221
|
+
// $schema and must not lean on resourceGroup() expressions. Mixed sets
|
|
222
|
+
// with no common scope fall back to resource-group scope and AZR030
|
|
223
|
+
// reports the resources that cannot deploy there. A deploymentScope()
|
|
224
|
+
// pin overrides the inference — policy definitions and assignments
|
|
225
|
+
// deploy at several scopes, so a management-group guardrail project
|
|
226
|
+
// cannot be told from a subscription one by its resource set alone
|
|
227
|
+
//; AZR030 still flags resources the pinned scope cannot hold.
|
|
228
|
+
let pinnedScope;
|
|
229
|
+
const emittedTypes = [];
|
|
230
|
+
for (const [, entity] of entities) {
|
|
231
|
+
if (isStackOutput(entity) || isDefaultTags(entity) || isCoreParameter(entity))
|
|
232
|
+
continue;
|
|
233
|
+
if (isDeploymentScope(entity)) {
|
|
234
|
+
pinnedScope ?? (pinnedScope = entity.scope);
|
|
235
|
+
}
|
|
236
|
+
else if (isChildProject(entity)) {
|
|
237
|
+
emittedTypes.push("Microsoft.Resources/deployments");
|
|
238
|
+
}
|
|
239
|
+
else if (!isPropertyDeclarable(entity)) {
|
|
240
|
+
emittedTypes.push(entity.entityType);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const templateScope = pinnedScope ?? resolveTemplateScope(emittedTypes);
|
|
244
|
+
const template = {
|
|
245
|
+
$schema: TEMPLATE_SCHEMAS[templateScope],
|
|
246
|
+
contentVersion: "1.0.0.0",
|
|
247
|
+
resources: [],
|
|
248
|
+
};
|
|
249
|
+
// Build reverse map: entity -> name
|
|
250
|
+
const entityNames = new Map();
|
|
251
|
+
for (const [name, entity] of entities) {
|
|
252
|
+
entityNames.set(entity, name);
|
|
253
|
+
}
|
|
254
|
+
// Collect default tags. The ownership marker is stamped as tags, seeded
|
|
255
|
+
// first so user default tags (and explicit per-resource tags) win.
|
|
256
|
+
const defaultTagEntries = [];
|
|
257
|
+
if (ownership) {
|
|
258
|
+
for (const [key, value] of Object.entries(ownershipEntries(AZURE_TAG_OWNERSHIP_KEYS, ownership))) {
|
|
259
|
+
defaultTagEntries.push({ key, value });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
for (const [, entity] of entities) {
|
|
263
|
+
if (isDefaultTags(entity)) {
|
|
264
|
+
defaultTagEntries.push(...entity.tags);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Process entities
|
|
268
|
+
for (const [name, entity] of entities) {
|
|
269
|
+
// Skip non-resource types
|
|
270
|
+
if (isStackOutput(entity) || isDefaultTags(entity) || isDeploymentScope(entity))
|
|
271
|
+
continue;
|
|
272
|
+
if (isCoreParameter(entity)) {
|
|
273
|
+
if (!template.parameters)
|
|
274
|
+
template.parameters = {};
|
|
275
|
+
const param = {
|
|
276
|
+
type: mapParameterType(entity.parameterType),
|
|
277
|
+
};
|
|
278
|
+
if ("description" in entity && typeof entity.description === "string") {
|
|
279
|
+
param.metadata = { description: entity.description };
|
|
280
|
+
}
|
|
281
|
+
if ("defaultValue" in entity && entity.defaultValue !== undefined) {
|
|
282
|
+
param.defaultValue = entity.defaultValue;
|
|
283
|
+
}
|
|
284
|
+
template.parameters[name] = param;
|
|
285
|
+
}
|
|
286
|
+
else if (isChildProject(entity)) {
|
|
287
|
+
// ChildProjectInstance → linked deployment
|
|
288
|
+
const childProject = entity;
|
|
289
|
+
const childName = childProject.logicalName;
|
|
290
|
+
const filename = `${childName}.template.json`;
|
|
291
|
+
const resource = {
|
|
292
|
+
type: "Microsoft.Resources/deployments",
|
|
293
|
+
apiVersion: "2021-04-01",
|
|
294
|
+
name: childName,
|
|
295
|
+
properties: {
|
|
296
|
+
mode: "Incremental",
|
|
297
|
+
templateLink: { uri: filename },
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
template.resources.push(resource);
|
|
301
|
+
}
|
|
302
|
+
else if (!isPropertyDeclarable(entity)) {
|
|
303
|
+
const resourceType = entity.entityType;
|
|
304
|
+
// Prefer apiVersion from entity attributes (set by composites) over registry lookup
|
|
305
|
+
const attrs0 = (isResourceDeclarable(entity) && typeof entity.attributes === "object" && entity.attributes !== null)
|
|
306
|
+
? entity.attributes
|
|
307
|
+
: undefined;
|
|
308
|
+
const apiVersion = (typeof attrs0?.apiVersion === "string") ? attrs0.apiVersion : getApiVersion(resourceType);
|
|
309
|
+
const resource = {
|
|
310
|
+
type: resourceType,
|
|
311
|
+
apiVersion,
|
|
312
|
+
name: name,
|
|
313
|
+
};
|
|
314
|
+
// Extract all props
|
|
315
|
+
if (isResourceDeclarable(entity) && typeof entity.props === "object" && entity.props !== null) {
|
|
316
|
+
const props = entity.props;
|
|
317
|
+
const armProperties = {};
|
|
318
|
+
for (const [key, value] of Object.entries(props)) {
|
|
319
|
+
if (value === undefined)
|
|
320
|
+
continue;
|
|
321
|
+
if (RESOURCE_LEVEL_FIELDS.has(key)) {
|
|
322
|
+
// Hoist to resource level
|
|
323
|
+
resource[key] = toArmValue(value, entityNames);
|
|
324
|
+
}
|
|
325
|
+
else if (key === "name") {
|
|
326
|
+
resource.name = toArmValue(value, entityNames);
|
|
327
|
+
}
|
|
328
|
+
else if (key === "properties" && isPlainObject(value)) {
|
|
329
|
+
// The generated types take ARM's own `properties` object, so
|
|
330
|
+
// merge it, not nest it (#4183).
|
|
331
|
+
for (const [inner, innerValue] of Object.entries(value)) {
|
|
332
|
+
if (innerValue !== undefined)
|
|
333
|
+
armProperties[inner] = toArmValue(innerValue, entityNames);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
armProperties[key] = toArmValue(value, entityNames);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (Object.keys(armProperties).length > 0) {
|
|
341
|
+
resource.properties = armProperties;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// Default location to resource group location if not specified.
|
|
345
|
+
// Above resource-group scope there is no resource group to read a
|
|
346
|
+
// location from, and the scoped resources (management groups, policy
|
|
347
|
+
// definitions, ...) have no location field at all (#1545).
|
|
348
|
+
if (!resource.location && templateScope === "resourceGroup") {
|
|
349
|
+
resource.location = "[resourceGroup().location]";
|
|
350
|
+
}
|
|
351
|
+
// Handle DependsOn
|
|
352
|
+
const attrs = (isResourceDeclarable(entity) && typeof entity.attributes === "object" && entity.attributes !== null)
|
|
353
|
+
? entity.attributes
|
|
354
|
+
: undefined;
|
|
355
|
+
if (attrs?.DependsOn !== undefined) {
|
|
356
|
+
const resolved = resolveDependsOn(attrs.DependsOn, entityNames, name);
|
|
357
|
+
if (resolved.length > 0) {
|
|
358
|
+
resource.dependsOn = resolved.map((dep) => entityNamed(entityNames, dep) ? `[${armResourceId(entityNames, dep)}]` : dep);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
template.resources.push(resource);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// Inject default tags into taggable resources
|
|
365
|
+
if (defaultTagEntries.length > 0) {
|
|
366
|
+
const taggable = loadTaggableResources();
|
|
367
|
+
for (const resource of template.resources) {
|
|
368
|
+
if (!taggable.has(resource.type))
|
|
369
|
+
continue;
|
|
370
|
+
const resolved = {};
|
|
371
|
+
for (const t of defaultTagEntries) {
|
|
372
|
+
resolved[t.key] = toArmValue(t.value, entityNames);
|
|
373
|
+
}
|
|
374
|
+
const existing = resource.tags ?? {};
|
|
375
|
+
// Explicit tags take precedence
|
|
376
|
+
resource.tags = { ...resolved, ...existing };
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// Auto-infer dependsOn from resourceId()/reference() expressions in properties.
|
|
380
|
+
// ARM only auto-infers dependencies from reference(), not resourceId().
|
|
381
|
+
const resourceNameSet = new Set(template.resources.map((r) => r.name));
|
|
382
|
+
for (const resource of template.resources) {
|
|
383
|
+
const refs = findArmResourceRefs(resource.properties);
|
|
384
|
+
// Also scan resource-level fields (location, tags, identity, etc.)
|
|
385
|
+
for (const field of RESOURCE_LEVEL_FIELDS) {
|
|
386
|
+
const val = resource[field];
|
|
387
|
+
if (val !== undefined) {
|
|
388
|
+
for (const ref of findArmResourceRefs(val)) {
|
|
389
|
+
refs.add(ref);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// Also scan the name field for parent references (e.g. "vnet/subnet")
|
|
394
|
+
if (typeof resource.name === "string") {
|
|
395
|
+
for (const ref of findArmResourceRefs(resource.name)) {
|
|
396
|
+
refs.add(ref);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// Infer parent dependency for child resources (e.g. "vnet/subnet" depends on "vnet")
|
|
400
|
+
if (typeof resource.name === "string" && resource.name.includes("/")) {
|
|
401
|
+
const parentName = resource.name.split("/")[0];
|
|
402
|
+
if (resourceNameSet.has(parentName)) {
|
|
403
|
+
refs.add(parentName);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (refs.size > 0) {
|
|
407
|
+
const existing = new Set(resource.dependsOn ?? []);
|
|
408
|
+
for (const refName of refs) {
|
|
409
|
+
// Only add if the referenced resource exists in this template and isn't self
|
|
410
|
+
if (refName === resource.name)
|
|
411
|
+
continue;
|
|
412
|
+
if (!resourceNameSet.has(refName))
|
|
413
|
+
continue;
|
|
414
|
+
// Find the resource to build the resourceId expression
|
|
415
|
+
const depResource = template.resources.find((r) => r.name === refName);
|
|
416
|
+
if (depResource) {
|
|
417
|
+
const depExpr = `[resourceId('${depResource.type}', ${refName.split("/").map((seg) => `'${seg}'`).join(", ")})]`;
|
|
418
|
+
if (!existing.has(depExpr)) {
|
|
419
|
+
existing.add(depExpr);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (existing.size > 0) {
|
|
424
|
+
resource.dependsOn = [...existing];
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// Emit StackOutput entities as ARM outputs
|
|
429
|
+
for (const [name, entity] of entities) {
|
|
430
|
+
if (isStackOutput(entity)) {
|
|
431
|
+
if (!template.outputs)
|
|
432
|
+
template.outputs = {};
|
|
433
|
+
const stackOutput = entity;
|
|
434
|
+
// `unknown` so isAttrRefLike narrows cleanly across the workspace/published
|
|
435
|
+
// type boundary.
|
|
436
|
+
const ref = stackOutput.sourceRef;
|
|
437
|
+
if (isAttrRefLike(ref)) {
|
|
438
|
+
const logicalName = ref.getLogicalName();
|
|
439
|
+
if (logicalName) {
|
|
440
|
+
template.outputs[name] = {
|
|
441
|
+
type: "string",
|
|
442
|
+
value: armAttrExpression(entityNames, logicalName, ref.attribute),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
const value = armOutputValue(isIntrinsicLike(ref) ? ref.toJSON() : ref);
|
|
448
|
+
if (value)
|
|
449
|
+
template.outputs[name] = value;
|
|
450
|
+
else
|
|
451
|
+
warnings.push(`azure: stack output "${name}" is not written: its value has no ARM form.`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// Add LexiconOutputs
|
|
456
|
+
if (outputs && outputs.length > 0) {
|
|
457
|
+
template.outputs = template.outputs ?? {};
|
|
458
|
+
for (const output of outputs) {
|
|
459
|
+
if (output.sourceAttribute !== null) {
|
|
460
|
+
if (output.sourceEntity) {
|
|
461
|
+
template.outputs[output.outputName] = {
|
|
462
|
+
type: "string",
|
|
463
|
+
value: armAttrExpression(entityNames, output.sourceEntity, output.sourceAttribute),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
warnings.push(`azure: output "${output.outputName}" is not written: its resource has no name in this build.`);
|
|
468
|
+
}
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
// A literal or an intrinsic (#4181). It has no source lexicon, so every
|
|
472
|
+
// lexicon receives it; a CloudFormation-shaped value is aws's to write.
|
|
473
|
+
const raw = output.getOutputValue();
|
|
474
|
+
const value = armOutputValue(raw);
|
|
475
|
+
if (value)
|
|
476
|
+
template.outputs[output.outputName] = value;
|
|
477
|
+
else if (!isCloudFormationShaped(raw)) {
|
|
478
|
+
warnings.push(`azure: output "${output.outputName}" is not written: its value has no ARM form.`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (Object.keys(template.outputs).length === 0)
|
|
482
|
+
delete template.outputs;
|
|
483
|
+
}
|
|
484
|
+
return template;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Map chant parameter types to ARM parameter types.
|
|
488
|
+
*/
|
|
489
|
+
function mapParameterType(paramType) {
|
|
490
|
+
switch (paramType) {
|
|
491
|
+
case "String": return "string";
|
|
492
|
+
case "Number": return "int";
|
|
493
|
+
case "CommaDelimitedList": return "array";
|
|
494
|
+
default: return "string";
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Azure ARM template serializer implementation.
|
|
499
|
+
*/
|
|
500
|
+
export const azureSerializer = {
|
|
501
|
+
name: "azure",
|
|
502
|
+
rulePrefix: "AZR",
|
|
503
|
+
serialize(entities, outputs, context) {
|
|
504
|
+
// Check for child projects (linked templates)
|
|
505
|
+
let hasChildProjects = false;
|
|
506
|
+
const allFiles = {};
|
|
507
|
+
for (const [, entity] of entities) {
|
|
508
|
+
if (isChildProject(entity)) {
|
|
509
|
+
hasChildProjects = true;
|
|
510
|
+
const childProject = entity;
|
|
511
|
+
if (childProject.buildResult) {
|
|
512
|
+
const childOutput = childProject.buildResult.outputs.get("azure");
|
|
513
|
+
if (childOutput) {
|
|
514
|
+
const childName = childProject.logicalName;
|
|
515
|
+
const filename = `${childName}.template.json`;
|
|
516
|
+
if (typeof childOutput === "string") {
|
|
517
|
+
allFiles[filename] = childOutput;
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
allFiles[filename] = childOutput.primary;
|
|
521
|
+
if (childOutput.files) {
|
|
522
|
+
for (const [f, c] of Object.entries(childOutput.files)) {
|
|
523
|
+
allFiles[f] = c;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const warnings = [];
|
|
532
|
+
const template = serializeToTemplate(entities, outputs, context?.ownership, warnings);
|
|
533
|
+
const primary = JSON.stringify(template, null, 2);
|
|
534
|
+
if (!hasChildProjects && warnings.length === 0) {
|
|
535
|
+
return primary;
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
primary,
|
|
539
|
+
...(hasChildProjects ? { files: allFiles } : {}),
|
|
540
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
541
|
+
};
|
|
542
|
+
},
|
|
543
|
+
};
|