@intentius/chant-lexicon-gcp 0.124.38 → 0.124.39

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 (52) hide show
  1. package/dist/api/read-client.d.ts +7 -0
  2. package/dist/api/read-client.d.ts.map +1 -1
  3. package/dist/api/read-client.js +24 -9
  4. package/dist/composites/catalog.d.ts.map +1 -1
  5. package/dist/composites/catalog.js +187 -0
  6. package/dist/composites/index.d.ts +4 -0
  7. package/dist/composites/index.d.ts.map +1 -1
  8. package/dist/composites/index.js +2 -0
  9. package/dist/composites/private-egress-network.d.ts +42 -0
  10. package/dist/composites/private-egress-network.d.ts.map +1 -0
  11. package/dist/composites/private-egress-network.js +48 -0
  12. package/dist/composites/public-cloud-run.d.ts +85 -0
  13. package/dist/composites/public-cloud-run.d.ts.map +1 -0
  14. package/dist/composites/public-cloud-run.js +144 -0
  15. package/dist/deep-observe.d.ts +5 -0
  16. package/dist/deep-observe.d.ts.map +1 -1
  17. package/dist/deep-observe.js +34 -2
  18. package/dist/health.d.ts +26 -2
  19. package/dist/health.d.ts.map +1 -1
  20. package/dist/health.js +85 -6
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/integrity.json +3 -3
  25. package/dist/manifest.json +1 -1
  26. package/dist/op/activities/gcp-apply.d.ts +19 -0
  27. package/dist/op/activities/gcp-apply.d.ts.map +1 -1
  28. package/dist/op/activities/gcp-apply.js +16 -0
  29. package/dist/op/activities/gcp-compute.d.ts +9 -0
  30. package/dist/op/activities/gcp-compute.d.ts.map +1 -1
  31. package/dist/op/activities/gcp-compute.js +107 -11
  32. package/dist/plugin.d.ts.map +1 -1
  33. package/dist/plugin.js +6 -2
  34. package/dist/skills/chant-gcp.md +2 -0
  35. package/package.json +2 -2
  36. package/src/api/read-client.ts +23 -8
  37. package/src/composites/catalog.ts +187 -0
  38. package/src/composites/cloud-run-lb.test.ts +93 -0
  39. package/src/composites/index.ts +4 -0
  40. package/src/composites/private-egress-network.ts +82 -0
  41. package/src/composites/public-cloud-run.ts +245 -0
  42. package/src/deep-observe.test.ts +46 -0
  43. package/src/deep-observe.ts +38 -2
  44. package/src/health.test.ts +65 -1
  45. package/src/health.ts +88 -11
  46. package/src/index.ts +2 -1
  47. package/src/op/activities/gcp-apply.ts +31 -1
  48. package/src/op/activities/gcp-compute-reconcile.test.ts +150 -0
  49. package/src/op/activities/gcp-compute.test.ts +4 -2
  50. package/src/op/activities/gcp-compute.ts +129 -11
  51. package/src/plugin.ts +6 -2
  52. package/src/skills/chant-gcp.md +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-gcp",
3
- "version": "0.124.38",
3
+ "version": "0.124.39",
4
4
  "description": "Google Cloud lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -76,6 +76,6 @@
76
76
  "typescript": "^5.9.3"
77
77
  },
78
78
  "peerDependencies": {
79
- "@intentius/chant": "^0.124.38"
79
+ "@intentius/chant": "^0.124.39"
80
80
  }
81
81
  }
