@intentius/chant-lexicon-gcp 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 (63) 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/components/capability-plugin.d.ts +14 -0
  5. package/dist/components/capability-plugin.d.ts.map +1 -0
  6. package/dist/components/capability-plugin.js +21 -0
  7. package/dist/components/gcp-apply.d.ts +104 -0
  8. package/dist/components/gcp-apply.d.ts.map +1 -0
  9. package/dist/components/gcp-apply.js +335 -0
  10. package/dist/composites/catalog.d.ts.map +1 -1
  11. package/dist/composites/catalog.js +187 -0
  12. package/dist/composites/index.d.ts +4 -0
  13. package/dist/composites/index.d.ts.map +1 -1
  14. package/dist/composites/index.js +2 -0
  15. package/dist/composites/private-egress-network.d.ts +42 -0
  16. package/dist/composites/private-egress-network.d.ts.map +1 -0
  17. package/dist/composites/private-egress-network.js +48 -0
  18. package/dist/composites/public-cloud-run.d.ts +85 -0
  19. package/dist/composites/public-cloud-run.d.ts.map +1 -0
  20. package/dist/composites/public-cloud-run.js +144 -0
  21. package/dist/deep-observe.d.ts +5 -0
  22. package/dist/deep-observe.d.ts.map +1 -1
  23. package/dist/deep-observe.js +34 -2
  24. package/dist/health.d.ts +26 -2
  25. package/dist/health.d.ts.map +1 -1
  26. package/dist/health.js +85 -6
  27. package/dist/index.d.ts +4 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +5 -1
  30. package/dist/integrity.json +3 -3
  31. package/dist/manifest.json +1 -1
  32. package/dist/op/activities/gcp-apply.d.ts +61 -4
  33. package/dist/op/activities/gcp-apply.d.ts.map +1 -1
  34. package/dist/op/activities/gcp-apply.js +54 -22
  35. package/dist/op/activities/gcp-compute.d.ts +9 -0
  36. package/dist/op/activities/gcp-compute.d.ts.map +1 -1
  37. package/dist/op/activities/gcp-compute.js +107 -11
  38. package/dist/plugin.d.ts.map +1 -1
  39. package/dist/plugin.js +6 -2
  40. package/dist/skills/chant-gcp.md +2 -0
  41. package/package.json +2 -2
  42. package/src/api/read-client.ts +23 -8
  43. package/src/components/capability-plugin.ts +25 -0
  44. package/src/components/gcp-apply-floci.e2e.test.ts +80 -0
  45. package/src/components/gcp-apply.test.ts +259 -0
  46. package/src/components/gcp-apply.ts +406 -0
  47. package/src/composites/catalog.ts +187 -0
  48. package/src/composites/cloud-run-lb.test.ts +93 -0
  49. package/src/composites/index.ts +4 -0
  50. package/src/composites/private-egress-network.ts +82 -0
  51. package/src/composites/public-cloud-run.ts +245 -0
  52. package/src/deep-observe.test.ts +46 -0
  53. package/src/deep-observe.ts +38 -2
  54. package/src/health.test.ts +65 -1
  55. package/src/health.ts +88 -11
  56. package/src/index.ts +13 -1
  57. package/src/op/activities/gcp-apply.ts +95 -24
  58. package/src/op/activities/gcp-compute-reconcile.test.ts +150 -0
  59. package/src/op/activities/gcp-compute.test.ts +4 -2
  60. package/src/op/activities/gcp-compute.ts +129 -11
  61. package/src/plugin.test.ts +10 -0
  62. package/src/plugin.ts +6 -2
  63. package/src/skills/chant-gcp.md +2 -0
@@ -170,6 +170,12 @@ export const compositeCatalog = [
170
170
  "required": false,
171
171
  "description": "Environment variables."
172
172
  },
173
+ {
174
+ "name": "serviceAccount",
175
+ "type": "string",
176
+ "required": false,
177
+ "description": "Service account the service runs as: an email, or the account id of an IAMServiceAccount in the project."
178
+ },
173
179
  {
174
180
  "name": "labels",
175
181
  "type": "Record<string, string>",
@@ -950,6 +956,59 @@ export const compositeCatalog = [
950
956
  }
951
957
  ]
952
958
  },
