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 type CustomerContact,
15 hasAlchemyLabelMap,
16 listAtLocation,
17 normalizeLocation,
18 parentOf,
19 parseName,
20 replaceOnIdentity,
21 resourceNameOf,
22 retryConflict,
23 retryQuota,
24 toPhysicalId,
25 userLabels,
26 waitUntilExists,
27 waitUntilGone,
28 waitUntilReady,
29} from "./internal.ts";
30import { waitForOperation } from "./operations.ts";
31
32const COLLECTION = "cloudExadataInfrastructures";
33const FALLBACK_ID = "exadata";
34
35export type MaintenanceWindow = {
36 /** Scheduling preference (`CUSTOM_PREFERENCE`, `NO_PREFERENCE`). */
37 preference?: oracle.MaintenanceWindowPreferenceEnum | (string & {});
38 /** 4-hour UTC slots (`0`, `4`, `8`, `12`, `16`, `20`). */
39 hoursOfDay?: number[];
40 /** Custom action timeout in minutes (15-120). */
41 customActionTimeoutMins?: number;
42 /** Months when maintenance should run. */
43 months?: Array<oracle.MaintenanceWindowMonthsItemEnum | (string & {})>;
44 /** Patching mode (`ROLLING`, `NONROLLING`). */
45 patchingMode?: oracle.MaintenanceWindowPatchingModeEnum | (string & {});
46 /** Lead time in weeks (1-4). */
47 leadTimeWeek?: number;
48 /** Days of week. */
49 daysOfWeek?: Array<
50 oracle.MaintenanceWindowDaysOfWeekItemEnum | (string & {})
51 >;
52 /** Enable custom action timeout. */
53 isCustomActionTimeoutEnabled?: boolean;
54 /** Weeks of month (1, 2, 3, 4). */
55 weeksOfMonth?: number[];
56};
57
58export type CloudExadataInfrastructurePropertiesInput = {
59 /** Shape (e.g. `Exadata.X9M`). Immutable. */
60 shape?: string;
61 /** Number of compute servers. */
62 computeCount?: number;
63 /** Number of storage servers. */
64 storageCount?: number;
65 /** Total storage size in GB. */
66 totalStorageSizeGb?: number;
67 /** Maintenance window. */
68 maintenanceWindow?: MaintenanceWindow;
69 /** Customer contacts. */
70 customerContacts?: CustomerContact[];
71};
72
73export type CloudExadataInfrastructureProps = {
74 /**
75 * Exadata Infrastructure id (the `{cloud_exadata_infrastructure}`
76 * segment). If omitted, a unique RFC1035 name is generated. Immutable.
77 */
78 cloudExadataInfrastructureId?: string;
79 /**
80 * Region. Immutable.
81 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
82 */
83 location?: string;
84 /**
85 * User-friendly display name.
86 */
87 displayName?: string;
88 /**
89 * GCP Oracle zone (e.g. `us-east4-b-r2`). Immutable. If omitted the
90 * service picks a zone.
91 */
92 gcpOracleZone?: string;
93 /**
94 * User labels. Alchemy ownership labels are merged in automatically.
95 */
96 labels?: Record<string, string>;
97 /**
98 * Infrastructure properties. `shape` is required on create.
99 */
100 properties?: CloudExadataInfrastructurePropertiesInput;
101 /** Shape. Convenience alias for `properties.shape`. */
102 shape?: string;
103 /** Compute server count. Convenience alias for `properties.computeCount`. */
104 computeCount?: number;
105 /** Storage server count. Convenience alias for `properties.storageCount`. */
106 storageCount?: number;
107};
108
109export type CloudExadataInfrastructure = Resource<
110 "GCP.OracleDatabase.CloudExadataInfrastructure",
111 CloudExadataInfrastructureProps,
112 {
113 /** Full resource name. */
114 name: string;
115 /** Infrastructure id. */
116 cloudExadataInfrastructureId: string;
117 /** Project id. */
118 project: string;
119 /** Location id. */
120 location: string;
121 /** Display name. */
122 displayName: string | undefined;
123 /** GCP Oracle zone. */
124 gcpOracleZone: string | undefined;
125 /** User labels (Alchemy ownership labels stripped). */
126 labels: Record<string, string>;
127 /** Entitlement id. */
128 entitlementId: string | undefined;
129 /** Lifecycle state. */
130 state: string | undefined;
131 /** Shape. */
132 shape: string | undefined;
133 /** Compute server count. */
134 computeCount: number | undefined;
135 /** Storage server count. */
136 storageCount: number | undefined;
137 /** OCID. */
138 ocid: string | undefined;
139 /** RFC3339 creation timestamp. */
140 createTime: string | undefined;
141 },
142 never,
143 Providers
144>;
145
146/**
147 * An Oracle Exadata Infrastructure on Google Cloud.
148 *
149 * Changing `cloudExadataInfrastructureId`, `location`, `gcpOracleZone`,
150 * or `shape` replaces the infrastructure. There is no patch API in the
151 * distilled SDK, so labels and counts are applied at create.
152 *
153 * Provisioning typically takes tens of minutes and requires an Oracle
154 * Database@Google Cloud entitlement. Delete sends `force=true` so child
155 * VM clusters do not block teardown.
156 *
157 * ### Creating Exadata Infrastructure
158 * **Example:** Generated name
159 * ```typescript
160 * const infra = yield* GCP.OracleDatabase.CloudExadataInfrastructure("Exa", {
161 * displayName: "exa",
162 * shape: "Exadata.X9M",
163 * computeCount: 2,
164 * storageCount: 3,
165 * });
166 * ```
167 *
168 * @resource
169 * @category OracleDatabase
170 */
171export const CloudExadataInfrastructure = Resource<CloudExadataInfrastructure>(
172 "GCP.OracleDatabase.CloudExadataInfrastructure",
173);
174
175const mergedProperties = (
176 news: CloudExadataInfrastructureProps,
177): CloudExadataInfrastructurePropertiesInput => ({
178 ...(news.properties ?? {}),
179 shape: news.shape ?? news.properties?.shape,
180 computeCount: news.computeCount ?? news.properties?.computeCount,
181 storageCount: news.storageCount ?? news.properties?.storageCount,
182});
183
184const toCreateBody = (
185 news: CloudExadataInfrastructureProps,
186 desiredLabels: Record<string, string>,
187): oracle.CloudExadataInfrastructure => {
188 const props = mergedProperties(news);
189 const properties: oracle.CloudExadataInfrastructureProperties = {};
190 if (props.shape !== undefined) properties.shape = props.shape;
191 if (props.computeCount !== undefined) {
192 properties.computeCount = props.computeCount;
193 }
194 if (props.storageCount !== undefined) {
195 properties.storageCount = props.storageCount;
196 }
197 if (props.totalStorageSizeGb !== undefined) {
198 properties.totalStorageSizeGb = props.totalStorageSizeGb;
199 }
200 if (props.maintenanceWindow !== undefined) {
201 properties.maintenanceWindow = props.maintenanceWindow;
202 }
203 if (props.customerContacts !== undefined) {
204 properties.customerContacts = props.customerContacts;
205 }
206 const body: oracle.CloudExadataInfrastructure = {
207 labels: desiredLabels,
208 properties,
209 };
210 if (news.displayName !== undefined) body.displayName = news.displayName;
211 if (news.gcpOracleZone !== undefined) {
212 body.gcpOracleZone = news.gcpOracleZone;
213 }
214 return body;
215};
216
217const toAttrs = (infra: oracle.CloudExadataInfrastructure, project: string) => {
218 const name = infra.name ?? "";
219 const parsed = parseName(name, COLLECTION);
220 return {
221 name,
222 cloudExadataInfrastructureId: parsed.id,
223 project: parsed.project || project,
224 location: parsed.location,
225 displayName: infra.displayName,
226 gcpOracleZone: infra.gcpOracleZone,
227 labels: userLabels(infra.labels),
228 entitlementId: infra.entitlementId,
229 state: infra.properties?.state,
230 shape: infra.properties?.shape,
231 computeCount: infra.properties?.computeCount,
232 storageCount: infra.properties?.storageCount,
233 ocid: infra.properties?.ocid,
234 createTime: infra.createTime,
235 };
236};
237
238const getByName = (name: string) =>
239 retryQuota(
240 oracle.getProjectsLocationsCloudExadataInfrastructures({ name }),
241 ).pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
242
243const listInfras = (project: string, region: string) => {
244 const collect = (parent: string) =>
245 collectPages(
246 oracle.listProjectsLocationsCloudExadataInfrastructures.pages({
247 parent,
248 pageSize: 1000,
249 }),
250 (page) => page.cloudExadataInfrastructures,
251 ).pipe(
252 Effect.map((items) =>
253 items.filter((item) => hasAlchemyLabelMap(item.labels)),
254 ),
255 );
256 return listAtLocation(project, region, collect).pipe(
257 Effect.catchTag("NotFound", () => Effect.succeed([])),
258 );
259};
260
261export const CloudExadataInfrastructureProvider = () =>
262 Provider.succeed(CloudExadataInfrastructure, {
263 stables: [
264 "name",
265 "cloudExadataInfrastructureId",
266 "project",
267 "location",
268 "createTime",
269 "ocid",
270 ],
271
272 diff: Effect.fn(function* ({ news, olds, output }) {
273 if (!isResolved(news)) return undefined;
274 const env = yield* GcpEnvironment.current;
275 const props = mergedProperties(news);
276 const previousShape =
277 olds?.shape ?? olds?.properties?.shape ?? output?.shape ?? "";
278 const nextShape = props.shape ?? previousShape;
279 const previousZone = olds?.gcpOracleZone ?? output?.gcpOracleZone ?? "";
280 const nextZone = news.gcpOracleZone ?? previousZone;
281 return replaceOnIdentity({
282 previousId:
283 olds?.cloudExadataInfrastructureId ??
284 output?.cloudExadataInfrastructureId,
285 nextId:
286 news.cloudExadataInfrastructureId ??
287 olds?.cloudExadataInfrastructureId ??
288 output?.cloudExadataInfrastructureId,
289 previousLocation: normalizeLocation(
290 olds?.location ?? output?.location ?? env.region,
291 ),
292 nextLocation: normalizeLocation(
293 news.location ?? olds?.location ?? output?.location ?? env.region,
294 ),
295 extra: nextShape !== previousShape || nextZone !== previousZone,
296 });
297 }),
298
299 read: Effect.fn(function* ({ id, olds, output }) {
300 const env = yield* GcpEnvironment.current;
301 const cloudExadataInfrastructureId = yield* toPhysicalId(
302 id,
303 olds?.cloudExadataInfrastructureId,
304 output?.cloudExadataInfrastructureId,
305 FALLBACK_ID,
306 );
307 const location = normalizeLocation(
308 olds?.location ?? output?.location ?? env.region,
309 );
310 const name =
311 output?.name ??
312 resourceNameOf(
313 env.project,
314 location,
315 COLLECTION,
316 cloudExadataInfrastructureId,
317 );
318 const existing = yield* getByName(name);
319 if (existing === undefined) return undefined;
320 const attrs = toAttrs(existing, env.project);
321 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
322 ? attrs
323 : Unowned(attrs);
324 }),
325
326 list: () =>
327 Effect.gen(function* () {
328 const env = yield* GcpEnvironment.current;
329 const items = yield* listInfras(env.project, env.region);
330 return items.map((item) => toAttrs(item, env.project));
331 }),
332
333 reconcile: Effect.fn(function* ({ id, news, output }) {
334 const env = yield* GcpEnvironment.current;
335 const cloudExadataInfrastructureId = yield* toPhysicalId(
336 id,
337 news.cloudExadataInfrastructureId,
338 output?.cloudExadataInfrastructureId,
339 FALLBACK_ID,
340 );
341 const location = normalizeLocation(
342 news.location ?? output?.location ?? env.region,
343 );
344 const name = resourceNameOf(
345 env.project,
346 location,
347 COLLECTION,
348 cloudExadataInfrastructureId,
349 );
350 const desiredLabels = {
351 ...toLabels(news.labels),
352 ...(yield* createInternalLabels(id)),
353 };
354
355 let current = yield* getByName(output?.name ?? name);
356
357 if (current === undefined) {
358 const created = yield* oracle
359 .createProjectsLocationsCloudExadataInfrastructures({
360 parent: parentOf(env.project, location),
361 cloudExadataInfrastructureId,
362 body: toCreateBody(news, desiredLabels),
363 })
364 .pipe(
365 retryQuota,
366 Effect.catchTag("Conflict", () => Effect.succeed(undefined)),
367 );
368 if (created !== undefined) {
369 yield* waitForOperation(created);
370 }
371 current = yield* waitUntilExists(getByName(name), name);
372 }
373
374 if (current === undefined) {
375 return yield* new OracleDatabaseNotResolved({ name });
376 }
377
378 const ready = yield* waitUntilReady(
379 getByName(current.name ?? name),
380 current.name ?? name,
381 (value) => value.properties?.state,
382 );
383
384 return toAttrs(ready, env.project);
385 }),
386
387 delete: Effect.fn(function* ({ output }) {
388 const operation = yield* oracle
389 .deleteProjectsLocationsCloudExadataInfrastructures({
390 name: output.name,
391 force: true,
392 })
393 .pipe(
394 retryConflict,
395 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
396 );
397 if (operation !== undefined) {
398 yield* waitForOperation(operation, { notFoundOk: true });
399 }
400 yield* waitUntilGone(getByName(output.name), output.name);
401 }),
402 });