@@ -98,6 +98,28 @@ export function mapperForKind(kind: string): ResourceMapper | undefined {
98
98
  * build a URL — the same input the applier hands it. Returns the parsed body,
99
99
  * or throws {@link GcpReadError} carrying the status.
100
100
  */
101
+ /**
102
+ * The URL a read of this resource GETs: the applier's own `plan().getUrl`, so
103
+ * a health read's sub-calls (`getHealth`) land on the same resource path.
104
+ * Throws a {@link GcpReadError} when the declared spec can't produce one,
105
+ * which is a hole, never an absence.
106
+ */
107
+ export function readUrl(client: GcpReadClientOptions, kind: string, name: string, declared?: Record<string, unknown>): string {
108
+ const mapper = mapperForKind(kind);
109
+ if (!mapper) throw new GcpReadError(`no REST mapper for kind ${kind}`);
110
+ const base = (client.endpoint ?? mapper.defaultHost).replace(/\/$/, "");
111
+ const resource: GcpResource = {
112
+ kind,
113
+ metadata: { name },
114
+ spec: (declared?.spec as Record<string, unknown> | undefined) ?? declared ?? {},
115
+ };
116
+ try {
117
+ return mapper.plan(resource, { base, project: client.project, ...(client.manifest ? { manifest: client.manifest } : {}) }).getUrl;
118
+ } catch (err) {
119
+ throw new GcpReadError(`could not build a read URL for ${kind}/${name}: ${err instanceof Error ? err.message : String(err)}`);
120
+ }
121
+ }
122
+
101
123
  export async function getResource(
102
124
  client: GcpReadClientOptions,
103
125
  kind: string,
@@ -128,14 +150,7 @@ export async function getResource(
128
150
  throw new GcpReadError(`could not read ${kind}/${name}: ${err instanceof Error ? err.message : String(err)}`);
129
151
  }
130
152
  } else {
131
- let getUrl: string;
132
- try {
133
- getUrl = mapper.plan(resource, ctx).getUrl;
134
- } catch (err) {
135
- // A mapper that cannot plan from the declared spec cannot produce a URL, so
136
- // there is nothing to read — a hole, never an absence.
137
- throw new GcpReadError(`could not build a read URL for ${kind}/${name}: ${err instanceof Error ? err.message : String(err)}`);
138
- }
153
+ const getUrl = readUrl(client, kind, name, declared);
139
154
  try {
140
155
  res = await http("GET", getUrl);
141
156
  } catch (err) {
@@ -175,6 +175,12 @@ export const compositeCatalog: CompositeEntry[] = [
175
175
  "required": false,
176
176
  "description": "Environment variables."
177
177
  },
178
+ {
179
+ "name": "serviceAccount",
180
+ "type": "string",
181
+ "required": false,
182
+ "description": "Service account the service runs as: an email, or the account id of an IAMServiceAccount in the project."
183
+ },
178
184
  {
179
185
  "name": "labels",
180
186
  "type": "Record<string, string>",
@@ -955,6 +961,59 @@ export const compositeCatalog: CompositeEntry[] = [
955
961
  }
956
962
  ]
957
963
  },
964
+ {
965
+ "name": "PrivateEgressNetwork",
966
+ "lexicon": "gcp",
967
+ "description": "Create a PrivateEgressNetwork composite.",
968
+ "bundles": [
969
+ "Subnetwork",
970
+ "VPCNetwork"
971
+ ],
972
+ "params": [
973
+ {
974
+ "name": "name",
975
+ "type": "string",
976
+ "required": true,
977
+ "description": "Name prefix: the network is `<name>-vpc`, its subnet `<name>-apps`."
978
+ },
979
+ {
980
+ "name": "region",
981
+ "type": "string",
982
+ "required": true,
983
+ "description": "GCP region of the subnet."
984
+ },
985
+ {
986
+ "name": "subnetCidr",
987
+ "type": "string",
988
+ "required": true,
989
+ "description": "IP CIDR range of the subnet."
990
+ },
991
+ {
992
+ "name": "privateIpGoogleAccess",
993
+ "type": "boolean",
994
+ "required": false,
995
+ "description": "Enable private Google access on the subnet (default: true)."
996
+ },
997
+ {
998
+ "name": "labels",
999
+ "type": "Record<string, string>",
1000
+ "required": false,
1001
+ "description": "Additional labels."
1002
+ },
1003
+ {
1004
+ "name": "namespace",
1005
+ "type": "string",
1006
+ "required": false,
1007
+ "description": "Namespace for all resources."
1008
+ },
1009
+ {
1010
+ "name": "defaults",
1011
+ "type": "{ vpc?: Partial<ConstructorParameters<typeof VPCNetwork>[0]>; subnet?: Partial<ConstructorParameters<typeof Subnetwor...",
1012
+ "required": false,
1013
+ "description": "Per-member defaults for customizing individual resources."
1014
+ }
1015
+ ]
1016
+ },
958
1017
  {
959
1018
  "name": "PrivateService",
960
1019
  "lexicon": "gcp",
@@ -1033,6 +1092,134 @@ export const compositeCatalog: CompositeEntry[] = [
1033
1092
  }
1034
1093
  ]
1035
1094
  },
1095
+ {
1096
+ "name": "PublicCloudRun",
1097
+ "lexicon": "gcp",
1098
+ "description": "Create a PublicCloudRun composite.",
1099
+ "bundles": [
1100
+ "BackendService",
1101
+ "CloudRunService",
1102
+ "ComputeAddress",
1103
+ "ForwardingRule",
1104
+ "GCPServiceAccount",
1105
+ "IAMPolicyMember",
1106
+ "ManagedSSLCertificate",
1107
+ "RegionNetworkEndpointGroup",
1108
+ "TargetHTTPProxy",
1109
+ "TargetHTTPSProxy",
1110
+ "URLMap"
1111
+ ],
1112
+ "params": [
1113
+ {
1114
+ "name": "name",
1115
+ "type": "string",
1116
+ "required": true,
1117
+ "description": "Name prefix every resource is named with."
1118
+ },
1119
+ {
1120
+ "name": "image",
1121
+ "type": "string",
1122
+ "required": true,
1123
+ "description": "Container image."
1124
+ },
1125
+ {
1126
+ "name": "region",
1127
+ "type": "string",
1128
+ "required": true,
1129
+ "description": "GCP region of the service and its network endpoint group."
1130
+ },
1131
+ {
1132
+ "name": "projectId",
1133
+ "type": "string",
1134
+ "required": false,
1135
+ "description": "GCP project, as `projectRef.external` (default: GCP.ProjectId, resolved from GCP_PROJECT_ID at build)."
1136
+ },
1137
+ {
1138
+ "name": "vpc",
1139
+ "type": "{ name: string }",
1140
+ "required": true,
1141
+ "description": "VPC the service's egress goes into, such as a PrivateEgressNetwork's `vpc`."
1142
+ },
1143
+ {
1144
+ "name": "subnet",
1145
+ "type": "{ name: string }",
1146
+ "required": true,
1147
+ "description": "Subnet the service's egress goes into, such as a PrivateEgressNetwork's `subnet`."
1148
+ },
1149
+ {
1150
+ "name": "vpcEgress",
1151
+ "type": "string",
1152
+ "required": false,
1153
+ "description": "Egress sent through the VPC: \"PRIVATE_RANGES_ONLY\" or \"ALL_TRAFFIC\" (default: \"PRIVATE_RANGES_ONLY\")."
1154
+ },
1155
+ {
1156
+ "name": "port",
1157
+ "type": "number",
1158
+ "required": false,
1159
+ "description": "Port the container listens on (default: 8080)."
1160
+ },
1161
+ {
1162
+ "name": "frontendPort",
1163
+ "type": "number",
1164
+ "required": false,
1165
+ "description": "Port the HTTP forwarding rule listens on (default: 80)."
1166
+ },
1167
+ {
1168
+ "name": "minInstanceCount",
1169
+ "type": "number",
1170
+ "required": false,
1171
+ "description": "Minimum instances (default: 0)."
1172
+ },
1173
+ {
1174
+ "name": "maxInstanceCount",
1175
+ "type": "number",
1176
+ "required": false,
1177
+ "description": "Maximum instances (default: 100)."
1178
+ },
1179
+ {
1180
+ "name": "env",
1181
+ "type": "Array<{ name: string; value: string }>",
1182
+ "required": false,
1183
+ "description": "Environment variables."
1184
+ },
1185
+ {
1186
+ "name": "serviceAccount",
1187
+ "type": "string",
1188
+ "required": false,
1189
+ "description": "An existing service account to run as: an email, or the account id of an IAMServiceAccount in the project."
1190
+ },
1191
+ {
1192
+ "name": "domains",
1193
+ "type": "string[]",
1194
+ "required": false,
1195
+ "description": "Domains for a Google-managed certificate."
1196
+ },
1197
+ {
1198
+ "name": "securityPolicy",
1199
+ "type": "string",
1200
+ "required": false,
1201
+ "description": "Name of a ComputeSecurityPolicy (Cloud Armor) attached to the backend service."
1202
+ },
1203
+ {
1204
+ "name": "labels",
1205
+ "type": "Record<string, string>",
1206
+ "required": false,
1207
+ "description": "Additional labels."
1208
+ },
1209
+ {
1210
+ "name": "namespace",
1211
+ "type": "string",
1212
+ "required": false,
1213
+ "description": "Namespace for all resources."
1214
+ },
1215
+ {
1216
+ "name": "defaults",
1217
+ "type": "{ runtime?: Partial<ConstructorParameters<typeof GCPServiceAccount>[0]>; service?: Record<string, unknown>; publicAcc...",
1218
+ "required": false,
1219
+ "description": "Per-member defaults for customizing individual resources."
1220
+ }
1221
+ ]
1222
+ },
1036
1223
  {
1037
1224
  "name": "PubSubPipeline",
1038
1225
  "lexicon": "gcp",
@@ -0,0 +1,93 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { PrivateEgressNetwork } from "./private-egress-network";
3
+ import { PublicCloudRun } from "./public-cloud-run";
4
+
5
+ /** Helper to extract props from a Declarable member. */
6
+ function p(member: unknown): Record<string, any> {
7
+ return (member as any).props;
8
+ }
9
+
10
+ /** The metadata.name a `{ name }` reference resolves to: a literal, or another member's name attribute. */
11
+ function refName(ref: { name: unknown }): unknown {
12
+ const value = ref.name as any;
13
+ return value?.parent?.deref ? p(value.parent.deref()).metadata.name : value;
14
+ }
15
+
16
+ const net = { vpc: { name: "web-vpc" }, subnet: { name: "web-apps" } };
17
+ const base = { name: "web", projectId: "proj", region: "us-central1", image: "img:1", ...net };
18
+
19
+ describe("PrivateEgressNetwork (#4263)", () => {
20
+ test("a custom-mode VPC and one subnet in the region", () => {
21
+ const { vpc, subnet } = PrivateEgressNetwork({ name: "web", region: "us-central1", subnetCidr: "10.10.0.0/24" });
22
+ expect(p(vpc).metadata.name).toBe("web-vpc");
23
+ expect(p(vpc).autoCreateSubnetworks).toBe(false);
24
+ expect(p(vpc).routingMode).toBe("REGIONAL");
25
+ expect(p(subnet).metadata.name).toBe("web-apps");
26
+ expect(p(subnet).region).toBe("us-central1");
27
+ expect(p(subnet).ipCidrRange).toBe("10.10.0.0/24");
28
+ expect(p(subnet).privateIpGoogleAccess).toBe(true);
29
+ });
30
+ });
31
+
32
+ describe("PublicCloudRun (#4263)", () => {
33
+ test("creates its own service account and runs the service as it", () => {
34
+ const r = PublicCloudRun(base);
35
+ expect(p(r.runtime).metadata.name).toBe("web-run");
36
+ expect(p(r.service).metadata.name).toBe("web-site");
37
+ expect(refName(p(r.service).template.serviceAccountRef)).toBe("web-run");
38
+ expect(p(r.service).projectRef).toEqual({ external: "proj" });
39
+ expect(p(r.publicAccess).member).toBe("allUsers");
40
+ });
41
+
42
+ test("an existing serviceAccount creates no account", () => {
43
+ const r = PublicCloudRun({ ...base, serviceAccount: "x@o.iam.gserviceaccount.com" });
44
+ expect(r.runtime).toBeUndefined();
45
+ expect(p(r.service).template.serviceAccountRef).toEqual({ external: "x@o.iam.gserviceaccount.com" });
46
+ });
47
+
48
+ test("Direct VPC egress into the subnet for private ranges", () => {
49
+ const r = PublicCloudRun(base);
50
+ expect(p(r.service).template.vpcAccess).toEqual({
51
+ egress: "PRIVATE_RANGES_ONLY",
52
+ networkInterfaces: [{ networkRef: { name: "web-vpc" }, subnetworkRef: { name: "web-apps" } }],
53
+ });
54
+ });
55
+
56
+ test("the load balancer chain: NEG, backend, URL map, HTTP proxy, address, rule", () => {
57
+ const r = PublicCloudRun(base);
58
+ expect(p(r.neg).networkEndpointType).toBe("SERVERLESS");
59
+ expect(refName(p(r.neg).cloudRun.serviceRef)).toBe("web-site");
60
+ expect(p(r.backend).loadBalancingScheme).toBe("EXTERNAL_MANAGED");
61
+ expect(p(r.backend).healthChecks).toBeUndefined();
62
+ expect(refName(p(r.backend).backend[0].group.networkEndpointGroupRef)).toBe("web-neg");
63
+ expect(refName(p(r.urlMap).defaultService.backendServiceRef)).toBe("web-backend");
64
+ expect(refName(p(r.proxy).urlMapRef)).toBe("web-map");
65
+ expect(p(r.address).location).toBe("global");
66
+ expect(p(r.rule).portRange).toBe("80");
67
+ expect(refName(p(r.rule).ipAddress.addressRef)).toBe("web-ip");
68
+ expect(refName(p(r.rule).target.targetHTTPProxyRef)).toBe("web-proxy");
69
+ expect(r.certificate).toBeUndefined();
70
+ });
71
+
72
+ test("port sets the container, frontendPort the HTTP rule", () => {
73
+ const r = PublicCloudRun({ ...base, port: 3000, frontendPort: 8080 });
74
+ expect(p(r.service).template.containers[0].ports[0].containerPort).toBe(3000);
75
+ expect(p(r.rule).portRange).toBe("8080");
76
+ });
77
+
78
+ test("domains add a managed certificate and HTTPS, and HTTP redirects", () => {
79
+ const r = PublicCloudRun({ ...base, domains: ["example.com"] });
80
+ expect(p(r.certificate).managed.domains).toEqual(["example.com"]);
81
+ expect(refName(p(r.httpsProxy).urlMapRef)).toBe("web-map");
82
+ expect(refName(p(r.httpsProxy).sslCertificates[0])).toBe("web-cert");
83
+ expect(p(r.httpsRule).portRange).toBe("443");
84
+ expect(refName(p(r.httpsRule).target.targetHTTPSProxyRef)).toBe("web-https-proxy");
85
+ expect(p(r.redirectMap).defaultUrlRedirect.httpsRedirect).toBe(true);
86
+ expect(refName(p(r.proxy).urlMapRef)).toBe("web-redirect");
87
+ });
88
+
89
+ test("securityPolicy attaches to the backend service", () => {
90
+ const r = PublicCloudRun({ ...base, securityPolicy: "edge" });
91
+ expect(refName(p(r.backend).securityPolicyRef)).toBe("edge");
92
+ });
93
+ });
@@ -32,3 +32,7 @@ export { GkeCrdbRegion } from "./gke-crdb-region";
32
32
  export type { GkeCrdbRegionConfig, GkeCrdbRegionNodeConfig } from "./gke-crdb-region";
33
33
  export { GpuNodePool } from "./gpu-node-pool";
34
34
  export type { GpuNodePoolProps, GpuNodePoolTaint } from "./gpu-node-pool";
35
+ export { PrivateEgressNetwork } from "./private-egress-network";
36
+ export type { PrivateEgressNetworkProps } from "./private-egress-network";
37
+ export { PublicCloudRun } from "./public-cloud-run";
38
+ export type { PublicCloudRunProps } from "./public-cloud-run";
@@ -0,0 +1,82 @@
1
+ /**
2
+ * PrivateEgressNetwork composite: a custom-mode VPC with one subnet a service sends its egress into.
3
+ */
4
+
5
+ import { Composite, mergeDefaults } from "@intentius/chant";
6
+ import { VPCNetwork, Subnetwork } from "../generated";
7
+
8
+ export interface PrivateEgressNetworkProps {
9
+ /** Name prefix: the network is `<name>-vpc`, its subnet `<name>-apps`. */
10
+ name: string;
11
+ /** GCP region of the subnet. */
12
+ region: string;
13
+ /** IP CIDR range of the subnet. */
14
+ subnetCidr: string;
15
+ /** Enable private Google access on the subnet (default: true). */
16
+ privateIpGoogleAccess?: boolean;
17
+ /** Additional labels. */
18
+ labels?: Record<string, string>;
19
+ /** Namespace for all resources. */
20
+ namespace?: string;
21
+ /** Per-member defaults for customizing individual resources. */
22
+ defaults?: {
23
+ vpc?: Partial<ConstructorParameters<typeof VPCNetwork>[0]>;
24
+ subnet?: Partial<ConstructorParameters<typeof Subnetwork>[0]>;
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Create a PrivateEgressNetwork composite.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * import { PrivateEgressNetwork } from "@intentius/chant-lexicon-gcp";
34
+ *
35
+ * const { vpc, subnet } = PrivateEgressNetwork({
36
+ * name: "web",
37
+ * region: "us-central1",
38
+ * subnetCidr: "10.10.0.0/24",
39
+ * });
40
+ * ```
41
+ */
42
+ export const PrivateEgressNetwork = Composite((props: PrivateEgressNetworkProps) => {
43
+ const {
44
+ name,
45
+ region,
46
+ subnetCidr,
47
+ privateIpGoogleAccess = true,
48
+ labels: extraLabels = {},
49
+ namespace,
50
+ defaults: defs,
51
+ } = props;
52
+
53
+ const commonLabels: Record<string, string> = {
54
+ "app.kubernetes.io/name": name,
55
+ "app.kubernetes.io/managed-by": "chant",
56
+ ...extraLabels,
57
+ };
58
+
59
+ const vpc = new VPCNetwork(mergeDefaults({
60
+ metadata: {
61
+ name: `${name}-vpc`,
62
+ ...(namespace && { namespace }),
63
+ labels: { ...commonLabels, "app.kubernetes.io/component": "network" },
64
+ },
65
+ autoCreateSubnetworks: false,
66
+ routingMode: "REGIONAL",
67
+ } as Record<string, unknown>, defs?.vpc));
68
+
69
+ const subnet = new Subnetwork(mergeDefaults({
70
+ metadata: {
71
+ name: `${name}-apps`,
72
+ ...(namespace && { namespace }),
73
+ labels: { ...commonLabels, "app.kubernetes.io/component": "subnet" },
74
+ },
75
+ region,
76
+ ipCidrRange: subnetCidr,
77
+ privateIpGoogleAccess,
78
+ networkRef: { name: vpc.name },
79
+ } as Record<string, unknown>, defs?.subnet));
80
+
81
+ return { vpc, subnet };
82
+ }, "PrivateEgressNetwork");