@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.
- package/dist/components/arm-deploy.d.ts +80 -0
- package/dist/components/arm-deploy.d.ts.map +1 -0
- package/dist/components/arm-deploy.js +398 -0
- package/dist/components/capability-plugin.d.ts +6 -0
- package/dist/components/capability-plugin.d.ts.map +1 -0
- package/dist/components/capability-plugin.js +17 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +2 -0
- package/dist/composites/catalog.d.ts.map +1 -1
- package/dist/composites/catalog.js +120 -0
- package/dist/composites/container-app.d.ts +102 -0
- package/dist/composites/container-app.d.ts.map +1 -0
- package/dist/composites/container-app.js +130 -0
- package/dist/composites/index.d.ts +2 -0
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/composites/index.js +1 -0
- package/dist/deep-observe.d.ts +30 -0
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/deep-observe.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/integrity.json +5 -5
- package/dist/manifest.json +1 -1
- package/dist/skills/chant-azure-patterns.md +16 -0
- package/dist/skills/chant-azure-security.md +1 -0
- package/dist/skills/chant-azure.md +2 -0
- package/package.json +7 -2
- package/src/components/arm-deploy-floci.e2e.test.ts +80 -0
- package/src/components/arm-deploy.test.ts +256 -0
- package/src/components/arm-deploy.ts +491 -0
- package/src/components/capability-plugin.ts +26 -0
- package/src/components/index.ts +9 -0
- package/src/composites/catalog.ts +120 -0
- package/src/composites/composites.test.ts +128 -0
- package/src/composites/container-app.ts +230 -0
- package/src/composites/index.ts +5 -0
- package/src/deep-observe.ts +2 -2
- package/src/index.ts +5 -0
- package/src/plugin.test.ts +10 -0
- package/src/skills/chant-azure-patterns.md +16 -0
- package/src/skills/chant-azure-security.md +1 -0
- 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");
|
package/src/composites/index.ts
CHANGED
|
@@ -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,
|
package/src/deep-observe.ts
CHANGED
|
@@ -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,
|
package/src/plugin.test.ts
CHANGED
|
@@ -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 |
|