@intentius/chant-lexicon-azure 0.124.30 → 0.124.32
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.js +16598 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/governance.js +144 -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 +2 -2
- 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.js +94 -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/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 +472 -0
- package/dist/pseudo.js +40 -0
- package/dist/serializer.js +457 -0
- package/dist/spec/api-versions.js +130 -0
- package/dist/spec/fetch.js +196 -0
- package/dist/spec/parse.js +308 -0
- package/dist/taggable.js +34 -0
- package/dist/validate-cli.js +16 -0
- package/dist/validate.js +53 -0
- package/package.json +8 -8
- package/src/codegen/package.ts +2 -2
- package/src/deploy-scopes.ts +2 -1
- package/src/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
- package/src/plugin.ts +3 -2
- package/src/serializer.ts +2 -2
- package/src/taggable.ts +2 -2
|
@@ -0,0 +1,457 @@
|
|
|
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
|
+
* ARM-specific visitor for the generic serializer walker.
|
|
126
|
+
*/
|
|
127
|
+
function armVisitor(entityNames) {
|
|
128
|
+
return {
|
|
129
|
+
attrRef: (name, attr) => `[reference('${name}').${attr}]`,
|
|
130
|
+
resourceRef: (name) => {
|
|
131
|
+
// Look up the resource type from entityNames
|
|
132
|
+
const entity = [...entityNames.entries()].find(([, n]) => n === name)?.[0];
|
|
133
|
+
if (entity) {
|
|
134
|
+
return `[resourceId('${entity.entityType}', '${name}')]`;
|
|
135
|
+
}
|
|
136
|
+
return `[resourceId('${name}')]`;
|
|
137
|
+
},
|
|
138
|
+
propertyDeclarable: (entity, walk) => {
|
|
139
|
+
if (!isResourceDeclarable(entity) || typeof entity.props !== "object" || entity.props === null) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
const props = entity.props;
|
|
143
|
+
const armProps = {};
|
|
144
|
+
for (const [key, value] of Object.entries(props)) {
|
|
145
|
+
if (value !== undefined) {
|
|
146
|
+
armProps[key] = walk(value);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return Object.keys(armProps).length > 0 ? armProps : undefined;
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Convert a value to ARM-compatible JSON using the generic walker.
|
|
155
|
+
*/
|
|
156
|
+
function toArmValue(value, entityNames) {
|
|
157
|
+
return walkValue(value, entityNames, armVisitor(entityNames));
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Serialize entities into an ARM template object.
|
|
161
|
+
*/
|
|
162
|
+
function serializeToTemplate(entities, outputs, ownership) {
|
|
163
|
+
// Resolve the deployment scope from the resource types being emitted
|
|
164
|
+
// (#1545): a template of tenant-only resources (management groups, policy
|
|
165
|
+
// definitions at management-group scope, ...) must carry the matching
|
|
166
|
+
// $schema and must not lean on resourceGroup() expressions. Mixed sets
|
|
167
|
+
// with no common scope fall back to resource-group scope and AZR030
|
|
168
|
+
// reports the resources that cannot deploy there. A deploymentScope()
|
|
169
|
+
// pin overrides the inference — policy definitions and assignments
|
|
170
|
+
// deploy at several scopes, so a management-group guardrail project
|
|
171
|
+
// cannot be told from a subscription one by its resource set alone
|
|
172
|
+
//; AZR030 still flags resources the pinned scope cannot hold.
|
|
173
|
+
let pinnedScope;
|
|
174
|
+
const emittedTypes = [];
|
|
175
|
+
for (const [, entity] of entities) {
|
|
176
|
+
if (isStackOutput(entity) || isDefaultTags(entity) || isCoreParameter(entity))
|
|
177
|
+
continue;
|
|
178
|
+
if (isDeploymentScope(entity)) {
|
|
179
|
+
pinnedScope ?? (pinnedScope = entity.scope);
|
|
180
|
+
}
|
|
181
|
+
else if (isChildProject(entity)) {
|
|
182
|
+
emittedTypes.push("Microsoft.Resources/deployments");
|
|
183
|
+
}
|
|
184
|
+
else if (!isPropertyDeclarable(entity)) {
|
|
185
|
+
emittedTypes.push(entity.entityType);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const templateScope = pinnedScope ?? resolveTemplateScope(emittedTypes);
|
|
189
|
+
const template = {
|
|
190
|
+
$schema: TEMPLATE_SCHEMAS[templateScope],
|
|
191
|
+
contentVersion: "1.0.0.0",
|
|
192
|
+
resources: [],
|
|
193
|
+
};
|
|
194
|
+
// Build reverse map: entity -> name
|
|
195
|
+
const entityNames = new Map();
|
|
196
|
+
for (const [name, entity] of entities) {
|
|
197
|
+
entityNames.set(entity, name);
|
|
198
|
+
}
|
|
199
|
+
// Collect default tags. The ownership marker is stamped as tags, seeded
|
|
200
|
+
// first so user default tags (and explicit per-resource tags) win.
|
|
201
|
+
const defaultTagEntries = [];
|
|
202
|
+
if (ownership) {
|
|
203
|
+
for (const [key, value] of Object.entries(ownershipEntries(AZURE_TAG_OWNERSHIP_KEYS, ownership))) {
|
|
204
|
+
defaultTagEntries.push({ key, value });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
for (const [, entity] of entities) {
|
|
208
|
+
if (isDefaultTags(entity)) {
|
|
209
|
+
defaultTagEntries.push(...entity.tags);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Process entities
|
|
213
|
+
for (const [name, entity] of entities) {
|
|
214
|
+
// Skip non-resource types
|
|
215
|
+
if (isStackOutput(entity) || isDefaultTags(entity) || isDeploymentScope(entity))
|
|
216
|
+
continue;
|
|
217
|
+
if (isCoreParameter(entity)) {
|
|
218
|
+
if (!template.parameters)
|
|
219
|
+
template.parameters = {};
|
|
220
|
+
const param = {
|
|
221
|
+
type: mapParameterType(entity.parameterType),
|
|
222
|
+
};
|
|
223
|
+
if ("description" in entity && typeof entity.description === "string") {
|
|
224
|
+
param.metadata = { description: entity.description };
|
|
225
|
+
}
|
|
226
|
+
if ("defaultValue" in entity && entity.defaultValue !== undefined) {
|
|
227
|
+
param.defaultValue = entity.defaultValue;
|
|
228
|
+
}
|
|
229
|
+
template.parameters[name] = param;
|
|
230
|
+
}
|
|
231
|
+
else if (isChildProject(entity)) {
|
|
232
|
+
// ChildProjectInstance → linked deployment
|
|
233
|
+
const childProject = entity;
|
|
234
|
+
const childName = childProject.logicalName;
|
|
235
|
+
const filename = `${childName}.template.json`;
|
|
236
|
+
const resource = {
|
|
237
|
+
type: "Microsoft.Resources/deployments",
|
|
238
|
+
apiVersion: "2021-04-01",
|
|
239
|
+
name: childName,
|
|
240
|
+
properties: {
|
|
241
|
+
mode: "Incremental",
|
|
242
|
+
templateLink: { uri: filename },
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
template.resources.push(resource);
|
|
246
|
+
}
|
|
247
|
+
else if (!isPropertyDeclarable(entity)) {
|
|
248
|
+
const resourceType = entity.entityType;
|
|
249
|
+
// Prefer apiVersion from entity attributes (set by composites) over registry lookup
|
|
250
|
+
const attrs0 = (isResourceDeclarable(entity) && typeof entity.attributes === "object" && entity.attributes !== null)
|
|
251
|
+
? entity.attributes
|
|
252
|
+
: undefined;
|
|
253
|
+
const apiVersion = (typeof attrs0?.apiVersion === "string") ? attrs0.apiVersion : getApiVersion(resourceType);
|
|
254
|
+
const resource = {
|
|
255
|
+
type: resourceType,
|
|
256
|
+
apiVersion,
|
|
257
|
+
name: name,
|
|
258
|
+
};
|
|
259
|
+
// Extract all props
|
|
260
|
+
if (isResourceDeclarable(entity) && typeof entity.props === "object" && entity.props !== null) {
|
|
261
|
+
const props = entity.props;
|
|
262
|
+
const armProperties = {};
|
|
263
|
+
for (const [key, value] of Object.entries(props)) {
|
|
264
|
+
if (value === undefined)
|
|
265
|
+
continue;
|
|
266
|
+
if (RESOURCE_LEVEL_FIELDS.has(key)) {
|
|
267
|
+
// Hoist to resource level
|
|
268
|
+
resource[key] = toArmValue(value, entityNames);
|
|
269
|
+
}
|
|
270
|
+
else if (key === "name") {
|
|
271
|
+
resource.name = toArmValue(value, entityNames);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
armProperties[key] = toArmValue(value, entityNames);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (Object.keys(armProperties).length > 0) {
|
|
278
|
+
resource.properties = armProperties;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Default location to resource group location if not specified.
|
|
282
|
+
// Above resource-group scope there is no resource group to read a
|
|
283
|
+
// location from, and the scoped resources (management groups, policy
|
|
284
|
+
// definitions, ...) have no location field at all (#1545).
|
|
285
|
+
if (!resource.location && templateScope === "resourceGroup") {
|
|
286
|
+
resource.location = "[resourceGroup().location]";
|
|
287
|
+
}
|
|
288
|
+
// Handle DependsOn
|
|
289
|
+
const attrs = (isResourceDeclarable(entity) && typeof entity.attributes === "object" && entity.attributes !== null)
|
|
290
|
+
? entity.attributes
|
|
291
|
+
: undefined;
|
|
292
|
+
if (attrs?.DependsOn !== undefined) {
|
|
293
|
+
const resolved = resolveDependsOn(attrs.DependsOn, entityNames, name);
|
|
294
|
+
if (resolved.length > 0) {
|
|
295
|
+
resource.dependsOn = resolved.map((dep) => {
|
|
296
|
+
// Convert to resourceId expression
|
|
297
|
+
const depEntity = [...entityNames.entries()].find(([, n]) => n === dep)?.[0];
|
|
298
|
+
if (depEntity) {
|
|
299
|
+
return `[resourceId('${depEntity.entityType}', '${dep}')]`;
|
|
300
|
+
}
|
|
301
|
+
return dep;
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
template.resources.push(resource);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Inject default tags into taggable resources
|
|
309
|
+
if (defaultTagEntries.length > 0) {
|
|
310
|
+
const taggable = loadTaggableResources();
|
|
311
|
+
for (const resource of template.resources) {
|
|
312
|
+
if (!taggable.has(resource.type))
|
|
313
|
+
continue;
|
|
314
|
+
const resolved = {};
|
|
315
|
+
for (const t of defaultTagEntries) {
|
|
316
|
+
resolved[t.key] = toArmValue(t.value, entityNames);
|
|
317
|
+
}
|
|
318
|
+
const existing = resource.tags ?? {};
|
|
319
|
+
// Explicit tags take precedence
|
|
320
|
+
resource.tags = { ...resolved, ...existing };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// Auto-infer dependsOn from resourceId()/reference() expressions in properties.
|
|
324
|
+
// ARM only auto-infers dependencies from reference(), not resourceId().
|
|
325
|
+
const resourceNameSet = new Set(template.resources.map((r) => r.name));
|
|
326
|
+
for (const resource of template.resources) {
|
|
327
|
+
const refs = findArmResourceRefs(resource.properties);
|
|
328
|
+
// Also scan resource-level fields (location, tags, identity, etc.)
|
|
329
|
+
for (const field of RESOURCE_LEVEL_FIELDS) {
|
|
330
|
+
const val = resource[field];
|
|
331
|
+
if (val !== undefined) {
|
|
332
|
+
for (const ref of findArmResourceRefs(val)) {
|
|
333
|
+
refs.add(ref);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Also scan the name field for parent references (e.g. "vnet/subnet")
|
|
338
|
+
if (typeof resource.name === "string") {
|
|
339
|
+
for (const ref of findArmResourceRefs(resource.name)) {
|
|
340
|
+
refs.add(ref);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Infer parent dependency for child resources (e.g. "vnet/subnet" depends on "vnet")
|
|
344
|
+
if (typeof resource.name === "string" && resource.name.includes("/")) {
|
|
345
|
+
const parentName = resource.name.split("/")[0];
|
|
346
|
+
if (resourceNameSet.has(parentName)) {
|
|
347
|
+
refs.add(parentName);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (refs.size > 0) {
|
|
351
|
+
const existing = new Set(resource.dependsOn ?? []);
|
|
352
|
+
for (const refName of refs) {
|
|
353
|
+
// Only add if the referenced resource exists in this template and isn't self
|
|
354
|
+
if (refName === resource.name)
|
|
355
|
+
continue;
|
|
356
|
+
if (!resourceNameSet.has(refName))
|
|
357
|
+
continue;
|
|
358
|
+
// Find the resource to build the resourceId expression
|
|
359
|
+
const depResource = template.resources.find((r) => r.name === refName);
|
|
360
|
+
if (depResource) {
|
|
361
|
+
const depExpr = `[resourceId('${depResource.type}', '${refName}')]`;
|
|
362
|
+
if (!existing.has(depExpr)) {
|
|
363
|
+
existing.add(depExpr);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (existing.size > 0) {
|
|
368
|
+
resource.dependsOn = [...existing];
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// Emit StackOutput entities as ARM outputs
|
|
373
|
+
for (const [name, entity] of entities) {
|
|
374
|
+
if (isStackOutput(entity)) {
|
|
375
|
+
if (!template.outputs)
|
|
376
|
+
template.outputs = {};
|
|
377
|
+
const stackOutput = entity;
|
|
378
|
+
// `unknown` so isAttrRefLike narrows cleanly across the workspace/published
|
|
379
|
+
// type boundary. An intrinsic-wrapped output (#517) has no bare ARM
|
|
380
|
+
// reference form, so skip it here (only aws/CFN emits those).
|
|
381
|
+
const ref = stackOutput.sourceRef;
|
|
382
|
+
if (isAttrRefLike(ref)) {
|
|
383
|
+
const logicalName = ref.getLogicalName();
|
|
384
|
+
if (logicalName) {
|
|
385
|
+
template.outputs[name] = {
|
|
386
|
+
type: "string",
|
|
387
|
+
value: `[reference('${logicalName}').${ref.attribute}]`,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// Add LexiconOutputs
|
|
394
|
+
if (outputs && outputs.length > 0) {
|
|
395
|
+
template.outputs = template.outputs ?? {};
|
|
396
|
+
for (const output of outputs) {
|
|
397
|
+
template.outputs[output.outputName] = {
|
|
398
|
+
type: "string",
|
|
399
|
+
value: `[reference('${output.sourceEntity}').${output.sourceAttribute}]`,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return template;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Map chant parameter types to ARM parameter types.
|
|
407
|
+
*/
|
|
408
|
+
function mapParameterType(paramType) {
|
|
409
|
+
switch (paramType) {
|
|
410
|
+
case "String": return "string";
|
|
411
|
+
case "Number": return "int";
|
|
412
|
+
case "CommaDelimitedList": return "array";
|
|
413
|
+
default: return "string";
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Azure ARM template serializer implementation.
|
|
418
|
+
*/
|
|
419
|
+
export const azureSerializer = {
|
|
420
|
+
name: "azure",
|
|
421
|
+
rulePrefix: "AZR",
|
|
422
|
+
serialize(entities, outputs, context) {
|
|
423
|
+
// Check for child projects (linked templates)
|
|
424
|
+
let hasChildProjects = false;
|
|
425
|
+
const allFiles = {};
|
|
426
|
+
for (const [, entity] of entities) {
|
|
427
|
+
if (isChildProject(entity)) {
|
|
428
|
+
hasChildProjects = true;
|
|
429
|
+
const childProject = entity;
|
|
430
|
+
if (childProject.buildResult) {
|
|
431
|
+
const childOutput = childProject.buildResult.outputs.get("azure");
|
|
432
|
+
if (childOutput) {
|
|
433
|
+
const childName = childProject.logicalName;
|
|
434
|
+
const filename = `${childName}.template.json`;
|
|
435
|
+
if (typeof childOutput === "string") {
|
|
436
|
+
allFiles[filename] = childOutput;
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
allFiles[filename] = childOutput.primary;
|
|
440
|
+
if (childOutput.files) {
|
|
441
|
+
for (const [f, c] of Object.entries(childOutput.files)) {
|
|
442
|
+
allFiles[f] = c;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const template = serializeToTemplate(entities, outputs, context?.ownership);
|
|
451
|
+
const primary = JSON.stringify(template, null, 2);
|
|
452
|
+
if (!hasChildProjects) {
|
|
453
|
+
return primary;
|
|
454
|
+
}
|
|
455
|
+
return { primary, files: allFiles };
|
|
456
|
+
},
|
|
457
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API version resolution for Azure Resource Manager schemas.
|
|
3
|
+
*
|
|
4
|
+
* ARM schemas are organized by API date:
|
|
5
|
+
* schemas/{date}/Microsoft.{Provider}.json
|
|
6
|
+
*
|
|
7
|
+
* This module picks the latest API version per provider.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Parse an API date string (e.g. "2023-01-01" or "2023-01-01-preview").
|
|
11
|
+
* Returns a numeric value for comparison: date timestamp with preview
|
|
12
|
+
* versions ranked below their GA counterpart.
|
|
13
|
+
*/
|
|
14
|
+
export function parseApiDate(dateStr) {
|
|
15
|
+
const isPreview = dateStr.endsWith("-preview");
|
|
16
|
+
const cleaned = isPreview ? dateStr.replace(/-preview$/, "") : dateStr;
|
|
17
|
+
const ts = new Date(cleaned).getTime();
|
|
18
|
+
// Preview versions sort just below their GA date
|
|
19
|
+
return isPreview ? ts - 1 : ts;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Compare two API date strings. Returns positive if a > b.
|
|
23
|
+
*/
|
|
24
|
+
export function compareApiDates(a, b) {
|
|
25
|
+
return parseApiDate(a) - parseApiDate(b);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Extract provider name and API version from a schema file path.
|
|
29
|
+
*
|
|
30
|
+
* Input: "azure-resource-manager-schemas-main/schemas/2023-01-01/Microsoft.Storage.json"
|
|
31
|
+
* Output: { provider: "Microsoft.Storage", apiVersion: "2023-01-01" }
|
|
32
|
+
*/
|
|
33
|
+
export function parseSchemaPath(path) {
|
|
34
|
+
// Match schemas/{date}/Microsoft.{Provider}.json
|
|
35
|
+
const match = path.match(/schemas\/(\d{4}-\d{2}-\d{2}(?:-preview)?)\/(Microsoft\.[^/]+)\.json$/);
|
|
36
|
+
if (!match)
|
|
37
|
+
return null;
|
|
38
|
+
return { apiVersion: match[1], provider: match[2] };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Per-provider API-version pins.
|
|
42
|
+
*
|
|
43
|
+
* `latestVersionPerProvider` picks the single newest version per provider, but
|
|
44
|
+
* Azure spreads resources across versions — a newer version can DROP a resource
|
|
45
|
+
* an older one defined. Microsoft.Authorization's latest preview no longer
|
|
46
|
+
* includes the plain `roleAssignments` / `roleDefinitions` resources; they live
|
|
47
|
+
* in 2022-04-01, the latest stable that still has them. Pin it so those
|
|
48
|
+
* generate (the naming table maps both). See #223.
|
|
49
|
+
*
|
|
50
|
+
* Microsoft.Compute has the same issue (#1144): at the schema commit pinned
|
|
51
|
+
* in ../spec/fetch.ts, the newest dated `Microsoft.Compute.json` (2026-03-02)
|
|
52
|
+
* is a narrow disk-only delta that drops `virtualMachines`,
|
|
53
|
+
* `virtualMachineScaleSets`, and `availabilitySets` entirely — not a rename,
|
|
54
|
+
* an omission, since Azure alternates between a "VM flavor" and a "disk
|
|
55
|
+
* flavor" of this file across dates rather than publishing one cumulative
|
|
56
|
+
* file. 2026-03-01 is the most recent date that still has the VM family
|
|
57
|
+
* (validate.ts's REQUIRED_NAMES and composites/vm-linux.ts depend on it);
|
|
58
|
+
* it drops the disk-only resources (disks, diskAccesses, diskEncryptionSets,
|
|
59
|
+
* snapshots), which nothing here currently references.
|
|
60
|
+
*
|
|
61
|
+
* A pin can list several versions (#1545): Azure publishes some providers as
|
|
62
|
+
* per-family deltas, so no single date carries every resource we need. The
|
|
63
|
+
* files merge in the order listed — the first file that defines a resource
|
|
64
|
+
* name wins, later files only contribute resources not yet seen.
|
|
65
|
+
* Microsoft.Authorization needs three: 2022-04-01 for roleAssignments /
|
|
66
|
+
* roleDefinitions (#223), 2026-06-01 (latest stable policy file) for
|
|
67
|
+
* policyAssignments / policyDefinitions / policySetDefinitions and their
|
|
68
|
+
* _versions children, and 2022-07-01-preview for policyExemptions, which has
|
|
69
|
+
* no GA date and whose newer preview files (2024-12-01-preview and later)
|
|
70
|
+
* drag in policy variables we don't want yet.
|
|
71
|
+
*
|
|
72
|
+
* Microsoft.AzureStackHCI splits the same way (#1795): the newest dated file
|
|
73
|
+
* (2026-05-01-preview) carries only the cluster/edge-device family, while the
|
|
74
|
+
* VM family (virtualMachineInstances, virtualNetworks, networkInterfaces,
|
|
75
|
+
* publicIPAddresses, galleryImages, virtualHardDisks, ...) last appears in
|
|
76
|
+
* 2026-04-01-preview. Pin both, newest first — the cluster/edge family
|
|
77
|
+
* generates from 2026-05-01-preview and the VM family fills in from
|
|
78
|
+
* 2026-04-01-preview. `Microsoft.DataReplication/replicationVaults_
|
|
79
|
+
* privateEndpointConnectionProxies` dropped out in the same re-baseline but
|
|
80
|
+
* is NOT pinnable back: no dated Microsoft.DataReplication.json at the
|
|
81
|
+
* AZURE_SCHEMA_COMMIT pin (2021-02-16-preview, 2024-09-01, 2026-05-01)
|
|
82
|
+
* defines it — upstream removed the resource, so it stays gone.
|
|
83
|
+
*
|
|
84
|
+
* Microsoft.Management and Microsoft.Subscription are pinned to their latest
|
|
85
|
+
* GA dates: the naive latest for both is a preview (2024-02-01-preview adds
|
|
86
|
+
* serviceGroups; 2025-11-01-preview adds changeTenantRequest), and the
|
|
87
|
+
* management-group hierarchy should not author against preview apiVersions.
|
|
88
|
+
*/
|
|
89
|
+
export const PROVIDER_VERSION_OVERRIDES = {
|
|
90
|
+
"Microsoft.Authorization": ["2022-04-01", "2026-06-01", "2022-07-01-preview"],
|
|
91
|
+
"Microsoft.AzureStackHCI": ["2026-05-01-preview", "2026-04-01-preview"],
|
|
92
|
+
"Microsoft.Compute": ["2026-03-01"],
|
|
93
|
+
"Microsoft.Management": ["2023-04-01"],
|
|
94
|
+
"Microsoft.Subscription": ["2021-10-01"],
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Given a set of schema paths, return the schema files to generate from, per
|
|
98
|
+
* provider: the single latest API version, unless
|
|
99
|
+
* {@link PROVIDER_VERSION_OVERRIDES} pins the provider to one or more chosen
|
|
100
|
+
* versions (used where "latest" drops resources we depend on). For pinned
|
|
101
|
+
* providers the array preserves the pin order — callers merge the files
|
|
102
|
+
* first-wins per resource name.
|
|
103
|
+
*
|
|
104
|
+
* Returns a Map of provider → [{ path, apiVersion }, ...].
|
|
105
|
+
*/
|
|
106
|
+
export function latestVersionPerProvider(paths) {
|
|
107
|
+
const best = new Map();
|
|
108
|
+
for (const p of paths) {
|
|
109
|
+
const parsed = parseSchemaPath(p);
|
|
110
|
+
if (!parsed)
|
|
111
|
+
continue;
|
|
112
|
+
const pinned = PROVIDER_VERSION_OVERRIDES[parsed.provider];
|
|
113
|
+
if (pinned) {
|
|
114
|
+
// Only accept the pinned versions for an overridden provider,
|
|
115
|
+
// slotted into pin order.
|
|
116
|
+
if (pinned.includes(parsed.apiVersion)) {
|
|
117
|
+
const files = best.get(parsed.provider) ?? [];
|
|
118
|
+
files.push({ path: p, apiVersion: parsed.apiVersion });
|
|
119
|
+
files.sort((a, b) => pinned.indexOf(a.apiVersion) - pinned.indexOf(b.apiVersion));
|
|
120
|
+
best.set(parsed.provider, files);
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const existing = best.get(parsed.provider);
|
|
125
|
+
if (!existing || compareApiDates(parsed.apiVersion, existing[0].apiVersion) > 0) {
|
|
126
|
+
best.set(parsed.provider, [{ path: p, apiVersion: parsed.apiVersion }]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return best;
|
|
130
|
+
}
|