@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
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure governance authoring: the desired-state config
|
|
3
|
+
* shape an external governance reconciler reconciles, and `landingZoneConfig()` — the
|
|
4
|
+
* typed authoring layer that emits it.
|
|
5
|
+
*
|
|
6
|
+
* This is deliberately NOT a composite. The evaluability rules (EVL002/004,
|
|
7
|
+
* #916/#952) require composites to declare a fixed set of resources, so a
|
|
8
|
+
* data-driven management-group tree walker cannot be one. The split mirrors
|
|
9
|
+
* the AWS/GCP slices (lexicons/{aws,gcp}/src/governance.ts): authoring emits
|
|
10
|
+
* config (this module, arbitrary cardinality); resources for greenfield
|
|
11
|
+
* bootstrap are the fixed-shape composites in `composites/landing-zone.ts`;
|
|
12
|
+
* the reconciler consumes only the config.
|
|
13
|
+
*
|
|
14
|
+
* Azure has no OrganizationRoot analog — the tenant and its root management
|
|
15
|
+
* group come from Entra ID, never from the resource API. Subscriptions the
|
|
16
|
+
* tree declares reconcile through Microsoft.Subscription/aliases (existing
|
|
17
|
+
* ids are placed, missing ones created from `billingScope`); aliases deploy
|
|
18
|
+
* at tenant scope only (#1545).
|
|
19
|
+
*/
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Baseline guardrails (shared with the bootstrap composites)
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
export const DENY_CLASSIC_RESOURCES = {
|
|
24
|
+
description: "Classic (ASM) compute, network, and storage resources cannot be created.",
|
|
25
|
+
displayName: "Deny classic resources",
|
|
26
|
+
mode: "All",
|
|
27
|
+
policyRule: {
|
|
28
|
+
if: { field: "type", like: "Microsoft.Classic*" },
|
|
29
|
+
then: { effect: "deny" },
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
export const DENY_UNMANAGED_DISKS = {
|
|
33
|
+
description: "Virtual machines and scale sets must use managed disks.",
|
|
34
|
+
displayName: "Deny unmanaged disks",
|
|
35
|
+
mode: "All",
|
|
36
|
+
policyRule: {
|
|
37
|
+
if: {
|
|
38
|
+
anyOf: [
|
|
39
|
+
{
|
|
40
|
+
allOf: [
|
|
41
|
+
{ field: "type", equals: "Microsoft.Compute/virtualMachines" },
|
|
42
|
+
{ field: "Microsoft.Compute/virtualMachines/osDisk.uri", exists: "true" },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
allOf: [
|
|
47
|
+
{ field: "type", equals: "Microsoft.Compute/virtualMachineScaleSets" },
|
|
48
|
+
{
|
|
49
|
+
anyOf: [
|
|
50
|
+
{ field: "Microsoft.Compute/VirtualMachineScaleSets/osDisk.vhdContainers", exists: "true" },
|
|
51
|
+
{ field: "Microsoft.Compute/VirtualMachineScaleSets/osdisk.imageUrl", exists: "true" },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
then: { effect: "deny" },
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
/** Allow resource creation only in `locations` ("global" is always excepted). */
|
|
62
|
+
export function locationRestriction(locations) {
|
|
63
|
+
return {
|
|
64
|
+
description: `Resources may only be created in [${locations.join(", ")}].`,
|
|
65
|
+
displayName: "Allowed locations",
|
|
66
|
+
mode: "Indexed",
|
|
67
|
+
policyRule: {
|
|
68
|
+
if: {
|
|
69
|
+
allOf: [
|
|
70
|
+
{ field: "location", notIn: [...locations] },
|
|
71
|
+
{ field: "location", notEquals: "global" },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
then: { effect: "deny" },
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The built-in DeployIfNotExists policy "Configure Azure Activity logs to
|
|
80
|
+
* stream to specified Log Analytics workspace" — the activity-log audit
|
|
81
|
+
* sink is wired by assigning it, not by declaring diagnostic settings in
|
|
82
|
+
* every subscription (Microsoft.Insights/diagnosticSettings is not in the
|
|
83
|
+
* generated surface, and the assignment covers future subscriptions too).
|
|
84
|
+
*/
|
|
85
|
+
export const ACTIVITY_LOG_TO_LOG_ANALYTICS_DEFINITION_ID = "/providers/Microsoft.Authorization/policyDefinitions/2465583e-4e78-4c15-b6be-a36cbc7c8b0f";
|
|
86
|
+
/**
|
|
87
|
+
* The recommended foundation management-group structure. Exported so
|
|
88
|
+
* callers can extend it rather than restate it.
|
|
89
|
+
*/
|
|
90
|
+
export const FOUNDATION_MANAGEMENT_GROUPS = {
|
|
91
|
+
Security: {},
|
|
92
|
+
Infrastructure: {},
|
|
93
|
+
Sandbox: {},
|
|
94
|
+
Workloads: {},
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Author the desired-state tree an external governance reconciler reconciles. Pure.
|
|
98
|
+
* Throws when the tree assigns a policy name with no definition; only
|
|
99
|
+
* policies the tree actually assigns are included in the output.
|
|
100
|
+
*/
|
|
101
|
+
export function landingZoneConfig(props = {}) {
|
|
102
|
+
const foundation = props.foundation ?? true;
|
|
103
|
+
const policies = {
|
|
104
|
+
...(foundation
|
|
105
|
+
? { "deny-classic-resources": DENY_CLASSIC_RESOURCES, "deny-unmanaged-disks": DENY_UNMANAGED_DISKS }
|
|
106
|
+
: {}),
|
|
107
|
+
...(props.allowedLocations?.length
|
|
108
|
+
? { "location-restriction": locationRestriction(props.allowedLocations) }
|
|
109
|
+
: {}),
|
|
110
|
+
...props.policies,
|
|
111
|
+
};
|
|
112
|
+
const managementGroups = { ...(foundation ? FOUNDATION_MANAGEMENT_GROUPS : {}), ...props.managementGroups };
|
|
113
|
+
const rootPolicies = [
|
|
114
|
+
...(foundation ? ["deny-classic-resources", "deny-unmanaged-disks"] : []),
|
|
115
|
+
...(props.allowedLocations?.length ? ["location-restriction"] : []),
|
|
116
|
+
...(props.rootPolicies ?? []),
|
|
117
|
+
];
|
|
118
|
+
const assigned = new Set(rootPolicies);
|
|
119
|
+
const collect = (tree) => {
|
|
120
|
+
for (const node of Object.values(tree)) {
|
|
121
|
+
for (const p of node.policies ?? [])
|
|
122
|
+
assigned.add(p);
|
|
123
|
+
if (node.children)
|
|
124
|
+
collect(node.children);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
collect(managementGroups);
|
|
128
|
+
for (const name of assigned) {
|
|
129
|
+
if (!policies[name]) {
|
|
130
|
+
throw new Error(`landingZoneConfig: policy "${name}" is assigned but not defined in policies`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
tenant: {
|
|
135
|
+
...(rootPolicies.length ? { policies: rootPolicies } : {}),
|
|
136
|
+
...(foundation ? { blockSubscriptionsLeavingTenant: true } : {}),
|
|
137
|
+
},
|
|
138
|
+
managementGroups,
|
|
139
|
+
policies: Object.fromEntries(Object.entries(policies).filter(([n]) => assigned.has(n))),
|
|
140
|
+
...(props.activityLogWorkspaceId
|
|
141
|
+
? { auditSinks: { activityLog: { workspaceId: props.activityLogWorkspaceId } } }
|
|
142
|
+
: {}),
|
|
143
|
+
};
|
|
144
|
+
}
|
package/dist/health.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime health for Azure (#4177): Container Apps.
|
|
3
|
+
*
|
|
4
|
+
* For each declared `Microsoft.App/containerApps`, GET the app (a 404 is
|
|
5
|
+
* absent), then its revisions, and judge the active ones by their running and
|
|
6
|
+
* health state. Same transport and endpoint rule as `describe-resources.ts`, so
|
|
7
|
+
* `AZURE_ENDPOINT_URL` (floci-az) answers.
|
|
8
|
+
*
|
|
9
|
+
* When the revisions list cannot be read but the app carries a
|
|
10
|
+
* `runningStatus` (newer API versions do), that answers instead, and the detail
|
|
11
|
+
* says so.
|
|
12
|
+
*/
|
|
13
|
+
import type { HealthEntry, HealthReadOptions, HealthReadResult } from "@intentius/chant/lexicon";
|
|
14
|
+
import type { AzHttp } from "./op/activities/az-apply.js";
|
|
15
|
+
export declare const AZURE_HEALTH_TYPES: readonly ["Microsoft.App/containerApps"];
|
|
16
|
+
export interface AzureHealthOptions extends HealthReadOptions {
|
|
17
|
+
http?: AzHttp;
|
|
18
|
+
now?: () => Date;
|
|
19
|
+
}
|
|
20
|
+
/** The fields of one revision that decide health. */
|
|
21
|
+
export interface RevisionState {
|
|
22
|
+
name?: string;
|
|
23
|
+
properties?: {
|
|
24
|
+
active?: boolean;
|
|
25
|
+
runningState?: string;
|
|
26
|
+
healthState?: string;
|
|
27
|
+
replicas?: number;
|
|
28
|
+
trafficWeight?: number;
|
|
29
|
+
lastActiveTime?: string;
|
|
30
|
+
createdTime?: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The active revisions' verdict: unhealthy if none is active or any active one
|
|
35
|
+
* failed, degraded, stopped or reports `Unhealthy`; healthy if every active one
|
|
36
|
+
* is running and none is unhealthy; otherwise unknown (still provisioning).
|
|
37
|
+
*/
|
|
38
|
+
export declare function containerAppEntry(revisions: RevisionState[], observedAt: string): HealthEntry;
|
|
39
|
+
/** An app's own `runningStatus`, for when its revisions cannot be listed. */
|
|
40
|
+
export declare function appRunningStatusEntry(runningStatus: string, observedAt: string): HealthEntry;
|
|
41
|
+
export declare function observeHealthAzure(options: AzureHealthOptions): Promise<HealthReadResult>;
|
|
42
|
+
//# sourceMappingURL=health.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../src/health.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,gBAAgB,EAAoB,MAAM,0BAA0B,CAAC;AAGnH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAEvD,eAAO,MAAM,kBAAkB,YAAI,6BAA6B,CAAU,CAAC;AAK3E,MAAM,WAAW,kBAAmB,SAAQ,iBAAiB;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE;QACX,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;CACH;AAKD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,aAAa,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,WAAW,CAyB7F;AAED,6EAA6E;AAC7E,wBAAgB,qBAAqB,CAAC,aAAa,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,WAAW,CAS5F;AAiCD,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAyC/F"}
|
package/dist/health.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime health for Azure (#4177): Container Apps.
|
|
3
|
+
*
|
|
4
|
+
* For each declared `Microsoft.App/containerApps`, GET the app (a 404 is
|
|
5
|
+
* absent), then its revisions, and judge the active ones by their running and
|
|
6
|
+
* health state. Same transport and endpoint rule as `describe-resources.ts`, so
|
|
7
|
+
* `AZURE_ENDPOINT_URL` (floci-az) answers.
|
|
8
|
+
*
|
|
9
|
+
* When the revisions list cannot be read but the app carries a
|
|
10
|
+
* `runningStatus` (newer API versions do), that answers instead, and the detail
|
|
11
|
+
* says so.
|
|
12
|
+
*/
|
|
13
|
+
import { armResourceReadUrl, AzureReadError, getResource, isNotFound } from "./api/read-client.js";
|
|
14
|
+
import { classifyArmFailure } from "./describe-resources.js";
|
|
15
|
+
export const AZURE_HEALTH_TYPES = ["Microsoft.App/containerApps"];
|
|
16
|
+
/** The Container Apps API version whose revisions carry `runningState` and `healthState`. */
|
|
17
|
+
const CONTAINER_APPS_API_VERSION = "2024-03-01";
|
|
18
|
+
const RUNNING = new Set(["running", "runningatmaxscale"]);
|
|
19
|
+
const DOWN = new Set(["failed", "degraded", "stopped"]);
|
|
20
|
+
/**
|
|
21
|
+
* The active revisions' verdict: unhealthy if none is active or any active one
|
|
22
|
+
* failed, degraded, stopped or reports `Unhealthy`; healthy if every active one
|
|
23
|
+
* is running and none is unhealthy; otherwise unknown (still provisioning).
|
|
24
|
+
*/
|
|
25
|
+
export function containerAppEntry(revisions, observedAt) {
|
|
26
|
+
const type = "Microsoft.App/containerApps";
|
|
27
|
+
const active = revisions.filter((r) => r.properties?.active === true);
|
|
28
|
+
if (active.length === 0) {
|
|
29
|
+
return { type, state: "unhealthy", detail: `no active revision (${revisions.length} revision(s))`, observedAt };
|
|
30
|
+
}
|
|
31
|
+
const describe = (r) => {
|
|
32
|
+
const p = r.properties ?? {};
|
|
33
|
+
const bits = [p.runningState ?? "no running state"];
|
|
34
|
+
if (p.healthState && p.healthState !== "None")
|
|
35
|
+
bits.push(p.healthState);
|
|
36
|
+
if (typeof p.replicas === "number")
|
|
37
|
+
bits.push(`${p.replicas} replica(s)`);
|
|
38
|
+
if (typeof p.trafficWeight === "number")
|
|
39
|
+
bits.push(`${p.trafficWeight}% traffic`);
|
|
40
|
+
return `${r.name ?? "revision"} ${bits.join(", ")}`;
|
|
41
|
+
};
|
|
42
|
+
const detail = active.map(describe).join("; ");
|
|
43
|
+
const running = (r) => RUNNING.has((r.properties?.runningState ?? "").toLowerCase());
|
|
44
|
+
const down = (r) => DOWN.has((r.properties?.runningState ?? "").toLowerCase()) || r.properties?.healthState?.toLowerCase() === "unhealthy";
|
|
45
|
+
const checkedAt = active
|
|
46
|
+
.map((r) => r.properties?.lastActiveTime ?? r.properties?.createdTime)
|
|
47
|
+
.filter((t) => typeof t === "string")
|
|
48
|
+
.sort()
|
|
49
|
+
.pop();
|
|
50
|
+
const state = active.some(down) ? "unhealthy" : active.every(running) ? "healthy" : "unknown";
|
|
51
|
+
return { type, state, detail, observedAt, ...(checkedAt ? { checkedAt } : {}) };
|
|
52
|
+
}
|
|
53
|
+
/** An app's own `runningStatus`, for when its revisions cannot be listed. */
|
|
54
|
+
export function appRunningStatusEntry(runningStatus, observedAt) {
|
|
55
|
+
const lower = runningStatus.toLowerCase();
|
|
56
|
+
const state = RUNNING.has(lower) ? "healthy" : DOWN.has(lower) ? "unhealthy" : "unknown";
|
|
57
|
+
return {
|
|
58
|
+
type: "Microsoft.App/containerApps",
|
|
59
|
+
state,
|
|
60
|
+
detail: `app runningStatus ${runningStatus} (revisions not readable)`,
|
|
61
|
+
observedAt,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function hole(err) {
|
|
65
|
+
const { reason, detail } = classifyArmFailure(err);
|
|
66
|
+
return { type: "Microsoft.App/containerApps", reason, detail };
|
|
67
|
+
}
|
|
68
|
+
async function listRevisions(client, name) {
|
|
69
|
+
const http = client.http;
|
|
70
|
+
const url = armResourceReadUrl(client, "Microsoft.App/containerApps", `${name}/revisions`, CONTAINER_APPS_API_VERSION);
|
|
71
|
+
const res = await http("GET", url, undefined, client.signal);
|
|
72
|
+
if (res.status >= 300)
|
|
73
|
+
throw new AzureReadError(`revisions read failed with HTTP ${res.status}`, res.status);
|
|
74
|
+
let body;
|
|
75
|
+
try {
|
|
76
|
+
body = JSON.parse(res.text);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
throw new AzureReadError("unparseable revisions list", res.status);
|
|
80
|
+
}
|
|
81
|
+
const value = body && typeof body === "object" ? body.value : undefined;
|
|
82
|
+
if (!Array.isArray(value))
|
|
83
|
+
throw new AzureReadError("unparseable revisions list", res.status);
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
const defaultHttp = async (method, url, body, signal) => {
|
|
87
|
+
const res = await fetch(url, {
|
|
88
|
+
method,
|
|
89
|
+
headers: body === undefined ? undefined : { "content-type": "application/json" },
|
|
90
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
91
|
+
signal,
|
|
92
|
+
});
|
|
93
|
+
return { status: res.status, text: await res.text() };
|
|
94
|
+
};
|
|
95
|
+
export async function observeHealthAzure(options) {
|
|
96
|
+
const observedAt = (options.now?.() ?? new Date()).toISOString();
|
|
97
|
+
const client = {
|
|
98
|
+
resourceGroup: options.environment,
|
|
99
|
+
...(process.env.AZURE_ENDPOINT_URL ? { endpoint: process.env.AZURE_ENDPOINT_URL } : {}),
|
|
100
|
+
...(process.env.AZURE_SUBSCRIPTION_ID ? { subscriptionId: process.env.AZURE_SUBSCRIPTION_ID } : {}),
|
|
101
|
+
http: options.http ?? defaultHttp,
|
|
102
|
+
};
|
|
103
|
+
const entities = {};
|
|
104
|
+
const unobserved = {};
|
|
105
|
+
const absent = [];
|
|
106
|
+
await Promise.all([...options.entities].map(async ([entityName, { entityType, props }]) => {
|
|
107
|
+
const name = props.name;
|
|
108
|
+
if (!name) {
|
|
109
|
+
unobserved[entityName] = { type: entityType, reason: "read-failed", detail: "declared entity has no name to query by" };
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
let app;
|
|
113
|
+
try {
|
|
114
|
+
app = await getResource(client, entityType, name, CONTAINER_APPS_API_VERSION);
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
if (isNotFound(err))
|
|
118
|
+
absent.push(entityName);
|
|
119
|
+
else
|
|
120
|
+
unobserved[entityName] = hole(err);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
entities[entityName] = containerAppEntry(await listRevisions(client, name), observedAt);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
const runningStatus = app.properties?.runningStatus;
|
|
128
|
+
if (typeof runningStatus === "string" && runningStatus.length > 0) {
|
|
129
|
+
entities[entityName] = appRunningStatusEntry(runningStatus, observedAt);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
unobserved[entityName] = hole(err);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}));
|
|
136
|
+
return { entities, unobserved, absent };
|
|
137
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARM template → TypeScript code generator.
|
|
3
|
+
*
|
|
4
|
+
* Converts intermediate representation to idiomatic chant TypeScript.
|
|
5
|
+
*/
|
|
6
|
+
import { loadLexiconRegistry } from "@intentius/chant/codegen/registry";
|
|
7
|
+
import { createRequire } from "module";
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
/**
|
|
11
|
+
* TypeScript code generator for ARM templates.
|
|
12
|
+
*/
|
|
13
|
+
export class ArmGenerator {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.typeToClass = new Map();
|
|
16
|
+
// Was a silent catch, which left this map empty and made the import emit
|
|
17
|
+
// `// Unknown resource type: Microsoft.…` — indistinguishable from a
|
|
18
|
+
// genuine coverage gap in the lexicon. A missing registry now says so
|
|
19
|
+
// (#1367).
|
|
20
|
+
const meta = loadLexiconRegistry(join(import.meta.dirname, "../.."), "azure");
|
|
21
|
+
for (const [className, entry] of Object.entries(meta)) {
|
|
22
|
+
if (entry.kind === "resource" && !className.includes("_")) {
|
|
23
|
+
this.typeToClass.set(entry.resourceType, className);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
generate(ir) {
|
|
28
|
+
const lines = [];
|
|
29
|
+
// Collect imports
|
|
30
|
+
const importedClasses = new Set();
|
|
31
|
+
const importedIntrinsics = new Set();
|
|
32
|
+
let needsAzure = false;
|
|
33
|
+
for (const resource of ir.resources) {
|
|
34
|
+
const cls = this.typeToClass.get(resource.type);
|
|
35
|
+
if (cls)
|
|
36
|
+
importedClasses.add(cls);
|
|
37
|
+
// Check for Azure pseudo-parameters in values
|
|
38
|
+
if (this.hasAzurePseudo(resource.properties)) {
|
|
39
|
+
needsAzure = true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (ir.parameters.length > 0) {
|
|
43
|
+
// Parameters would need a Parameter import from core
|
|
44
|
+
}
|
|
45
|
+
// Build import statement
|
|
46
|
+
const symbols = [...importedClasses].sort();
|
|
47
|
+
if (importedIntrinsics.size > 0) {
|
|
48
|
+
symbols.push(...[...importedIntrinsics].sort());
|
|
49
|
+
}
|
|
50
|
+
if (needsAzure) {
|
|
51
|
+
symbols.push("Azure");
|
|
52
|
+
}
|
|
53
|
+
if (symbols.length > 0) {
|
|
54
|
+
lines.push(`import { ${symbols.join(", ")} } from "@intentius/chant-lexicon-azure";`);
|
|
55
|
+
lines.push("");
|
|
56
|
+
}
|
|
57
|
+
// Generate resources
|
|
58
|
+
for (const resource of ir.resources) {
|
|
59
|
+
const cls = this.typeToClass.get(resource.type);
|
|
60
|
+
const varName = this.toVarName(resource.logicalId);
|
|
61
|
+
if (cls) {
|
|
62
|
+
lines.push(`export const ${varName} = new ${cls}(${this.generateProps(resource.properties, 1)});`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
lines.push(`// Unknown resource type: ${resource.type}`);
|
|
66
|
+
lines.push(`// export const ${varName} = new ${resource.type}(${this.generateProps(resource.properties, 1)});`);
|
|
67
|
+
}
|
|
68
|
+
lines.push("");
|
|
69
|
+
}
|
|
70
|
+
return [
|
|
71
|
+
{
|
|
72
|
+
path: "main.ts",
|
|
73
|
+
content: lines.join("\n") + "\n",
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
toVarName(name) {
|
|
78
|
+
// Convert to camelCase
|
|
79
|
+
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
80
|
+
}
|
|
81
|
+
generateProps(props, depth) {
|
|
82
|
+
if (!props || Object.keys(props).length === 0)
|
|
83
|
+
return "{}";
|
|
84
|
+
const indent = " ".repeat(depth);
|
|
85
|
+
const innerIndent = " ".repeat(depth + 1);
|
|
86
|
+
const entries = [];
|
|
87
|
+
for (const [key, value] of Object.entries(props)) {
|
|
88
|
+
entries.push(`${innerIndent}${key}: ${this.generateValue(value, depth + 1)},`);
|
|
89
|
+
}
|
|
90
|
+
return `{\n${entries.join("\n")}\n${indent}}`;
|
|
91
|
+
}
|
|
92
|
+
generateValue(value, depth) {
|
|
93
|
+
if (value === null || value === undefined)
|
|
94
|
+
return "undefined";
|
|
95
|
+
if (typeof value === "string")
|
|
96
|
+
return JSON.stringify(value);
|
|
97
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
98
|
+
return String(value);
|
|
99
|
+
if (Array.isArray(value)) {
|
|
100
|
+
if (value.length === 0)
|
|
101
|
+
return "[]";
|
|
102
|
+
const items = value.map((v) => this.generateValue(v, depth + 1));
|
|
103
|
+
return `[${items.join(", ")}]`;
|
|
104
|
+
}
|
|
105
|
+
if (typeof value === "object") {
|
|
106
|
+
const obj = value;
|
|
107
|
+
// Handle intrinsic markers
|
|
108
|
+
if ("__intrinsic" in obj) {
|
|
109
|
+
return this.generateIntrinsic(obj);
|
|
110
|
+
}
|
|
111
|
+
return this.generateProps(obj, depth);
|
|
112
|
+
}
|
|
113
|
+
return String(value);
|
|
114
|
+
}
|
|
115
|
+
generateIntrinsic(obj) {
|
|
116
|
+
const name = obj.__intrinsic;
|
|
117
|
+
switch (name) {
|
|
118
|
+
case "ResourceGroup":
|
|
119
|
+
return `Azure.ResourceGroup${obj.property?.charAt(0).toUpperCase()}${obj.property?.slice(1)}`;
|
|
120
|
+
case "Subscription":
|
|
121
|
+
return `Azure.SubscriptionId`;
|
|
122
|
+
case "Ref":
|
|
123
|
+
return obj.name;
|
|
124
|
+
case "ResourceId":
|
|
125
|
+
return `ResourceId(${obj.args.map((a) => JSON.stringify(a)).join(", ")})`;
|
|
126
|
+
case "Reference":
|
|
127
|
+
return `Reference(${obj.args.map((a) => JSON.stringify(a)).join(", ")})`;
|
|
128
|
+
case "Concat":
|
|
129
|
+
return `Concat(${obj.args.map((a) => JSON.stringify(a)).join(", ")})`;
|
|
130
|
+
case "UniqueString":
|
|
131
|
+
return `UniqueString(${obj.args.map((a) => JSON.stringify(a)).join(", ")})`;
|
|
132
|
+
default:
|
|
133
|
+
return JSON.stringify(obj);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
hasAzurePseudo(props) {
|
|
137
|
+
const json = JSON.stringify(props);
|
|
138
|
+
return json.includes("resourceGroup()") || json.includes("subscription()");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { hasOwnershipMarker } from "@intentius/chant/ownership";
|
|
2
|
+
import { AZURE_TAG_OWNERSHIP_KEYS } from "../ownership.js";
|
|
3
|
+
import { ArmParser } from "./parser.js";
|
|
4
|
+
/**
|
|
5
|
+
* Map a live ARM template (from `az group export`) to full-fidelity export IR.
|
|
6
|
+
*
|
|
7
|
+
* `az group export --output json` returns an ARM template — either as a JSON
|
|
8
|
+
* object or, via the CLI, a JSON string. `ArmParser` already turns the template
|
|
9
|
+
* into `TemplateIR` — the same IR the static-import path uses — so the result
|
|
10
|
+
* feeds `ArmGenerator` (templateGenerator) unchanged. Pure: all I/O stays in
|
|
11
|
+
* the caller (`../export-resources`).
|
|
12
|
+
*
|
|
13
|
+
* Mirrors AWS `parseStackTemplate`. The one shape difference: ARM tags are a
|
|
14
|
+
* `Record<string,string>` map (not the `{Key,Value}[]` array CloudFormation
|
|
15
|
+
* uses), so the ownership filter reads `properties.tags` directly without
|
|
16
|
+
* `tagArrayToMap`, against the `azure-tag` channel (`chant-managed-by`).
|
|
17
|
+
*/
|
|
18
|
+
export function parseExportedTemplate(templateBody, selector, owned) {
|
|
19
|
+
const content = typeof templateBody === "string" ? templateBody : JSON.stringify(templateBody);
|
|
20
|
+
const ir = new ArmParser().parse(content);
|
|
21
|
+
const hasSelector = selector && (selector.type !== undefined || selector.name !== undefined);
|
|
22
|
+
if (!hasSelector && !owned)
|
|
23
|
+
return ir;
|
|
24
|
+
return {
|
|
25
|
+
...ir,
|
|
26
|
+
resources: ir.resources.filter((r) => {
|
|
27
|
+
if (selector?.type !== undefined && r.type !== selector.type)
|
|
28
|
+
return false;
|
|
29
|
+
if (selector?.name !== undefined && r.logicalId !== selector.name)
|
|
30
|
+
return false;
|
|
31
|
+
if (owned) {
|
|
32
|
+
const tags = r.properties.tags;
|
|
33
|
+
if (!hasOwnershipMarker(tags, AZURE_TAG_OWNERSHIP_KEYS))
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
}
|