959
+ {
960
+ "name": "PrivateEgressNetwork",
961
+ "lexicon": "gcp",
962
+ "description": "Create a PrivateEgressNetwork composite.",
963
+ "bundles": [
964
+ "Subnetwork",
965
+ "VPCNetwork"
966
+ ],
967
+ "params": [
968
+ {
969
+ "name": "name",
970
+ "type": "string",
971
+ "required": true,
972
+ "description": "Name prefix: the network is `<name>-vpc`, its subnet `<name>-apps`."
973
+ },
974
+ {
975
+ "name": "region",
976
+ "type": "string",
977
+ "required": true,
978
+ "description": "GCP region of the subnet."
979
+ },
980
+ {
981
+ "name": "subnetCidr",
982
+ "type": "string",
983
+ "required": true,
984
+ "description": "IP CIDR range of the subnet."
985
+ },
986
+ {
987
+ "name": "privateIpGoogleAccess",
988
+ "type": "boolean",
989
+ "required": false,
990
+ "description": "Enable private Google access on the subnet (default: true)."
991
+ },
992
+ {
993
+ "name": "labels",
994
+ "type": "Record<string, string>",
995
+ "required": false,
996
+ "description": "Additional labels."
997
+ },
998
+ {
999
+ "name": "namespace",
1000
+ "type": "string",
1001
+ "required": false,
1002
+ "description": "Namespace for all resources."
1003
+ },
1004
+ {
1005
+ "name": "defaults",
1006
+ "type": "{ vpc?: Partial<ConstructorParameters<typeof VPCNetwork>[0]>; subnet?: Partial<ConstructorParameters<typeof Subnetwor...",
1007
+ "required": false,
1008
+ "description": "Per-member defaults for customizing individual resources."
1009
+ }
1010
+ ]
1011
+ },
953
1012
  {
954
1013
  "name": "PrivateService",
955
1014
  "lexicon": "gcp",
@@ -1028,6 +1087,134 @@ export const compositeCatalog = [
1028
1087
  }
1029
1088
  ]
1030
1089
  },
