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 hasAlchemyLabelMap,
15 listAtLocation,
16 normalizeLocation,
17 parentOf,
18 parseName,
19 replaceOnIdentity,
20 resourceNameOf,
21 retryConflict,
22 retryQuota,
23 type TimeZone,
24 toPhysicalId,
25 userLabels,
26 waitUntilExists,
27 waitUntilGone,
28 waitUntilReady,
29} from "./internal.ts";
30import { waitForOperation } from "./operations.ts";
31
32const COLLECTION = "dbSystems";
33const FALLBACK_ID = "dbsystem";
34
35export type DbHome = {
36 /** Enable unified auditing. */
37 isUnifiedAuditingEnabled?: boolean;
38 /** Database Home display name. */
39 displayName?: string;
40 /** Oracle Database version. */
41 dbVersion?: string;
42 /** Database resource created in the home. */
43 database?: oracle.Database;
44};
45
46export type DbSystemOptions = {
47 /** Storage management (`ASM`, `LVM`). */
48 storageManagement?:
49 | oracle.DbSystemOptionsStorageManagementEnum
50 | (string & {});
51};
52
53export type DataCollectionOptionsDbSystem = {
54 /** Enable incident logs and traces. */
55 isIncidentLogsEnabled?: boolean;
56 /** Enable diagnostics events. */
57 isDiagnosticsEventsEnabled?: boolean;
58};
59
60export type DbSystemPropertiesInput = {
61 /** Shape. Required on create. */
62 shape?: string;
63 /** SSH public keys. Required on create. */
64 sshPublicKeys?: string[];
65 /** Initial data storage size in GB. Required on create. */
66 initialDataStorageSizeGb?: number;
67 /** CPU cores. Required on create. */
68 computeCount?: number;
69 /** License model. Required on create. */
70 licenseModel?: oracle.DbSystemPropertiesLicenseModelEnum | (string & {});
71 /** Database edition. Required on create. */
72 databaseEdition?:
73 | oracle.DbSystemPropertiesDatabaseEditionEnum
74 | (string & {});
75 /** Hostname prefix. */
76 hostnamePrefix?: string;
77 /** Time zone. */
78 timeZone?: TimeZone;
79 /** Database Home. */
80 dbHome?: DbHome;
81 /** Private IP. */
82 privateIp?: string;
83 /** DbSystem options. */
84 dbSystemOptions?: DbSystemOptions;
85 /** Reco/redo storage size in GB. */
86 recoStorageSizeGb?: number;
87 /** Host domain. */
88 domain?: string;
89 /** Memory in GB. */
90 memorySizeGb?: number;
91 /** Diagnostics collection. */
92 dataCollectionOptions?: DataCollectionOptionsDbSystem;
93 /** Node count. */
94 nodeCount?: number;
95 /** Compute model (`ECPU`, `OCPU`). */
96 computeModel?: oracle.DbSystemPropertiesComputeModelEnum | (string & {});
97};
98
99export type DbSystemProps = {
100 /**
101 * DbSystem id. If omitted, a unique RFC1035 name is generated.
102 * Immutable.
103 */
104 dbSystemId?: string;
105 /**
106 * Region. Immutable.
107 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
108 */
109 location?: string;
110 /**
111 * Display name. Required on create.
112 */
113 displayName?: string;
114 /**
115 * ODB Network. Immutable.
116 */
117 odbNetwork?: string;
118 /**
119 * ODB Subnet for IP allocation. Required on create. Immutable.
120 */
121 odbSubnet?: string;
122 /**
123 * GCP Oracle zone. Immutable.
124 */
125 gcpOracleZone?: string;
126 /**
127 * User labels. Alchemy ownership labels are merged in automatically.
128 */
129 labels?: Record<string, string>;
130 /**
131 * DbSystem properties.
132 */
133 properties?: DbSystemPropertiesInput;
134 /** Shape. Convenience alias for `properties.shape`. */
135 shape?: string;
136 /** SSH public keys. Convenience alias for `properties.sshPublicKeys`. */
137 sshPublicKeys?: string[];
138 /** CPU cores. Convenience alias for `properties.computeCount`. */
139 computeCount?: number;
140 /** License model. Convenience alias for `properties.licenseModel`. */
141 licenseModel?: oracle.DbSystemPropertiesLicenseModelEnum | (string & {});
142 /** Database edition. Convenience alias for `properties.databaseEdition`. */
143 databaseEdition?:
144 | oracle.DbSystemPropertiesDatabaseEditionEnum
145 | (string & {});
146 /** Initial data storage in GB. Convenience alias. */
147 initialDataStorageSizeGb?: number;
148};
149
150export type DbSystem = Resource<
151 "GCP.OracleDatabase.DbSystem",
152 DbSystemProps,
153 {
154 /** Full resource name. */
155 name: string;
156 /** DbSystem id. */
157 dbSystemId: string;
158 /** Project id. */
159 project: string;
160 /** Location id. */
161 location: string;
162 /** Display name. */
163 displayName: string | undefined;
164 /** ODB Network. */
165 odbNetwork: string | undefined;
166 /** ODB Subnet. */
167 odbSubnet: string | undefined;
168 /** GCP Oracle zone. */
169 gcpOracleZone: string | undefined;
170 /** User labels (Alchemy ownership labels stripped). */
171 labels: Record<string, string>;
172 /** Entitlement id. */
173 entitlementId: string | undefined;
174 /** Lifecycle state. */
175 lifecycleState: string | undefined;
176 /** Shape. */
177 shape: string | undefined;
178 /** Hostname. */
179 hostname: string | undefined;
180 /** OCID. */
181 ocid: string | undefined;
182 /** RFC3339 creation timestamp. */
183 createTime: string | undefined;
184 },
185 never,
186 Providers
187>;
188
189/**
190 * An Oracle BaseDB system on Google Cloud.
191 *
192 * Changing `dbSystemId`, `location`, `odbSubnet`, `odbNetwork`, `shape`,
193 * or `gcpOracleZone` replaces the system. There is no patch API in the
194 * distilled SDK, so labels are applied at create.
195 *
196 * ### Creating a DbSystem
197 * **Example:** Generated name
198 * ```typescript
199 * const db = yield* GCP.OracleDatabase.DbSystem("BaseDb", {
200 * displayName: "basedb",
201 * odbSubnet: subnet.name,
202 * shape: "VM.Standard.E4.Flex",
203 * sshPublicKeys: [publicKey],
204 * computeCount: 2,
205 * initialDataStorageSizeGb: 256,
206 * licenseModel: "LICENSE_INCLUDED",
207 * databaseEdition: "ENTERPRISE_EDITION",
208 * });
209 * ```
210 *
211 * @resource
212 * @category OracleDatabase
213 */
214export const DbSystem = Resource<DbSystem>("GCP.OracleDatabase.DbSystem");
215
216const mergedProperties = (news: DbSystemProps): DbSystemPropertiesInput => ({
217 ...(news.properties ?? {}),
218 shape: news.shape ?? news.properties?.shape,
219 sshPublicKeys: news.sshPublicKeys ?? news.properties?.sshPublicKeys,
220 computeCount: news.computeCount ?? news.properties?.computeCount,
221 licenseModel: news.licenseModel ?? news.properties?.licenseModel,
222 databaseEdition: news.databaseEdition ?? news.properties?.databaseEdition,
223 initialDataStorageSizeGb:
224 news.initialDataStorageSizeGb ?? news.properties?.initialDataStorageSizeGb,
225});
226
227const toCreateBody = (
228 news: DbSystemProps,
229 desiredLabels: Record<string, string>,
230): oracle.DbSystem => {
231 const props = mergedProperties(news);
232 const properties: oracle.DbSystemProperties = {};
233 if (props.shape !== undefined) properties.shape = props.shape;
234 if (props.sshPublicKeys !== undefined) {
235 properties.sshPublicKeys = props.sshPublicKeys;
236 }
237 if (props.initialDataStorageSizeGb !== undefined) {
238 properties.initialDataStorageSizeGb = props.initialDataStorageSizeGb;
239 }
240 if (props.computeCount !== undefined) {
241 properties.computeCount = props.computeCount;
242 }
243 if (props.licenseModel !== undefined) {
244 properties.licenseModel = props.licenseModel;
245 }
246 if (props.databaseEdition !== undefined) {
247 properties.databaseEdition = props.databaseEdition;
248 }
249 if (props.hostnamePrefix !== undefined) {
250 properties.hostnamePrefix = props.hostnamePrefix;
251 }
252 if (props.timeZone !== undefined) properties.timeZone = props.timeZone;
253 if (props.dbHome !== undefined) properties.dbHome = props.dbHome;
254 if (props.privateIp !== undefined) properties.privateIp = props.privateIp;
255 if (props.dbSystemOptions !== undefined) {
256 properties.dbSystemOptions = props.dbSystemOptions;
257 }
258 if (props.recoStorageSizeGb !== undefined) {
259 properties.recoStorageSizeGb = props.recoStorageSizeGb;
260 }
261 if (props.domain !== undefined) properties.domain = props.domain;
262 if (props.memorySizeGb !== undefined) {
263 properties.memorySizeGb = props.memorySizeGb;
264 }
265 if (props.dataCollectionOptions !== undefined) {
266 properties.dataCollectionOptions = props.dataCollectionOptions;
267 }
268 if (props.nodeCount !== undefined) properties.nodeCount = props.nodeCount;
269 if (props.computeModel !== undefined) {
270 properties.computeModel = props.computeModel;
271 }
272 const body: oracle.DbSystem = {
273 labels: desiredLabels,
274 properties,
275 };
276 if (news.displayName !== undefined) body.displayName = news.displayName;
277 if (news.odbNetwork !== undefined) body.odbNetwork = news.odbNetwork;
278 if (news.odbSubnet !== undefined) body.odbSubnet = news.odbSubnet;
279 if (news.gcpOracleZone !== undefined) body.gcpOracleZone = news.gcpOracleZone;
280 return body;
281};
282
283const toAttrs = (system: oracle.DbSystem, project: string) => {
284 const name = system.name ?? "";
285 const parsed = parseName(name, COLLECTION);
286 return {
287 name,
288 dbSystemId: parsed.id,
289 project: parsed.project || project,
290 location: parsed.location,
291 displayName: system.displayName,
292 odbNetwork: system.odbNetwork,
293 odbSubnet: system.odbSubnet,
294 gcpOracleZone: system.gcpOracleZone,
295 labels: userLabels(system.labels),
296 entitlementId: system.entitlementId,
297 lifecycleState: system.properties?.lifecycleState,
298 shape: system.properties?.shape,
299 hostname: system.properties?.hostname,
300 ocid: system.properties?.ocid,
301 createTime: system.createTime,
302 };
303};
304
305const getByName = (name: string) =>
306 retryQuota(oracle.getProjectsLocationsDbSystems({ name })).pipe(
307 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
308 );
309
310const listSystems = (project: string, region: string) => {
311 const collect = (parent: string) =>
312 collectPages(
313 oracle.listProjectsLocationsDbSystems.pages({
314 parent,
315 pageSize: 1000,
316 }),
317 (page) => page.dbSystems,
318 ).pipe(
319 Effect.map((items) =>
320 items.filter((item) => hasAlchemyLabelMap(item.labels)),
321 ),
322 );
323 return listAtLocation(project, region, collect).pipe(
324 Effect.catchTag("NotFound", () => Effect.succeed([])),
325 );
326};
327
328export const DbSystemProvider = () =>
329 Provider.succeed(DbSystem, {
330 stables: [
331 "name",
332 "dbSystemId",
333 "project",
334 "location",
335 "createTime",
336 "ocid",
337 ],
338
339 diff: Effect.fn(function* ({ news, olds, output }) {
340 if (!isResolved(news)) return undefined;
341 const env = yield* GcpEnvironment.current;
342 const previousOdb = olds?.odbSubnet ?? output?.odbSubnet ?? "";
343 const nextOdb = news.odbSubnet ?? previousOdb;
344 const previousShape =
345 olds?.shape ?? olds?.properties?.shape ?? output?.shape ?? "";
346 const nextShape = news.shape ?? news.properties?.shape ?? previousShape;
347 return replaceOnIdentity({
348 previousId: olds?.dbSystemId ?? output?.dbSystemId,
349 nextId: news.dbSystemId ?? olds?.dbSystemId ?? output?.dbSystemId,
350 previousLocation: normalizeLocation(
351 olds?.location ?? output?.location ?? env.region,
352 ),
353 nextLocation: normalizeLocation(
354 news.location ?? olds?.location ?? output?.location ?? env.region,
355 ),
356 extra: nextOdb !== previousOdb || nextShape !== previousShape,
357 });
358 }),
359
360 read: Effect.fn(function* ({ id, olds, output }) {
361 const env = yield* GcpEnvironment.current;
362 const dbSystemId = yield* toPhysicalId(
363 id,
364 olds?.dbSystemId,
365 output?.dbSystemId,
366 FALLBACK_ID,
367 );
368 const location = normalizeLocation(
369 olds?.location ?? output?.location ?? env.region,
370 );
371 const name =
372 output?.name ??
373 resourceNameOf(env.project, location, COLLECTION, dbSystemId);
374 const existing = yield* getByName(name);
375 if (existing === undefined) return undefined;
376 const attrs = toAttrs(existing, env.project);
377 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
378 ? attrs
379 : Unowned(attrs);
380 }),
381
382 list: () =>
383 Effect.gen(function* () {
384 const env = yield* GcpEnvironment.current;
385 const items = yield* listSystems(env.project, env.region);
386 return items.map((item) => toAttrs(item, env.project));
387 }),
388
389 reconcile: Effect.fn(function* ({ id, news, output }) {
390 const env = yield* GcpEnvironment.current;
391 const dbSystemId = yield* toPhysicalId(
392 id,
393 news.dbSystemId,
394 output?.dbSystemId,
395 FALLBACK_ID,
396 );
397 const location = normalizeLocation(
398 news.location ?? output?.location ?? env.region,
399 );
400 const name = resourceNameOf(
401 env.project,
402 location,
403 COLLECTION,
404 dbSystemId,
405 );
406 const desiredLabels = {
407 ...toLabels(news.labels),
408 ...(yield* createInternalLabels(id)),
409 };
410
411 let current = yield* getByName(output?.name ?? name);
412
413 if (current === undefined) {
414 const created = yield* oracle
415 .createProjectsLocationsDbSystems({
416 parent: parentOf(env.project, location),
417 dbSystemId,
418 body: toCreateBody(news, desiredLabels),
419 })
420 .pipe(
421 retryQuota,
422 Effect.catchTag("Conflict", () => Effect.succeed(undefined)),
423 );
424 if (created !== undefined) {
425 yield* waitForOperation(created);
426 }
427 current = yield* waitUntilExists(getByName(name), name);
428 }
429
430 if (current === undefined) {
431 return yield* new OracleDatabaseNotResolved({ name });
432 }
433
434 const ready = yield* waitUntilReady(
435 getByName(current.name ?? name),
436 current.name ?? name,
437 (value) => value.properties?.lifecycleState,
438 );
439
440 return toAttrs(ready, env.project);
441 }),
442
443 delete: Effect.fn(function* ({ output }) {
444 const operation = yield* oracle
445 .deleteProjectsLocationsDbSystems({
446 name: output.name,
447 })
448 .pipe(
449 retryConflict,
450 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
451 );
452 if (operation !== undefined) {
453 yield* waitForOperation(operation, { notFoundOk: true });
454 }
455 yield* waitUntilGone(getByName(output.name), output.name);
456 }),
457 });