@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/taggable.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared taggable resource lookup — loads from the generated lexicon JSON.
|
|
3
|
+
*
|
|
4
|
+
* Lazy-loaded and cached for the lifetime of the process.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { moduleSourceDir } from "@intentius/chant/module-source";
|
|
9
|
+
let _cached;
|
|
10
|
+
const __dirname_ = moduleSourceDir(import.meta.url);
|
|
11
|
+
/**
|
|
12
|
+
* Load taggable resource types from the lexicon JSON.
|
|
13
|
+
* Result is cached after first call.
|
|
14
|
+
*/
|
|
15
|
+
export function loadTaggableResources() {
|
|
16
|
+
if (_cached)
|
|
17
|
+
return _cached;
|
|
18
|
+
const set = new Set();
|
|
19
|
+
try {
|
|
20
|
+
const lexiconPath = join(__dirname_, "generated", "lexicon-azure.json");
|
|
21
|
+
const content = readFileSync(lexiconPath, "utf-8");
|
|
22
|
+
const data = JSON.parse(content);
|
|
23
|
+
for (const [_name, entry] of Object.entries(data)) {
|
|
24
|
+
if (entry.kind === "resource" && entry.resourceType && entry.tagging?.taggable) {
|
|
25
|
+
set.add(entry.resourceType);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Lexicon not available — skip
|
|
31
|
+
}
|
|
32
|
+
_cached = set;
|
|
33
|
+
return set;
|
|
34
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -4232,7 +4232,12 @@ export declare class containerApps {
|
|
|
4232
4232
|
/** Resource tags. */
|
|
4233
4233
|
tags?: Record<string, unknown>;
|
|
4234
4234
|
}, attributes?: ArmResourceAttributes);
|
|
4235
|
+
readonly configuration_ingress_fqdn: string;
|
|
4235
4236
|
readonly id: string;
|
|
4237
|
+
readonly latestRevisionFqdn: string;
|
|
4238
|
+
readonly latestRevisionName: string;
|
|
4239
|
+
readonly outboundIpAddresses: string[];
|
|
4240
|
+
readonly provisioningState: string;
|
|
4236
4241
|
}
|
|
4237
4242
|
|
|
4238
4243
|
export declare class containerApps_authConfigs {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Thin entry point for `npm run validate` in lexicon-azure.
|
|
4
|
+
*/
|
|
5
|
+
import { validate } from "./validate.js";
|
|
6
|
+
const result = await validate();
|
|
7
|
+
for (const check of result.checks) {
|
|
8
|
+
const status = check.ok ? "OK" : "FAIL";
|
|
9
|
+
const msg = check.error ? ` — ${check.error}` : "";
|
|
10
|
+
console.error(` [${status}] ${check.name}${msg}`);
|
|
11
|
+
}
|
|
12
|
+
if (!result.success) {
|
|
13
|
+
console.error("Validation failed");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
console.error("All validation checks passed.");
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate generated lexicon-azure artifacts.
|
|
3
|
+
*/
|
|
4
|
+
import { dirname } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { validateLexiconArtifacts } from "@intentius/chant/codegen/validate";
|
|
7
|
+
import { CURATED_PROPERTY_TYPE_DEFS } from "./spec/parse.js";
|
|
8
|
+
const REQUIRED_NAMES = [
|
|
9
|
+
// Core compute resources
|
|
10
|
+
"VirtualMachine", "VirtualMachineScaleSet", "AvailabilitySet",
|
|
11
|
+
// Networking
|
|
12
|
+
"VirtualNetwork", "Subnet", "NetworkInterface",
|
|
13
|
+
"NetworkSecurityGroup", "PublicIPAddress", "LoadBalancer",
|
|
14
|
+
// Storage
|
|
15
|
+
"StorageAccount", "BlobContainer",
|
|
16
|
+
// Web & App
|
|
17
|
+
"WebApp", "AppServicePlan",
|
|
18
|
+
// Containers
|
|
19
|
+
"ManagedCluster", "ContainerRegistry",
|
|
20
|
+
// Databases
|
|
21
|
+
"SqlServer", "SqlDatabase",
|
|
22
|
+
// Security & Identity
|
|
23
|
+
"KeyVault", "ManagedIdentity",
|
|
24
|
+
// Governance — org hierarchy + policy primitives (#1545). once claimed
|
|
25
|
+
// these existed when they did not; this gate keeps them from vanishing again.
|
|
26
|
+
"ManagementGroup", "SubscriptionAlias", "PolicyDefinition", "PolicyAssignment",
|
|
27
|
+
// DNS & Traffic
|
|
28
|
+
"DnsZone", "TrafficManagerProfile",
|
|
29
|
+
// Curated property types kept typed by the codegen (#438) — single source of truth.
|
|
30
|
+
...CURATED_PROPERTY_TYPE_DEFS,
|
|
31
|
+
];
|
|
32
|
+
export async function validate(opts) {
|
|
33
|
+
const basePath = opts?.basePath ?? dirname(dirname(fileURLToPath(import.meta.url)));
|
|
34
|
+
return validateLexiconArtifacts({
|
|
35
|
+
lexiconJsonFilename: "lexicon-azure.json",
|
|
36
|
+
requiredNames: REQUIRED_NAMES,
|
|
37
|
+
// Curated property types are emitted under resource-prefixed/variant names
|
|
38
|
+
// after bounding (#438), so match required names by substring.
|
|
39
|
+
requiredNamesMatchSubstring: true,
|
|
40
|
+
basePath,
|
|
41
|
+
// chant #1475 — azure generates from a commit sha of
|
|
42
|
+
// azure-resource-manager-schemas (#1144), so a fresh generate is
|
|
43
|
+
// deterministic. Gate the surface on every validate: the #1144 pin left
|
|
44
|
+
// the snapshot 483 entries behind until #1572 re-baselined it, because
|
|
45
|
+
// nothing compared the two on a PR.
|
|
46
|
+
checkSurfaceSnapshot: "always",
|
|
47
|
+
coverageThresholds: {
|
|
48
|
+
minPropertyPct: 1,
|
|
49
|
+
minLifecyclePct: 1,
|
|
50
|
+
minAttrPct: 1,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-azure",
|
|
3
|
-
"version": "0.124.
|
|
3
|
+
"version": "0.124.33",
|
|
4
4
|
"description": "Azure lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -32,27 +32,27 @@
|
|
|
32
32
|
".": {
|
|
33
33
|
"development": "./src/index.ts",
|
|
34
34
|
"types": "./dist/index.d.ts",
|
|
35
|
-
"default": "./
|
|
35
|
+
"default": "./dist/index.js"
|
|
36
36
|
},
|
|
37
37
|
"./*": {
|
|
38
38
|
"development": "./src/*.ts",
|
|
39
39
|
"types": "./dist/*.d.ts",
|
|
40
|
-
"default": "./
|
|
40
|
+
"default": "./dist/*.js"
|
|
41
41
|
},
|
|
42
42
|
"./detect": {
|
|
43
43
|
"development": "./src/detect.ts",
|
|
44
44
|
"types": "./dist/detect.d.ts",
|
|
45
|
-
"default": "./
|
|
45
|
+
"default": "./dist/detect.js"
|
|
46
46
|
},
|
|
47
47
|
"./op/activities": {
|
|
48
48
|
"development": "./src/op/activities/index.ts",
|
|
49
49
|
"types": "./dist/op/activities/index.d.ts",
|
|
50
|
-
"default": "./
|
|
50
|
+
"default": "./dist/op/activities/index.js"
|
|
51
51
|
},
|
|
52
52
|
"./lint/post-synth": {
|
|
53
53
|
"development": "./src/lint/post-synth/index.ts",
|
|
54
54
|
"types": "./dist/lint/post-synth/index.d.ts",
|
|
55
|
-
"default": "./
|
|
55
|
+
"default": "./dist/lint/post-synth/index.js"
|
|
56
56
|
},
|
|
57
57
|
"./manifest": "./dist/manifest.json",
|
|
58
58
|
"./meta": "./dist/meta.json",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"validate": "tsx src/validate-cli.ts",
|
|
65
65
|
"docs": "tsx src/codegen/docs-cli.ts",
|
|
66
66
|
"prepack": "npm run generate && npm run bundle && npm run validate && npm run build",
|
|
67
|
-
"build": "../../scripts/tsc7.sh -p tsconfig.build.json && tsc-alias -p tsconfig.build.json
|
|
67
|
+
"build": "../../scripts/tsc7.sh -p tsconfig.build.json --sourceMap false && tsc-alias -p tsconfig.build.json"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
70
|
"fflate": "^0.8.2"
|
|
@@ -74,6 +74,6 @@
|
|
|
74
74
|
"typescript": "^5.9.3"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"@intentius/chant": "^0.124.
|
|
77
|
+
"@intentius/chant": "^0.124.33"
|
|
78
78
|
}
|
|
79
79
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Container App's hostname is referenceable (#4179), so DNS in another
|
|
3
|
+
* lexicon can point at it: `configuration.ingress.fqdn` is read through
|
|
4
|
+
* `configuration_ingress_fqdn`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "vitest";
|
|
8
|
+
import { AttrRef } from "@intentius/chant";
|
|
9
|
+
import { containerApps } from "./index";
|
|
10
|
+
|
|
11
|
+
describe("Container App attributes", () => {
|
|
12
|
+
test("configuration.ingress.fqdn and latestRevisionFqdn are AttrRefs", () => {
|
|
13
|
+
const app = new containerApps({ name: "web", location: "eastus" } as never) as unknown as Record<string, unknown>;
|
|
14
|
+
expect((app.configuration_ingress_fqdn as AttrRef).attribute).toBe("configuration.ingress.fqdn");
|
|
15
|
+
expect(app.latestRevisionFqdn).toBeInstanceOf(AttrRef);
|
|
16
|
+
});
|
|
17
|
+
});
|
package/src/codegen/generate.ts
CHANGED
|
@@ -95,7 +95,7 @@ function generateRuntimeIndex(
|
|
|
95
95
|
// Build attrs map
|
|
96
96
|
const attrs: Record<string, string> = {};
|
|
97
97
|
for (const a of r.resource.attributes) {
|
|
98
|
-
attrs[a.name] = a.name;
|
|
98
|
+
attrs[a.name.replace(/\./g, "_")] = a.name;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
resourceEntries.push({ tsName, resourceType: armType, attrs });
|
package/src/codegen/package.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { readFileSync } from "fs";
|
|
7
7
|
import { azurePlugin } from "../plugin";
|
|
8
8
|
import { dirname } from "path";
|
|
9
|
-
import {
|
|
9
|
+
import { moduleSourceDir } from "@intentius/chant/module-source";
|
|
10
10
|
import type { IntrinsicDef } from "@intentius/chant/lexicon";
|
|
11
11
|
import {
|
|
12
12
|
packagePipeline,
|
|
@@ -18,7 +18,7 @@ import { generate } from "./generate";
|
|
|
18
18
|
|
|
19
19
|
export type { PackageOptions, PackageResult };
|
|
20
20
|
|
|
21
|
-
const pkgDir = dirname(
|
|
21
|
+
const pkgDir = dirname(moduleSourceDir(import.meta.url));
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Map intrinsic name to its ARM output key.
|
package/src/deploy-scopes.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { readFileSync } from "fs";
|
|
15
15
|
import { join, dirname } from "path";
|
|
16
16
|
import { fileURLToPath } from "url";
|
|
17
|
+
import { moduleSourceDir } from "@intentius/chant/module-source";
|
|
17
18
|
import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable";
|
|
18
19
|
|
|
19
20
|
export type DeployScope = "resourceGroup" | "subscription" | "managementGroup" | "tenant";
|
|
@@ -94,7 +95,7 @@ let _cached: Map<string, DeployScope[]> | undefined;
|
|
|
94
95
|
*/
|
|
95
96
|
function moduleDir(): string | undefined {
|
|
96
97
|
try {
|
|
97
|
-
return
|
|
98
|
+
return moduleSourceDir(import.meta.url);
|
|
98
99
|
} catch {
|
|
99
100
|
return undefined;
|
|
100
101
|
}
|
package/src/generated/index.d.ts
CHANGED
|
@@ -4232,7 +4232,12 @@ export declare class containerApps {
|
|
|
4232
4232
|
/** Resource tags. */
|
|
4233
4233
|
tags?: Record<string, unknown>;
|
|
4234
4234
|
}, attributes?: ArmResourceAttributes);
|
|
4235
|
+
readonly configuration_ingress_fqdn: string;
|
|
4235
4236
|
readonly id: string;
|
|
4237
|
+
readonly latestRevisionFqdn: string;
|
|
4238
|
+
readonly latestRevisionName: string;
|
|
4239
|
+
readonly outboundIpAddresses: string[];
|
|
4240
|
+
readonly provisioningState: string;
|
|
4236
4241
|
}
|
|
4237
4242
|
|
|
4238
4243
|
export declare class containerApps_authConfigs {
|
package/src/generated/index.ts
CHANGED
|
@@ -314,7 +314,7 @@ export const connections_flows = /* @__PURE__ */ createResource("Microsoft.Azure
|
|
|
314
314
|
export const connectors = /* @__PURE__ */ createResource("Microsoft.Workloads/connectors", "azure", {"id":"id"});
|
|
315
315
|
export const connectors_acssBackups = /* @__PURE__ */ createResource("Microsoft.Workloads/connectors_acssBackups", "azure", {"id":"id"});
|
|
316
316
|
export const Consumptionbudgets = /* @__PURE__ */ createResource("Microsoft.Consumption/budgets", "azure", {"id":"id"});
|
|
317
|
-
export const containerApps = /* @__PURE__ */ createResource("Microsoft.App/containerApps", "azure", {"id":"id"});
|
|
317
|
+
export const containerApps = /* @__PURE__ */ createResource("Microsoft.App/containerApps", "azure", {"id":"id","configuration_ingress_fqdn":"configuration.ingress.fqdn","latestRevisionFqdn":"latestRevisionFqdn","latestRevisionName":"latestRevisionName","outboundIpAddresses":"outboundIpAddresses","provisioningState":"provisioningState"});
|
|
318
318
|
export const containerApps_authConfigs = /* @__PURE__ */ createResource("Microsoft.App/containerApps_authConfigs", "azure", {"id":"id"});
|
|
319
319
|
export const containerApps_resiliencyPolicies = /* @__PURE__ */ createResource("Microsoft.App/containerApps_resiliencyPolicies", "azure", {"id":"id"});
|
|
320
320
|
export const containerApps_sourcecontrols = /* @__PURE__ */ createResource("Microsoft.App/containerApps_sourcecontrols", "azure", {"id":"id"});
|
|
@@ -33235,7 +33235,12 @@
|
|
|
33235
33235
|
"lexicon": "azure",
|
|
33236
33236
|
"apiVersion": "2025-02-02-preview",
|
|
33237
33237
|
"attrs": {
|
|
33238
|
-
"id": "id"
|
|
33238
|
+
"id": "id",
|
|
33239
|
+
"configuration_ingress_fqdn": "configuration.ingress.fqdn",
|
|
33240
|
+
"latestRevisionFqdn": "latestRevisionFqdn",
|
|
33241
|
+
"latestRevisionName": "latestRevisionName",
|
|
33242
|
+
"outboundIpAddresses": "outboundIpAddresses",
|
|
33243
|
+
"provisioningState": "provisioningState"
|
|
33239
33244
|
},
|
|
33240
33245
|
"propertyConstraints": {
|
|
33241
33246
|
"kind": {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
2
|
+
import { appRunningStatusEntry, containerAppEntry, observeHealthAzure } from "./health";
|
|
3
|
+
import type { AzHttp } from "./op/activities/az-apply";
|
|
4
|
+
|
|
5
|
+
const AT = "2026-10-20T21:00:00.000Z";
|
|
6
|
+
const now = () => new Date(AT);
|
|
7
|
+
const entities = new Map([["web", { entityType: "Microsoft.App/containerApps", props: { name: "web-app" } }]]);
|
|
8
|
+
|
|
9
|
+
describe("azure health (#4177)", () => {
|
|
10
|
+
let saved: string | undefined;
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
saved = process.env.AZURE_ENDPOINT_URL;
|
|
13
|
+
process.env.AZURE_ENDPOINT_URL = "http://localhost:4577";
|
|
14
|
+
});
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (saved === undefined) delete process.env.AZURE_ENDPOINT_URL;
|
|
17
|
+
else process.env.AZURE_ENDPOINT_URL = saved;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("judges the active revisions", () => {
|
|
21
|
+
const rev = (runningState: string, extra: Record<string, unknown> = {}) => ({
|
|
22
|
+
name: "web-app--r1",
|
|
23
|
+
properties: { active: true, runningState, replicas: 2, trafficWeight: 100, ...extra },
|
|
24
|
+
});
|
|
25
|
+
expect(containerAppEntry([rev("Running")], AT)).toMatchObject({ state: "healthy", detail: "web-app--r1 Running, 2 replica(s), 100% traffic" });
|
|
26
|
+
expect(containerAppEntry([rev("Running", { healthState: "Unhealthy" })], AT).state).toBe("unhealthy");
|
|
27
|
+
expect(containerAppEntry([rev("Failed")], AT).state).toBe("unhealthy");
|
|
28
|
+
expect(containerAppEntry([rev("Processing")], AT).state).toBe("unknown");
|
|
29
|
+
expect(containerAppEntry([{ name: "old", properties: { active: false, runningState: "Running" } }], AT)).toMatchObject({
|
|
30
|
+
state: "unhealthy",
|
|
31
|
+
detail: "no active revision (1 revision(s))",
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("the app's runningStatus answers when revisions cannot be listed", () => {
|
|
36
|
+
expect(appRunningStatusEntry("Running", AT).state).toBe("healthy");
|
|
37
|
+
expect(appRunningStatusEntry("Stopped", AT).state).toBe("unhealthy");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("reads the app, then its revisions, at the endpoint override", async () => {
|
|
41
|
+
const urls: string[] = [];
|
|
42
|
+
const http: AzHttp = async (_method, url) => {
|
|
43
|
+
urls.push(url);
|
|
44
|
+
if (url.includes("/revisions")) {
|
|
45
|
+
return { status: 200, text: JSON.stringify({ value: [{ name: "web-app--r1", properties: { active: true, runningState: "Running", replicas: 1 } }] }) };
|
|
46
|
+
}
|
|
47
|
+
return { status: 200, text: JSON.stringify({ id: "/x", properties: {} }) };
|
|
48
|
+
};
|
|
49
|
+
const r = await observeHealthAzure({ environment: "rg", buildOutput: "", entities, http, now });
|
|
50
|
+
expect(r.entities.web).toMatchObject({ state: "healthy", observedAt: AT });
|
|
51
|
+
expect(urls[1]).toBe(
|
|
52
|
+
"http://localhost:4577/subscriptions/00000000-0000-0000-0000-000000000001/resourceGroups/rg/providers/Microsoft.App/containerApps/web-app/revisions?api-version=2024-03-01",
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("a 404 app is absent; an unreadable revisions list with no runningStatus is a hole", async () => {
|
|
57
|
+
const notFound: AzHttp = async () => ({ status: 404, text: JSON.stringify({ error: { code: "ResourceNotFound", message: "gone" } }) });
|
|
58
|
+
expect((await observeHealthAzure({ environment: "rg", buildOutput: "", entities, http: notFound })).absent).toEqual(["web"]);
|
|
59
|
+
|
|
60
|
+
const noRevisions: AzHttp = async (_m, url) =>
|
|
61
|
+
url.includes("/revisions") ? { status: 404, text: "" } : { status: 200, text: JSON.stringify({ id: "/x", properties: {} }) };
|
|
62
|
+
const r = await observeHealthAzure({ environment: "rg", buildOutput: "", entities, http: noRevisions });
|
|
63
|
+
expect(r.entities).toEqual({});
|
|
64
|
+
expect(r.unobserved.web?.reason).toBe("read-failed");
|
|
65
|
+
});
|
|
66
|
+
});
|
package/src/health.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
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
|
+
|
|
14
|
+
import type { HealthEntry, HealthReadOptions, HealthReadResult, UnobservedEntity } from "@intentius/chant/lexicon";
|
|
15
|
+
import { armResourceReadUrl, AzureReadError, getResource, isNotFound, type AzureReadClientOptions } from "./api/read-client";
|
|
16
|
+
import { classifyArmFailure } from "./describe-resources";
|
|
17
|
+
import type { AzHttp } from "./op/activities/az-apply";
|
|
18
|
+
|
|
19
|
+
export const AZURE_HEALTH_TYPES = ["Microsoft.App/containerApps"] as const;
|
|
20
|
+
|
|
21
|
+
/** The Container Apps API version whose revisions carry `runningState` and `healthState`. */
|
|
22
|
+
const CONTAINER_APPS_API_VERSION = "2024-03-01";
|
|
23
|
+
|
|
24
|
+
export interface AzureHealthOptions extends HealthReadOptions {
|
|
25
|
+
http?: AzHttp;
|
|
26
|
+
now?: () => Date;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The fields of one revision that decide health. */
|
|
30
|
+
export interface RevisionState {
|
|
31
|
+
name?: string;
|
|
32
|
+
properties?: {
|
|
33
|
+
active?: boolean;
|
|
34
|
+
runningState?: string;
|
|
35
|
+
healthState?: string;
|
|
36
|
+
replicas?: number;
|
|
37
|
+
trafficWeight?: number;
|
|
38
|
+
lastActiveTime?: string;
|
|
39
|
+
createdTime?: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const RUNNING = new Set(["running", "runningatmaxscale"]);
|
|
44
|
+
const DOWN = new Set(["failed", "degraded", "stopped"]);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The active revisions' verdict: unhealthy if none is active or any active one
|
|
48
|
+
* failed, degraded, stopped or reports `Unhealthy`; healthy if every active one
|
|
49
|
+
* is running and none is unhealthy; otherwise unknown (still provisioning).
|
|
50
|
+
*/
|
|
51
|
+
export function containerAppEntry(revisions: RevisionState[], observedAt: string): HealthEntry {
|
|
52
|
+
const type = "Microsoft.App/containerApps";
|
|
53
|
+
const active = revisions.filter((r) => r.properties?.active === true);
|
|
54
|
+
if (active.length === 0) {
|
|
55
|
+
return { type, state: "unhealthy", detail: `no active revision (${revisions.length} revision(s))`, observedAt };
|
|
56
|
+
}
|
|
57
|
+
const describe = (r: RevisionState): string => {
|
|
58
|
+
const p = r.properties ?? {};
|
|
59
|
+
const bits = [p.runningState ?? "no running state"];
|
|
60
|
+
if (p.healthState && p.healthState !== "None") bits.push(p.healthState);
|
|
61
|
+
if (typeof p.replicas === "number") bits.push(`${p.replicas} replica(s)`);
|
|
62
|
+
if (typeof p.trafficWeight === "number") bits.push(`${p.trafficWeight}% traffic`);
|
|
63
|
+
return `${r.name ?? "revision"} ${bits.join(", ")}`;
|
|
64
|
+
};
|
|
65
|
+
const detail = active.map(describe).join("; ");
|
|
66
|
+
const running = (r: RevisionState) => RUNNING.has((r.properties?.runningState ?? "").toLowerCase());
|
|
67
|
+
const down = (r: RevisionState) =>
|
|
68
|
+
DOWN.has((r.properties?.runningState ?? "").toLowerCase()) || r.properties?.healthState?.toLowerCase() === "unhealthy";
|
|
69
|
+
const checkedAt = active
|
|
70
|
+
.map((r) => r.properties?.lastActiveTime ?? r.properties?.createdTime)
|
|
71
|
+
.filter((t): t is string => typeof t === "string")
|
|
72
|
+
.sort()
|
|
73
|
+
.pop();
|
|
74
|
+
const state = active.some(down) ? "unhealthy" : active.every(running) ? "healthy" : "unknown";
|
|
75
|
+
return { type, state, detail, observedAt, ...(checkedAt ? { checkedAt } : {}) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** An app's own `runningStatus`, for when its revisions cannot be listed. */
|
|
79
|
+
export function appRunningStatusEntry(runningStatus: string, observedAt: string): HealthEntry {
|
|
80
|
+
const lower = runningStatus.toLowerCase();
|
|
81
|
+
const state = RUNNING.has(lower) ? "healthy" : DOWN.has(lower) ? "unhealthy" : "unknown";
|
|
82
|
+
return {
|
|
83
|
+
type: "Microsoft.App/containerApps",
|
|
84
|
+
state,
|
|
85
|
+
detail: `app runningStatus ${runningStatus} (revisions not readable)`,
|
|
86
|
+
observedAt,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function hole(err: unknown): UnobservedEntity {
|
|
91
|
+
const { reason, detail } = classifyArmFailure(err);
|
|
92
|
+
return { type: "Microsoft.App/containerApps", reason, detail };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function listRevisions(client: AzureReadClientOptions, name: string): Promise<RevisionState[]> {
|
|
96
|
+
const http = client.http!;
|
|
97
|
+
const url = armResourceReadUrl(client, "Microsoft.App/containerApps", `${name}/revisions`, CONTAINER_APPS_API_VERSION);
|
|
98
|
+
const res = await http("GET", url, undefined, client.signal);
|
|
99
|
+
if (res.status >= 300) throw new AzureReadError(`revisions read failed with HTTP ${res.status}`, res.status);
|
|
100
|
+
let body: unknown;
|
|
101
|
+
try {
|
|
102
|
+
body = JSON.parse(res.text);
|
|
103
|
+
} catch {
|
|
104
|
+
throw new AzureReadError("unparseable revisions list", res.status);
|
|
105
|
+
}
|
|
106
|
+
const value = body && typeof body === "object" ? (body as { value?: unknown }).value : undefined;
|
|
107
|
+
if (!Array.isArray(value)) throw new AzureReadError("unparseable revisions list", res.status);
|
|
108
|
+
return value as RevisionState[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const defaultHttp: AzHttp = async (method, url, body, signal) => {
|
|
112
|
+
const res = await fetch(url, {
|
|
113
|
+
method,
|
|
114
|
+
headers: body === undefined ? undefined : { "content-type": "application/json" },
|
|
115
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
116
|
+
signal,
|
|
117
|
+
});
|
|
118
|
+
return { status: res.status, text: await res.text() };
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export async function observeHealthAzure(options: AzureHealthOptions): Promise<HealthReadResult> {
|
|
122
|
+
const observedAt = (options.now?.() ?? new Date()).toISOString();
|
|
123
|
+
const client: AzureReadClientOptions = {
|
|
124
|
+
resourceGroup: options.environment,
|
|
125
|
+
...(process.env.AZURE_ENDPOINT_URL ? { endpoint: process.env.AZURE_ENDPOINT_URL } : {}),
|
|
126
|
+
...(process.env.AZURE_SUBSCRIPTION_ID ? { subscriptionId: process.env.AZURE_SUBSCRIPTION_ID } : {}),
|
|
127
|
+
http: options.http ?? defaultHttp,
|
|
128
|
+
};
|
|
129
|
+
const entities: Record<string, HealthEntry> = {};
|
|
130
|
+
const unobserved: Record<string, UnobservedEntity> = {};
|
|
131
|
+
const absent: string[] = [];
|
|
132
|
+
|
|
133
|
+
await Promise.all(
|
|
134
|
+
[...options.entities].map(async ([entityName, { entityType, props }]) => {
|
|
135
|
+
const name = props.name as string | undefined;
|
|
136
|
+
if (!name) {
|
|
137
|
+
unobserved[entityName] = { type: entityType, reason: "read-failed", detail: "declared entity has no name to query by" };
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
let app: Awaited<ReturnType<typeof getResource>>;
|
|
141
|
+
try {
|
|
142
|
+
app = await getResource(client, entityType, name, CONTAINER_APPS_API_VERSION);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
if (isNotFound(err)) absent.push(entityName);
|
|
145
|
+
else unobserved[entityName] = hole(err);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
entities[entityName] = containerAppEntry(await listRevisions(client, name), observedAt);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
const runningStatus = app.properties?.runningStatus;
|
|
152
|
+
if (typeof runningStatus === "string" && runningStatus.length > 0) {
|
|
153
|
+
entities[entityName] = appRunningStatusEntry(runningStatus, observedAt);
|
|
154
|
+
} else {
|
|
155
|
+
unobserved[entityName] = hole(err);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return { entities, unobserved, absent };
|
|
162
|
+
}
|
|
@@ -76,17 +76,19 @@ export function isBracketExpression(value: unknown): boolean {
|
|
|
76
76
|
*
|
|
77
77
|
* Handles:
|
|
78
78
|
* - [resourceId('Type', 'name')] -> extracts 'name'
|
|
79
|
+
* - [resourceId('Type', 'a', 'b')] -> extracts 'a/b'
|
|
79
80
|
* - [reference('name')] -> extracts 'name'
|
|
80
81
|
* - [reference('name').prop] -> extracts 'name'
|
|
81
82
|
*/
|
|
82
83
|
export function extractRefsFromExpression(expr: string): string[] {
|
|
83
84
|
const refs: string[] = [];
|
|
84
85
|
|
|
85
|
-
// Match resourceId('
|
|
86
|
-
|
|
86
|
+
// Match resourceId('Type', 'name', ...): the name is every quoted argument
|
|
87
|
+
// after the type, joined with "/" for a child resource ('vnet', 'subnet').
|
|
88
|
+
const resourceIdPattern = /resourceId\s*\(\s*'[^']+'((?:\s*,\s*'[^']+')+)\s*\)/g;
|
|
87
89
|
let match: RegExpExecArray | null;
|
|
88
90
|
while ((match = resourceIdPattern.exec(expr)) !== null) {
|
|
89
|
-
refs.push(match[1]);
|
|
91
|
+
refs.push([...match[1].matchAll(/'([^']+)'/g)].map((m) => m[1]).join("/"));
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
// Match reference('name') — extract the name
|
|
@@ -33,7 +33,7 @@ describe("findArmResourceRefs", () => {
|
|
|
33
33
|
const refs = findArmResourceRefs({
|
|
34
34
|
subnet: "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'myVnet', 'mySubnet')]",
|
|
35
35
|
});
|
|
36
|
-
expect(refs.has("mySubnet")).toBe(true);
|
|
36
|
+
expect(refs.has("myVnet/mySubnet")).toBe(true);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
test("extracts reference targets", () => {
|
package/src/lsp/completions.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createRequire } from "module";
|
|
2
|
+
import { moduleSourceUrl } from "@intentius/chant/module-source";
|
|
2
3
|
import type { CompletionContext, CompletionItem } from "@intentius/chant/lsp/types";
|
|
3
4
|
import { LexiconIndex, lexiconCompletions, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
|
|
4
|
-
const require = createRequire(import.meta.url);
|
|
5
|
+
const require = createRequire(moduleSourceUrl(import.meta.url));
|
|
5
6
|
|
|
6
7
|
let cachedIndex: LexiconIndex | null = null;
|
|
7
8
|
|
package/src/lsp/hover.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createRequire } from "module";
|
|
2
|
+
import { moduleSourceUrl } from "@intentius/chant/module-source";
|
|
2
3
|
import type { HoverContext, HoverInfo } from "@intentius/chant/lsp/types";
|
|
3
4
|
import { LexiconIndex, lexiconHover, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
|
|
4
|
-
const require = createRequire(import.meta.url);
|
|
5
|
+
const require = createRequire(moduleSourceUrl(import.meta.url));
|
|
5
6
|
|
|
6
7
|
let cachedIndex: LexiconIndex | null = null;
|
|
7
8
|
|
package/src/plugin.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { createSkillsLoader } from "@intentius/chant/lexicon-plugin-helpers";
|
|
|
13
13
|
import { readFileSync, readdirSync, existsSync } from "fs";
|
|
14
14
|
import { join, dirname } from "path";
|
|
15
15
|
import { fileURLToPath } from "url";
|
|
16
|
+
import { moduleSourceDir } from "@intentius/chant/module-source";
|
|
16
17
|
import { azureSerializer } from "./serializer";
|
|
17
18
|
import { hardcodedLocationRule } from "./lint/rules/hardcoded-location";
|
|
18
19
|
import { storageHttpsRule } from "./lint/rules/storage-https";
|
|
@@ -373,7 +374,7 @@ export const tags = defaultTags([
|
|
|
373
374
|
required: ["query"],
|
|
374
375
|
},
|
|
375
376
|
handler: async (args: Record<string, unknown>) => {
|
|
376
|
-
const dir =
|
|
377
|
+
const dir = moduleSourceDir(import.meta.url);
|
|
377
378
|
try {
|
|
378
379
|
const lexicon = JSON.parse(
|
|
379
380
|
readFileSync(join(dir, "generated", "lexicon-azure.json"), "utf-8"),
|
|
@@ -398,7 +399,7 @@ export const tags = defaultTags([
|
|
|
398
399
|
description: "All available Azure resource types",
|
|
399
400
|
mimeType: "application/json",
|
|
400
401
|
handler: async () => {
|
|
401
|
-
const dir =
|
|
402
|
+
const dir = moduleSourceDir(import.meta.url);
|
|
402
403
|
try {
|
|
403
404
|
return readFileSync(join(dir, "generated", "lexicon-azure.json"), "utf-8");
|
|
404
405
|
} catch {
|
|
@@ -485,6 +486,14 @@ export const tags = defaultTags([
|
|
|
485
486
|
/** The noise rules the deep pass applies to both the live and declared trees. */
|
|
486
487
|
deepNormalizationHooks: azureDeepNormalizationHooks,
|
|
487
488
|
|
|
489
|
+
/** Runtime health (#4177): Container Apps. Implementation in ./health.ts. */
|
|
490
|
+
healthTypes: ["Microsoft.App/containerApps"],
|
|
491
|
+
|
|
492
|
+
async observeHealth(options) {
|
|
493
|
+
const { observeHealthAzure } = await import("./health");
|
|
494
|
+
return observeHealthAzure(options);
|
|
495
|
+
},
|
|
496
|
+
|
|
488
497
|
async exportResources(options) {
|
|
489
498
|
const { exportResources } = await import("./export-resources");
|
|
490
499
|
return exportResources(options);
|