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 { createInternalLabels, hasAlchemyLabels, toLabels } from "../Labels.ts";
10import type { Providers } from "../Providers.ts";
11import {
12 OracleDatabaseNotResolved,
13 collectPages,
14 expandNetwork,
15 hasAlchemyLabelMap,
16 listAtLocation,
17 normalizeLocation,
18 parentOf,
19 parseName,
20 replaceOnIdentity,
21 resourceNameOf,
22 retryConflict,
23 retryQuota,
24 type TimeZone,
25 toPhysicalId,
26 userLabels,
27 waitUntilExists,
28 waitUntilGone,
29 waitUntilReady,
30} from "./internal.ts";
31import { waitForOperation } from "./operations.ts";
32
33const COLLECTION = "cloudVmClusters";
34const FALLBACK_ID = "vmcluster";
35
36export type DataCollectionOptions = {
37 /** Enable incident logs and traces. */
38 incidentLogsEnabled?: boolean;
39 /** Enable diagnostic collection. */
40 diagnosticsEventsEnabled?: boolean;
41 /** Enable health monitoring. */
42 healthMonitoringEnabled?: boolean;
43};
44
45export type CloudVmClusterPropertiesInput = {
46 /** License type. Required on create. */
47 licenseType?: oracle.CloudVmClusterPropertiesLicenseTypeEnum | (string & {});
48 /** Enabled CPU cores. Required on create. */
49 cpuCoreCount?: number;
50 /** OCPU count per VM. */
51 ocpuCount?: number;
52 /** Node count. */
53 nodeCount?: number;
54 /** Grid Infrastructure version. */
55 giVersion?: string;
56 /** OS image version. */
57 systemVersion?: string;
58 /** Memory in GB. */
59 memorySizeGb?: number;
60 /** Local storage per VM in GB. */
61 dbNodeStorageSizeGb?: number;
62 /** DATA disk group size in TB. */
63 dataStorageSizeTb?: number;
64 /** Hostname prefix. */
65 hostnamePrefix?: string;
66 /** SSH public keys. */
67 sshPublicKeys?: string[];
68 /** SCAN listener TCP port. */
69 scanListenerPortTcp?: number;
70 /** SCAN listener TLS port. */
71 scanListenerPortTcpSsl?: number;
72 /** Disk redundancy (`HIGH`, `NORMAL`). */
73 diskRedundancy?:
74 | oracle.CloudVmClusterPropertiesDiskRedundancyEnum
75 | (string & {});
76 /** Time zone. */
77 timeZone?: TimeZone;
78 /** OCI cluster name. */
79 clusterName?: string;
80 /** Use local backup. */
81 localBackupEnabled?: boolean;
82 /** Use sparse disk group. */
83 sparseDiskgroupEnabled?: boolean;
84 /** Diagnostics collection options. */
85 diagnosticsDataCollectionOptions?: DataCollectionOptions;
86 /** Database server OCIDs. */
87 dbServerOcids?: string[];
88};
89
90export type CloudVmClusterProps = {
91 /**
92 * VM Cluster id. If omitted, a unique RFC1035 name is generated.
93 * Immutable.
94 */
95 cloudVmClusterId?: 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 * Exadata Infrastructure
103 * (`projects/{project}/locations/{region}/cloudExadataInfrastructures/{id}`).
104 * Immutable.
105 */
106 exadataInfrastructure?: string;
107 /**
108 * Display name.
109 */
110 displayName?: string;
111 /**
112 * VPC network. Immutable. Required unless `odbSubnet` is set.
113 */
114 network?: string;
115 /**
116 * Cluster IP CIDR. Immutable.
117 */
118 cidr?: string;
119 /**
120 * Backup subnet CIDR. Immutable.
121 */
122 backupSubnetCidr?: string;
123 /**
124 * ODB Network. Immutable.
125 */
126 odbNetwork?: string;
127 /**
128 * ODB Subnet for IP allocation. Immutable.
129 */
130 odbSubnet?: string;
131 /**
132 * Backup ODB Subnet. Immutable.
133 */
134 backupOdbSubnet?: string;
135 /**
136 * Exascale storage vault. Immutable.
137 */
138 exascaleDbStorageVault?: string;
139 /**
140 * User labels. Alchemy ownership labels are merged in automatically.
141 */
142 labels?: Record<string, string>;
143 /**
144 * VM cluster properties.
145 */
146 properties?: CloudVmClusterPropertiesInput;
147 /** License type. Convenience alias for `properties.licenseType`. */
148 licenseType?: oracle.CloudVmClusterPropertiesLicenseTypeEnum | (string & {});
149 /** CPU core count. Convenience alias for `properties.cpuCoreCount`. */
150 cpuCoreCount?: number;
151 /** Grid Infrastructure version. Convenience alias for `properties.giVersion`. */
152 giVersion?: string;
153 /** Hostname prefix. Convenience alias for `properties.hostnamePrefix`. */
154 hostnamePrefix?: string;
155 /** SSH public keys. Convenience alias for `properties.sshPublicKeys`. */
156 sshPublicKeys?: string[];
157};
158
159export type CloudVmCluster = Resource<
160 "GCP.OracleDatabase.CloudVmCluster",
161 CloudVmClusterProps,
162 {
163 /** Full resource name. */
164 name: string;
165 /** VM Cluster id. */
166 cloudVmClusterId: string;
167 /** Project id. */
168 project: string;
169 /** Location id. */
170 location: string;
171 /** Exadata Infrastructure. */
172 exadataInfrastructure: string | undefined;
173 /** Display name. */
174 displayName: string | undefined;
175 /** VPC network. */
176 network: string | undefined;
177 /** Cluster CIDR. */
178 cidr: string | undefined;
179 /** Backup subnet CIDR. */
180 backupSubnetCidr: string | undefined;
181 /** ODB Network. */
182 odbNetwork: string | undefined;
183 /** ODB Subnet. */
184 odbSubnet: string | undefined;
185 /** Backup ODB Subnet. */
186 backupOdbSubnet: string | undefined;
187 /** User labels (Alchemy ownership labels stripped). */
188 labels: Record<string, string>;
189 /** GCP Oracle zone. */
190 gcpOracleZone: string | undefined;
191 /** Lifecycle state. */
192 state: string | undefined;
193 /** License type. */
194 licenseType: string | undefined;
195 /** CPU cores. */
196 cpuCoreCount: number | undefined;
197 /** Hostname. */
198 hostname: string | undefined;
199 /** OCID. */
200 ocid: string | undefined;
201 /** RFC3339 creation timestamp. */
202 createTime: string | undefined;
203 },
204 never,
205 Providers
206>;
207
208/**
209 * An Oracle Exadata VM Cluster on Google Cloud.
210 *
211 * Changing `cloudVmClusterId`, `location`, `exadataInfrastructure`,
212 * network identity (`network`/`cidr`/`odbSubnet`), or license type
213 * replaces the cluster. There is no patch API in the distilled SDK, so
214 * labels are applied at create.
215 *
216 * Delete sends `force=true` so child resources do not block teardown.
217 *
218 * ### Creating a VM Cluster
219 * **Example:** Attach to Exadata Infrastructure
220 * ```typescript
221 * const cluster = yield* GCP.OracleDatabase.CloudVmCluster("Vms", {
222 * exadataInfrastructure: infra.name,
223 * network: "default",
224 * cidr: "10.10.0.0/24",
225 * backupSubnetCidr: "10.10.1.0/24",
226 * licenseType: "LICENSE_INCLUDED",
227 * cpuCoreCount: 4,
228 * giVersion: "19.0.0.0",
229 * hostnamePrefix: "exa",
230 * sshPublicKeys: [publicKey],
231 * });
232 * ```
233 *
234 * @resource
235 * @category OracleDatabase
236 */
237export const CloudVmCluster = Resource<CloudVmCluster>(
238 "GCP.OracleDatabase.CloudVmCluster",
239);
240
241const mergedProperties = (
242 news: CloudVmClusterProps,
243): CloudVmClusterPropertiesInput => ({
244 ...(news.properties ?? {}),
245 licenseType: news.licenseType ?? news.properties?.licenseType,
246 cpuCoreCount: news.cpuCoreCount ?? news.properties?.cpuCoreCount,
247 giVersion: news.giVersion ?? news.properties?.giVersion,
248 hostnamePrefix: news.hostnamePrefix ?? news.properties?.hostnamePrefix,
249 sshPublicKeys: news.sshPublicKeys ?? news.properties?.sshPublicKeys,
250});
251
252const toCreateBody = (
253 news: CloudVmClusterProps,
254 desiredLabels: Record<string, string>,
255 project: string,
256): oracle.CloudVmCluster => {
257 const props = mergedProperties(news);
258 const properties: oracle.CloudVmClusterProperties = {};
259 if (props.licenseType !== undefined)
260 properties.licenseType = props.licenseType;
261 if (props.cpuCoreCount !== undefined) {
262 properties.cpuCoreCount = props.cpuCoreCount;
263 }
264 if (props.ocpuCount !== undefined) properties.ocpuCount = props.ocpuCount;
265 if (props.nodeCount !== undefined) properties.nodeCount = props.nodeCount;
266 if (props.giVersion !== undefined) properties.giVersion = props.giVersion;
267 if (props.systemVersion !== undefined) {
268 properties.systemVersion = props.systemVersion;
269 }
270 if (props.memorySizeGb !== undefined) {
271 properties.memorySizeGb = props.memorySizeGb;
272 }
273 if (props.dbNodeStorageSizeGb !== undefined) {
274 properties.dbNodeStorageSizeGb = props.dbNodeStorageSizeGb;
275 }
276 if (props.dataStorageSizeTb !== undefined) {
277 properties.dataStorageSizeTb = props.dataStorageSizeTb;
278 }
279 if (props.hostnamePrefix !== undefined) {
280 properties.hostnamePrefix = props.hostnamePrefix;
281 }
282 if (props.sshPublicKeys !== undefined) {
283 properties.sshPublicKeys = props.sshPublicKeys;
284 }
285 if (props.scanListenerPortTcp !== undefined) {
286 properties.scanListenerPortTcp = props.scanListenerPortTcp;
287 }
288 if (props.scanListenerPortTcpSsl !== undefined) {
289 properties.scanListenerPortTcpSsl = props.scanListenerPortTcpSsl;
290 }
291 if (props.diskRedundancy !== undefined) {
292 properties.diskRedundancy = props.diskRedundancy;
293 }
294 if (props.timeZone !== undefined) properties.timeZone = props.timeZone;
295 if (props.clusterName !== undefined)
296 properties.clusterName = props.clusterName;
297 if (props.localBackupEnabled !== undefined) {
298 properties.localBackupEnabled = props.localBackupEnabled;
299 }
300 if (props.sparseDiskgroupEnabled !== undefined) {
301 properties.sparseDiskgroupEnabled = props.sparseDiskgroupEnabled;
302 }
303 if (props.diagnosticsDataCollectionOptions !== undefined) {
304 properties.diagnosticsDataCollectionOptions =
305 props.diagnosticsDataCollectionOptions;
306 }
307 if (props.dbServerOcids !== undefined) {
308 properties.dbServerOcids = props.dbServerOcids;
309 }
310 const body: oracle.CloudVmCluster = {
311 labels: desiredLabels,
312 properties,
313 };
314 if (news.exadataInfrastructure !== undefined) {
315 body.exadataInfrastructure = news.exadataInfrastructure;
316 }
317 if (news.displayName !== undefined) body.displayName = news.displayName;
318 const network = expandNetwork(project, news.network);
319 if (network !== undefined) body.network = network;
320 if (news.cidr !== undefined) body.cidr = news.cidr;
321 if (news.backupSubnetCidr !== undefined) {
322 body.backupSubnetCidr = news.backupSubnetCidr;
323 }
324 if (news.odbNetwork !== undefined) body.odbNetwork = news.odbNetwork;
325 if (news.odbSubnet !== undefined) body.odbSubnet = news.odbSubnet;
326 if (news.backupOdbSubnet !== undefined) {
327 body.backupOdbSubnet = news.backupOdbSubnet;
328 }
329 if (news.exascaleDbStorageVault !== undefined) {
330 body.exascaleDbStorageVault = news.exascaleDbStorageVault;
331 }
332 return body;
333};
334
335const toAttrs = (cluster: oracle.CloudVmCluster, project: string) => {
336 const name = cluster.name ?? "";
337 const parsed = parseName(name, COLLECTION);
338 return {
339 name,
340 cloudVmClusterId: parsed.id,
341 project: parsed.project || project,
342 location: parsed.location,
343 exadataInfrastructure: cluster.exadataInfrastructure,
344 displayName: cluster.displayName,
345 network: cluster.network,
346 cidr: cluster.cidr,
347 backupSubnetCidr: cluster.backupSubnetCidr,
348 odbNetwork: cluster.odbNetwork,
349 odbSubnet: cluster.odbSubnet,
350 backupOdbSubnet: cluster.backupOdbSubnet,
351 labels: userLabels(cluster.labels),
352 gcpOracleZone: cluster.gcpOracleZone,
353 state: cluster.properties?.state,
354 licenseType: cluster.properties?.licenseType,
355 cpuCoreCount: cluster.properties?.cpuCoreCount,
356 hostname: cluster.properties?.hostname,
357 ocid: cluster.properties?.ocid,
358 createTime: cluster.createTime,
359 };
360};
361
362const getByName = (name: string) =>
363 retryQuota(oracle.getProjectsLocationsCloudVmClusters({ name })).pipe(
364 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
365 );
366
367const listClusters = (project: string, region: string) => {
368 const collect = (parent: string) =>
369 collectPages(
370 oracle.listProjectsLocationsCloudVmClusters.pages({
371 parent,
372 pageSize: 1000,
373 }),
374 (page) => page.cloudVmClusters,
375 ).pipe(
376 Effect.map((items) =>
377 items.filter((item) => hasAlchemyLabelMap(item.labels)),
378 ),
379 );
380 return listAtLocation(project, region, collect).pipe(
381 Effect.catchTag("NotFound", () => Effect.succeed([])),
382 );
383};
384
385export const CloudVmClusterProvider = () =>
386 Provider.succeed(CloudVmCluster, {
387 stables: [
388 "name",
389 "cloudVmClusterId",
390 "project",
391 "location",
392 "createTime",
393 "ocid",
394 ],
395
396 diff: Effect.fn(function* ({ news, olds, output }) {
397 if (!isResolved(news)) return undefined;
398 const env = yield* GcpEnvironment.current;
399 const previousInfra =
400 olds?.exadataInfrastructure ?? output?.exadataInfrastructure ?? "";
401 const nextInfra = news.exadataInfrastructure ?? previousInfra;
402 const previousNetwork = olds?.network ?? output?.network ?? "";
403 const nextNetwork = news.network ?? previousNetwork;
404 const previousOdb = olds?.odbSubnet ?? output?.odbSubnet ?? "";
405 const nextOdb = news.odbSubnet ?? previousOdb;
406 return replaceOnIdentity({
407 previousId: olds?.cloudVmClusterId ?? output?.cloudVmClusterId,
408 nextId:
409 news.cloudVmClusterId ??
410 olds?.cloudVmClusterId ??
411 output?.cloudVmClusterId,
412 previousLocation: normalizeLocation(
413 olds?.location ?? output?.location ?? env.region,
414 ),
415 nextLocation: normalizeLocation(
416 news.location ?? olds?.location ?? output?.location ?? env.region,
417 ),
418 extra:
419 nextInfra !== previousInfra ||
420 nextNetwork !== previousNetwork ||
421 nextOdb !== previousOdb,
422 });
423 }),
424
425 read: Effect.fn(function* ({ id, olds, output }) {
426 const env = yield* GcpEnvironment.current;
427 const cloudVmClusterId = yield* toPhysicalId(
428 id,
429 olds?.cloudVmClusterId,
430 output?.cloudVmClusterId,
431 FALLBACK_ID,
432 );
433 const location = normalizeLocation(
434 olds?.location ?? output?.location ?? env.region,
435 );
436 const name =
437 output?.name ??
438 resourceNameOf(env.project, location, COLLECTION, cloudVmClusterId);
439 const existing = yield* getByName(name);
440 if (existing === undefined) return undefined;
441 const attrs = toAttrs(existing, env.project);
442 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
443 ? attrs
444 : Unowned(attrs);
445 }),
446
447 list: () =>
448 Effect.gen(function* () {
449 const env = yield* GcpEnvironment.current;
450 const items = yield* listClusters(env.project, env.region);
451 return items.map((item) => toAttrs(item, env.project));
452 }),
453
454 reconcile: Effect.fn(function* ({ id, news, output }) {
455 const env = yield* GcpEnvironment.current;
456 const cloudVmClusterId = yield* toPhysicalId(
457 id,
458 news.cloudVmClusterId,
459 output?.cloudVmClusterId,
460 FALLBACK_ID,
461 );
462 const location = normalizeLocation(
463 news.location ?? output?.location ?? env.region,
464 );
465 const name = resourceNameOf(
466 env.project,
467 location,
468 COLLECTION,
469 cloudVmClusterId,
470 );
471 const desiredLabels = {
472 ...toLabels(news.labels),
473 ...(yield* createInternalLabels(id)),
474 };
475
476 let current = yield* getByName(output?.name ?? name);
477
478 if (current === undefined) {
479 const created = yield* oracle
480 .createProjectsLocationsCloudVmClusters({
481 parent: parentOf(env.project, location),
482 cloudVmClusterId,
483 body: toCreateBody(news, desiredLabels, env.project),
484 })
485 .pipe(
486 retryQuota,
487 Effect.catchTag("Conflict", () => Effect.succeed(undefined)),
488 );
489 if (created !== undefined) {
490 yield* waitForOperation(created);
491 }
492 current = yield* waitUntilExists(getByName(name), name);
493 }
494
495 if (current === undefined) {
496 return yield* new OracleDatabaseNotResolved({ name });
497 }
498
499 const ready = yield* waitUntilReady(
500 getByName(current.name ?? name),
501 current.name ?? name,
502 (value) => value.properties?.state,
503 );
504
505 return toAttrs(ready, env.project);
506 }),
507
508 delete: Effect.fn(function* ({ output }) {
509 const operation = yield* oracle
510 .deleteProjectsLocationsCloudVmClusters({
511 name: output.name,
512 force: true,
513 })
514 .pipe(
515 retryConflict,
516 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
517 );
518 if (operation !== undefined) {
519 yield* waitForOperation(operation, { notFoundOk: true });
520 }
521 yield* waitUntilGone(getByName(output.name), output.name);
522 }),
523 });