@intentius/chant-lexicon-azure 0.124.38 → 0.124.40

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.
Files changed (44) hide show
  1. package/dist/components/arm-deploy.d.ts +80 -0
  2. package/dist/components/arm-deploy.d.ts.map +1 -0
  3. package/dist/components/arm-deploy.js +398 -0
  4. package/dist/components/capability-plugin.d.ts +6 -0
  5. package/dist/components/capability-plugin.d.ts.map +1 -0
  6. package/dist/components/capability-plugin.js +17 -0
  7. package/dist/components/index.d.ts +3 -0
  8. package/dist/components/index.d.ts.map +1 -0
  9. package/dist/components/index.js +2 -0
  10. package/dist/composites/catalog.d.ts.map +1 -1
  11. package/dist/composites/catalog.js +120 -0
  12. package/dist/composites/container-app.d.ts +102 -0
  13. package/dist/composites/container-app.d.ts.map +1 -0
  14. package/dist/composites/container-app.js +130 -0
  15. package/dist/composites/index.d.ts +2 -0
  16. package/dist/composites/index.d.ts.map +1 -1
  17. package/dist/composites/index.js +1 -0
  18. package/dist/deep-observe.d.ts +30 -0
  19. package/dist/deep-observe.d.ts.map +1 -1
  20. package/dist/deep-observe.js +1 -1
  21. package/dist/index.d.ts +2 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +4 -1
  24. package/dist/integrity.json +5 -5
  25. package/dist/manifest.json +1 -1
  26. package/dist/skills/chant-azure-patterns.md +16 -0
  27. package/dist/skills/chant-azure-security.md +1 -0
  28. package/dist/skills/chant-azure.md +2 -0
  29. package/package.json +7 -2
  30. package/src/components/arm-deploy-floci.e2e.test.ts +80 -0
  31. package/src/components/arm-deploy.test.ts +256 -0
  32. package/src/components/arm-deploy.ts +491 -0
  33. package/src/components/capability-plugin.ts +26 -0
  34. package/src/components/index.ts +9 -0
  35. package/src/composites/catalog.ts +120 -0
  36. package/src/composites/composites.test.ts +128 -0
  37. package/src/composites/container-app.ts +230 -0
  38. package/src/composites/index.ts +5 -0
  39. package/src/deep-observe.ts +2 -2
  40. package/src/index.ts +5 -0
  41. package/src/plugin.test.ts +10 -0
  42. package/src/skills/chant-azure-patterns.md +16 -0
  43. package/src/skills/chant-azure-security.md +1 -0
  44. package/src/skills/chant-azure.md +2 -0
@@ -14,6 +14,7 @@ import { ApplicationGateway } from "./application-gateway";
14
14
  import { ContainerInstance } from "./container-instance";
15
15
  import { RedisCache } from "./redis-cache";
16
16
  import { PrivateEndpoint } from "./private-endpoint";
17
+ import { ContainerAppNetwork, ContainerAppSite } from "./container-app";
17
18
  import { isCompositeInstance } from "@intentius/chant";
18
19
 
19
20
  /** Helper to get flat props from a generated resource instance. */
@@ -744,3 +745,130 @@ describe("PrivateEndpoint", () => {
744
745
  expect(peTags["managed-by"]).toBe("chant");
745
746
  });
746
747
  });
