@microagi/alchemy-gcp 0.1.2

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 (123) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +88 -0
  3. package/lib/Auth/AuthProvider.d.ts +48 -0
  4. package/lib/Auth/AuthProvider.d.ts.map +1 -0
  5. package/lib/Auth/AuthProvider.js +84 -0
  6. package/lib/Auth/AuthProvider.js.map +1 -0
  7. package/lib/Auth/Credentials.d.ts +28 -0
  8. package/lib/Auth/Credentials.d.ts.map +1 -0
  9. package/lib/Auth/Credentials.js +41 -0
  10. package/lib/Auth/Credentials.js.map +1 -0
  11. package/lib/CloudResourceManager/Project.d.ts +143 -0
  12. package/lib/CloudResourceManager/Project.d.ts.map +1 -0
  13. package/lib/CloudResourceManager/Project.js +272 -0
  14. package/lib/CloudResourceManager/Project.js.map +1 -0
  15. package/lib/CloudResourceManager/index.d.ts +3 -0
  16. package/lib/CloudResourceManager/index.d.ts.map +1 -0
  17. package/lib/CloudResourceManager/index.js +2 -0
  18. package/lib/CloudResourceManager/index.js.map +1 -0
  19. package/lib/Compute/GlobalAddress.d.ts +101 -0
  20. package/lib/Compute/GlobalAddress.d.ts.map +1 -0
  21. package/lib/Compute/GlobalAddress.js +134 -0
  22. package/lib/Compute/GlobalAddress.js.map +1 -0
  23. package/lib/Compute/Network.d.ts +114 -0
  24. package/lib/Compute/Network.d.ts.map +1 -0
  25. package/lib/Compute/Network.js +145 -0
  26. package/lib/Compute/Network.js.map +1 -0
  27. package/lib/Compute/Operations.d.ts +34 -0
  28. package/lib/Compute/Operations.d.ts.map +1 -0
  29. package/lib/Compute/Operations.js +62 -0
  30. package/lib/Compute/Operations.js.map +1 -0
  31. package/lib/Compute/SharedVpcHost.d.ts +44 -0
  32. package/lib/Compute/SharedVpcHost.d.ts.map +1 -0
  33. package/lib/Compute/SharedVpcHost.js +71 -0
  34. package/lib/Compute/SharedVpcHost.js.map +1 -0
  35. package/lib/Compute/SharedVpcServiceProject.d.ts +46 -0
  36. package/lib/Compute/SharedVpcServiceProject.d.ts.map +1 -0
  37. package/lib/Compute/SharedVpcServiceProject.js +96 -0
  38. package/lib/Compute/SharedVpcServiceProject.js.map +1 -0
  39. package/lib/Compute/Subnetwork.d.ts +108 -0
  40. package/lib/Compute/Subnetwork.d.ts.map +1 -0
  41. package/lib/Compute/Subnetwork.js +201 -0
  42. package/lib/Compute/Subnetwork.js.map +1 -0
  43. package/lib/Compute/index.d.ts +6 -0
  44. package/lib/Compute/index.d.ts.map +1 -0
  45. package/lib/Compute/index.js +6 -0
  46. package/lib/Compute/index.js.map +1 -0
  47. package/lib/Container/Cluster.d.ts +216 -0
  48. package/lib/Container/Cluster.d.ts.map +1 -0
  49. package/lib/Container/Cluster.js +326 -0
  50. package/lib/Container/Cluster.js.map +1 -0
  51. package/lib/Container/NodePool.d.ts +214 -0
  52. package/lib/Container/NodePool.d.ts.map +1 -0
  53. package/lib/Container/NodePool.js +344 -0
  54. package/lib/Container/NodePool.js.map +1 -0
  55. package/lib/Container/Operations.d.ts +50 -0
  56. package/lib/Container/Operations.d.ts.map +1 -0
  57. package/lib/Container/Operations.js +51 -0
  58. package/lib/Container/Operations.js.map +1 -0
  59. package/lib/Container/index.d.ts +5 -0
  60. package/lib/Container/index.d.ts.map +1 -0
  61. package/lib/Container/index.js +3 -0
  62. package/lib/Container/index.js.map +1 -0
  63. package/lib/Parallelstore/Instance.d.ts +119 -0
  64. package/lib/Parallelstore/Instance.d.ts.map +1 -0
  65. package/lib/Parallelstore/Instance.js +180 -0
  66. package/lib/Parallelstore/Instance.js.map +1 -0
  67. package/lib/Parallelstore/index.d.ts +2 -0
  68. package/lib/Parallelstore/index.d.ts.map +1 -0
  69. package/lib/Parallelstore/index.js +2 -0
  70. package/lib/Parallelstore/index.js.map +1 -0
  71. package/lib/Providers.d.ts +24 -0
  72. package/lib/Providers.d.ts.map +1 -0
  73. package/lib/Providers.js +51 -0
  74. package/lib/Providers.js.map +1 -0
  75. package/lib/ServiceNetworking/PsaConnection.d.ts +84 -0
  76. package/lib/ServiceNetworking/PsaConnection.d.ts.map +1 -0
  77. package/lib/ServiceNetworking/PsaConnection.js +130 -0
  78. package/lib/ServiceNetworking/PsaConnection.js.map +1 -0
  79. package/lib/ServiceNetworking/index.d.ts +2 -0
  80. package/lib/ServiceNetworking/index.d.ts.map +1 -0
  81. package/lib/ServiceNetworking/index.js +2 -0
  82. package/lib/ServiceNetworking/index.js.map +1 -0
  83. package/lib/ServiceUsage/ApiEnable.d.ts +78 -0
  84. package/lib/ServiceUsage/ApiEnable.d.ts.map +1 -0
  85. package/lib/ServiceUsage/ApiEnable.js +132 -0
  86. package/lib/ServiceUsage/ApiEnable.js.map +1 -0
  87. package/lib/ServiceUsage/index.d.ts +3 -0
  88. package/lib/ServiceUsage/index.d.ts.map +1 -0
  89. package/lib/ServiceUsage/index.js +2 -0
  90. package/lib/ServiceUsage/index.js.map +1 -0
  91. package/lib/Tags.d.ts +57 -0
  92. package/lib/Tags.d.ts.map +1 -0
  93. package/lib/Tags.js +106 -0
  94. package/lib/Tags.js.map +1 -0
  95. package/lib/index.d.ts +11 -0
  96. package/lib/index.d.ts.map +1 -0
  97. package/lib/index.js +10 -0
  98. package/lib/index.js.map +1 -0
  99. package/package.json +70 -0
  100. package/src/Auth/AuthProvider.ts +180 -0
  101. package/src/Auth/Credentials.ts +69 -0
  102. package/src/CloudResourceManager/Project.ts +517 -0
  103. package/src/CloudResourceManager/index.ts +2 -0
  104. package/src/Compute/GlobalAddress.ts +283 -0
  105. package/src/Compute/Network.ts +306 -0
  106. package/src/Compute/Operations.ts +108 -0
  107. package/src/Compute/SharedVpcHost.ts +141 -0
  108. package/src/Compute/SharedVpcServiceProject.ts +185 -0
  109. package/src/Compute/Subnetwork.ts +375 -0
  110. package/src/Compute/index.ts +5 -0
  111. package/src/Container/Cluster.ts +599 -0
  112. package/src/Container/NodePool.ts +620 -0
  113. package/src/Container/Operations.ts +88 -0
  114. package/src/Container/index.ts +8 -0
  115. package/src/Parallelstore/Instance.ts +371 -0
  116. package/src/Parallelstore/index.ts +1 -0
  117. package/src/Providers.ts +84 -0
  118. package/src/ServiceNetworking/PsaConnection.ts +269 -0
  119. package/src/ServiceNetworking/index.ts +1 -0
  120. package/src/ServiceUsage/ApiEnable.ts +267 -0
  121. package/src/ServiceUsage/index.ts +2 -0
  122. package/src/Tags.ts +130 -0
  123. package/src/index.ts +16 -0