1090
+ {
1091
+ "name": "PublicCloudRun",
1092
+ "lexicon": "gcp",
1093
+ "description": "Create a PublicCloudRun composite.",
1094
+ "bundles": [
1095
+ "BackendService",
1096
+ "CloudRunService",
1097
+ "ComputeAddress",
1098
+ "ForwardingRule",
1099
+ "GCPServiceAccount",
1100
+ "IAMPolicyMember",
1101
+ "ManagedSSLCertificate",
1102
+ "RegionNetworkEndpointGroup",
1103
+ "TargetHTTPProxy",
1104
+ "TargetHTTPSProxy",
1105
+ "URLMap"
1106
+ ],
1107
+ "params": [
1108
+ {
1109
+ "name": "name",
1110
+ "type": "string",
1111
+ "required": true,
1112
+ "description": "Name prefix every resource is named with."
1113
+ },
1114
+ {
1115
+ "name": "image",
1116
+ "type": "string",
1117
+ "required": true,
1118
+ "description": "Container image."
1119
+ },
1120
+ {
1121
+ "name": "region",
1122
+ "type": "string",
1123
+ "required": true,
1124
+ "description": "GCP region of the service and its network endpoint group."
1125
+ },
1126
+ {
1127
+ "name": "projectId",
1128
+ "type": "string",
1129
+ "required": false,
1130
+ "description": "GCP project, as `projectRef.external` (default: GCP.ProjectId, resolved from GCP_PROJECT_ID at build)."
1131
+ },
1132
+ {
1133
+ "name": "vpc",
1134
+ "type": "{ name: string }",
1135
+ "required": true,
1136
+ "description": "VPC the service's egress goes into, such as a PrivateEgressNetwork's `vpc`."
1137
+ },
1138
+ {
1139
+ "name": "subnet",
1140
+ "type": "{ name: string }",
1141
+ "required": true,
1142
+ "description": "Subnet the service's egress goes into, such as a PrivateEgressNetwork's `subnet`."
1143
+ },
1144
+ {
1145
+ "name": "vpcEgress",
1146
+ "type": "string",
1147
+ "required": false,
1148
+ "description": "Egress sent through the VPC: \"PRIVATE_RANGES_ONLY\" or \"ALL_TRAFFIC\" (default: \"PRIVATE_RANGES_ONLY\")."
1149
+ },
1150
+ {
1151
+ "name": "port",
1152
+ "type": "number",
1153
+ "required": false,
1154
+ "description": "Port the container listens on (default: 8080)."
1155
+ },
1156
+ {
1157
+ "name": "frontendPort",
1158
+ "type": "number",
1159
+ "required": false,
1160
+ "description": "Port the HTTP forwarding rule listens on (default: 80)."
1161
+ },
1162
+ {
1163
+ "name": "minInstanceCount",
1164
+ "type": "number",
1165
+ "required": false,
1166
+ "description": "Minimum instances (default: 0)."
1167
+ },
1168
+ {
1169
+ "name": "maxInstanceCount",
1170
+ "type": "number",
1171
+ "required": false,
1172
+ "description": "Maximum instances (default: 100)."
1173
+ },
1174
+ {
1175
+ "name": "env",
1176
+ "type": "Array<{ name: string; value: string }>",
1177
+ "required": false,
1178
+ "description": "Environment variables."
1179
+ },
1180
+ {
1181
+ "name": "serviceAccount",
1182
+ "type": "string",
1183
+ "required": false,
1184
+ "description": "An existing service account to run as: an email, or the account id of an IAMServiceAccount in the project."
1185
+ },
1186
+ {
1187
+ "name": "domains",
1188
+ "type": "string[]",
1189
+ "required": false,
1190
+ "description": "Domains for a Google-managed certificate."
1191
+ },
1192
+ {
1193
+ "name": "securityPolicy",
1194
+ "type": "string",
1195
+ "required": false,
1196
+ "description": "Name of a ComputeSecurityPolicy (Cloud Armor) attached to the backend service."
1197
+ },
1198
+ {
1199
+ "name": "labels",
1200
+ "type": "Record<string, string>",
1201
+ "required": false,
1202
+ "description": "Additional labels."
1203
+ },
1204
+ {
1205
+ "name": "namespace",
1206
+ "type": "string",
1207
+ "required": false,
1208
+ "description": "Namespace for all resources."
1209
+ },
1210
+ {
1211
+ "name": "defaults",
1212
+ "type": "{ runtime?: Partial<ConstructorParameters<typeof GCPServiceAccount>[0]>; service?: Record<string, unknown>; publicAcc...",
1213
+ "required": false,
1214
+ "description": "Per-member defaults for customizing individual resources."
1215
+ }
1216
+ ]
1217
+ },
1031
1218
  {
1032
1219
  "name": "PubSubPipeline",
1033
1220
  "lexicon": "gcp",
@@ -28,4 +28,8 @@ export { GkeCrdbRegion } from "./gke-crdb-region.js";
28
28
  export type { GkeCrdbRegionConfig, GkeCrdbRegionNodeConfig } from "./gke-crdb-region.js";
29
29
  export { GpuNodePool } from "./gpu-node-pool.js";
30
30
  export type { GpuNodePoolProps, GpuNodePoolTaint } from "./gpu-node-pool.js";
31
+ export { PrivateEgressNetwork } from "./private-egress-network.js";
32
+ export type { PrivateEgressNetworkProps } from "./private-egress-network.js";
33
+ export { PublicCloudRun } from "./public-cloud-run.js";
34
+ export type { PublicCloudRunProps } from "./public-cloud-run.js";
31
35
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/composites/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AACpG,YAAY,EACV,yBAAyB,EACzB,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,YAAY,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAC5D,YAAY,EAAE,6BAA6B,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,YAAY,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AACrF,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,YAAY,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AACtF,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/composites/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AACpG,YAAY,EACV,yBAAyB,EACzB,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,YAAY,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAC5D,YAAY,EAAE,6BAA6B,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,YAAY,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AACrF,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,YAAY,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AACtF,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,YAAY,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,YAAY,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC"}
@@ -13,3 +13,5 @@ export { MemorystoreRedis } from "./memorystore-redis.js";
13
13
  export { MultiRegionVpc } from "./multi-region-vpc.js";
14
14
  export { GkeCrdbRegion } from "./gke-crdb-region.js";
15
15
  export { GpuNodePool } from "./gpu-node-pool.js";
16
+ export { PrivateEgressNetwork } from "./private-egress-network.js";
17
+ export { PublicCloudRun } from "./public-cloud-run.js";
@@ -0,0 +1,42 @@
1
+ /**
2
+ * PrivateEgressNetwork composite: a custom-mode VPC with one subnet a service sends its egress into.
3
+ */
4
+ import { VPCNetwork, Subnetwork } from "../generated/index.js";
5
+ export interface PrivateEgressNetworkProps {
6
+ /** Name prefix: the network is `<name>-vpc`, its subnet `<name>-apps`. */
7
+ name: string;
8
+ /** GCP region of the subnet. */
9
+ region: string;
10
+ /** IP CIDR range of the subnet. */
11
+ subnetCidr: string;
12
+ /** Enable private Google access on the subnet (default: true). */
13
+ privateIpGoogleAccess?: boolean;
14
+ /** Additional labels. */
15
+ labels?: Record<string, string>;
16
+ /** Namespace for all resources. */
17
+ namespace?: string;
18
+ /** Per-member defaults for customizing individual resources. */
19
+ defaults?: {
20
+ vpc?: Partial<ConstructorParameters<typeof VPCNetwork>[0]>;
21
+ subnet?: Partial<ConstructorParameters<typeof Subnetwork>[0]>;
22
+ };
23
+ }
24
+ /**
25
+ * Create a PrivateEgressNetwork composite.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * import { PrivateEgressNetwork } from "@intentius/chant-lexicon-gcp";
30
+ *
31
+ * const { vpc, subnet } = PrivateEgressNetwork({
32
+ * name: "web",
33
+ * region: "us-central1",
34
+ * subnetCidr: "10.10.0.0/24",
35
+ * });
36
+ * ```
37
+ */
38
+ export declare const PrivateEgressNetwork: import("@intentius/chant").CompositeDefinition<PrivateEgressNetworkProps, {
39
+ vpc: import("@intentius/chant").Declarable & Record<string, string>;
40
+ subnet: import("@intentius/chant").Declarable & Record<string, string>;
41
+ }>;
42
+ //# sourceMappingURL=private-egress-network.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"private-egress-network.d.ts","sourceRoot":"","sources":["../../src/composites/private-egress-network.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAEtD,MAAM,WAAW,yBAAyB;IACxC,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,yBAAyB;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,QAAQ,CAAC,EAAE;QACT,GAAG,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3D,MAAM,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KAC/D,CAAC;CACH;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,oBAAoB;;;EAwCP,CAAC"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * PrivateEgressNetwork composite: a custom-mode VPC with one subnet a service sends its egress into.
3
+ */
4
+ import { Composite, mergeDefaults } from "@intentius/chant";
5
+ import { VPCNetwork, Subnetwork } from "../generated/index.js";
6
+ /**
7
+ * Create a PrivateEgressNetwork composite.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { PrivateEgressNetwork } from "@intentius/chant-lexicon-gcp";
12
+ *
13
+ * const { vpc, subnet } = PrivateEgressNetwork({
14
+ * name: "web",
15
+ * region: "us-central1",
16
+ * subnetCidr: "10.10.0.0/24",
17
+ * });
18
+ * ```
19
+ */
20
+ export const PrivateEgressNetwork = Composite((props) => {
21
+ const { name, region, subnetCidr, privateIpGoogleAccess = true, labels: extraLabels = {}, namespace, defaults: defs, } = props;
22
+ const commonLabels = {
23
+ "app.kubernetes.io/name": name,
24
+ "app.kubernetes.io/managed-by": "chant",
25
+ ...extraLabels,
26
+ };
27
+ const vpc = new VPCNetwork(mergeDefaults({
28
+ metadata: {
29
+ name: `${name}-vpc`,
30
+ ...(namespace && { namespace }),
31
+ labels: { ...commonLabels, "app.kubernetes.io/component": "network" },
32
+ },
33
+ autoCreateSubnetworks: false,
34
+ routingMode: "REGIONAL",
35
+ }, defs?.vpc));
36
+ const subnet = new Subnetwork(mergeDefaults({
37
+ metadata: {
38
+ name: `${name}-apps`,
39
+ ...(namespace && { namespace }),
40
+ labels: { ...commonLabels, "app.kubernetes.io/component": "subnet" },
41
+ },
42
+ region,
43
+ ipCidrRange: subnetCidr,
44
+ privateIpGoogleAccess,
45
+ networkRef: { name: vpc.name },
46
+ }, defs?.subnet));
47
+ return { vpc, subnet };
48
+ }, "PrivateEgressNetwork");
@@ -0,0 +1,85 @@
1
+ /**
2
+ * PublicCloudRun composite: a Cloud Run service behind a global external
3
+ * Application Load Balancer, running as its own service account with Direct
4
+ * VPC egress into a subnet.
5
+ */
6
+ import { BackendService, ComputeAddress, ForwardingRule, GCPServiceAccount, ManagedSSLCertificate, RegionNetworkEndpointGroup, TargetHTTPProxy, TargetHTTPSProxy, URLMap } from "../generated/index.js";
7
+ export interface PublicCloudRunProps {
8
+ /** Name prefix every resource is named with. */
9
+ name: string;
10
+ /** Container image. */
11
+ image: string;
12
+ /** GCP region of the service and its network endpoint group. */
13
+ region: string;
14
+ /** GCP project, as `projectRef.external` (default: GCP.ProjectId, resolved from GCP_PROJECT_ID at build). */
15
+ projectId?: string;
16
+ /** VPC the service's egress goes into, such as a PrivateEgressNetwork's `vpc`. */
17
+ vpc: {
18
+ name: string;
19
+ };
20
+ /** Subnet the service's egress goes into, such as a PrivateEgressNetwork's `subnet`. */
21
+ subnet: {
22
+ name: string;
23
+ };
24
+ /** Egress sent through the VPC: "PRIVATE_RANGES_ONLY" or "ALL_TRAFFIC" (default: "PRIVATE_RANGES_ONLY"). */
25
+ vpcEgress?: string;
26
+ /** Port the container listens on (default: 8080). */
27
+ port?: number;
28
+ /** Port the HTTP forwarding rule listens on (default: 80). */
29
+ frontendPort?: number;
30
+ /** Minimum instances (default: 0). */
31
+ minInstanceCount?: number;
32
+ /** Maximum instances (default: 100). */
33
+ maxInstanceCount?: number;
34
+ /** Environment variables. */
35
+ env?: Array<{
36
+ name: string;
37
+ value: string;
38
+ }>;
39
+ /** An existing service account to run as: an email, or the account id of an IAMServiceAccount in the project. When unset, the composite creates `<name>-run`. */
40
+ serviceAccount?: string;
41
+ /** Domains for a Google-managed certificate. When set, the composite adds an HTTPS proxy and a rule on 443, and the HTTP rule redirects to HTTPS. */
42
+ domains?: string[];
43
+ /** Name of a ComputeSecurityPolicy (Cloud Armor) attached to the backend service. */
44
+ securityPolicy?: string;
45
+ /** Additional labels. */
46
+ labels?: Record<string, string>;
47
+ /** Namespace for all resources. */
48
+ namespace?: string;
49
+ /** Per-member defaults for customizing individual resources. */
50
+ defaults?: {
51
+ runtime?: Partial<ConstructorParameters<typeof GCPServiceAccount>[0]>;
52
+ service?: Record<string, unknown>;
53
+ publicAccess?: Record<string, unknown>;
54
+ neg?: Partial<ConstructorParameters<typeof RegionNetworkEndpointGroup>[0]>;
55
+ backend?: Partial<ConstructorParameters<typeof BackendService>[0]>;
56
+ urlMap?: Partial<ConstructorParameters<typeof URLMap>[0]>;
57
+ proxy?: Partial<ConstructorParameters<typeof TargetHTTPProxy>[0]>;
58
+ address?: Partial<ConstructorParameters<typeof ComputeAddress>[0]>;
59
+ rule?: Partial<ConstructorParameters<typeof ForwardingRule>[0]>;
60
+ certificate?: Partial<ConstructorParameters<typeof ManagedSSLCertificate>[0]>;
61
+ httpsProxy?: Partial<ConstructorParameters<typeof TargetHTTPSProxy>[0]>;
62
+ httpsRule?: Partial<ConstructorParameters<typeof ForwardingRule>[0]>;
63
+ redirectMap?: Partial<ConstructorParameters<typeof URLMap>[0]>;
64
+ };
65
+ }
66
+ /**
67
+ * Create a PublicCloudRun composite.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * import { PrivateEgressNetwork, PublicCloudRun } from "@intentius/chant-lexicon-gcp";
72
+ *
73
+ * const network = PrivateEgressNetwork({ name: "web", region: "us-central1", subnetCidr: "10.10.0.0/24" });
74
+ * const web = PublicCloudRun({
75
+ * name: "web",
76
+ * projectId: "my-project",
77
+ * region: "us-central1",
78
+ * vpc: network.vpc,
79
+ * subnet: network.subnet,
80
+ * image: "us-docker.pkg.dev/my-project/web/site:1",
81
+ * });
82
+ * ```
83
+ */
84
+ export declare const PublicCloudRun: import("@intentius/chant").CompositeDefinition<PublicCloudRunProps, Record<string, any>>;
85
+ //# sourceMappingURL=public-cloud-run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-cloud-run.d.ts","sourceRoot":"","sources":["../../src/composites/public-cloud-run.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,144 @@
1
+ /**
2
+ * PublicCloudRun composite: a Cloud Run service behind a global external
3
+ * Application Load Balancer, running as its own service account with Direct
4
+ * VPC egress into a subnet.
5
+ */
6
+ import { Composite, mergeDefaults } from "@intentius/chant";
7
+ import { BackendService, ComputeAddress, ForwardingRule, GCPServiceAccount, ManagedSSLCertificate, RegionNetworkEndpointGroup, TargetHTTPProxy, TargetHTTPSProxy, URLMap, } from "../generated/index.js";
8
+ import { GCP } from "../pseudo.js";
9
+ import { CloudRunServiceComposite } from "./cloud-run-service.js";
10
+ /**
11
+ * Create a PublicCloudRun composite.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { PrivateEgressNetwork, PublicCloudRun } from "@intentius/chant-lexicon-gcp";
16
+ *
17
+ * const network = PrivateEgressNetwork({ name: "web", region: "us-central1", subnetCidr: "10.10.0.0/24" });
18
+ * const web = PublicCloudRun({
19
+ * name: "web",
20
+ * projectId: "my-project",
21
+ * region: "us-central1",
22
+ * vpc: network.vpc,
23
+ * subnet: network.subnet,
24
+ * image: "us-docker.pkg.dev/my-project/web/site:1",
25
+ * });
26
+ * ```
27
+ */
28
+ export const PublicCloudRun = Composite((props) => {
29
+ const { name, image, region, projectId, vpc, subnet, vpcEgress = "PRIVATE_RANGES_ONLY", port = 8080, frontendPort = 80, minInstanceCount = 0, maxInstanceCount = 100, env, serviceAccount, domains, securityPolicy, labels: extraLabels = {}, namespace, defaults: defs, } = props;
30
+ const commonLabels = {
31
+ "app.kubernetes.io/name": name,
32
+ "app.kubernetes.io/managed-by": "chant",
33
+ ...extraLabels,
34
+ };
35
+ const meta = (suffix, component) => ({
36
+ name: `${name}-${suffix}`,
37
+ ...(namespace && { namespace }),
38
+ labels: { ...commonLabels, "app.kubernetes.io/component": component },
39
+ });
40
+ const result = {};
41
+ if (!serviceAccount) {
42
+ result.runtime = new GCPServiceAccount(mergeDefaults({
43
+ metadata: meta("run", "service-account"),
44
+ displayName: `${name} Cloud Run runtime`,
45
+ }, defs?.runtime));
46
+ }
47
+ const run = CloudRunServiceComposite({
48
+ name: `${name}-site`,
49
+ image,
50
+ projectId,
51
+ location: region,
52
+ port,
53
+ minInstanceCount,
54
+ maxInstanceCount,
55
+ env,
56
+ publicAccess: true,
57
+ serviceAccount: serviceAccount ?? `${name}-run`,
58
+ labels: extraLabels,
59
+ namespace,
60
+ defaults: {
61
+ service: mergeDefaults({
62
+ template: {
63
+ vpcAccess: {
64
+ egress: vpcEgress,
65
+ networkInterfaces: [{ networkRef: { name: vpc.name }, subnetworkRef: { name: subnet.name } }],
66
+ },
67
+ },
68
+ }, defs?.service),
69
+ publicIam: defs?.publicAccess,
70
+ },
71
+ });
72
+ result.service = run.service;
73
+ result.publicAccess = run.publicIam;
74
+ const neg = new RegionNetworkEndpointGroup(mergeDefaults({
75
+ metadata: meta("neg", "neg"),
76
+ region,
77
+ networkEndpointType: "SERVERLESS",
78
+ cloudRun: { serviceRef: { name: run.service.name } },
79
+ }, defs?.neg));
80
+ // A backend service over a serverless NEG takes no health check: Cloud Run
81
+ // reports its own readiness.
82
+ const backend = new BackendService(mergeDefaults({
83
+ metadata: meta("backend", "backend"),
84
+ location: "global",
85
+ loadBalancingScheme: "EXTERNAL_MANAGED",
86
+ protocol: "HTTP",
87
+ backend: [{ group: { networkEndpointGroupRef: { name: neg.name } } }],
88
+ ...(securityPolicy && { securityPolicyRef: { name: securityPolicy } }),
89
+ }, defs?.backend));
90
+ const urlMap = new URLMap(mergeDefaults({
91
+ metadata: meta("map", "url-map"),
92
+ location: "global",
93
+ defaultService: { backendServiceRef: { name: backend.name } },
94
+ }, defs?.urlMap));
95
+ const address = new ComputeAddress(mergeDefaults({
96
+ metadata: meta("ip", "address"),
97
+ location: "global",
98
+ }, defs?.address));
99
+ Object.assign(result, { neg, backend, urlMap, address });
100
+ // With domains, HTTPS serves the URL map and HTTP serves a redirect to it.
101
+ let httpMapName = urlMap.name;
102
+ if (domains && domains.length > 0) {
103
+ const certificate = new ManagedSSLCertificate(mergeDefaults({
104
+ metadata: meta("cert", "certificate"),
105
+ projectRef: { external: projectId ?? GCP.ProjectId },
106
+ managed: { domains },
107
+ }, defs?.certificate));
108
+ const httpsProxy = new TargetHTTPSProxy(mergeDefaults({
109
+ metadata: meta("https-proxy", "proxy"),
110
+ location: "global",
111
+ urlMapRef: { name: urlMap.name },
112
+ sslCertificates: [{ name: certificate.name }],
113
+ }, defs?.httpsProxy));
114
+ const httpsRule = new ForwardingRule(mergeDefaults({
115
+ metadata: meta("https-rule", "forwarding-rule"),
116
+ location: "global",
117
+ loadBalancingScheme: "EXTERNAL_MANAGED",
118
+ portRange: "443",
119
+ ipAddress: { addressRef: { name: address.name } },
120
+ target: { targetHTTPSProxyRef: { name: httpsProxy.name } },
121
+ }, defs?.httpsRule));
122
+ const redirectMap = new URLMap(mergeDefaults({
123
+ metadata: meta("redirect", "url-map"),
124
+ location: "global",
125
+ defaultUrlRedirect: { httpsRedirect: true, stripQuery: false },
126
+ }, defs?.redirectMap));
127
+ httpMapName = redirectMap.name;
128
+ Object.assign(result, { certificate, httpsProxy, httpsRule, redirectMap });
129
+ }
130
+ result.proxy = new TargetHTTPProxy(mergeDefaults({
131
+ metadata: meta("proxy", "proxy"),
132
+ location: "global",
133
+ urlMapRef: { name: httpMapName },
134
+ }, defs?.proxy));
135
+ result.rule = new ForwardingRule(mergeDefaults({
136
+ metadata: meta("rule", "forwarding-rule"),
137
+ location: "global",
138
+ loadBalancingScheme: "EXTERNAL_MANAGED",
139
+ portRange: String(frontendPort),
140
+ ipAddress: { addressRef: { name: address.name } },
141
+ target: { targetHTTPProxyRef: { name: result.proxy.name } },
142
+ }, defs?.rule));
143
+ return result;
144
+ }, "PublicCloudRun");
@@ -82,6 +82,11 @@ export declare function restToCnrmShape(body: Record<string, unknown>): Record<s
82
82
  interface RefineContext {
83
83
  project: string;
84
84
  restName?: string;
85
+ /** A forwarding rule's declared `ipAddress.addressRef` and that address's live IP (#4238). */
86
+ address?: {
87
+ ip: string;
88
+ ref: unknown;
89
+ };
85
90
  }
86
91
  /**
87
92
  * A REST payload as a CNRM-shaped property tree, in the declared side's
@@ -1 +1 @@
1
- {"version":3,"file":"deep-observe.d.ts","sourceRoot":"","sources":["../src/deep-observe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,OAAO,KAAK,EACV,qBAAqB,EAGtB,MAAM,0BAA0B,CAAC;AAKlC,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAajE,OAAO,EAAE,yBAAyB,EAAE,CAAC;AAErC,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAWtF;AAUD;6EAC6E;AAC7E,UAAU,aAAa;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AA0OD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASnH;AAED;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAyF5G"}
1
+ {"version":3,"file":"deep-observe.d.ts","sourceRoot":"","sources":["../src/deep-observe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,OAAO,KAAK,EACV,qBAAqB,EAGtB,MAAM,0BAA0B,CAAC;AAKlC,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAajE,OAAO,EAAE,yBAAyB,EAAE,CAAC;AAErC,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAWtF;AAUD;6EAC6E;AAC7E,UAAU,aAAa;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8FAA8F;IAC9F,OAAO,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,OAAO,CAAA;KAAE,CAAC;CACxC;AAgPD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASnH;AA6BD;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA0F5G"}
@@ -299,8 +299,14 @@ const CNRM_REFINERS = {
299
299
  delete tree.gatewayAddress;
300
300
  delete tree.fingerprint;
301
301
  },
302
- ComputeForwardingRule(tree) {
302
+ ComputeForwardingRule(tree, ctx) {
303
303
  computeLocation(tree);
304
+ // Compute returns the IP; CNRM writes `ipAddress: { addressRef }` or `{ ip }`.
305
+ // The IP is the declared address's own when the read found it so (#4238).
306
+ if (typeof tree.IPAddress === "string") {
307
+ tree.ipAddress = ctx.address && ctx.address.ip === tree.IPAddress ? { addressRef: ctx.address.ref } : { ip: tree.IPAddress };
308
+ delete tree.IPAddress;
309
+ }
304
310
  if (tree.IPProtocol !== undefined) {
305
311
  tree.ipProtocol = tree.IPProtocol;
306
312
  delete tree.IPProtocol;
@@ -352,6 +358,31 @@ export function toCnrmTree(kind, body, ctx) {
352
358
  CNRM_REFINERS[kind]?.(tree, { ...ctx, restName });
353
359
  return tree;
354
360
  }
361
+ /**
362
+ * The live IP of the address a forwarding rule declares in
363
+ * `ipAddress.addressRef`, read with the address's own declaration when the
364
+ * manifest holds it, so the rule's `IPAddress` can be matched to the ref
365
+ * rather than read as drift (#4238). Undefined when there is no ref or the
366
+ * read fails; the rule then shows the IP as it is.
367
+ */
368
+ async function declaredAddressIp(client, props, manifest) {
369
+ const spec = props.spec ?? props;
370
+ const ref = spec.ipAddress?.addressRef;
371
+ if (!ref)
372
+ return undefined;
373
+ const raw = typeof ref.name === "string" ? ref.name : typeof ref.external === "string" ? ref.external : undefined;
374
+ if (!raw)
375
+ return undefined;
376
+ const name = raw.slice(raw.lastIndexOf("/") + 1);
377
+ const declared = manifest.find((r) => r.kind === "ComputeAddress" && r.metadata?.name === name);
378
+ try {
379
+ const body = await getResource(client, "ComputeAddress", name, declared?.spec ?? { location: spec.location });
380
+ return typeof body.address === "string" ? { ip: body.address, ref } : undefined;
381
+ }
382
+ catch {
383
+ return undefined;
384
+ }
385
+ }
355
386
  /**
356
387
  * Read the live property tree for each declared entity over the applier's
357
388
  * REST transport, normalized into the declared CNRM shape (see the module
@@ -399,6 +430,7 @@ export async function observeResourcesDeepGcp(options) {
399
430
  try {
400
431
  const obj = await getResource(client, gvk.kind, name, props);
401
432
  const labels = obj.labels;
433
+ const address = gvk.kind === "ComputeForwardingRule" ? await declaredAddressIp(client, props, manifest) : undefined;
402
434
  // owned filter: withhold what does not carry chant's marker. Withheld is
403
435
  // not absent (#1089). Where the payload has no labels at all there is
404
436
  // nothing to filter on, so the resource passes through — the same
@@ -414,7 +446,7 @@ export async function observeResourcesDeepGcp(options) {
414
446
  resources[entityName] = {
415
447
  type: entityType,
416
448
  physicalId: obj.id ?? obj.selfLink ?? obj.name,
417
- properties: normalizeDeepProperties(toCnrmTree(gvk.kind, obj, { project: client.project }), {
449
+ properties: normalizeDeepProperties(toCnrmTree(gvk.kind, obj, { project: client.project, ...(address ? { address } : {}) }), {
418
450
  entityType,
419
451
  side: "live",
420
452
  // The static table is the whole prune here — there is no per-resource