748
+
749
+ // --- ContainerAppNetwork ---
750
+
751
+ describe("ContainerAppNetwork", () => {
752
+ test("returns a VNet and its subnet", () => {
753
+ const result = ContainerAppNetwork({ name: "web" });
754
+ expect(Object.keys(result.members)).toEqual(["virtualNetwork", "subnet"]);
755
+ expect(et(result.virtualNetwork)).toBe("Microsoft.Network/virtualNetworks");
756
+ expect(et(result.subnet)).toBe("Microsoft.Network/virtualNetworks_subnets");
757
+ });
758
+
759
+ test("names the subnet as a child of <name>-vnet and delegates it to Container Apps", () => {
760
+ const { virtualNetwork, subnet } = ContainerAppNetwork({ name: "web", subnetPrefix: "10.1.0.0/23" });
761
+ expect(p(virtualNetwork).name).toBe("web-vnet");
762
+ expect(p(subnet).name).toBe("web-vnet/apps");
763
+ expect(p(subnet).addressPrefix).toBe("10.1.0.0/23");
764
+ const delegations = p(subnet).delegations as Array<{ properties: { serviceName: string } }>;
765
+ expect(delegations[0].properties.serviceName).toBe("Microsoft.App/environments");
766
+ });
767
+
768
+ test("lists the subnet inline on the VNet too", () => {
769
+ const { virtualNetwork } = ContainerAppNetwork({ name: "web" });
770
+ const subnets = p(virtualNetwork).subnets as Array<{ name: string; properties: { addressPrefix: string } }>;
771
+ expect(subnets).toHaveLength(1);
772
+ expect(subnets[0].name).toBe("apps");
773
+ expect(subnets[0].properties.addressPrefix).toBe("10.0.0.0/23");
774
+ });
775
+
776
+ test("defaults override a member's props", () => {
777
+ const { virtualNetwork } = ContainerAppNetwork({ name: "web", defaults: { virtualNetwork: { location: "westeurope" } } });
778
+ expect(p(virtualNetwork).location).toBe("westeurope");
779
+ });
780
+
781
+ type Inline = { name: string; properties: Record<string, unknown> };
782
+
783
+ test("defaults.subnet reaches the inline entry as well as the Subnet resource", () => {
784
+ const nsg = { id: "[resourceId('Microsoft.Network/networkSecurityGroups', 'web-nsg')]" };
785
+ const routeTable = { id: "[resourceId('Microsoft.Network/routeTables', 'web-rt')]" };
786
+ const { virtualNetwork, subnet } = ContainerAppNetwork({
787
+ name: "web",
788
+ defaults: { subnet: { networkSecurityGroup: nsg, routeTable } as any },
789
+ });
790
+ const inline = (p(virtualNetwork).subnets as Inline[])[0];
791
+ expect(p(subnet).networkSecurityGroup).toEqual(nsg);
792
+ expect(inline.properties.networkSecurityGroup).toEqual(nsg);
793
+ expect(inline.properties.routeTable).toEqual(routeTable);
794
+ const { name: _n, ...flat } = p(subnet);
795
+ expect(inline.properties).toEqual(flat);
796
+ });
797
+
798
+ test("defaults.virtualNetwork.subnets adds subnets after apps and keeps its delegation", () => {
799
+ const { virtualNetwork } = ContainerAppNetwork({
800
+ name: "web",
801
+ defaults: { virtualNetwork: { subnets: [{ name: "db", properties: { addressPrefix: "10.0.2.0/24" } }] } as any },
802
+ });
803
+ const subnets = p(virtualNetwork).subnets as Inline[];
804
+ expect(subnets.map((s) => s.name)).toEqual(["apps", "db"]);
805
+ const delegations = subnets[0].properties.delegations as Array<{ properties: { serviceName: string } }>;
806
+ expect(delegations[0].properties.serviceName).toBe("Microsoft.App/environments");
807
+ });
808
+
809
+ test("a caller's own apps entry in defaults.virtualNetwork.subnets is replaced by the merged one", () => {
810
+ const { virtualNetwork } = ContainerAppNetwork({
811
+ name: "web",
812
+ defaults: { virtualNetwork: { subnets: [{ name: "apps", properties: { addressPrefix: "10.9.0.0/23" } }] } as any },
813
+ });
814
+ const subnets = p(virtualNetwork).subnets as Inline[];
815
+ expect(subnets).toHaveLength(1);
816
+ expect(subnets[0].properties.addressPrefix).toBe("10.0.0.0/23");
817
+ expect(subnets[0].properties.delegations).toBeDefined();
818
+ });
819
+ });
820
+
821
+ // --- ContainerAppSite ---
822
+
823
+ describe("ContainerAppSite", () => {
824
+ const subnetId = "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'web-vnet', 'apps')]";
825
+
826
+ test("returns a workspace, identity, environment and app", () => {
827
+ const result = ContainerAppSite({ name: "web", app: "frontend", subnetId });
828
+ expect(Object.keys(result.members)).toEqual(["workspace", "identity", "environment", "containerApp"]);
829
+ expect(et(result.workspace)).toBe("Microsoft.OperationalInsights/workspaces");
830
+ expect(et(result.identity)).toBe("Microsoft.ManagedIdentity/userAssignedIdentities");
831
+ expect(et(result.environment)).toBe("Microsoft.App/managedEnvironments");
832
+ expect(et(result.containerApp)).toBe("Microsoft.App/containerApps");
833
+ });
834
+
835
+ test("puts the environment in the subnet on the Consumption profile, logging to the workspace", () => {
836
+ const { workspace, environment } = ContainerAppSite({ name: "web", app: "frontend", subnetId });
837
+ expect(p(workspace).name).toBe("web-logs");
838
+ const env = p(environment);
839
+ expect(env.vnetConfiguration).toEqual({ infrastructureSubnetId: subnetId, internal: false });
840
+ expect(env.workloadProfiles).toEqual([{ name: "Consumption", workloadProfileType: "Consumption" }]);
841
+ const logs = (env.appLogsConfiguration as { logAnalyticsConfiguration: Record<string, string> }).logAnalyticsConfiguration;
842
+ expect(logs.customerId).toContain("reference(resourceId('Microsoft.OperationalInsights/workspaces', 'web-logs')");
843
+ expect(logs.sharedKey).toContain("listKeys(resourceId('Microsoft.OperationalInsights/workspaces', 'web-logs')");
844
+ });
845
+
846
+ test("runs the app as the identity behind external ingress", () => {
847
+ const { identity, containerApp } = ContainerAppSite({ name: "web", app: "frontend", subnetId, image: "nginx:1.27", port: 8080, replicas: 2 });
848
+ expect(p(identity).name).toBe("web-frontend");
849
+ const app = p(containerApp);
850
+ expect(app.name).toBe("frontend");
851
+ const ids = (app.identity as { userAssignedIdentities: Record<string, unknown> }).userAssignedIdentities;
852
+ expect(Object.keys(ids)).toEqual(["[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'web-frontend')]"]);
853
+ expect(app.configuration).toEqual({ ingress: { external: true, targetPort: 8080, transport: "auto" } });
854
+ const template = app.template as { containers: Array<{ image: string }>; scale: Record<string, number> };
855
+ expect(template.containers[0].image).toBe("nginx:1.27");
856
+ expect(template.scale).toEqual({ minReplicas: 2, maxReplicas: 2 });
857
+ });
858
+
859
+ test("internal makes the environment VNet-only", () => {
860
+ const { environment } = ContainerAppSite({ name: "web", app: "frontend", subnetId, internal: true });
861
+ expect((p(environment).vnetConfiguration as Record<string, unknown>).internal).toBe(true);
862
+ });
863
+
864
+ test("defaults override a member's props", () => {
865
+ const { workspace } = ContainerAppSite({ name: "web", app: "frontend", subnetId, defaults: { workspace: { retentionInDays: 90 } } });
866
+ expect(p(workspace).retentionInDays).toBe(90);
867
+ });
868
+
869
+ test("takes a ContainerAppNetwork's subnet id", () => {
870
+ const network = ContainerAppNetwork({ name: "web" });
871
+ const { environment } = ContainerAppSite({ name: "web", app: "frontend", subnetId: network.subnet.id });
872
+ expect((p(environment).vnetConfiguration as Record<string, unknown>).infrastructureSubnetId).toBe(network.subnet.id);
873
+ });
874
+ });
@@ -0,0 +1,230 @@
1
+ /**
2
+ * ContainerAppNetwork and ContainerAppSite composites: a website on Azure
3
+ * Container Apps and the network it sits in.
4
+ *
5
+ * Split the way the aws lexicon splits VpcDefault from FargateAlb, so several
6
+ * sites can share one network. Container Apps brings its own ingress and load
7
+ * balancing, so there is no load balancer to declare.
8
+ */
9
+
10
+ import { Composite, mergeDefaults } from "@intentius/chant";
11
+ import {
12
+ VirtualNetwork,
13
+ Subnet,
14
+ LogAnalyticsWorkspace,
15
+ ManagedIdentity,
16
+ managedEnvironments,
17
+ containerApps,
18
+ } from "../generated";
19
+
20
+ export interface ContainerAppNetworkProps {
21
+ /** Network name prefix: the VNet is `<name>-vnet`, its subnet `apps`. */
22
+ name: string;
23
+ /** Azure region (default: resource group location). */
24
+ location?: string;
25
+ /** VNet address space (default: "10.0.0.0/16"). */
26
+ addressSpace?: string;
27
+ /** The environment subnet's prefix, a /23 or larger (default: "10.0.0.0/23"). */
28
+ subnetPrefix?: string;
29
+ /** Resource tags. */
30
+ tags?: Record<string, string>;
31
+ /** Per-member defaults. */
32
+ defaults?: {
33
+ virtualNetwork?: Partial<ConstructorParameters<typeof VirtualNetwork>[0]>;
34
+ subnet?: Partial<ConstructorParameters<typeof Subnet>[0]>;
35
+ };
36
+ }
37
+
38
+ export interface ContainerAppNetworkResult {
39
+ virtualNetwork: InstanceType<typeof VirtualNetwork>;
40
+ subnet: InstanceType<typeof Subnet>;
41
+ }
42
+
43
+ /**
44
+ * A VNet with a subnet delegated to `Microsoft.App/environments`, for
45
+ * ContainerAppSite environments to sit in.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * import { ContainerAppNetwork } from "@intentius/chant-lexicon-azure";
50
+ *
51
+ * export const network = ContainerAppNetwork({ name: "web" });
52
+ * ```
53
+ */
54
+ export const ContainerAppNetwork = Composite((props: ContainerAppNetworkProps) => {
55
+ const {
56
+ name,
57
+ location = "[resourceGroup().location]",
58
+ addressSpace = "10.0.0.0/16",
59
+ subnetPrefix = "10.0.0.0/23",
60
+ tags = {},
61
+ defaults,
62
+ } = props;
63
+
64
+ const mergedTags = { "managed-by": "chant", ...tags };
65
+ const vnetName = `${name}-vnet`;
66
+ const delegations = [{ name: "apps", properties: { serviceName: "Microsoft.App/environments" } }];
67
+
68
+ const subnet = new Subnet(mergeDefaults({
69
+ name: `${vnetName}/apps`,
70
+ addressPrefix: subnetPrefix,
71
+ delegations,
72
+ }, defaults?.subnet), { apiVersion: "2023-05-01" });
73
+
74
+ // The subnet is also listed inline, as VnetDefault does, so a later
75
+ // deployment of the VNet does not try to remove it. The inline entry is
76
+ // built from the Subnet resource's merged props, so `defaults.subnet`
77
+ // reaches both copies. The apps entry always comes first; a caller's
78
+ // `defaults.virtualNetwork.subnets` adds the other subnets after it, and
79
+ // an entry of theirs named "apps" is replaced by this one.
80
+ const { subnets: extraSubnets, ...vnetDefaults } = (defaults?.virtualNetwork ?? {}) as {
81
+ subnets?: Array<{ name?: string }>;
82
+ } & Record<string, unknown>;
83
+ const virtualNetwork = new VirtualNetwork(mergeDefaults({
84
+ name: vnetName,
85
+ location,
86
+ tags: mergedTags,
87
+ addressSpace: { addressPrefixes: [addressSpace] },
88
+ subnets: [
89
+ inlineSubnet("apps", subnetProps(subnet)),
90
+ ...(extraSubnets ?? []).filter((s) => s?.name !== "apps"),
91
+ ],
92
+ }, vnetDefaults), { apiVersion: "2023-05-01" });
93
+
94
+ return { virtualNetwork, subnet };
95
+ }, "ContainerAppNetwork");
96
+
97
+ /** A Subnet resource's props, flat or under `properties`. */
98
+ function subnetProps(subnet: InstanceType<typeof Subnet>): Record<string, unknown> {
99
+ return (subnet as unknown as { props: Record<string, unknown> }).props;
100
+ }
101
+
102
+ /** The VNet's inline entry for a subnet, from that Subnet resource's props. */
103
+ function inlineSubnet(name: string, props: Record<string, unknown>) {
104
+ const { name: _name, id: _id, properties, ...flat } = props;
105
+ return { name, properties: { ...flat, ...(properties as Record<string, unknown> | undefined) } };
106
+ }
107
+
108
+ export interface ContainerAppSiteProps {
109
+ /** Environment name; the workspace is `<name>-logs` and the identity `<name>-<app>`. */
110
+ name: string;
111
+ /** Container app name. */
112
+ app: string;
113
+ /** Subnet resource ID for the environment, such as a ContainerAppNetwork's `subnet.id`. */
114
+ subnetId: string;
115
+ /** Azure region (default: resource group location). */
116
+ location?: string;
117
+ /** Container image (default: "mcr.microsoft.com/k8se/quickstart:latest"). */
118
+ image?: string;
119
+ /** Port the container listens on; ingress forwards to it (default: 80). */
120
+ port?: number;
121
+ /** Fixed replica count (default: 1). */
122
+ replicas?: number;
123
+ /** Make the environment reachable only from its VNet (default: false). */
124
+ internal?: boolean;
125
+ /** Resource tags. */
126
+ tags?: Record<string, string>;
127
+ /** Per-member defaults. */
128
+ defaults?: {
129
+ workspace?: Partial<ConstructorParameters<typeof LogAnalyticsWorkspace>[0]>;
130
+ identity?: Partial<ConstructorParameters<typeof ManagedIdentity>[0]>;
131
+ environment?: Partial<ConstructorParameters<typeof managedEnvironments>[0]>;
132
+ containerApp?: Partial<ConstructorParameters<typeof containerApps>[0]>;
133
+ };
134
+ }
135
+
136
+ export interface ContainerAppSiteResult {
137
+ workspace: InstanceType<typeof LogAnalyticsWorkspace>;
138
+ identity: InstanceType<typeof ManagedIdentity>;
139
+ environment: InstanceType<typeof managedEnvironments>;
140
+ containerApp: InstanceType<typeof containerApps>;
141
+ }
142
+
143
+ /**
144
+ * A Container Apps environment in a subnet, shipping logs to its own Log
145
+ * Analytics workspace, with one app running as a user-assigned identity.
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * import { ContainerAppNetwork, ContainerAppSite } from "@intentius/chant-lexicon-azure";
150
+ *
151
+ * export const network = ContainerAppNetwork({ name: "web" });
152
+ * export const site = ContainerAppSite({
153
+ * name: "web",
154
+ * app: "frontend",
155
+ * subnetId: network.subnet.id,
156
+ * image: "nginx:1.27",
157
+ * });
158
+ * ```
159
+ */
160
+ export const ContainerAppSite = Composite((props: ContainerAppSiteProps) => {
161
+ const {
162
+ name,
163
+ app,
164
+ subnetId,
165
+ location = "[resourceGroup().location]",
166
+ image = "mcr.microsoft.com/k8se/quickstart:latest",
167
+ port = 80,
168
+ replicas = 1,
169
+ internal = false,
170
+ tags = {},
171
+ defaults,
172
+ } = props;
173
+
174
+ const mergedTags = { "managed-by": "chant", ...tags };
175
+ const workspaceName = `${name}-logs`;
176
+ const identityName = `${name}-${app}`;
177
+ const workspaceId = `resourceId('Microsoft.OperationalInsights/workspaces', '${workspaceName}')`;
178
+
179
+ const workspace = new LogAnalyticsWorkspace(mergeDefaults({
180
+ name: workspaceName,
181
+ location,
182
+ tags: mergedTags,
183
+ // A workspace's sku sits inside its properties, not beside them, so it is
184
+ // passed there rather than hoisted to the resource like other skus.
185
+ properties: { sku: { name: "PerGB2018" } },
186
+ retentionInDays: 30,
187
+ }, defaults?.workspace), { apiVersion: "2023-09-01" });
188
+
189
+ const identity = new ManagedIdentity(mergeDefaults({
190
+ name: identityName,
191
+ location,
192
+ tags: mergedTags,
193
+ }, defaults?.identity), { apiVersion: "2023-01-31" });
194
+
195
+ const environment = new managedEnvironments(mergeDefaults({
196
+ name,
197
+ location,
198
+ tags: mergedTags,
199
+ vnetConfiguration: { infrastructureSubnetId: subnetId, internal },
200
+ appLogsConfiguration: {
201
+ destination: "log-analytics",
202
+ logAnalyticsConfiguration: {
203
+ customerId: `[reference(${workspaceId}, '2023-09-01').customerId]`,
204
+ sharedKey: `[listKeys(${workspaceId}, '2023-09-01').primarySharedKey]`,
205
+ },
206
+ },
207
+ workloadProfiles: [{ name: "Consumption", workloadProfileType: "Consumption" }],
208
+ }, defaults?.environment), { apiVersion: "2024-03-01" });
209
+
210
+ const containerApp = new containerApps(mergeDefaults({
211
+ name: app,
212
+ location,
213
+ tags: mergedTags,
214
+ identity: {
215
+ type: "UserAssigned",
216
+ userAssignedIdentities: {
217
+ [`[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', '${identityName}')]`]: {},
218
+ },
219
+ },
220
+ managedEnvironmentId: environment.id,
221
+ workloadProfileName: "Consumption",
222
+ configuration: { ingress: { external: true, targetPort: port, transport: "auto" } },
223
+ template: {
224
+ containers: [{ name: app, image, resources: { cpu: 0.25, memory: "0.5Gi" } }],
225
+ scale: { minReplicas: replicas, maxReplicas: replicas },
226
+ },
227
+ }, defaults?.containerApp), { apiVersion: "2024-03-01" });
228
+
229
+ return { workspace, identity, environment, containerApp };
230
+ }, "ContainerAppSite");
@@ -28,6 +28,11 @@ export { RedisCache } from "./redis-cache";
28
28
  export type { RedisCacheProps, RedisCacheResult } from "./redis-cache";