@@ -0,0 +1,620 @@
1
+ import * as cont from "@distilled.cloud/gcp/container-v1";
2
+ import { Resource } from "alchemy";
3
+ import { Unowned } from "alchemy/AdoptPolicy";
4
+ import type { ScopedPlanStatusSession } from "alchemy/Cli/Cli";
5
+ import { deepEqual, isResolved, somePropsAreDifferent } from "alchemy/Diff";
6
+ import { createPhysicalName } from "alchemy/PhysicalName";
7
+ import * as Provider from "alchemy/Provider";
8
+ import * as Effect from "effect/Effect";
9
+ import { gcpInternalLabels, hasAlchemyLabels } from "../Tags.ts";
10
+ import type * as GCP from "../Providers.ts";
11
+ import { makeAwaitOperation, qualifyOperationName } from "./Operations.ts";
12
+
13
+ export type NodePoolProps = {
14
+ /** GCP project ID. Immutable — replace if changed. */
15
+ project: string;
16
+ /**
17
+ * Cluster zone (e.g. `us-central1-a`) for a zonal cluster, or region
18
+ * (e.g. `us-central1`) for a regional cluster. Immutable — replace.
19
+ */
20
+ location: string;
21
+ /**
22
+ * Parent cluster name. Immutable — node pools cannot be reparented; a
23
+ * change here triggers a replace.
24
+ */
25
+ clusterName: string;
26
+ /**
27
+ * Pool name. Defaults to `createPhysicalName({ id, lowercase: true,
28
+ * maxLength: 40 })`. Lowercase alphanumeric + hyphens, ≤40 chars.
29
+ * Immutable — replace if changed.
30
+ */
31
+ name?: string;
32
+ /**
33
+ * Initial size at create time. GKE Standard rejects pool creation
34
+ * without a positive node count, so when omitted we default to
35
+ * `autoscaling.minNodeCount ?? 1` on the create path.
36
+ *
37
+ * Subsequent size changes go via `setSize` when `autoscaling` is
38
+ * disabled, or are GKE-managed when `autoscaling.enabled` is true
39
+ * (the size sync is skipped to avoid fighting the autoscaler).
40
+ */
41
+ initialNodeCount?: number;
42
+ /**
43
+ * Per-pool zones. Override the cluster default. Mutable via
44
+ * `update` — sent in a standalone update body to avoid the SDK's
45
+ * "do not combine with other fields" warning.
46
+ */
47
+ nodeLocations?: ReadonlyArray<string>;
48
+ /** Node configuration (machine, disk, accelerators, taints, labels, …). */
49
+ config: {
50
+ /** GCE machine type, e.g. `n2-standard-4`. Mutable via `update`. */
51
+ machineType: string;
52
+ /** Boot disk size GB. Mutable via `update`. */
53
+ diskSizeGb?: number;
54
+ /** Boot disk type, e.g. `pd-balanced`. Mutable via `update`. */
55
+ diskType?: string;
56
+ /**
57
+ * Image, e.g. `COS_CONTAINERD`, `UBUNTU_CONTAINERD`. Omit to let
58
+ * GCP apply the GKE default for the cluster's release channel.
59
+ * Mutable via `update`.
60
+ */
61
+ imageType?: string;
62
+ /** GPUs / TPUs. Mutable via `update`. */
63
+ accelerators?: ReadonlyArray<{
64
+ type: string;
65
+ count: number;
66
+ gpuPartitionSize?: string;
67
+ gpuDriverInstallationConfig?: { gpuDriverVersion?: string };
68
+ }>;
69
+ /**
70
+ * GCE service account. Omit to let GCP fall back to the project's
71
+ * default compute SA. **Replace-only** — not in `UpdateNodePoolRequest`.
72
+ */
73
+ serviceAccount?: string;
74
+ /**
75
+ * GCP API scopes. Omit to let GCP apply its default. **Replace-only**.
76
+ */
77
+ oauthScopes?: ReadonlyArray<string>;
78
+ /** Kubernetes node taints. Mutable via `update`. */
79
+ taints?: ReadonlyArray<{
80
+ key: string;
81
+ value: string;
82
+ effect: "NO_SCHEDULE" | "PREFER_NO_SCHEDULE" | "NO_EXECUTE";
83
+ }>;
84
+ /** Kubernetes node labels (NOT GCP labels). Mutable via `update`. */
85
+ labels?: Record<string, string>;
86
+ /**
87
+ * GCE resource labels. Alchemy internal labels are merged on top
88
+ * automatically. Mutable via `update`.
89
+ */
90
+ resourceLabels?: Record<string, string>;
91
+ /** Custom GCE metadata. **Replace-only**. */
92
+ metadata?: Record<string, string>;
93
+ /** Custom GCE tags (firewall). Mutable via `update`. */
94
+ tags?: ReadonlyArray<string>;
95
+ /** Min CPU platform. **Replace-only**. */
96
+ minCpuPlatform?: string;
97
+ /** Boot disk CMEK. **Replace-only**. */
98
+ bootDiskKmsKey?: string;
99
+ /** GKE Sandbox (gVisor). **Replace-only**. */
100
+ sandboxConfig?: { type: "GVISOR" };
101
+ /** Spot or PVM. **Replace-only**. */
102
+ preemptible?: boolean;
103
+ /** Spot VM. **Replace-only**. */
104
+ spot?: boolean;
105
+ /** Reservation affinity. **Replace-only**. */
106
+ reservationAffinity?: {
107
+ consumeReservationType: string;
108
+ key?: string;
109
+ values?: ReadonlyArray<string>;
110
+ };
111
+ /** Shielded VM. **Replace-only**. */
112
+ shieldedInstanceConfig?: {
113
+ enableSecureBoot?: boolean;
114
+ enableIntegrityMonitoring?: boolean;
115
+ };
116
+ /** Local SSDs. **Replace-only**. */
117
+ localSsdCount?: number;
118
+ /** Workload Identity metadata. Mutable via `update`. */
119
+ workloadMetadataConfig?: { mode: "GKE_METADATA" | "GCE_METADATA" };
120
+ /** GVNIC. Mutable via `update`. */
121
+ gvnic?: { enabled: boolean };
122
+ /** Confidential VM. Mutable via `update`. */
123
+ confidentialNodes?: { enabled: boolean };
124
+ };
125
+ /** Cluster autoscaler. Mutable via `setAutoscaling`. */
126
+ autoscaling?: {
127
+ enabled: boolean;
128
+ minNodeCount?: number;
129
+ maxNodeCount?: number;
130
+ locationPolicy?: "BALANCED" | "ANY";
131
+ };
132
+ /** Auto-upgrade / auto-repair. Mutable via `setManagement`. */
133
+ management?: { autoUpgrade?: boolean; autoRepair?: boolean };
134
+ /** Surge upgrade settings. Mutable via `update`. */
135
+ upgradeSettings?: {
136
+ maxSurge?: number;
137
+ maxUnavailable?: number;
138
+ strategy?: "SURGE" | "BLUE_GREEN";
139
+ };
140
+ };
141
+
142
+ export type NodePoolAttributes = {
143
+ /** Pool name (matches the GKE resource). */
144
+ name: string;
145
+ /** Server-defined URL for the pool. */
146
+ selfLink: string;
147
+ /** GCP project ID, threaded through from props for `delete`/`read`. */
148
+ project: string;
149
+ /** Cluster zone or region, threaded through from props. */
150
+ location: string;
151
+ /** Parent cluster name. */
152
+ clusterName: string;
153
+ /** Current Kubernetes version of nodes in this pool. */
154
+ version: string | undefined;
155
+ /** Lifecycle status. */
156
+ status: string | undefined;
157
+ /** Managed instance group URLs backing the pool. */
158
+ instanceGroupUrls: ReadonlyArray<string>;
159
+ };
160
+
161
+ /**
162
+ * A node pool inside a GKE Standard cluster.
163
+ *
164
+ * Lifecycle is observe → ensure (create) → sync (size, autoscaling,
165
+ * management, generic update) → return.
166
+ *
167
+ * Adoption: a pool whose `config.resourceLabels` lack our `alchemy_*`
168
+ * keys is wrapped in `Unowned(attrs)`. Useful for adopting the inline
169
+ * bootstrap pool created by `GCP.Cluster.initialNodePool` (which
170
+ * intentionally carries no alchemy labels).
171
+ *
172
+ * @section Creating a node pool
173
+ * @example CPU pool
174
+ * ```typescript
175
+ * const compute = yield* GCP.NodePool("compute", {
176
+ * project: project.projectId,
177
+ * location: cluster.location,
178
+ * clusterName: cluster.name,
179
+ * initialNodeCount: 1,
180
+ * config: { machineType: "n2-standard-16" },
181
+ * });
182
+ * ```
183
+ *
184
+ * @example GPU pool with autoscaling
185
+ * ```typescript
186
+ * const rtx6000 = yield* GCP.NodePool("rtx6000", {
187
+ * project: project.projectId,
188
+ * location: cluster.location,
189
+ * clusterName: cluster.name,
190
+ * config: {
191
+ * machineType: "g2-standard-12",
192
+ * accelerators: [{ type: "nvidia-l40s", count: 1 }],
193
+ * },
194
+ * autoscaling: { enabled: true, minNodeCount: 0, maxNodeCount: 4 },
195
+ * });
196
+ * ```
197
+ */
198
+ export type NodePool = Resource<
199
+ "GCP.NodePool",
200
+ NodePoolProps,
201
+ NodePoolAttributes,
202
+ never,
203
+ GCP.Providers
204
+ >;
205
+ export const NodePool = Resource<NodePool>("GCP.NodePool");
206
+
207
+ /**
208
+ * Translate a public `accelerators` spec into the SDK's
209
+ * `AcceleratorConfig`. Same shape on both create and update paths.
210
+ */
211
+ const toAcceleratorConfig = (
212
+ accelerators: NodePoolProps["config"]["accelerators"] | undefined,
213
+ ): ReadonlyArray<cont.AcceleratorConfig> | undefined =>
214
+ accelerators?.map((a) => ({
215
+ acceleratorType: a.type,
216
+ acceleratorCount: String(a.count),
217
+ ...(a.gpuPartitionSize ? { gpuPartitionSize: a.gpuPartitionSize } : {}),
218
+ ...(a.gpuDriverInstallationConfig
219
+ ? { gpuDriverInstallationConfig: a.gpuDriverInstallationConfig }
220
+ : {}),
221
+ }));
222
+
223
+ /**
224
+ * Build the `NodeConfig` sub-object for a create body. Numbers stay as
225
+ * numbers; collections stay unwrapped (this is what GKE expects on
226
+ * create — see SDK NodeConfig at container-v1.ts:1583).
227
+ *
228
+ * Internal alchemy labels are merged into `resourceLabels` here so the
229
+ * pool is recognizable to `read`'s adoption gate. Exported for reuse by
230
+ * `Cluster.toInitialNodePoolBody`.
231
+ */
232
+ export const toNodeConfigCreateBody = (
233
+ config: NodePoolProps["config"],
234
+ desiredResourceLabels: Record<string, string>,
235
+ ): cont.NodeConfig => {
236
+ const accelerators = toAcceleratorConfig(config.accelerators);
237
+ return {
238
+ machineType: config.machineType,
239
+ ...(config.diskSizeGb !== undefined ? { diskSizeGb: config.diskSizeGb } : {}),
240
+ ...(config.diskType ? { diskType: config.diskType } : {}),
241
+ ...(config.imageType ? { imageType: config.imageType } : {}),
242
+ ...(accelerators ? { accelerators } : {}),
243
+ ...(config.serviceAccount ? { serviceAccount: config.serviceAccount } : {}),
244
+ ...(config.oauthScopes ? { oauthScopes: config.oauthScopes } : {}),
245
+ ...(config.taints ? { taints: config.taints } : {}),
246
+ ...(config.labels ? { labels: config.labels } : {}),
247
+ resourceLabels: desiredResourceLabels,
248
+ ...(config.metadata ? { metadata: config.metadata } : {}),
249
+ ...(config.tags ? { tags: config.tags } : {}),
250
+ ...(config.minCpuPlatform ? { minCpuPlatform: config.minCpuPlatform } : {}),
251
+ ...(config.bootDiskKmsKey ? { bootDiskKmsKey: config.bootDiskKmsKey } : {}),
252
+ ...(config.sandboxConfig ? { sandboxConfig: config.sandboxConfig } : {}),
253
+ ...(config.preemptible !== undefined ? { preemptible: config.preemptible } : {}),
254
+ ...(config.spot !== undefined ? { spot: config.spot } : {}),
255
+ ...(config.reservationAffinity
256
+ ? { reservationAffinity: config.reservationAffinity }
257
+ : {}),
258
+ ...(config.shieldedInstanceConfig
259
+ ? { shieldedInstanceConfig: config.shieldedInstanceConfig }
260
+ : {}),
261
+ ...(config.localSsdCount !== undefined
262
+ ? { localSsdCount: config.localSsdCount }
263
+ : {}),
264
+ ...(config.workloadMetadataConfig
265
+ ? { workloadMetadataConfig: config.workloadMetadataConfig }
266
+ : {}),
267
+ ...(config.gvnic ? { gvnic: config.gvnic } : {}),
268
+ ...(config.confidentialNodes ? { confidentialNodes: config.confidentialNodes } : {}),
269
+ };
270
+ };
271
+
272
+ /**
273
+ * Build a non-location `UpdateNodePoolRequest` body, populating only
274
+ * fields whose desired value differs from observed. Wrapping (NetworkTags
275
+ * / NodeLabels / NodeTaints / ResourceLabels) and number→string for
276
+ * `diskSizeGb` are applied at this boundary (SDK shapes diverge between
277
+ * create and update — see container-v1.ts:4289).
278
+ *
279
+ * Returns `undefined` if no field requires updating.
280
+ */
281
+ const toNodePoolUpdateBody = (
282
+ observed: cont.NodePool,
283
+ news: NodePoolProps,
284
+ desiredResourceLabels: Record<string, string>,
285
+ ): cont.UpdateNodePoolRequest | undefined => {
286
+ const body: cont.UpdateNodePoolRequest = {};
287
+ const oc = observed.config ?? {};
288
+ const nc = news.config;
289
+
290
+ if (nc.machineType !== undefined && nc.machineType !== oc.machineType) {
291
+ body.machineType = nc.machineType;
292
+ }
293
+ if (nc.imageType !== undefined && nc.imageType !== oc.imageType) {
294
+ body.imageType = nc.imageType;
295
+ }
296
+ if (nc.diskType !== undefined && nc.diskType !== oc.diskType) {
297
+ body.diskType = nc.diskType;
298
+ }
299
+ if (
300
+ nc.diskSizeGb !== undefined &&
301
+ String(nc.diskSizeGb) !== String(oc.diskSizeGb ?? "")
302
+ ) {
303
+ body.diskSizeGb = String(nc.diskSizeGb);
304
+ }
305
+
306
+ const desiredAccelerators = toAcceleratorConfig(nc.accelerators);
307
+ if (
308
+ desiredAccelerators &&
309
+ !deepEqual(oc.accelerators ?? [], desiredAccelerators)
310
+ ) {
311
+ body.accelerators = desiredAccelerators;
312
+ }
313
+
314
+ if (nc.tags && !deepEqual(oc.tags ?? [], nc.tags)) {
315
+ body.tags = { tags: nc.tags };
316
+ }
317
+ if (nc.labels && !deepEqual(oc.labels ?? {}, nc.labels)) {
318
+ body.labels = { labels: nc.labels };
319
+ }
320
+ if (nc.taints && !deepEqual(oc.taints ?? [], nc.taints)) {
321
+ body.taints = { taints: nc.taints };
322
+ }
323
+ if (!deepEqual(oc.resourceLabels ?? {}, desiredResourceLabels)) {
324
+ body.resourceLabels = { labels: desiredResourceLabels };
325
+ }
326
+
327
+ if (nc.gvnic && !deepEqual(oc.gvnic, nc.gvnic)) {
328
+ body.gvnic = nc.gvnic;
329
+ }
330
+ if (nc.confidentialNodes && !deepEqual(oc.confidentialNodes, nc.confidentialNodes)) {
331
+ body.confidentialNodes = nc.confidentialNodes;
332
+ }
333
+ if (
334
+ nc.workloadMetadataConfig &&
335
+ !deepEqual(oc.workloadMetadataConfig, nc.workloadMetadataConfig)
336
+ ) {
337
+ body.workloadMetadataConfig = nc.workloadMetadataConfig;
338
+ }
339
+ if (
340
+ news.upgradeSettings &&
341
+ !deepEqual(observed.upgradeSettings, news.upgradeSettings)
342
+ ) {
343
+ body.upgradeSettings = news.upgradeSettings;
344
+ }
345
+
346
+ return Object.keys(body).length === 0 ? undefined : body;
347
+ };
348
+
349
+ const toNodePoolAttributes = (
350
+ pool: cont.NodePool,
351
+ parent: { project: string; location: string; clusterName: string },
352
+ ): NodePoolAttributes => ({
353
+ name: pool.name ?? "",
354
+ selfLink: pool.selfLink ?? "",
355
+ project: parent.project,
356
+ location: parent.location,
357
+ clusterName: parent.clusterName,
358
+ version: pool.version,
359
+ status: pool.status,
360
+ instanceGroupUrls: pool.instanceGroupUrls ?? [],
361
+ });
362
+
363
+ export const NodePoolProvider = () =>
364
+ Provider.effect(
365
+ NodePool,
366
+ Effect.gen(function* () {
367
+ const getNodePools = yield* cont.getProjectsLocationsClustersNodePools;
368
+ const createNodePools = yield* cont.createProjectsLocationsClustersNodePools;
369
+ const updateNodePools = yield* cont.updateProjectsLocationsClustersNodePools;
370
+ const setSize = yield* cont.setSizeProjectsLocationsClustersNodePools;
371
+ const setAutoscaling = yield* cont.setAutoscalingProjectsLocationsClustersNodePools;
372
+ const setManagement = yield* cont.setManagementProjectsLocationsClustersNodePools;
373
+ const deleteNodePools = yield* cont.deleteProjectsLocationsClustersNodePools;
374
+ const getOperations = yield* cont.getProjectsLocationsOperations;
375
+ const awaitOperation = makeAwaitOperation(getOperations);
376
+
377
+ // Qualify a bare GKE operation id using the parent path of the
378
+ // resource it targets. NodePool fqNames have the shape
379
+ // `projects/{p}/locations/{l}/clusters/{c}/nodePools/{n}`; the
380
+ // first four segments give us the parent context for the
381
+ // operation, identical to Cluster's helper.
382
+ const qualifyOp = (fqName: string, opName: string) =>
383
+ qualifyOperationName(
384
+ fqName.split("/")[1],
385
+ fqName.split("/")[3],
386
+ opName,
387
+ );
388
+
389
+ const syncNodePoolSize = Effect.fn(function* (args: {
390
+ fqName: string;
391
+ observed: cont.NodePool;
392
+ news: NodePoolProps;
393
+ session: ScopedPlanStatusSession;
394
+ }) {
395
+ // Skip when the autoscaler is enabled; fighting it creates churn.
396
+ if (args.news.autoscaling?.enabled) return;
397
+ if (args.news.initialNodeCount === undefined) return;
398
+ const observedCount = args.observed.initialNodeCount;
399
+ if (observedCount === args.news.initialNodeCount) return;
400
+ const op = yield* setSize({
401
+ name: args.fqName,
402
+ body: { nodeCount: args.news.initialNodeCount },
403
+ });
404
+ if (op.name) yield* awaitOperation(qualifyOp(args.fqName, op.name), args.session);
405
+ });
406
+
407
+ const syncNodePoolAutoscaling = Effect.fn(function* (args: {
408
+ fqName: string;
409
+ observed: cont.NodePool;
410
+ news: NodePoolProps;
411
+ session: ScopedPlanStatusSession;
412
+ }) {
413
+ if (!args.news.autoscaling) return;
414
+ if (deepEqual(args.observed.autoscaling, args.news.autoscaling)) return;
415
+ const op = yield* setAutoscaling({
416
+ name: args.fqName,
417
+ body: { autoscaling: args.news.autoscaling },
418
+ });
419
+ if (op.name) yield* awaitOperation(qualifyOp(args.fqName, op.name), args.session);
420
+ });
421
+
422
+ const syncNodePoolManagement = Effect.fn(function* (args: {
423
+ fqName: string;
424
+ observed: cont.NodePool;
425
+ news: NodePoolProps;
426
+ session: ScopedPlanStatusSession;
427
+ }) {
428
+ if (!args.news.management) return;
429
+ if (deepEqual(args.observed.management, args.news.management)) return;
430
+ const op = yield* setManagement({
431
+ name: args.fqName,
432
+ body: { management: args.news.management },
433
+ });
434
+ if (op.name) yield* awaitOperation(qualifyOp(args.fqName, op.name), args.session);
435
+ });
436
+
437
+ const syncNodePoolUpdate = Effect.fn(function* (args: {
438
+ fqName: string;
439
+ observed: cont.NodePool;
440
+ news: NodePoolProps;
441
+ desiredResourceLabels: Record<string, string>;
442
+ session: ScopedPlanStatusSession;
443
+ }) {
444
+ // 1. Locations — sent as a standalone update body. The SDK
445
+ // explicitly warns not to combine `locations` with other
446
+ // fields; if you do, the other fields only apply to newly-
447
+ // created nodes.
448
+ if (
449
+ args.news.nodeLocations &&
450
+ !deepEqual(args.observed.locations ?? [], args.news.nodeLocations)
451
+ ) {
452
+ const op = yield* updateNodePools({
453
+ name: args.fqName,
454
+ body: { locations: args.news.nodeLocations },
455
+ });
456
+ if (op.name) yield* awaitOperation(qualifyOp(args.fqName, op.name), args.session);
457
+ }
458
+
459
+ // 2. Re-observe so the non-location update diffs against fresh
460
+ // state (the location update may have rewritten labels/taints
461
+ // on new nodes already).
462
+ const fresh = yield* getNodePools({ name: args.fqName });
463
+ const body = toNodePoolUpdateBody(fresh, args.news, args.desiredResourceLabels);
464
+ if (!body) return;
465
+
466
+ const op = yield* updateNodePools({ name: args.fqName, body });
467
+ if (op.name) yield* awaitOperation(qualifyOp(args.fqName, op.name), args.session);
468
+ });
469
+
470
+ return {
471
+ stables: ["name", "selfLink", "project", "location", "clusterName"],
472
+ diff: Effect.fn(function* ({ news, olds = {} }) {
473
+ if (!isResolved(news)) return undefined;
474
+ if (
475
+ somePropsAreDifferent(olds as NodePoolProps, news, [
476
+ "project",
477
+ "location",
478
+ "clusterName",
479
+ "name",
480
+ ])
481
+ ) {
482
+ return { action: "replace" } as const;
483
+ }
484
+ const oc = olds.config ?? ({} as NodePoolProps["config"]);
485
+ const nc = news.config;
486
+ if (
487
+ somePropsAreDifferent(oc, nc, [
488
+ "serviceAccount",
489
+ "minCpuPlatform",
490
+ "bootDiskKmsKey",
491
+ "preemptible",
492
+ "spot",
493
+ "localSsdCount",
494
+ ])
495
+ ) {
496
+ return { action: "replace" } as const;
497
+ }
498
+ if (
499
+ !deepEqual(oc.oauthScopes, nc.oauthScopes) ||
500
+ !deepEqual(oc.metadata, nc.metadata) ||
501
+ !deepEqual(oc.sandboxConfig, nc.sandboxConfig) ||
502
+ !deepEqual(oc.reservationAffinity, nc.reservationAffinity) ||
503
+ !deepEqual(oc.shieldedInstanceConfig, nc.shieldedInstanceConfig)
504
+ ) {
505
+ return { action: "replace" } as const;
506
+ }
507
+ return undefined;
508
+ }),
509
+ reconcile: Effect.fn(function* ({ id, news, session }) {
510
+ const internalLabels = yield* gcpInternalLabels(id);
511
+ const desiredName =
512
+ news.name ??
513
+ (yield* createPhysicalName({ id, maxLength: 40 })).toLowerCase();
514
+ const clusterPath = `projects/${news.project}/locations/${news.location}/clusters/${news.clusterName}`;
515
+ const fqName = `${clusterPath}/nodePools/${desiredName}`;
516
+ const desiredResourceLabels: Record<string, string> = {
517
+ ...(news.config.resourceLabels ?? {}),
518
+ ...internalLabels,
519
+ };
520
+
521
+ // 1. Observe — collapse 403 to "missing" alongside 404, mirroring
522
+ // Project.observeProject's stance that GCP returns 403 for
523
+ // invisible resources.
524
+ let observed = yield* getNodePools({ name: fqName }).pipe(
525
+ Effect.catchTag("NotFound", () => Effect.succeed(undefined as cont.NodePool | undefined)),
526
+ Effect.catchTag("Forbidden", () => Effect.succeed(undefined as cont.NodePool | undefined)),
527
+ );
528
+
529
+ // 2. Ensure — create if missing. `Conflict` covers concurrent
530
+ // creates and state-persistence races; fall through and
531
+ // re-observe.
532
+ if (!observed) {
533
+ const config = toNodeConfigCreateBody(news.config, desiredResourceLabels);
534
+ // GKE Standard rejects creates without a positive node
535
+ // count. If the user didn't specify, fall back to the
536
+ // autoscaler floor (or 1 if there's no autoscaler config).
537
+ const createNodeCount =
538
+ news.initialNodeCount ?? news.autoscaling?.minNodeCount ?? 1;
539
+ const op = yield* createNodePools({
540
+ parent: clusterPath,
541
+ body: {
542
+ nodePool: {
543
+ name: desiredName,
544
+ initialNodeCount: createNodeCount,
545
+ ...(news.nodeLocations ? { locations: news.nodeLocations } : {}),
546
+ config,
547
+ ...(news.autoscaling ? { autoscaling: news.autoscaling } : {}),
548
+ ...(news.management ? { management: news.management } : {}),
549
+ ...(news.upgradeSettings ? { upgradeSettings: news.upgradeSettings } : {}),
550
+ },
551
+ },
552
+ }).pipe(
553
+ Effect.catchTag("Conflict", () =>
554
+ Effect.succeed(undefined as cont.Operation | undefined),
555
+ ),
556
+ );
557
+ if (op?.name) yield* awaitOperation(qualifyOp(fqName, op.name), session);
558
+ observed = yield* getNodePools({ name: fqName });
559
+ }
560
+
561
+ // 3. Sync — sequential, each aspect independently idempotent.
562
+ yield* syncNodePoolSize({ fqName, observed, news, session });
563
+ yield* syncNodePoolAutoscaling({ fqName, observed, news, session });
564
+ yield* syncNodePoolManagement({ fqName, observed, news, session });
565
+ yield* syncNodePoolUpdate({
566
+ fqName,
567
+ observed,
568
+ news,
569
+ desiredResourceLabels,
570
+ session,
571
+ });
572
+
573
+ const final = yield* getNodePools({ name: fqName });
574
+ return toNodePoolAttributes(final, {
575
+ project: news.project,
576
+ location: news.location,
577
+ clusterName: news.clusterName,
578
+ });
579
+ }),
580
+ delete: Effect.fn(function* ({ output, session }) {
581
+ const fqName = `projects/${output.project}/locations/${output.location}/clusters/${output.clusterName}/nodePools/${output.name}`;
582
+ yield* deleteNodePools({ name: fqName }).pipe(
583
+ Effect.flatMap((op) =>
584
+ op.name
585
+ ? awaitOperation(qualifyOp(fqName, op.name), session)
586
+ : Effect.succeed(op),
587
+ ),
588
+ Effect.catchTag("NotFound", () => Effect.void),
589
+ );
590
+ }),
591
+ read: Effect.fn(function* ({ id, output, olds }) {
592
+ // ProviderService.read receives `{ id, instanceId, olds, output }`
593
+ // — no `news`. Identity must come from output (post-create
594
+ // cache) or olds (last-persisted props).
595
+ const project = output?.project ?? olds?.project;
596
+ const location = output?.location ?? olds?.location;
597
+ const clusterName = output?.clusterName ?? olds?.clusterName;
598
+ if (!project || !location || !clusterName) return undefined;
599
+ const name =
600
+ output?.name ??
601
+ olds?.name ??
602
+ (yield* createPhysicalName({ id, maxLength: 40 })).toLowerCase();
603
+ const fqName = `projects/${project}/locations/${location}/clusters/${clusterName}/nodePools/${name}`;
604
+ const observed = yield* getNodePools({ name: fqName }).pipe(
605
+ Effect.catchTag("NotFound", () => Effect.succeed(undefined as cont.NodePool | undefined)),
606
+ Effect.catchTag("Forbidden", () => Effect.succeed(undefined as cont.NodePool | undefined)),
607
+ );
608
+ if (!observed) return undefined;
609
+ const attrs = toNodePoolAttributes(observed, {
610
+ project,
611
+ location,
612
+ clusterName,
613
+ });
614
+ return (yield* hasAlchemyLabels(id, observed.config?.resourceLabels))
615
+ ? attrs
616
+ : Unowned(attrs);
617
+ }),
618
+ };
619
+ }),
620
+ );