@microagi/alchemy-gcp 0.5.1 → 0.7.0
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/CHANGELOG.md +47 -0
- package/lib/Auth/Credentials.d.ts +10 -4
- package/lib/Auth/Credentials.d.ts.map +1 -1
- package/lib/Auth/Credentials.js +13 -4
- package/lib/Auth/Credentials.js.map +1 -1
- package/lib/Compute/Address.d.ts +97 -0
- package/lib/Compute/Address.d.ts.map +1 -0
- package/lib/Compute/Address.js +143 -0
- package/lib/Compute/Address.js.map +1 -0
- package/lib/Compute/Firewall.d.ts +108 -0
- package/lib/Compute/Firewall.d.ts.map +1 -0
- package/lib/Compute/Firewall.js +163 -0
- package/lib/Compute/Firewall.js.map +1 -0
- package/lib/Compute/ForwardingRule.d.ts +106 -0
- package/lib/Compute/ForwardingRule.d.ts.map +1 -0
- package/lib/Compute/ForwardingRule.js +152 -0
- package/lib/Compute/ForwardingRule.js.map +1 -0
- package/lib/Compute/index.d.ts +3 -0
- package/lib/Compute/index.d.ts.map +1 -1
- package/lib/Compute/index.js +3 -0
- package/lib/Compute/index.js.map +1 -1
- package/lib/Container/Cluster.d.ts +20 -0
- package/lib/Container/Cluster.d.ts.map +1 -1
- package/lib/Container/Cluster.js +5 -2
- package/lib/Container/Cluster.js.map +1 -1
- package/lib/Container/NodePool.d.ts +26 -1
- package/lib/Container/NodePool.d.ts.map +1 -1
- package/lib/Container/NodePool.js +45 -8
- package/lib/Container/NodePool.js.map +1 -1
- package/lib/Kubernetes/KubernetesManifest.d.ts +100 -0
- package/lib/Kubernetes/KubernetesManifest.d.ts.map +1 -0
- package/lib/Kubernetes/KubernetesManifest.js +131 -0
- package/lib/Kubernetes/KubernetesManifest.js.map +1 -0
- package/lib/Kubernetes/Secret.d.ts +96 -0
- package/lib/Kubernetes/Secret.d.ts.map +1 -0
- package/lib/Kubernetes/Secret.js +173 -0
- package/lib/Kubernetes/Secret.js.map +1 -0
- package/lib/Kubernetes/client.d.ts +62 -0
- package/lib/Kubernetes/client.d.ts.map +1 -0
- package/lib/Kubernetes/client.js +233 -0
- package/lib/Kubernetes/client.js.map +1 -0
- package/lib/Kubernetes/connection.d.ts +48 -0
- package/lib/Kubernetes/connection.d.ts.map +1 -0
- package/lib/Kubernetes/connection.js +93 -0
- package/lib/Kubernetes/connection.js.map +1 -0
- package/lib/Kubernetes/index.d.ts +3 -0
- package/lib/Kubernetes/index.d.ts.map +1 -0
- package/lib/Kubernetes/index.js +3 -0
- package/lib/Kubernetes/index.js.map +1 -0
- package/lib/ManagedLustre/Instance.d.ts.map +1 -1
- package/lib/ManagedLustre/Instance.js +25 -5
- package/lib/ManagedLustre/Instance.js.map +1 -1
- package/lib/Providers.d.ts +1 -1
- package/lib/Providers.d.ts.map +1 -1
- package/lib/Providers.js +11 -1
- package/lib/Providers.js.map +1 -1
- package/lib/Sqladmin/Instance.d.ts.map +1 -1
- package/lib/Sqladmin/Instance.js +28 -6
- package/lib/Sqladmin/Instance.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/package.json +3 -1
- package/src/Auth/Credentials.ts +15 -5
- package/src/Compute/Address.ts +288 -0
- package/src/Compute/Firewall.ts +330 -0
- package/src/Compute/ForwardingRule.ts +301 -0
- package/src/Compute/index.ts +3 -0
- package/src/Container/Cluster.ts +25 -2
- package/src/Container/NodePool.ts +87 -13
- package/src/Kubernetes/KubernetesManifest.ts +301 -0
- package/src/Kubernetes/Secret.ts +348 -0
- package/src/Kubernetes/client.ts +386 -0
- package/src/Kubernetes/connection.ts +180 -0
- package/src/Kubernetes/index.ts +2 -0
- package/src/ManagedLustre/Instance.ts +35 -6
- package/src/Providers.ts +24 -0
- package/src/Sqladmin/Instance.ts +36 -7
- package/src/index.ts +1 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import * as compute from "@distilled.cloud/gcp/compute-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 { makeAwaitRegionOperation } from "./Operations.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A regional forwarding rule. The intended use here is a **Private
|
|
15
|
+
* Service Connect consumer endpoint** — binding a reserved internal
|
|
16
|
+
* {@link Address} to a producer *service attachment* (e.g. the one
|
|
17
|
+
* Cloud SQL publishes per PSC-enabled instance), so clients in this
|
|
18
|
+
* VPC reach the producer service at that IP.
|
|
19
|
+
*
|
|
20
|
+
* PSC endpoint rules carry no ports / load-balancing scheme — the API
|
|
21
|
+
* requires `loadBalancingScheme: ""` for them, which is what this
|
|
22
|
+
* resource sends. The endpoint must be created in the **same region as
|
|
23
|
+
* the service attachment**; set `allowPscGlobalAccess` so clients in
|
|
24
|
+
* other regions of the VPC can still use it.
|
|
25
|
+
*
|
|
26
|
+
* Other forwarding-rule shapes (L4 ILB, NetLB, proxy LBs) are not
|
|
27
|
+
* modelled — add props if/when needed.
|
|
28
|
+
*
|
|
29
|
+
* All connection-defining props are immutable here and trigger replace;
|
|
30
|
+
* a PSC endpoint is cheap to recreate. Labels are mutable.
|
|
31
|
+
*
|
|
32
|
+
* @section Creating a PSC endpoint
|
|
33
|
+
* @example Cloud SQL PSC endpoint with global access
|
|
34
|
+
* ```typescript
|
|
35
|
+
* const endpoint = yield* GCP.ForwardingRule("DbPscEndpoint", {
|
|
36
|
+
* project: hostProject.projectId,
|
|
37
|
+
* region: "europe-west1",
|
|
38
|
+
* network: vpc.selfLink,
|
|
39
|
+
* ipAddress: endpointIp.selfLink,
|
|
40
|
+
* target: "projects/<producer>/regions/europe-west1/serviceAttachments/<sa>",
|
|
41
|
+
* allowPscGlobalAccess: true,
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export type ForwardingRuleProps = {
|
|
46
|
+
/** GCP project ID hosting the rule. Immutable — replace if changed. */
|
|
47
|
+
project: string;
|
|
48
|
+
/** GCP region. Must match the target service attachment's region. Immutable — replace. */
|
|
49
|
+
region: string;
|
|
50
|
+
/**
|
|
51
|
+
* Rule name. Defaults to `createPhysicalName({ id, lowercase: true,
|
|
52
|
+
* maxLength: 63 })`. Immutable — replace if changed.
|
|
53
|
+
*/
|
|
54
|
+
name?: string;
|
|
55
|
+
/** Description (free-form). Set on create; immutable. */
|
|
56
|
+
description?: string;
|
|
57
|
+
/**
|
|
58
|
+
* URL of the consumer VPC. Pass `vpc.selfLink` from a `GCP.Network`
|
|
59
|
+
* to make alchemy sequence the rule after the VPC. Immutable — replace.
|
|
60
|
+
*/
|
|
61
|
+
network: string;
|
|
62
|
+
/**
|
|
63
|
+
* The endpoint IP — a URL of a reserved `GCP.Address` (pass
|
|
64
|
+
* `address.selfLink` for the dep edge) or a literal IP. Immutable —
|
|
65
|
+
* replace.
|
|
66
|
+
*/
|
|
67
|
+
ipAddress: string;
|
|
68
|
+
/**
|
|
69
|
+
* URI of the producer service attachment to connect to
|
|
70
|
+
* (`projects/{p}/regions/{r}/serviceAttachments/{name}`). Immutable —
|
|
71
|
+
* replace.
|
|
72
|
+
*/
|
|
73
|
+
target: string;
|
|
74
|
+
/**
|
|
75
|
+
* Let clients in any region of the VPC use this endpoint (PSC global
|
|
76
|
+
* access). Defaults to `false`. Immutable — replace.
|
|
77
|
+
*/
|
|
78
|
+
allowPscGlobalAccess?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Resource labels. Alchemy internal labels (`alchemy_app`,
|
|
81
|
+
* `alchemy_stage`, `alchemy_id`) are merged on top automatically and
|
|
82
|
+
* are reserved. Mutable via `setLabels`.
|
|
83
|
+
*/
|
|
84
|
+
labels?: Record<string, string>;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export type ForwardingRuleAttributes = {
|
|
88
|
+
/** Rule name. */
|
|
89
|
+
name: string;
|
|
90
|
+
/** GCP project ID, threaded through from props for delete/read. */
|
|
91
|
+
project: string;
|
|
92
|
+
/** GCP region, threaded through from props for delete/read. */
|
|
93
|
+
region: string;
|
|
94
|
+
/** Server-defined URL. */
|
|
95
|
+
selfLink: string;
|
|
96
|
+
/** Server-assigned numeric id. */
|
|
97
|
+
id: string;
|
|
98
|
+
/** The endpoint IP (the API always returns the literal IP). */
|
|
99
|
+
ipAddress: string;
|
|
100
|
+
/** Producer service attachment URI. */
|
|
101
|
+
target: string | undefined;
|
|
102
|
+
/** Consumer VPC URL. */
|
|
103
|
+
network: string | undefined;
|
|
104
|
+
/** PSC global access flag. */
|
|
105
|
+
allowPscGlobalAccess: boolean;
|
|
106
|
+
/** Server-assigned PSC connection id. */
|
|
107
|
+
pscConnectionId: string | undefined;
|
|
108
|
+
/** PSC connection status: `ACCEPTED`, `PENDING`, `REJECTED`, `CLOSED`, … */
|
|
109
|
+
pscConnectionStatus: string | undefined;
|
|
110
|
+
/** Labels currently set, including the alchemy internals. */
|
|
111
|
+
labels: Record<string, string>;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export type ForwardingRule = Resource<
|
|
115
|
+
"GCP.ForwardingRule",
|
|
116
|
+
ForwardingRuleProps,
|
|
117
|
+
ForwardingRuleAttributes,
|
|
118
|
+
never,
|
|
119
|
+
GCP.Providers
|
|
120
|
+
>;
|
|
121
|
+
export const ForwardingRule = Resource<ForwardingRule>("GCP.ForwardingRule");
|
|
122
|
+
|
|
123
|
+
const toForwardingRuleAttributes = (
|
|
124
|
+
f: compute.ForwardingRule,
|
|
125
|
+
parent: { project: string; region: string },
|
|
126
|
+
): ForwardingRuleAttributes => ({
|
|
127
|
+
name: f.name ?? "",
|
|
128
|
+
project: parent.project,
|
|
129
|
+
region: parent.region,
|
|
130
|
+
selfLink: f.selfLink ?? "",
|
|
131
|
+
id: f.id ?? "",
|
|
132
|
+
ipAddress: f.IPAddress ?? "",
|
|
133
|
+
target: f.target,
|
|
134
|
+
network: f.network,
|
|
135
|
+
allowPscGlobalAccess: f.allowPscGlobalAccess ?? false,
|
|
136
|
+
pscConnectionId: f.pscConnectionId,
|
|
137
|
+
pscConnectionStatus: f.pscConnectionStatus,
|
|
138
|
+
labels: { ...(f.labels ?? {}) },
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
export const ForwardingRuleProvider = () =>
|
|
142
|
+
Provider.effect(
|
|
143
|
+
ForwardingRule,
|
|
144
|
+
Effect.gen(function* () {
|
|
145
|
+
const getForwardingRules = yield* compute.getForwardingRules;
|
|
146
|
+
const insertForwardingRules = yield* compute.insertForwardingRules;
|
|
147
|
+
const deleteForwardingRules = yield* compute.deleteForwardingRules;
|
|
148
|
+
const setLabelsForwardingRules = yield* compute.setLabelsForwardingRules;
|
|
149
|
+
const getRegionOperations = yield* compute.getRegionOperations;
|
|
150
|
+
const awaitOp = makeAwaitRegionOperation(getRegionOperations);
|
|
151
|
+
|
|
152
|
+
const observe = (project: string, region: string, name: string) =>
|
|
153
|
+
getForwardingRules({ project, region, forwardingRule: name }).pipe(
|
|
154
|
+
Effect.catchTag("NotFound", () =>
|
|
155
|
+
Effect.succeed(undefined as compute.ForwardingRule | undefined),
|
|
156
|
+
),
|
|
157
|
+
Effect.catchTag("Forbidden", () =>
|
|
158
|
+
Effect.succeed(undefined as compute.ForwardingRule | undefined),
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const syncLabels = Effect.fn(function* (args: {
|
|
163
|
+
project: string;
|
|
164
|
+
region: string;
|
|
165
|
+
observed: compute.ForwardingRule;
|
|
166
|
+
desired: Record<string, string>;
|
|
167
|
+
session: ScopedPlanStatusSession;
|
|
168
|
+
}) {
|
|
169
|
+
if (deepEqual(args.observed.labels ?? {}, args.desired)) return;
|
|
170
|
+
const op = yield* setLabelsForwardingRules({
|
|
171
|
+
project: args.project,
|
|
172
|
+
region: args.region,
|
|
173
|
+
resource: args.observed.name ?? "",
|
|
174
|
+
body: {
|
|
175
|
+
labels: args.desired,
|
|
176
|
+
labelFingerprint: args.observed.labelFingerprint,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
if (op.name) {
|
|
180
|
+
yield* awaitOp(args.project, args.region, op.name, args.session);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
stables: ["name", "project", "region", "selfLink", "id"],
|
|
186
|
+
diff: Effect.fn(function* ({ news, olds = {} }) {
|
|
187
|
+
if (!isResolved(news)) return undefined;
|
|
188
|
+
if (
|
|
189
|
+
somePropsAreDifferent(olds as ForwardingRuleProps, news, [
|
|
190
|
+
"project",
|
|
191
|
+
"region",
|
|
192
|
+
"name",
|
|
193
|
+
"network",
|
|
194
|
+
"ipAddress",
|
|
195
|
+
"target",
|
|
196
|
+
"allowPscGlobalAccess",
|
|
197
|
+
])
|
|
198
|
+
) {
|
|
199
|
+
return { action: "replace" } as const;
|
|
200
|
+
}
|
|
201
|
+
return undefined;
|
|
202
|
+
}),
|
|
203
|
+
reconcile: Effect.fn(function* ({ id, news, session }) {
|
|
204
|
+
const internalLabels = yield* gcpInternalLabels(id);
|
|
205
|
+
const desiredName =
|
|
206
|
+
news.name ??
|
|
207
|
+
(yield* createPhysicalName({ id, maxLength: 63 })).toLowerCase();
|
|
208
|
+
const desiredLabels: Record<string, string> = {
|
|
209
|
+
...(news.labels ?? {}),
|
|
210
|
+
...internalLabels,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
let observed = yield* observe(news.project, news.region, desiredName);
|
|
214
|
+
|
|
215
|
+
if (!observed) {
|
|
216
|
+
const body: compute.ForwardingRule = {
|
|
217
|
+
name: desiredName,
|
|
218
|
+
...(news.description ? { description: news.description } : {}),
|
|
219
|
+
network: news.network,
|
|
220
|
+
IPAddress: news.ipAddress,
|
|
221
|
+
target: news.target,
|
|
222
|
+
// PSC consumer endpoints take no scheme — the API rejects
|
|
223
|
+
// anything other than the empty string for them.
|
|
224
|
+
loadBalancingScheme: "",
|
|
225
|
+
...(news.allowPscGlobalAccess !== undefined
|
|
226
|
+
? { allowPscGlobalAccess: news.allowPscGlobalAccess }
|
|
227
|
+
: {}),
|
|
228
|
+
labels: desiredLabels,
|
|
229
|
+
};
|
|
230
|
+
const op = yield* insertForwardingRules({
|
|
231
|
+
project: news.project,
|
|
232
|
+
region: news.region,
|
|
233
|
+
body,
|
|
234
|
+
}).pipe(
|
|
235
|
+
Effect.catchTag("Conflict", () =>
|
|
236
|
+
Effect.succeed(undefined as compute.Operation | undefined),
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
if (op?.name) {
|
|
240
|
+
yield* awaitOp(news.project, news.region, op.name, session);
|
|
241
|
+
}
|
|
242
|
+
observed = yield* getForwardingRules({
|
|
243
|
+
project: news.project,
|
|
244
|
+
region: news.region,
|
|
245
|
+
forwardingRule: desiredName,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
yield* syncLabels({
|
|
250
|
+
project: news.project,
|
|
251
|
+
region: news.region,
|
|
252
|
+
observed,
|
|
253
|
+
desired: desiredLabels,
|
|
254
|
+
session,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const final = yield* getForwardingRules({
|
|
258
|
+
project: news.project,
|
|
259
|
+
region: news.region,
|
|
260
|
+
forwardingRule: desiredName,
|
|
261
|
+
});
|
|
262
|
+
return toForwardingRuleAttributes(final, {
|
|
263
|
+
project: news.project,
|
|
264
|
+
region: news.region,
|
|
265
|
+
});
|
|
266
|
+
}),
|
|
267
|
+
delete: Effect.fn(function* ({ output, session }) {
|
|
268
|
+
yield* deleteForwardingRules({
|
|
269
|
+
project: output.project,
|
|
270
|
+
region: output.region,
|
|
271
|
+
forwardingRule: output.name,
|
|
272
|
+
}).pipe(
|
|
273
|
+
Effect.flatMap((op) =>
|
|
274
|
+
op.name
|
|
275
|
+
? awaitOp(output.project, output.region, op.name, session)
|
|
276
|
+
: Effect.succeed(op),
|
|
277
|
+
),
|
|
278
|
+
Effect.catchTag("NotFound", () => Effect.void),
|
|
279
|
+
);
|
|
280
|
+
}),
|
|
281
|
+
read: Effect.fn(function* ({ id, output, olds }) {
|
|
282
|
+
const project = output?.project ?? olds?.project;
|
|
283
|
+
const region = output?.region ?? olds?.region;
|
|
284
|
+
if (!project || !region) return undefined;
|
|
285
|
+
const name =
|
|
286
|
+
output?.name ??
|
|
287
|
+
olds?.name ??
|
|
288
|
+
(yield* createPhysicalName({ id, maxLength: 63 })).toLowerCase();
|
|
289
|
+
const observed = yield* observe(project, region, name);
|
|
290
|
+
if (!observed) return undefined;
|
|
291
|
+
const attrs = toForwardingRuleAttributes(observed, {
|
|
292
|
+
project,
|
|
293
|
+
region,
|
|
294
|
+
});
|
|
295
|
+
return (yield* hasAlchemyLabels(id, observed.labels))
|
|
296
|
+
? attrs
|
|
297
|
+
: Unowned(attrs);
|
|
298
|
+
}),
|
|
299
|
+
};
|
|
300
|
+
}),
|
|
301
|
+
);
|
package/src/Compute/index.ts
CHANGED
package/src/Container/Cluster.ts
CHANGED
|
@@ -74,6 +74,26 @@ export type ClusterProps = {
|
|
|
74
74
|
clusterSecondaryRangeName?: string;
|
|
75
75
|
servicesSecondaryRangeName?: string;
|
|
76
76
|
};
|
|
77
|
+
/**
|
|
78
|
+
* Cluster-level network settings. `datapathProvider:
|
|
79
|
+
* "ADVANCED_DATAPATH"` enables GKE Dataplane V2 (eBPF/Cilium
|
|
80
|
+
* dataplane with built-in NetworkPolicy enforcement). GKE only
|
|
81
|
+
* honors this at cluster creation — existing clusters cannot be
|
|
82
|
+
* migrated in place.
|
|
83
|
+
*
|
|
84
|
+
* **Consumed only on the create path** (like `initialNodePool`):
|
|
85
|
+
* changing it on an existing cluster is a silent no-op, NOT a
|
|
86
|
+
* replace trigger. A diff-driven same-name replace would be
|
|
87
|
+
* destructive here — the engine creates the new generation first,
|
|
88
|
+
* which observe-adopts the still-existing same-name cluster, and
|
|
89
|
+
* end-of-deploy GC would then delete that physical cluster out from
|
|
90
|
+
* under the "new" generation. To rebuild with a different datapath,
|
|
91
|
+
* delete the cluster out-of-band and redeploy; reconcile observes
|
|
92
|
+
* it missing and creates with the new setting.
|
|
93
|
+
*/
|
|
94
|
+
networkConfig?: {
|
|
95
|
+
datapathProvider?: "LEGACY_DATAPATH" | "ADVANCED_DATAPATH";
|
|
96
|
+
};
|
|
77
97
|
/** GKE release channel. Mutable via `update` (`desiredReleaseChannel`). */
|
|
78
98
|
releaseChannel?: { channel: "RAPID" | "REGULAR" | "STABLE" | "UNSPECIFIED" };
|
|
79
99
|
/**
|
|
@@ -470,8 +490,10 @@ export const ClusterProvider = () =>
|
|
|
470
490
|
if (!deepEqual(olds.ipAllocationPolicy, news.ipAllocationPolicy)) {
|
|
471
491
|
return { action: "replace" } as const;
|
|
472
492
|
}
|
|
473
|
-
// `initialNodePool`
|
|
474
|
-
// consumed only on create. See
|
|
493
|
+
// `initialNodePool` and `networkConfig` are intentionally NOT
|
|
494
|
+
// part of diff — both are consumed only on create. See the
|
|
495
|
+
// ClusterProps JSDoc (networkConfig explains why a same-name
|
|
496
|
+
// replace would be destructive).
|
|
475
497
|
return undefined;
|
|
476
498
|
}),
|
|
477
499
|
reconcile: Effect.fn(function* ({ id, news, session }) {
|
|
@@ -521,6 +543,7 @@ export const ClusterProvider = () =>
|
|
|
521
543
|
...(news.ipAllocationPolicy
|
|
522
544
|
? { ipAllocationPolicy: news.ipAllocationPolicy }
|
|
523
545
|
: {}),
|
|
546
|
+
...(news.networkConfig ? { networkConfig: news.networkConfig } : {}),
|
|
524
547
|
...(news.releaseChannel ? { releaseChannel: news.releaseChannel } : {}),
|
|
525
548
|
...(news.workloadIdentityConfig
|
|
526
549
|
? { workloadIdentityConfig: news.workloadIdentityConfig }
|
|
@@ -113,8 +113,31 @@ export type NodePoolProps = {
|
|
|
113
113
|
enableSecureBoot?: boolean;
|
|
114
114
|
enableIntegrityMonitoring?: boolean;
|
|
115
115
|
};
|
|
116
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* Local SSDs via the legacy SCSI-era count field. Prefer
|
|
118
|
+
* `localNvmeSsdBlockConfig` / `ephemeralStorageLocalSsdConfig` for
|
|
119
|
+
* NVMe-only machine families (Gen3+, Titanium SSD). Mutually
|
|
120
|
+
* exclusive with both. **Replace-only**.
|
|
121
|
+
*/
|
|
117
122
|
localSsdCount?: number;
|
|
123
|
+
/**
|
|
124
|
+
* Raw-block local NVMe SSDs — exposed to workloads as unformatted
|
|
125
|
+
* block devices (local PVs / your own RAID). For machine types with
|
|
126
|
+
* a fixed disk count (e.g. g4-standard-48 → 4), `localSsdCount`
|
|
127
|
+
* must equal that count or be 0/unset for the machine default.
|
|
128
|
+
* **Replace-only**.
|
|
129
|
+
*/
|
|
130
|
+
localNvmeSsdBlockConfig?: { localSsdCount?: number };
|
|
131
|
+
/**
|
|
132
|
+
* Local NVMe SSDs backing node ephemeral storage (emptyDir, image
|
|
133
|
+
* layers, logs) — GKE RAID0s the disks. Same fixed-count rule as
|
|
134
|
+
* `localNvmeSsdBlockConfig`. `dataCacheCount` carves disks out for
|
|
135
|
+
* GKE Data Cache. **Replace-only**.
|
|
136
|
+
*/
|
|
137
|
+
ephemeralStorageLocalSsdConfig?: {
|
|
138
|
+
localSsdCount?: number;
|
|
139
|
+
dataCacheCount?: number;
|
|
140
|
+
};
|
|
118
141
|
/** Workload Identity metadata. Mutable via `update`. */
|
|
119
142
|
workloadMetadataConfig?: { mode: "GKE_METADATA" | "GCE_METADATA" };
|
|
120
143
|
/** GVNIC. Mutable via `update`. */
|
|
@@ -261,6 +284,12 @@ export const toNodeConfigCreateBody = (
|
|
|
261
284
|
...(config.localSsdCount !== undefined
|
|
262
285
|
? { localSsdCount: config.localSsdCount }
|
|
263
286
|
: {}),
|
|
287
|
+
...(config.localNvmeSsdBlockConfig
|
|
288
|
+
? { localNvmeSsdBlockConfig: config.localNvmeSsdBlockConfig }
|
|
289
|
+
: {}),
|
|
290
|
+
...(config.ephemeralStorageLocalSsdConfig
|
|
291
|
+
? { ephemeralStorageLocalSsdConfig: config.ephemeralStorageLocalSsdConfig }
|
|
292
|
+
: {}),
|
|
264
293
|
...(config.workloadMetadataConfig
|
|
265
294
|
? { workloadMetadataConfig: config.workloadMetadataConfig }
|
|
266
295
|
: {}),
|
|
@@ -320,8 +349,23 @@ const toNodePoolUpdateBody = (
|
|
|
320
349
|
if (nc.taints && !deepEqual(oc.taints ?? [], nc.taints)) {
|
|
321
350
|
body.taints = { taints: nc.taints };
|
|
322
351
|
}
|
|
323
|
-
|
|
324
|
-
|
|
352
|
+
// GKE injects its own `goog-*` resource labels (accelerator type,
|
|
353
|
+
// provisioning model, …) after create. Treat them as server-managed:
|
|
354
|
+
// fold the observed ones into the desired set before diffing, or
|
|
355
|
+
// every reconcile issues a labels update — which on a large pool is
|
|
356
|
+
// a long rolling cluster operation that wedges all other mutations
|
|
357
|
+
// on the cluster ("incompatible operation").
|
|
358
|
+
const observedResourceLabels = oc.resourceLabels ?? {};
|
|
359
|
+
const desiredWithServerManaged = {
|
|
360
|
+
...Object.fromEntries(
|
|
361
|
+
Object.entries(observedResourceLabels).filter(([k]) =>
|
|
362
|
+
k.startsWith("goog-"),
|
|
363
|
+
),
|
|
364
|
+
),
|
|
365
|
+
...desiredResourceLabels,
|
|
366
|
+
};
|
|
367
|
+
if (!deepEqual(observedResourceLabels, desiredWithServerManaged)) {
|
|
368
|
+
body.resourceLabels = { labels: desiredWithServerManaged };
|
|
325
369
|
}
|
|
326
370
|
|
|
327
371
|
if (nc.gvnic && !deepEqual(oc.gvnic, nc.gvnic)) {
|
|
@@ -365,6 +409,7 @@ export const NodePoolProvider = () =>
|
|
|
365
409
|
NodePool,
|
|
366
410
|
Effect.gen(function* () {
|
|
367
411
|
const getNodePools = yield* cont.getProjectsLocationsClustersNodePools;
|
|
412
|
+
const listNodePools = yield* cont.listProjectsLocationsClustersNodePools;
|
|
368
413
|
const createNodePools = yield* cont.createProjectsLocationsClustersNodePools;
|
|
369
414
|
const updateNodePools = yield* cont.updateProjectsLocationsClustersNodePools;
|
|
370
415
|
const setSize = yield* cont.setSizeProjectsLocationsClustersNodePools;
|
|
@@ -500,7 +545,12 @@ export const NodePoolProvider = () =>
|
|
|
500
545
|
!deepEqual(oc.metadata, nc.metadata) ||
|
|
501
546
|
!deepEqual(oc.sandboxConfig, nc.sandboxConfig) ||
|
|
502
547
|
!deepEqual(oc.reservationAffinity, nc.reservationAffinity) ||
|
|
503
|
-
!deepEqual(oc.shieldedInstanceConfig, nc.shieldedInstanceConfig)
|
|
548
|
+
!deepEqual(oc.shieldedInstanceConfig, nc.shieldedInstanceConfig) ||
|
|
549
|
+
!deepEqual(oc.localNvmeSsdBlockConfig, nc.localNvmeSsdBlockConfig) ||
|
|
550
|
+
!deepEqual(
|
|
551
|
+
oc.ephemeralStorageLocalSsdConfig,
|
|
552
|
+
nc.ephemeralStorageLocalSsdConfig,
|
|
553
|
+
)
|
|
504
554
|
) {
|
|
505
555
|
return { action: "replace" } as const;
|
|
506
556
|
}
|
|
@@ -596,15 +646,39 @@ export const NodePoolProvider = () =>
|
|
|
596
646
|
const location = output?.location ?? olds?.location;
|
|
597
647
|
const clusterName = output?.clusterName ?? olds?.clusterName;
|
|
598
648
|
if (!project || !location || !clusterName) return undefined;
|
|
599
|
-
const name =
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
649
|
+
const name = output?.name ?? olds?.name;
|
|
650
|
+
let observed: cont.NodePool | undefined;
|
|
651
|
+
if (name) {
|
|
652
|
+
const fqName = `projects/${project}/locations/${location}/clusters/${clusterName}/nodePools/${name}`;
|
|
653
|
+
observed = yield* getNodePools({ name: fqName }).pipe(
|
|
654
|
+
Effect.catchTag("NotFound", () => Effect.succeed(undefined as cont.NodePool | undefined)),
|
|
655
|
+
Effect.catchTag("Forbidden", () => Effect.succeed(undefined as cont.NodePool | undefined)),
|
|
656
|
+
);
|
|
657
|
+
} else {
|
|
658
|
+
// Cold recovery (lost state): the pool name truncates the
|
|
659
|
+
// logical id and appends a random per-instance suffix that
|
|
660
|
+
// lived only in state, so a probe against a freshly-generated
|
|
661
|
+
// name can never hit. Scan the cluster's pools for our
|
|
662
|
+
// alchemy labels (stamped into config.resourceLabels).
|
|
663
|
+
const page = yield* listNodePools({
|
|
664
|
+
parent: `projects/${project}/locations/${location}/clusters/${clusterName}`,
|
|
665
|
+
}).pipe(
|
|
666
|
+
Effect.catchTag("NotFound", () =>
|
|
667
|
+
Effect.succeed(undefined as cont.ListNodePoolsResponse | undefined),
|
|
668
|
+
),
|
|
669
|
+
Effect.catchTag("Forbidden", () =>
|
|
670
|
+
Effect.succeed(undefined as cont.ListNodePoolsResponse | undefined),
|
|
671
|
+
),
|
|
672
|
+
);
|
|
673
|
+
for (const candidate of page?.nodePools ?? []) {
|
|
674
|
+
if (
|
|
675
|
+
yield* hasAlchemyLabels(id, candidate.config?.resourceLabels)
|
|
676
|
+
) {
|
|
677
|
+
observed = candidate;
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
608
682
|
if (!observed) return undefined;
|
|
609
683
|
const attrs = toNodePoolAttributes(observed, {
|
|
610
684
|
project,
|