29
29
  export { PrivateEndpoint } from "./private-endpoint";
30
30
  export type { PrivateEndpointProps, PrivateEndpointResult } from "./private-endpoint";
31
+ export { ContainerAppNetwork, ContainerAppSite } from "./container-app";
32
+ export type {
33
+ ContainerAppNetworkProps, ContainerAppNetworkResult,
34
+ ContainerAppSiteProps, ContainerAppSiteResult,
35
+ } from "./container-app";
31
36
  export { GovernanceFoundation, GovernanceBaseline, LocationRestriction, ActivityLogSink } from "./landing-zone";
32
37
  export type {
33
38
  GovernanceFoundationProps, GovernanceFoundationResult,
@@ -67,7 +67,7 @@ import { AZURE_TAG_OWNERSHIP_KEYS } from "./ownership";
67
67
 
68
68
 
69
69
  /** The full `az resource show -o json` shape this reader reads (a superset of the thin path's `AzResourceShowResponse`). */
70
- interface ArmResourceShowResponse {
70
+ export interface ArmResourceShowResponse {
71
71
  id?: string;
72
72
  name?: string;
73
73
  type?: string;
@@ -96,7 +96,7 @@ interface ArmResourceShowResponse {
96
96
  * returned 200 is the declared name. The leaf is folded back to the declared
97
97
  * spelling; a name that is not the leaf of the query is kept as returned.
98
98
  */
99
- function buildLiveProperties(obj: ArmResourceShowResponse, queriedName?: string): Record<string, unknown> {
99
+ export function buildLiveProperties(obj: ArmResourceShowResponse, queriedName?: string): Record<string, unknown> {
100
100
  const out: Record<string, unknown> = {};
101
101
  if (obj.name !== undefined) {
102
102
  out.name =
package/src/index.ts CHANGED
@@ -17,6 +17,9 @@ export { defaultTags, type TagEntry, type DefaultTags } from "./default-tags";
17
17
  export { deploymentScope, isDeploymentScope, type DeploymentScope, type DeployScope } from "./deploy-scopes";
18
18
  export { azureSerializer } from "./serializer";
19
19
  export { azurePlugin } from "./plugin";
20
+ // Core's capability-plugin loader scans this package's exports and loads
21
+ // `azureCapabilityPlugin` when a project's chant.config lists this lexicon (#4273).
22
+ export { azureCapabilityPlugin } from "./components/capability-plugin";
20
23
  export { Parameter } from "./parameter";
21
24
 
22
25
  // Deep observation (#1086): the `az resource show` reader and the noise
@@ -78,6 +81,8 @@ export {
78
81
  ContainerInstance, type ContainerInstanceProps, type ContainerInstanceResult,
79
82
  RedisCache, type RedisCacheProps, type RedisCacheResult,
80
83
  PrivateEndpoint, type PrivateEndpointProps, type PrivateEndpointResult,
84
+ ContainerAppNetwork, type ContainerAppNetworkProps, type ContainerAppNetworkResult,
85
+ ContainerAppSite, type ContainerAppSiteProps, type ContainerAppSiteResult,
81
86
  GovernanceFoundation, type GovernanceFoundationProps, type GovernanceFoundationResult,
82
87
  GovernanceBaseline, type GovernanceBaselineProps, type GovernanceBaselineResult,
83
88
  LocationRestriction, type LocationRestrictionProps, type LocationRestrictionResult,
@@ -1,6 +1,7 @@
1
1
  import { describe, test, expect } from "vitest";
2
2
  import { azurePlugin } from "./plugin";
3
3
  import { isLexiconPlugin } from "@intentius/chant/lexicon";
4
+ import { emulatorsOf, endpointEnvVars } from "@intentius/chant/op";
4
5
 
5
6
  describe("azurePlugin", () => {
6
7
  // -----------------------------------------------------------------------
@@ -233,3 +234,12 @@ describe("azurePlugin", () => {
233
234
  expect(ids).toContain("AZR011");
234
235
  });
235
236
  });
237
+
238
+ // #4281: `chant components pr-plan`, `pr-apply` and `run --components` apply an
239
+ // environment's declared endpoint to the vars the plugin's emulator names. `arm-deploy`
240
+ // reads AZURE_ENDPOINT_URL, so the plugin has to name it.
241
+ describe("azurePlugin endpoint var (#4281)", () => {
242
+ test("the emulator names AZURE_ENDPOINT_URL, the var arm-deploy reads", () => {
243
+ expect(emulatorsOf(azurePlugin.emulator).flatMap((cap) => endpointEnvVars(cap))).toEqual(["AZURE_ENDPOINT_URL"]);
244
+ });
245
+ });
@@ -408,6 +408,22 @@ const { privateEndpoint, privateDnsZone, dnsZoneGroup, vnetLink } = PrivateEndpo
408
408
  export { storageAccount, privateEndpoint, privateDnsZone, dnsZoneGroup, vnetLink };
409
409
  ```
410
410
 
411
+ ### ContainerAppNetwork and ContainerAppSite
412
+ A Container Apps site in its own delegated subnet; one network can carry several sites:
413
+
414
+ ```ts
415
+ import { ContainerAppNetwork, ContainerAppSite } from "@intentius/chant-lexicon-azure";
416
+
417
+ export const network = ContainerAppNetwork({ name: "web" });
418
+
419
+ export const site = ContainerAppSite({
420
+ name: "web",
421
+ app: "frontend",
422
+ subnetId: network.subnet.id,
423
+ image: "nginx:1.27",
424
+ });
425
+ ```
426
+
411
427
  ## ARM Template Intrinsic Reference
412
428
 
413
429
  | Function | Description | Example |
@@ -267,6 +267,7 @@ Use the secure composite variants which apply these patterns automatically:
267
267
  - `ContainerInstance` — Managed identity, no public IP by default
268
268
  - `RedisCache` — Non-SSL port disabled, TLS 1.2
269
269
  - `PrivateEndpoint` — Private connectivity pattern with DNS zone
270
+ - `ContainerAppSite`: user-assigned identity, app logs to Log Analytics, optional VNet-only environment
270
271
 
271
272
  ## Post-Synth Check Reference
272
273
 
@@ -157,6 +157,8 @@ Composites group related Azure resources with secure defaults. Each returns an o
157
157
  | `ContainerInstance` | ContainerGroup | No public IP by default, resource limits set |
158
158
  | `RedisCache` | RedisCache | TLS 1.2 required, non-SSL port disabled |
159
159
  | `PrivateEndpoint` | PrivateEndpoint + PrivateDnsZone + VnetLink | DNS zone auto-registration, manual approval disabled |
160
+ | `ContainerAppNetwork` | VirtualNetwork + Subnet | Subnet delegated to Container Apps, /23 by default |
161
+ | `ContainerAppSite` | LogAnalyticsWorkspace + ManagedIdentity + ManagedEnvironment + ContainerApp | App logs to Log Analytics, user-assigned identity, Consumption profile |
160
162
  | `AksCluster` | ManagedCluster + AgentPool | RBAC enabled, network policy (calico), managed identity, Azure CNI |
161
163
  | `SqlDatabase` | SQLServer + Database + FirewallRule + AuditingSettings | TDE enabled, auditing enabled, TLS 1.2, no Azure-wide firewall rule |
162
164
  | `KeyVaultSecure` | KeyVault + AccessPolicy | Soft delete, purge protection, RBAC authorization, network ACLs |