@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.
- package/LICENSE +201 -0
- package/README.md +88 -0
- package/lib/Auth/AuthProvider.d.ts +48 -0
- package/lib/Auth/AuthProvider.d.ts.map +1 -0
- package/lib/Auth/AuthProvider.js +84 -0
- package/lib/Auth/AuthProvider.js.map +1 -0
- package/lib/Auth/Credentials.d.ts +28 -0
- package/lib/Auth/Credentials.d.ts.map +1 -0
- package/lib/Auth/Credentials.js +41 -0
- package/lib/Auth/Credentials.js.map +1 -0
- package/lib/CloudResourceManager/Project.d.ts +143 -0
- package/lib/CloudResourceManager/Project.d.ts.map +1 -0
- package/lib/CloudResourceManager/Project.js +272 -0
- package/lib/CloudResourceManager/Project.js.map +1 -0
- package/lib/CloudResourceManager/index.d.ts +3 -0
- package/lib/CloudResourceManager/index.d.ts.map +1 -0
- package/lib/CloudResourceManager/index.js +2 -0
- package/lib/CloudResourceManager/index.js.map +1 -0
- package/lib/Compute/GlobalAddress.d.ts +101 -0
- package/lib/Compute/GlobalAddress.d.ts.map +1 -0
- package/lib/Compute/GlobalAddress.js +134 -0
- package/lib/Compute/GlobalAddress.js.map +1 -0
- package/lib/Compute/Network.d.ts +114 -0
- package/lib/Compute/Network.d.ts.map +1 -0
- package/lib/Compute/Network.js +145 -0
- package/lib/Compute/Network.js.map +1 -0
- package/lib/Compute/Operations.d.ts +34 -0
- package/lib/Compute/Operations.d.ts.map +1 -0
- package/lib/Compute/Operations.js +62 -0
- package/lib/Compute/Operations.js.map +1 -0
- package/lib/Compute/SharedVpcHost.d.ts +44 -0
- package/lib/Compute/SharedVpcHost.d.ts.map +1 -0
- package/lib/Compute/SharedVpcHost.js +71 -0
- package/lib/Compute/SharedVpcHost.js.map +1 -0
- package/lib/Compute/SharedVpcServiceProject.d.ts +46 -0
- package/lib/Compute/SharedVpcServiceProject.d.ts.map +1 -0
- package/lib/Compute/SharedVpcServiceProject.js +96 -0
- package/lib/Compute/SharedVpcServiceProject.js.map +1 -0
- package/lib/Compute/Subnetwork.d.ts +108 -0
- package/lib/Compute/Subnetwork.d.ts.map +1 -0
- package/lib/Compute/Subnetwork.js +201 -0
- package/lib/Compute/Subnetwork.js.map +1 -0
- package/lib/Compute/index.d.ts +6 -0
- package/lib/Compute/index.d.ts.map +1 -0
- package/lib/Compute/index.js +6 -0
- package/lib/Compute/index.js.map +1 -0
- package/lib/Container/Cluster.d.ts +216 -0
- package/lib/Container/Cluster.d.ts.map +1 -0
- package/lib/Container/Cluster.js +326 -0
- package/lib/Container/Cluster.js.map +1 -0
- package/lib/Container/NodePool.d.ts +214 -0
- package/lib/Container/NodePool.d.ts.map +1 -0
- package/lib/Container/NodePool.js +344 -0
- package/lib/Container/NodePool.js.map +1 -0
- package/lib/Container/Operations.d.ts +50 -0
- package/lib/Container/Operations.d.ts.map +1 -0
- package/lib/Container/Operations.js +51 -0
- package/lib/Container/Operations.js.map +1 -0
- package/lib/Container/index.d.ts +5 -0
- package/lib/Container/index.d.ts.map +1 -0
- package/lib/Container/index.js +3 -0
- package/lib/Container/index.js.map +1 -0
- package/lib/Parallelstore/Instance.d.ts +119 -0
- package/lib/Parallelstore/Instance.d.ts.map +1 -0
- package/lib/Parallelstore/Instance.js +180 -0
- package/lib/Parallelstore/Instance.js.map +1 -0
- package/lib/Parallelstore/index.d.ts +2 -0
- package/lib/Parallelstore/index.d.ts.map +1 -0
- package/lib/Parallelstore/index.js +2 -0
- package/lib/Parallelstore/index.js.map +1 -0
- package/lib/Providers.d.ts +24 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +51 -0
- package/lib/Providers.js.map +1 -0
- package/lib/ServiceNetworking/PsaConnection.d.ts +84 -0
- package/lib/ServiceNetworking/PsaConnection.d.ts.map +1 -0
- package/lib/ServiceNetworking/PsaConnection.js +130 -0
- package/lib/ServiceNetworking/PsaConnection.js.map +1 -0
- package/lib/ServiceNetworking/index.d.ts +2 -0
- package/lib/ServiceNetworking/index.d.ts.map +1 -0
- package/lib/ServiceNetworking/index.js +2 -0
- package/lib/ServiceNetworking/index.js.map +1 -0
- package/lib/ServiceUsage/ApiEnable.d.ts +78 -0
- package/lib/ServiceUsage/ApiEnable.d.ts.map +1 -0
- package/lib/ServiceUsage/ApiEnable.js +132 -0
- package/lib/ServiceUsage/ApiEnable.js.map +1 -0
- package/lib/ServiceUsage/index.d.ts +3 -0
- package/lib/ServiceUsage/index.d.ts.map +1 -0
- package/lib/ServiceUsage/index.js +2 -0
- package/lib/ServiceUsage/index.js.map +1 -0
- package/lib/Tags.d.ts +57 -0
- package/lib/Tags.d.ts.map +1 -0
- package/lib/Tags.js +106 -0
- package/lib/Tags.js.map +1 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +10 -0
- package/lib/index.js.map +1 -0
- package/package.json +70 -0
- package/src/Auth/AuthProvider.ts +180 -0
- package/src/Auth/Credentials.ts +69 -0
- package/src/CloudResourceManager/Project.ts +517 -0
- package/src/CloudResourceManager/index.ts +2 -0
- package/src/Compute/GlobalAddress.ts +283 -0
- package/src/Compute/Network.ts +306 -0
- package/src/Compute/Operations.ts +108 -0
- package/src/Compute/SharedVpcHost.ts +141 -0
- package/src/Compute/SharedVpcServiceProject.ts +185 -0
- package/src/Compute/Subnetwork.ts +375 -0
- package/src/Compute/index.ts +5 -0
- package/src/Container/Cluster.ts +599 -0
- package/src/Container/NodePool.ts +620 -0
- package/src/Container/Operations.ts +88 -0
- package/src/Container/index.ts +8 -0
- package/src/Parallelstore/Instance.ts +371 -0
- package/src/Parallelstore/index.ts +1 -0
- package/src/Providers.ts +84 -0
- package/src/ServiceNetworking/PsaConnection.ts +269 -0
- package/src/ServiceNetworking/index.ts +1 -0
- package/src/ServiceUsage/ApiEnable.ts +267 -0
- package/src/ServiceUsage/index.ts +2 -0
- package/src/Tags.ts +130 -0
- package/src/index.ts +16 -0
|
@@ -0,0 +1,599 @@
|
|
|
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 Duration from "effect/Duration";
|
|
9
|
+
import * as Effect from "effect/Effect";
|
|
10
|
+
import * as Schedule from "effect/Schedule";
|
|
11
|
+
import { gcpInternalLabels, hasAlchemyLabels } from "../Tags.ts";
|
|
12
|
+
import type * as GCP from "../Providers.ts";
|
|
13
|
+
import { type NodePoolProps, toNodeConfigCreateBody } from "./NodePool.ts";
|
|
14
|
+
import { makeAwaitOperation, qualifyOperationName } from "./Operations.ts";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Spec for the bootstrap node pool created inline with the cluster.
|
|
18
|
+
*
|
|
19
|
+
* Same shape as the create-time subset of `NodePoolProps`, minus the
|
|
20
|
+
* parent-identity fields (`project`, `location`, `clusterName`), which
|
|
21
|
+
* are inherited from the cluster.
|
|
22
|
+
*/
|
|
23
|
+
export type ClusterInitialNodePool = {
|
|
24
|
+
/** Pool name. Required — no default. Lowercase alphanumeric+hyphens, ≤40 chars. */
|
|
25
|
+
name: string;
|
|
26
|
+
/** Initial node count. Required — cluster needs nonzero capacity at create. */
|
|
27
|
+
initialNodeCount: number;
|
|
28
|
+
/** Per-pool zones. Defaults to the cluster's zone/region. */
|
|
29
|
+
nodeLocations?: ReadonlyArray<string>;
|
|
30
|
+
/** Node configuration (machine type, disk, accelerators, taints, …). */
|
|
31
|
+
config: NodePoolProps["config"];
|
|
32
|
+
/** Cluster autoscaler config. */
|
|
33
|
+
autoscaling?: NodePoolProps["autoscaling"];
|
|
34
|
+
/** Auto-upgrade / auto-repair. */
|
|
35
|
+
management?: NodePoolProps["management"];
|
|
36
|
+
/** Surge upgrade settings. */
|
|
37
|
+
upgradeSettings?: NodePoolProps["upgradeSettings"];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type ClusterProps = {
|
|
41
|
+
/** GCP project ID hosting the cluster. Immutable — replace if changed. */
|
|
42
|
+
project: string;
|
|
43
|
+
/**
|
|
44
|
+
* GCP zone (e.g. `us-central1-a`) for a zonal cluster, or region
|
|
45
|
+
* (e.g. `us-central1`) for a regional cluster. Immutable — replace.
|
|
46
|
+
*/
|
|
47
|
+
location: string;
|
|
48
|
+
/**
|
|
49
|
+
* Cluster name. Defaults to `createPhysicalName({ id, lowercase: true,
|
|
50
|
+
* maxLength: 40 })`. Lowercase alphanumeric + hyphens, ≤40 chars.
|
|
51
|
+
* Immutable — replace if changed.
|
|
52
|
+
*/
|
|
53
|
+
name?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Additional zones for cluster nodes. Mutable via `setLocations`.
|
|
56
|
+
*/
|
|
57
|
+
nodeLocations?: ReadonlyArray<string>;
|
|
58
|
+
/**
|
|
59
|
+
* VPC short name or self-link. Omit to use the project's `default`
|
|
60
|
+
* VPC. Immutable — replace if changed.
|
|
61
|
+
*/
|
|
62
|
+
network?: string;
|
|
63
|
+
/** Subnet within the VPC. Immutable — replace if changed. */
|
|
64
|
+
subnetwork?: string;
|
|
65
|
+
/**
|
|
66
|
+
* VPC-native settings. `useIpAliases: true` is GKE's default for new
|
|
67
|
+
* clusters and the only path that supports private clusters.
|
|
68
|
+
* Immutable — replace if changed.
|
|
69
|
+
*/
|
|
70
|
+
ipAllocationPolicy?: {
|
|
71
|
+
useIpAliases?: boolean;
|
|
72
|
+
clusterIpv4CidrBlock?: string;
|
|
73
|
+
servicesIpv4CidrBlock?: string;
|
|
74
|
+
clusterSecondaryRangeName?: string;
|
|
75
|
+
servicesSecondaryRangeName?: string;
|
|
76
|
+
};
|
|
77
|
+
/** GKE release channel. Mutable via `update` (`desiredReleaseChannel`). */
|
|
78
|
+
releaseChannel?: { channel: "RAPID" | "REGULAR" | "STABLE" | "UNSPECIFIED" };
|
|
79
|
+
/**
|
|
80
|
+
* Workload Identity binds K8s SAs to GCP SAs. Format:
|
|
81
|
+
* `<projectId>.svc.id.goog`. Mutable via `update`.
|
|
82
|
+
*/
|
|
83
|
+
workloadIdentityConfig?: { workloadPool: string };
|
|
84
|
+
/** Cluster addons. Mutable via dedicated `setAddons`. */
|
|
85
|
+
addonsConfig?: {
|
|
86
|
+
httpLoadBalancing?: { disabled?: boolean };
|
|
87
|
+
horizontalPodAutoscaling?: { disabled?: boolean };
|
|
88
|
+
networkPolicyConfig?: { disabled?: boolean };
|
|
89
|
+
gcePersistentDiskCsiDriverConfig?: { enabled?: boolean };
|
|
90
|
+
gcsFuseCsiDriverConfig?: { enabled?: boolean };
|
|
91
|
+
gkeBackupAgentConfig?: { enabled?: boolean };
|
|
92
|
+
dnsCacheConfig?: { enabled?: boolean };
|
|
93
|
+
configConnectorConfig?: { enabled?: boolean };
|
|
94
|
+
};
|
|
95
|
+
/** Logging service. Mutable via `setLogging`. */
|
|
96
|
+
loggingService?: string;
|
|
97
|
+
/** Monitoring service. Mutable via `setMonitoring`. */
|
|
98
|
+
monitoringService?: string;
|
|
99
|
+
/** Maintenance window/exclusions. Mutable via `setMaintenancePolicy`. */
|
|
100
|
+
maintenancePolicy?: {
|
|
101
|
+
window?: {
|
|
102
|
+
dailyMaintenanceWindow?: { startTime: string };
|
|
103
|
+
recurringWindow?: {
|
|
104
|
+
window: { startTime: string; endTime: string };
|
|
105
|
+
recurrence: string;
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Network policy enforcement (e.g. Calico). Mutable via
|
|
111
|
+
* `setNetworkPolicy`. Requires `networkPolicyConfig` addon.
|
|
112
|
+
*/
|
|
113
|
+
networkPolicy?: {
|
|
114
|
+
enabled?: boolean;
|
|
115
|
+
provider?: "PROVIDER_UNSPECIFIED" | "CALICO";
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Resource labels. Alchemy internal labels (`alchemy_app`,
|
|
119
|
+
* `alchemy_stage`, `alchemy_id`) are merged on top automatically and
|
|
120
|
+
* are reserved. Mutable via `setResourceLabels`.
|
|
121
|
+
*/
|
|
122
|
+
resourceLabels?: Record<string, string>;
|
|
123
|
+
/**
|
|
124
|
+
* Initial cluster version (`MASTER_DEFAULT` if omitted). Subsequent
|
|
125
|
+
* master upgrades are not exposed in v1.
|
|
126
|
+
*/
|
|
127
|
+
initialClusterVersion?: string;
|
|
128
|
+
/**
|
|
129
|
+
* Bootstrap node pool created inline with the cluster. **Required.**
|
|
130
|
+
* GKE rejects cluster creation without at least one pool.
|
|
131
|
+
*
|
|
132
|
+
* Owned by the cluster — its `config.resourceLabels` are stamped
|
|
133
|
+
* with the cluster's `alchemy_*` internal labels at create time.
|
|
134
|
+
* Consumed only on the create path: after the cluster exists,
|
|
135
|
+
* `Cluster.reconcile` does NOT observe, sync, or mutate this pool,
|
|
136
|
+
* and changes to `initialNodePool` props on subsequent runs are a
|
|
137
|
+
* silent no-op (NOT diffed, NOT a replace trigger).
|
|
138
|
+
*
|
|
139
|
+
* Don't declare a separate `GCP.NodePool` for the same physical
|
|
140
|
+
* pool — it would key its own `alchemy_id` against a different
|
|
141
|
+
* logical id and `read` would return `Unowned(...)`. If you want
|
|
142
|
+
* post-create management of the bootstrap pool's size/config, do
|
|
143
|
+
* it via gcloud or expand `Cluster.reconcile` to sync it; declare
|
|
144
|
+
* additional pools (`compute`, `gpu`, …) as separate
|
|
145
|
+
* `GCP.NodePool` resources.
|
|
146
|
+
*/
|
|
147
|
+
initialNodePool: ClusterInitialNodePool;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export type ClusterAttributes = {
|
|
151
|
+
/** Cluster name. */
|
|
152
|
+
name: string;
|
|
153
|
+
/** Server-defined URL. */
|
|
154
|
+
selfLink: string;
|
|
155
|
+
/** IP of the master endpoint. */
|
|
156
|
+
endpoint: string;
|
|
157
|
+
/** Base64-encoded master CA cert. */
|
|
158
|
+
clusterCaCertificate: string;
|
|
159
|
+
/** GCP project ID, threaded through from props for `delete`/`read`. */
|
|
160
|
+
project: string;
|
|
161
|
+
/** Zone or region, threaded through from props. */
|
|
162
|
+
location: string;
|
|
163
|
+
/** Current control-plane version. */
|
|
164
|
+
currentMasterVersion: string | undefined;
|
|
165
|
+
/** Lifecycle status. */
|
|
166
|
+
status: string | undefined;
|
|
167
|
+
/** Active release channel. */
|
|
168
|
+
releaseChannel: { channel?: string } | undefined;
|
|
169
|
+
/** Resource labels currently set, including `alchemy_*` internals. */
|
|
170
|
+
resourceLabels: Record<string, string>;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* A GKE Standard cluster.
|
|
175
|
+
*
|
|
176
|
+
* Lifecycle: observe → ensure (create with the user-supplied
|
|
177
|
+
* `initialNodePool` inline) → sync (labels, addons, maintenance,
|
|
178
|
+
* locations, network policy, logging, monitoring, generic update) →
|
|
179
|
+
* return.
|
|
180
|
+
*
|
|
181
|
+
* Adoption: a cluster whose `resourceLabels` lack our `alchemy_*` keys
|
|
182
|
+
* is wrapped in `Unowned(attrs)` from `read`, forcing the user to
|
|
183
|
+
* pass `--adopt` before takeover.
|
|
184
|
+
*
|
|
185
|
+
* @section Creating a cluster
|
|
186
|
+
* @example Minimal Standard cluster
|
|
187
|
+
* ```typescript
|
|
188
|
+
* const cluster = yield* GCP.Cluster("Main", {
|
|
189
|
+
* project: project.projectId,
|
|
190
|
+
* location: "us-central1-a",
|
|
191
|
+
* releaseChannel: { channel: "REGULAR" },
|
|
192
|
+
* initialNodePool: {
|
|
193
|
+
* name: "system",
|
|
194
|
+
* initialNodeCount: 1,
|
|
195
|
+
* config: { machineType: "e2-medium" },
|
|
196
|
+
* },
|
|
197
|
+
* });
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
200
|
+
export type Cluster = Resource<
|
|
201
|
+
"GCP.Cluster",
|
|
202
|
+
ClusterProps,
|
|
203
|
+
ClusterAttributes,
|
|
204
|
+
never,
|
|
205
|
+
GCP.Providers
|
|
206
|
+
>;
|
|
207
|
+
export const Cluster = Resource<Cluster>("GCP.Cluster");
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Build the inline `NodePool` entry for the cluster create body.
|
|
211
|
+
*
|
|
212
|
+
* The cluster's alchemy internal labels (`alchemy_app`, `alchemy_stage`,
|
|
213
|
+
* `alchemy_id` keyed by the cluster's logical id) are merged into the
|
|
214
|
+
* pool's `config.resourceLabels`, marking the bootstrap pool as part
|
|
215
|
+
* of the cluster — not a free-standing resource that needs adoption.
|
|
216
|
+
* It is owned by the cluster, not by any `GCP.NodePool` resource; a
|
|
217
|
+
* separate `GCP.NodePool` declared with a different logical id will
|
|
218
|
+
* still see `Unowned(...)` from `read` (its own `alchemy_id` won't
|
|
219
|
+
* match), which is correct: that pool belongs to the cluster.
|
|
220
|
+
*/
|
|
221
|
+
const toInitialNodePoolBody = (
|
|
222
|
+
spec: ClusterInitialNodePool,
|
|
223
|
+
clusterInternalLabels: Record<string, string>,
|
|
224
|
+
): cont.NodePool => ({
|
|
225
|
+
name: spec.name,
|
|
226
|
+
initialNodeCount: spec.initialNodeCount,
|
|
227
|
+
...(spec.nodeLocations ? { locations: spec.nodeLocations } : {}),
|
|
228
|
+
config: toNodeConfigCreateBody(spec.config, {
|
|
229
|
+
...(spec.config.resourceLabels ?? {}),
|
|
230
|
+
...clusterInternalLabels,
|
|
231
|
+
}),
|
|
232
|
+
...(spec.autoscaling ? { autoscaling: spec.autoscaling } : {}),
|
|
233
|
+
...(spec.management ? { management: spec.management } : {}),
|
|
234
|
+
...(spec.upgradeSettings ? { upgradeSettings: spec.upgradeSettings } : {}),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const toClusterAttributes = (
|
|
238
|
+
c: cont.Cluster,
|
|
239
|
+
parent: { project: string; location: string },
|
|
240
|
+
): ClusterAttributes => ({
|
|
241
|
+
name: c.name ?? "",
|
|
242
|
+
selfLink: c.selfLink ?? "",
|
|
243
|
+
endpoint: c.endpoint ?? "",
|
|
244
|
+
clusterCaCertificate: c.masterAuth?.clusterCaCertificate ?? "",
|
|
245
|
+
project: parent.project,
|
|
246
|
+
location: parent.location,
|
|
247
|
+
currentMasterVersion: c.currentMasterVersion,
|
|
248
|
+
status: c.status,
|
|
249
|
+
releaseChannel: c.releaseChannel,
|
|
250
|
+
resourceLabels: { ...(c.resourceLabels ?? {}) },
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
export const ClusterProvider = () =>
|
|
254
|
+
Provider.effect(
|
|
255
|
+
Cluster,
|
|
256
|
+
Effect.gen(function* () {
|
|
257
|
+
const getClusters = yield* cont.getProjectsLocationsClusters;
|
|
258
|
+
const createClusters = yield* cont.createProjectsLocationsClusters;
|
|
259
|
+
const updateClusters = yield* cont.updateProjectsLocationsClusters;
|
|
260
|
+
const setAddons = yield* cont.setAddonsProjectsLocationsClusters;
|
|
261
|
+
const setLocations = yield* cont.setLocationsProjectsLocationsClusters;
|
|
262
|
+
const setMaintenance = yield* cont.setMaintenancePolicyProjectsLocationsClusters;
|
|
263
|
+
const setNetworkPolicy = yield* cont.setNetworkPolicyProjectsLocationsClusters;
|
|
264
|
+
const setResourceLabels = yield* cont.setResourceLabelsProjectsLocationsClusters;
|
|
265
|
+
const setLogging = yield* cont.setLoggingProjectsLocationsClusters;
|
|
266
|
+
const setMonitoring = yield* cont.setMonitoringProjectsLocationsClusters;
|
|
267
|
+
const deleteClusters = yield* cont.deleteProjectsLocationsClusters;
|
|
268
|
+
const getOperations = yield* cont.getProjectsLocationsOperations;
|
|
269
|
+
const awaitOperation = makeAwaitOperation(getOperations);
|
|
270
|
+
|
|
271
|
+
// Qualify a bare operation id using the parent path of the
|
|
272
|
+
// resource it targets — every fqName in this provider has the
|
|
273
|
+
// shape `projects/{p}/locations/{l}/clusters/{c}`, so the first
|
|
274
|
+
// four segments give us the parent context. Avoids threading
|
|
275
|
+
// project/location through every sync helper's args type.
|
|
276
|
+
const qualifyOp = (fqName: string, opName: string) =>
|
|
277
|
+
qualifyOperationName(
|
|
278
|
+
fqName.split("/")[1],
|
|
279
|
+
fqName.split("/")[3],
|
|
280
|
+
opName,
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const syncClusterLabels = Effect.fn(function* (args: {
|
|
284
|
+
fqName: string;
|
|
285
|
+
observed: cont.Cluster;
|
|
286
|
+
desired: Record<string, string>;
|
|
287
|
+
session: ScopedPlanStatusSession;
|
|
288
|
+
}) {
|
|
289
|
+
if (deepEqual(args.observed.resourceLabels ?? {}, args.desired)) return;
|
|
290
|
+
// `setResourceLabels` requires `labelFingerprint` for optimistic
|
|
291
|
+
// concurrency — read it fresh from the observed cluster.
|
|
292
|
+
const op = yield* setResourceLabels({
|
|
293
|
+
name: args.fqName,
|
|
294
|
+
body: {
|
|
295
|
+
resourceLabels: args.desired,
|
|
296
|
+
labelFingerprint: args.observed.labelFingerprint,
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
if (op.name) yield* awaitOperation(qualifyOp(args.fqName, op.name), args.session);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const syncClusterAddons = Effect.fn(function* (args: {
|
|
303
|
+
fqName: string;
|
|
304
|
+
observed: cont.Cluster;
|
|
305
|
+
news: ClusterProps;
|
|
306
|
+
session: ScopedPlanStatusSession;
|
|
307
|
+
}) {
|
|
308
|
+
if (!args.news.addonsConfig) return;
|
|
309
|
+
if (deepEqual(args.observed.addonsConfig, args.news.addonsConfig)) return;
|
|
310
|
+
const op = yield* setAddons({
|
|
311
|
+
name: args.fqName,
|
|
312
|
+
body: { addonsConfig: args.news.addonsConfig },
|
|
313
|
+
});
|
|
314
|
+
if (op.name) yield* awaitOperation(qualifyOp(args.fqName, op.name), args.session);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const syncClusterMaintenance = Effect.fn(function* (args: {
|
|
318
|
+
fqName: string;
|
|
319
|
+
observed: cont.Cluster;
|
|
320
|
+
news: ClusterProps;
|
|
321
|
+
session: ScopedPlanStatusSession;
|
|
322
|
+
}) {
|
|
323
|
+
if (!args.news.maintenancePolicy) return;
|
|
324
|
+
if (deepEqual(args.observed.maintenancePolicy, args.news.maintenancePolicy)) return;
|
|
325
|
+
const op = yield* setMaintenance({
|
|
326
|
+
name: args.fqName,
|
|
327
|
+
body: { maintenancePolicy: args.news.maintenancePolicy },
|
|
328
|
+
});
|
|
329
|
+
if (op.name) yield* awaitOperation(qualifyOp(args.fqName, op.name), args.session);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const syncClusterLocations = Effect.fn(function* (args: {
|
|
333
|
+
fqName: string;
|
|
334
|
+
observed: cont.Cluster;
|
|
335
|
+
news: ClusterProps;
|
|
336
|
+
session: ScopedPlanStatusSession;
|
|
337
|
+
}) {
|
|
338
|
+
if (!args.news.nodeLocations) return;
|
|
339
|
+
if (deepEqual(args.observed.locations ?? [], args.news.nodeLocations)) return;
|
|
340
|
+
const op = yield* setLocations({
|
|
341
|
+
name: args.fqName,
|
|
342
|
+
body: { locations: args.news.nodeLocations },
|
|
343
|
+
});
|
|
344
|
+
if (op.name) yield* awaitOperation(qualifyOp(args.fqName, op.name), args.session);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const syncClusterNetworkPolicy = Effect.fn(function* (args: {
|
|
348
|
+
fqName: string;
|
|
349
|
+
observed: cont.Cluster;
|
|
350
|
+
news: ClusterProps;
|
|
351
|
+
session: ScopedPlanStatusSession;
|
|
352
|
+
}) {
|
|
353
|
+
if (!args.news.networkPolicy) return;
|
|
354
|
+
if (deepEqual(args.observed.networkPolicy, args.news.networkPolicy)) return;
|
|
355
|
+
const op = yield* setNetworkPolicy({
|
|
356
|
+
name: args.fqName,
|
|
357
|
+
body: { networkPolicy: args.news.networkPolicy },
|
|
358
|
+
});
|
|
359
|
+
if (op.name) yield* awaitOperation(qualifyOp(args.fqName, op.name), args.session);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const syncClusterLogging = Effect.fn(function* (args: {
|
|
363
|
+
fqName: string;
|
|
364
|
+
observed: cont.Cluster;
|
|
365
|
+
news: ClusterProps;
|
|
366
|
+
session: ScopedPlanStatusSession;
|
|
367
|
+
}) {
|
|
368
|
+
if (args.news.loggingService === undefined) return;
|
|
369
|
+
if (args.observed.loggingService === args.news.loggingService) return;
|
|
370
|
+
const op = yield* setLogging({
|
|
371
|
+
name: args.fqName,
|
|
372
|
+
body: { loggingService: args.news.loggingService },
|
|
373
|
+
});
|
|
374
|
+
if (op.name) yield* awaitOperation(qualifyOp(args.fqName, op.name), args.session);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const syncClusterMonitoring = Effect.fn(function* (args: {
|
|
378
|
+
fqName: string;
|
|
379
|
+
observed: cont.Cluster;
|
|
380
|
+
news: ClusterProps;
|
|
381
|
+
session: ScopedPlanStatusSession;
|
|
382
|
+
}) {
|
|
383
|
+
if (args.news.monitoringService === undefined) return;
|
|
384
|
+
if (args.observed.monitoringService === args.news.monitoringService) return;
|
|
385
|
+
const op = yield* setMonitoring({
|
|
386
|
+
name: args.fqName,
|
|
387
|
+
body: { monitoringService: args.news.monitoringService },
|
|
388
|
+
});
|
|
389
|
+
if (op.name) yield* awaitOperation(qualifyOp(args.fqName, op.name), args.session);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Catch-all generic update for fields without dedicated setters:
|
|
393
|
+
// currently `releaseChannel` and `workloadIdentityConfig`. The
|
|
394
|
+
// API auto-derives the field mask from which `desired*` keys are
|
|
395
|
+
// populated — no `updateMask` parameter needed.
|
|
396
|
+
const syncClusterUpdate = Effect.fn(function* (args: {
|
|
397
|
+
fqName: string;
|
|
398
|
+
observed: cont.Cluster;
|
|
399
|
+
news: ClusterProps;
|
|
400
|
+
session: ScopedPlanStatusSession;
|
|
401
|
+
}) {
|
|
402
|
+
const update: cont.ClusterUpdate = {};
|
|
403
|
+
if (
|
|
404
|
+
args.news.releaseChannel &&
|
|
405
|
+
!deepEqual(args.observed.releaseChannel, args.news.releaseChannel)
|
|
406
|
+
) {
|
|
407
|
+
update.desiredReleaseChannel = args.news.releaseChannel;
|
|
408
|
+
}
|
|
409
|
+
if (
|
|
410
|
+
args.news.workloadIdentityConfig &&
|
|
411
|
+
!deepEqual(args.observed.workloadIdentityConfig, args.news.workloadIdentityConfig)
|
|
412
|
+
) {
|
|
413
|
+
update.desiredWorkloadIdentityConfig = args.news.workloadIdentityConfig;
|
|
414
|
+
}
|
|
415
|
+
if (Object.keys(update).length === 0) return;
|
|
416
|
+
const op = yield* updateClusters({
|
|
417
|
+
name: args.fqName,
|
|
418
|
+
body: { update },
|
|
419
|
+
});
|
|
420
|
+
if (op.name) yield* awaitOperation(qualifyOp(args.fqName, op.name), args.session);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
stables: [
|
|
425
|
+
"name",
|
|
426
|
+
"selfLink",
|
|
427
|
+
"project",
|
|
428
|
+
"location",
|
|
429
|
+
"endpoint",
|
|
430
|
+
"clusterCaCertificate",
|
|
431
|
+
],
|
|
432
|
+
diff: Effect.fn(function* ({ news, olds = {} }) {
|
|
433
|
+
if (!isResolved(news)) return undefined;
|
|
434
|
+
if (
|
|
435
|
+
somePropsAreDifferent(olds as ClusterProps, news, [
|
|
436
|
+
"project",
|
|
437
|
+
"location",
|
|
438
|
+
"name",
|
|
439
|
+
"network",
|
|
440
|
+
"subnetwork",
|
|
441
|
+
])
|
|
442
|
+
) {
|
|
443
|
+
return { action: "replace" } as const;
|
|
444
|
+
}
|
|
445
|
+
if (!deepEqual(olds.ipAllocationPolicy, news.ipAllocationPolicy)) {
|
|
446
|
+
return { action: "replace" } as const;
|
|
447
|
+
}
|
|
448
|
+
// `initialNodePool` is intentionally NOT part of diff — it is
|
|
449
|
+
// consumed only on create. See ClusterProps JSDoc.
|
|
450
|
+
return undefined;
|
|
451
|
+
}),
|
|
452
|
+
reconcile: Effect.fn(function* ({ id, news, session }) {
|
|
453
|
+
const internalLabels = yield* gcpInternalLabels(id);
|
|
454
|
+
const desiredName =
|
|
455
|
+
news.name ??
|
|
456
|
+
(yield* createPhysicalName({ id, maxLength: 40 })).toLowerCase();
|
|
457
|
+
const parent = `projects/${news.project}/locations/${news.location}`;
|
|
458
|
+
const fqName = `${parent}/clusters/${desiredName}`;
|
|
459
|
+
const desiredLabels: Record<string, string> = {
|
|
460
|
+
...(news.resourceLabels ?? {}),
|
|
461
|
+
...internalLabels,
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// 1. Observe — collapse 403 to "missing" alongside 404. GCP
|
|
465
|
+
// returns 403 for invisible resources/projects.
|
|
466
|
+
let observed = yield* getClusters({ name: fqName }).pipe(
|
|
467
|
+
Effect.catchTag("NotFound", () => Effect.succeed(undefined as cont.Cluster | undefined)),
|
|
468
|
+
Effect.catchTag("Forbidden", () => Effect.succeed(undefined as cont.Cluster | undefined)),
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
// 2. Ensure — create with the user-supplied bootstrap pool
|
|
472
|
+
// inline. No default-pool dance: an explicit
|
|
473
|
+
// `nodePools: [...]` entry instructs GKE to skip the
|
|
474
|
+
// implicit default. `Conflict` covers concurrent creates
|
|
475
|
+
// and state-persistence races; fall through and re-observe.
|
|
476
|
+
if (!observed) {
|
|
477
|
+
const initialPoolBody = toInitialNodePoolBody(
|
|
478
|
+
news.initialNodePool,
|
|
479
|
+
internalLabels,
|
|
480
|
+
);
|
|
481
|
+
// GKE create can fire faster than ApiEnable's effect
|
|
482
|
+
// propagates: GCP returns 403 with "Kubernetes Engine API
|
|
483
|
+
// has not been used in project ... If you enabled this
|
|
484
|
+
// API recently, wait a few minutes …" even when the
|
|
485
|
+
// ApiEnable LRO has reported DONE. Retry the *create call*
|
|
486
|
+
// (not the LRO poll, which doesn't run until create
|
|
487
|
+
// succeeds) on this specific 403 for ~5 minutes; other
|
|
488
|
+
// Forbidden cases propagate normally.
|
|
489
|
+
const op = yield* createClusters({
|
|
490
|
+
parent,
|
|
491
|
+
body: {
|
|
492
|
+
cluster: {
|
|
493
|
+
name: desiredName,
|
|
494
|
+
...(news.network ? { network: news.network } : {}),
|
|
495
|
+
...(news.subnetwork ? { subnetwork: news.subnetwork } : {}),
|
|
496
|
+
...(news.ipAllocationPolicy
|
|
497
|
+
? { ipAllocationPolicy: news.ipAllocationPolicy }
|
|
498
|
+
: {}),
|
|
499
|
+
...(news.releaseChannel ? { releaseChannel: news.releaseChannel } : {}),
|
|
500
|
+
...(news.workloadIdentityConfig
|
|
501
|
+
? { workloadIdentityConfig: news.workloadIdentityConfig }
|
|
502
|
+
: {}),
|
|
503
|
+
...(news.addonsConfig ? { addonsConfig: news.addonsConfig } : {}),
|
|
504
|
+
...(news.loggingService ? { loggingService: news.loggingService } : {}),
|
|
505
|
+
...(news.monitoringService
|
|
506
|
+
? { monitoringService: news.monitoringService }
|
|
507
|
+
: {}),
|
|
508
|
+
...(news.maintenancePolicy
|
|
509
|
+
? { maintenancePolicy: news.maintenancePolicy }
|
|
510
|
+
: {}),
|
|
511
|
+
...(news.networkPolicy ? { networkPolicy: news.networkPolicy } : {}),
|
|
512
|
+
...(news.initialClusterVersion
|
|
513
|
+
? { initialClusterVersion: news.initialClusterVersion }
|
|
514
|
+
: {}),
|
|
515
|
+
...(news.nodeLocations ? { locations: news.nodeLocations } : {}),
|
|
516
|
+
resourceLabels: desiredLabels,
|
|
517
|
+
nodePools: [initialPoolBody],
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
}).pipe(
|
|
521
|
+
Effect.retry({
|
|
522
|
+
while: (e: { _tag?: string; message?: string }) =>
|
|
523
|
+
e?._tag === "Forbidden" &&
|
|
524
|
+
/enabled this API recently|has not been used/i.test(e.message ?? ""),
|
|
525
|
+
schedule: Schedule.spaced(Duration.seconds(15)).pipe(
|
|
526
|
+
Schedule.both(Schedule.recurs(20)), // 20 × 15s ≈ 5 min
|
|
527
|
+
Schedule.tapOutput(() =>
|
|
528
|
+
session.note(
|
|
529
|
+
"Waiting for Container API enablement to propagate…",
|
|
530
|
+
),
|
|
531
|
+
),
|
|
532
|
+
),
|
|
533
|
+
}),
|
|
534
|
+
Effect.catchTag("Conflict", () =>
|
|
535
|
+
Effect.succeed(undefined as cont.Operation | undefined),
|
|
536
|
+
),
|
|
537
|
+
);
|
|
538
|
+
if (op?.name) yield* awaitOperation(qualifyOp(fqName, op.name), session);
|
|
539
|
+
observed = yield* getClusters({ name: fqName });
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// 3. Sync — each aspect independently idempotent. Re-observe
|
|
543
|
+
// after the labels call so the fingerprint stays valid for
|
|
544
|
+
// subsequent setters (although they don't all use it,
|
|
545
|
+
// keeping the observed view fresh prevents subtle drift in
|
|
546
|
+
// the deepEqual short-circuits below).
|
|
547
|
+
yield* syncClusterLabels({
|
|
548
|
+
fqName,
|
|
549
|
+
observed,
|
|
550
|
+
desired: desiredLabels,
|
|
551
|
+
session,
|
|
552
|
+
});
|
|
553
|
+
yield* syncClusterAddons({ fqName, observed, news, session });
|
|
554
|
+
yield* syncClusterMaintenance({ fqName, observed, news, session });
|
|
555
|
+
yield* syncClusterLocations({ fqName, observed, news, session });
|
|
556
|
+
yield* syncClusterNetworkPolicy({ fqName, observed, news, session });
|
|
557
|
+
yield* syncClusterLogging({ fqName, observed, news, session });
|
|
558
|
+
yield* syncClusterMonitoring({ fqName, observed, news, session });
|
|
559
|
+
yield* syncClusterUpdate({ fqName, observed, news, session });
|
|
560
|
+
|
|
561
|
+
const final = yield* getClusters({ name: fqName });
|
|
562
|
+
return toClusterAttributes(final, {
|
|
563
|
+
project: news.project,
|
|
564
|
+
location: news.location,
|
|
565
|
+
});
|
|
566
|
+
}),
|
|
567
|
+
delete: Effect.fn(function* ({ output, session }) {
|
|
568
|
+
const fqName = `projects/${output.project}/locations/${output.location}/clusters/${output.name}`;
|
|
569
|
+
yield* deleteClusters({ name: fqName }).pipe(
|
|
570
|
+
Effect.flatMap((op) =>
|
|
571
|
+
op.name
|
|
572
|
+
? awaitOperation(qualifyOp(fqName, op.name), session)
|
|
573
|
+
: Effect.succeed(op),
|
|
574
|
+
),
|
|
575
|
+
Effect.catchTag("NotFound", () => Effect.void),
|
|
576
|
+
);
|
|
577
|
+
}),
|
|
578
|
+
read: Effect.fn(function* ({ id, output, olds }) {
|
|
579
|
+
const project = output?.project ?? olds?.project;
|
|
580
|
+
const location = output?.location ?? olds?.location;
|
|
581
|
+
if (!project || !location) return undefined;
|
|
582
|
+
const name =
|
|
583
|
+
output?.name ??
|
|
584
|
+
olds?.name ??
|
|
585
|
+
(yield* createPhysicalName({ id, maxLength: 40 })).toLowerCase();
|
|
586
|
+
const fqName = `projects/${project}/locations/${location}/clusters/${name}`;
|
|
587
|
+
const observed = yield* getClusters({ name: fqName }).pipe(
|
|
588
|
+
Effect.catchTag("NotFound", () => Effect.succeed(undefined as cont.Cluster | undefined)),
|
|
589
|
+
Effect.catchTag("Forbidden", () => Effect.succeed(undefined as cont.Cluster | undefined)),
|
|
590
|
+
);
|
|
591
|
+
if (!observed) return undefined;
|
|
592
|
+
const attrs = toClusterAttributes(observed, { project, location });
|
|
593
|
+
return (yield* hasAlchemyLabels(id, observed.resourceLabels))
|
|
594
|
+
? attrs
|
|
595
|
+
: Unowned(attrs);
|
|
596
|
+
}),
|
|
597
|
+
};
|
|
598
|
+
}),
|
|
599
|
+
);
|