1import * as oracle from "@distilled.cloud/gcp/oracledatabase_v1";
2import * as Effect from "effect/Effect";
3import { Unowned } from "../../AdoptPolicy.ts";
4import { isResolved } from "../../Diff.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import { tagRecord } from "../../Tags.ts";
8import { GcpEnvironment } from "../Environment.ts";
9import {
10 createInternalLabels,
11 diffLabels,
12 hasAlchemyLabels,
13 toLabels,
14} from "../Labels.ts";
15import type { Providers } from "../Providers.ts";
16import {
17 OracleDatabaseNotResolved,
18 collectPages,
19 hasAlchemyLabelMap,
20 listAtLocation,
21 normalizeLocation,
22 parentOf,
23 parseName,
24 replaceOnIdentity,
25 resourceNameOf,
26 retryConflict,
27 retryQuota,
28 specifiedEquals,
29 type TimeZone,
30 toPhysicalId,
31 userLabels,
32 waitUntilExists,
33 waitUntilGone,
34 waitUntilReady,
35} from "./internal.ts";
36import { waitForOperation } from "./operations.ts";
37
38const COLLECTION = "exadbVmClusters";
39const FALLBACK_ID = "exadbvm";
40
41export type ExadbVmClusterStorageDetails = {
42 /** Storage per node in GB. */
43 sizeInGbsPerNode?: number;
44};
45
46export type DataCollectionOptionsCommon = {
47 /** Enable incident logs. */
48 isIncidentLogsEnabled?: boolean;
49 /** Enable health monitoring. */
50 isHealthMonitoringEnabled?: boolean;
51 /** Enable diagnostics events. */
52 isDiagnosticsEventsEnabled?: boolean;
53};
54
55export type ExadbVmClusterPropertiesInput = {
56 /** OCI cluster name. Immutable. */
57 clusterName?: string;
58 /** Grid image id. Immutable. Required on create. */
59 gridImageId?: string;
60 /** License model. Immutable. */
61 licenseModel?:
62 | oracle.ExadbVmClusterPropertiesLicenseModelEnum
63 | (string & {});
64 /** VM file system storage. Immutable. Required on create. */
65 vmFileSystemStorage?: ExadbVmClusterStorageDetails;
66 /** Additional ECPUs per node. Immutable. */
67 additionalEcpuCountPerNode?: number;
68 /** Data collection options. Immutable. */
69 dataCollectionOptions?: DataCollectionOptionsCommon;
70 /** Shape attribute (`SMART_STORAGE`, `BLOCK_STORAGE`). Immutable. */
71 shapeAttribute?:
72 | oracle.ExadbVmClusterPropertiesShapeAttributeEnum
73 | (string & {});
74 /** Node/VM count. Mutable via patch. */
75 nodeCount?: number;
76 /** Enabled ECPUs per node. Immutable. */
77 enabledEcpuCountPerNode?: number;
78 /** SCAN listener TCP port. Immutable. */
79 scanListenerPortTcp?: number;
80 /** SSH public keys. Immutable. */
81 sshPublicKeys?: string[];
82 /** Time zone. Immutable. */
83 timeZone?: TimeZone;
84 /** Exascale storage vault. Immutable. */
85 exascaleDbStorageVault?: string;
86 /** Hostname prefix. Immutable. */
87 hostnamePrefix?: string;
88};
89
90export type ExadbVmClusterProps = {
91 /**
92 * Exadb VM Cluster id. If omitted, a unique RFC1035 name is generated.
93 * Immutable.
94 */
95 exadbVmClusterId?: string;
96 /**
97 * Region. Immutable.
98 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
99 */
100 location?: string;
101 /**
102 * Display name. Immutable.
103 */
104 displayName?: string;
105 /**
106 * Client ODB Subnet. Immutable.
107 */
108 odbSubnet?: string;
109 /**
110 * Backup ODB Subnet. Immutable.
111 */
112 backupOdbSubnet?: string;
113 /**
114 * ODB Network. Immutable.
115 */
116 odbNetwork?: string;
117 /**
118 * User labels. Alchemy ownership labels are merged in automatically.
119 */
120 labels?: Record<string, string>;
121 /**
122 * Cluster properties.
123 */
124 properties?: ExadbVmClusterPropertiesInput;
125 /** Node count. Convenience alias for `properties.nodeCount`. */
126 nodeCount?: number;
127 /** Grid image id. Convenience alias for `properties.gridImageId`. */
128 gridImageId?: string;
129 /** Hostname prefix. Convenience alias for `properties.hostnamePrefix`. */
130 hostnamePrefix?: string;
131 /** SSH public keys. Convenience alias for `properties.sshPublicKeys`. */
132 sshPublicKeys?: string[];
133 /** Exascale vault. Convenience alias. */
134 exascaleDbStorageVault?: string;
135 /** Enabled ECPUs per node. Convenience alias. */
136 enabledEcpuCountPerNode?: number;
137};
138
139export type ExadbVmCluster = Resource<
140 "GCP.OracleDatabase.ExadbVmCluster",
141 ExadbVmClusterProps,
142 {
143 /** Full resource name. */
144 name: string;
145 /** Cluster id. */
146 exadbVmClusterId: string;
147 /** Project id. */
148 project: string;
149 /** Location id. */
150 location: string;
151 /** Display name. */
152 displayName: string | undefined;
153 /** Client ODB Subnet. */
154 odbSubnet: string | undefined;
155 /** Backup ODB Subnet. */
156 backupOdbSubnet: string | undefined;
157 /** ODB Network. */
158 odbNetwork: string | undefined;
159 /** User labels (Alchemy ownership labels stripped). */
160 labels: Record<string, string>;
161 /** GCP Oracle zone. */
162 gcpOracleZone: string | undefined;
163 /** Entitlement id. */
164 entitlementId: string | undefined;
165 /** Lifecycle state. */
166 lifecycleState: string | undefined;
167 /** Node count. */
168 nodeCount: number | undefined;
169 /** Hostname. */
170 hostname: string | undefined;
171 /** Grid Infrastructure version. */
172 giVersion: string | undefined;
173 /** RFC3339 creation timestamp. */
174 createTime: string | undefined;
175 },
176 never,
177 Providers
178>;
179
180/**
181 * An Oracle Exascale VM Cluster on Google Cloud.
182 *
183 * Changing `exadbVmClusterId`, `location`, ODB identity, `gridImageId`,
184 * or `exascaleDbStorageVault` replaces the cluster. Labels and
185 * `nodeCount` patch in place (adding VMs). Removing VMs uses a separate
186 * API and is not modeled here.
187 *
188 * ### Creating an Exadb VM Cluster
189 * **Example:** Generated name
190 * ```typescript
191 * const cluster = yield* GCP.OracleDatabase.ExadbVmCluster("ExaVm", {
192 * displayName: "exavm",
193 * odbSubnet: subnet.name,
194 * backupOdbSubnet: backupSubnet.name,
195 * gridImageId: "19.0.0.0",
196 * hostnamePrefix: "exavm",
197 * sshPublicKeys: [publicKey],
198 * exascaleDbStorageVault: vault.name,
199 * enabledEcpuCountPerNode: 8,
200 * nodeCount: 2,
201 * properties: {
202 * vmFileSystemStorage: { sizeInGbsPerNode: 180 },
203 * shapeAttribute: "SMART_STORAGE",
204 * },
205 * });
206 * ```
207 *
208 * @resource
209 * @category OracleDatabase
210 */
211export const ExadbVmCluster = Resource<ExadbVmCluster>(
212 "GCP.OracleDatabase.ExadbVmCluster",
213);
214
215const mergedProperties = (
216 news: ExadbVmClusterProps,
217): ExadbVmClusterPropertiesInput => ({
218 ...(news.properties ?? {}),
219 nodeCount: news.nodeCount ?? news.properties?.nodeCount,
220 gridImageId: news.gridImageId ?? news.properties?.gridImageId,
221 hostnamePrefix: news.hostnamePrefix ?? news.properties?.hostnamePrefix,
222 sshPublicKeys: news.sshPublicKeys ?? news.properties?.sshPublicKeys,
223 exascaleDbStorageVault:
224 news.exascaleDbStorageVault ?? news.properties?.exascaleDbStorageVault,
225 enabledEcpuCountPerNode:
226 news.enabledEcpuCountPerNode ?? news.properties?.enabledEcpuCountPerNode,
227});
228
229const toCreateBody = (
230 news: ExadbVmClusterProps,
231 desiredLabels: Record<string, string>,
232): oracle.ExadbVmCluster => {
233 const props = mergedProperties(news);
234 const properties: oracle.ExadbVmClusterProperties = {};
235 if (props.clusterName !== undefined)
236 properties.clusterName = props.clusterName;
237 if (props.gridImageId !== undefined)
238 properties.gridImageId = props.gridImageId;
239 if (props.licenseModel !== undefined) {
240 properties.licenseModel = props.licenseModel;
241 }
242 if (props.vmFileSystemStorage !== undefined) {
243 properties.vmFileSystemStorage = props.vmFileSystemStorage;
244 }
245 if (props.additionalEcpuCountPerNode !== undefined) {
246 properties.additionalEcpuCountPerNode = props.additionalEcpuCountPerNode;
247 }
248 if (props.dataCollectionOptions !== undefined) {
249 properties.dataCollectionOptions = props.dataCollectionOptions;
250 }
251 if (props.shapeAttribute !== undefined) {
252 properties.shapeAttribute = props.shapeAttribute;
253 }
254 if (props.nodeCount !== undefined) properties.nodeCount = props.nodeCount;
255 if (props.enabledEcpuCountPerNode !== undefined) {
256 properties.enabledEcpuCountPerNode = props.enabledEcpuCountPerNode;
257 }
258 if (props.scanListenerPortTcp !== undefined) {
259 properties.scanListenerPortTcp = props.scanListenerPortTcp;
260 }
261 if (props.sshPublicKeys !== undefined) {
262 properties.sshPublicKeys = props.sshPublicKeys;
263 }
264 if (props.timeZone !== undefined) properties.timeZone = props.timeZone;
265 if (props.exascaleDbStorageVault !== undefined) {
266 properties.exascaleDbStorageVault = props.exascaleDbStorageVault;
267 }
268 if (props.hostnamePrefix !== undefined) {
269 properties.hostnamePrefix = props.hostnamePrefix;
270 }
271 const body: oracle.ExadbVmCluster = {
272 labels: desiredLabels,
273 properties,
274 };
275 if (news.displayName !== undefined) body.displayName = news.displayName;
276 if (news.odbSubnet !== undefined) body.odbSubnet = news.odbSubnet;
277 if (news.backupOdbSubnet !== undefined) {
278 body.backupOdbSubnet = news.backupOdbSubnet;
279 }
280 if (news.odbNetwork !== undefined) body.odbNetwork = news.odbNetwork;
281 return body;
282};
283
284const toAttrs = (cluster: oracle.ExadbVmCluster, project: string) => {
285 const name = cluster.name ?? "";
286 const parsed = parseName(name, COLLECTION);
287 return {
288 name,
289 exadbVmClusterId: parsed.id,
290 project: parsed.project || project,
291 location: parsed.location,
292 displayName: cluster.displayName,
293 odbSubnet: cluster.odbSubnet,
294 backupOdbSubnet: cluster.backupOdbSubnet,
295 odbNetwork: cluster.odbNetwork,
296 labels: userLabels(cluster.labels),
297 gcpOracleZone: cluster.gcpOracleZone,
298 entitlementId: cluster.entitlementId,
299 lifecycleState: cluster.properties?.lifecycleState,
300 nodeCount: cluster.properties?.nodeCount,
301 hostname: cluster.properties?.hostname,
302 giVersion: cluster.properties?.giVersion,
303 createTime: cluster.createTime,
304 };
305};
306
307const getByName = (name: string) =>
308 retryQuota(oracle.getProjectsLocationsExadbVmClusters({ name })).pipe(
309 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
310 );
311
312const listClusters = (project: string, region: string) => {
313 const collect = (parent: string) =>
314 collectPages(
315 oracle.listProjectsLocationsExadbVmClusters.pages({
316 parent,
317 pageSize: 1000,
318 }),
319 (page) => page.exadbVmClusters,
320 ).pipe(
321 Effect.map((items) =>
322 items.filter((item) => hasAlchemyLabelMap(item.labels)),
323 ),
324 );
325 return listAtLocation(project, region, collect).pipe(
326 Effect.catchTag("NotFound", () => Effect.succeed([])),
327 );
328};
329
330export const ExadbVmClusterProvider = () =>
331 Provider.succeed(ExadbVmCluster, {
332 stables: ["name", "exadbVmClusterId", "project", "location", "createTime"],
333
334 diff: Effect.fn(function* ({ news, olds, output }) {
335 if (!isResolved(news)) return undefined;
336 const env = yield* GcpEnvironment.current;
337 const previousOdb = olds?.odbSubnet ?? output?.odbSubnet ?? "";
338 const nextOdb = news.odbSubnet ?? previousOdb;
339 const previousBackup =
340 olds?.backupOdbSubnet ?? output?.backupOdbSubnet ?? "";
341 const nextBackup = news.backupOdbSubnet ?? previousBackup;
342 const previousVault =
343 olds?.exascaleDbStorageVault ??
344 olds?.properties?.exascaleDbStorageVault ??
345 "";
346 const nextVault =
347 news.exascaleDbStorageVault ??
348 news.properties?.exascaleDbStorageVault ??
349 previousVault;
350 return replaceOnIdentity({
351 previousId: olds?.exadbVmClusterId ?? output?.exadbVmClusterId,
352 nextId:
353 news.exadbVmClusterId ??
354 olds?.exadbVmClusterId ??
355 output?.exadbVmClusterId,
356 previousLocation: normalizeLocation(
357 olds?.location ?? output?.location ?? env.region,
358 ),
359 nextLocation: normalizeLocation(
360 news.location ?? olds?.location ?? output?.location ?? env.region,
361 ),
362 extra:
363 nextOdb !== previousOdb ||
364 nextBackup !== previousBackup ||
365 nextVault !== previousVault,
366 });
367 }),
368
369 read: Effect.fn(function* ({ id, olds, output }) {
370 const env = yield* GcpEnvironment.current;
371 const exadbVmClusterId = yield* toPhysicalId(
372 id,
373 olds?.exadbVmClusterId,
374 output?.exadbVmClusterId,
375 FALLBACK_ID,
376 );
377 const location = normalizeLocation(
378 olds?.location ?? output?.location ?? env.region,
379 );
380 const name =
381 output?.name ??
382 resourceNameOf(env.project, location, COLLECTION, exadbVmClusterId);
383 const existing = yield* getByName(name);
384 if (existing === undefined) return undefined;
385 const attrs = toAttrs(existing, env.project);
386 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
387 ? attrs
388 : Unowned(attrs);
389 }),
390
391 list: () =>
392 Effect.gen(function* () {
393 const env = yield* GcpEnvironment.current;
394 const items = yield* listClusters(env.project, env.region);
395 return items.map((item) => toAttrs(item, env.project));
396 }),
397
398 reconcile: Effect.fn(function* ({ id, news, output }) {
399 const env = yield* GcpEnvironment.current;
400 const exadbVmClusterId = yield* toPhysicalId(
401 id,
402 news.exadbVmClusterId,
403 output?.exadbVmClusterId,
404 FALLBACK_ID,
405 );
406 const location = normalizeLocation(
407 news.location ?? output?.location ?? env.region,
408 );
409 const name = resourceNameOf(
410 env.project,
411 location,
412 COLLECTION,
413 exadbVmClusterId,
414 );
415 const desiredLabels = {
416 ...toLabels(news.labels),
417 ...(yield* createInternalLabels(id)),
418 };
419
420 let current = yield* getByName(output?.name ?? name);
421
422 if (current === undefined) {
423 const created = yield* oracle
424 .createProjectsLocationsExadbVmClusters({
425 parent: parentOf(env.project, location),
426 exadbVmClusterId,
427 body: toCreateBody(news, desiredLabels),
428 })
429 .pipe(
430 retryQuota,
431 Effect.catchTag("Conflict", () => Effect.succeed(undefined)),
432 );
433 if (created !== undefined) {
434 yield* waitForOperation(created);
435 }
436 current = yield* waitUntilExists(getByName(name), name);
437 }
438
439 if (current === undefined) {
440 return yield* new OracleDatabaseNotResolved({ name });
441 }
442
443 const ready = yield* waitUntilReady(
444 getByName(current.name ?? name),
445 current.name ?? name,
446 (value) => value.properties?.lifecycleState,
447 );
448
449 const props = mergedProperties(news);
450 const observedLabels = tagRecord(ready.labels);
451 const { upsert, removed } = diffLabels(observedLabels, desiredLabels);
452 const labelsChanged = upsert.length > 0 || removed.length > 0;
453 const nodeCountChanged =
454 props.nodeCount !== undefined &&
455 !specifiedEquals(props.nodeCount, ready.properties?.nodeCount);
456
457 if (labelsChanged || nodeCountChanged) {
458 const mask = [
459 labelsChanged ? "labels" : undefined,
460 nodeCountChanged ? "properties.node_count" : undefined,
461 ].filter((field): field is string => field !== undefined);
462 const operation = yield* retryQuota(
463 oracle.patchProjectsLocationsExadbVmClusters({
464 name: ready.name ?? name,
465 updateMask: mask.join(","),
466 body: {
467 name: ready.name ?? name,
468 labels: desiredLabels,
469 properties: {
470 nodeCount: props.nodeCount,
471 },
472 },
473 }),
474 );
475 yield* waitForOperation(operation);
476 return toAttrs(
477 yield* waitUntilExists(
478 getByName(ready.name ?? name),
479 ready.name ?? name,
480 ),
481 env.project,
482 );
483 }
484
485 return toAttrs(ready, env.project);
486 }),
487
488 delete: Effect.fn(function* ({ output }) {
489 const operation = yield* oracle
490 .deleteProjectsLocationsExadbVmClusters({
491 name: output.name,
492 })
493 .pipe(
494 retryConflict,
495 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
496 );
497 if (operation !== undefined) {
498 yield* waitForOperation(operation, { notFoundOk: true });
499 }
500 yield* waitUntilGone(getByName(output.name), output.name);
501 }),
502 });