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 toPhysicalId,
24 userLabels,
25 waitUntilExists,
26 waitUntilGone,
27 waitUntilReady,
28} from "./internal.ts";
29import { waitForOperation } from "./operations.ts";
30
31const COLLECTION = "exascaleDbStorageVaults";
32const FALLBACK_ID = "exavault";
33
34export type ExascaleDbStorageDetails = {
35 /** Total storage allocation in GB. Required on create. */
36 totalSizeGbs?: number;
37};
38
39export type ExascaleDbStorageVaultPropertiesInput = {
40 /** Additional flash cache as a percent of high-capacity storage. */
41 additionalFlashCachePercent?: number;
42 /** Storage details. Required on create. */
43 exascaleDbStorageDetails?: ExascaleDbStorageDetails;
44 /** Vault description. */
45 description?: string;
46};
47
48export type ExascaleDbStorageVaultProps = {
49 /**
50 * Vault id. If omitted, a unique RFC1035 name is generated. Immutable.
51 */
52 exascaleDbStorageVaultId?: string;
53 /**
54 * Region. Immutable.
55 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
56 */
57 location?: string;
58 /**
59 * Display name. Required on create.
60 */
61 displayName?: string;
62 /**
63 * GCP Oracle zone. Immutable.
64 */
65 gcpOracleZone?: string;
66 /**
67 * Exadata Infrastructure this vault is created on. Immutable.
68 */
69 exadataInfrastructure?: string;
70 /**
71 * User labels. Alchemy ownership labels are merged in automatically.
72 */
73 labels?: Record<string, string>;
74 /**
75 * Vault properties.
76 */
77 properties?: ExascaleDbStorageVaultPropertiesInput;
78 /** Total size in GB. Convenience alias. */
79 totalSizeGbs?: number;
80 /** Vault description. Convenience alias. */
81 description?: string;
82};
83
84export type ExascaleDbStorageVault = Resource<
85 "GCP.OracleDatabase.ExascaleDbStorageVault",
86 ExascaleDbStorageVaultProps,
87 {
88 /** Full resource name. */
89 name: string;
90 /** Vault id. */
91 exascaleDbStorageVaultId: string;
92 /** Project id. */
93 project: string;
94 /** Location id. */
95 location: string;
96 /** Display name. */
97 displayName: string | undefined;
98 /** GCP Oracle zone. */
99 gcpOracleZone: string | undefined;
100 /** Exadata Infrastructure. */
101 exadataInfrastructure: string | undefined;
102 /** User labels (Alchemy ownership labels stripped). */
103 labels: Record<string, string>;
104 /** Entitlement id. */
105 entitlementId: string | undefined;
106 /** Lifecycle state. */
107 state: string | undefined;
108 /** Total size in GB. */
109 totalSizeGbs: number | undefined;
110 /** Available size in GB. */
111 availableSizeGbs: number | undefined;
112 /** Description. */
113 description: string | undefined;
114 /** OCID. */
115 ocid: string | undefined;
116 /** RFC3339 creation timestamp. */
117 createTime: string | undefined;
118 },
119 never,
120 Providers
121>;
122
123/**
124 * An Oracle Exascale DB storage vault on Google Cloud.
125 *
126 * Changing `exascaleDbStorageVaultId`, `location`,
127 * `exadataInfrastructure`, or `gcpOracleZone` replaces the vault. There
128 * is no patch API in the distilled SDK, so labels are applied at create.
129 *
130 * ### Creating a storage vault
131 * **Example:** Generated name
132 * ```typescript
133 * const vault = yield* GCP.OracleDatabase.ExascaleDbStorageVault("Vault", {
134 * displayName: "vault",
135 * totalSizeGbs: 300,
136 * });
137 * ```
138 *
139 * @resource
140 * @category OracleDatabase
141 */
142export const ExascaleDbStorageVault = Resource<ExascaleDbStorageVault>(
143 "GCP.OracleDatabase.ExascaleDbStorageVault",
144);
145
146const mergedProperties = (
147 news: ExascaleDbStorageVaultProps,
148): ExascaleDbStorageVaultPropertiesInput => ({
149 ...(news.properties ?? {}),
150 description: news.description ?? news.properties?.description,
151 exascaleDbStorageDetails: {
152 ...(news.properties?.exascaleDbStorageDetails ?? {}),
153 totalSizeGbs:
154 news.totalSizeGbs ??
155 news.properties?.exascaleDbStorageDetails?.totalSizeGbs,
156 },
157});
158
159const toCreateBody = (
160 news: ExascaleDbStorageVaultProps,
161 desiredLabels: Record<string, string>,
162): oracle.ExascaleDbStorageVault => {
163 const props = mergedProperties(news);
164 const properties: oracle.ExascaleDbStorageVaultProperties = {};
165 if (props.additionalFlashCachePercent !== undefined) {
166 properties.additionalFlashCachePercent = props.additionalFlashCachePercent;
167 }
168 if (props.exascaleDbStorageDetails !== undefined) {
169 properties.exascaleDbStorageDetails = props.exascaleDbStorageDetails;
170 }
171 if (props.description !== undefined)
172 properties.description = props.description;
173 const body: oracle.ExascaleDbStorageVault = {
174 labels: desiredLabels,
175 properties,
176 };
177 if (news.displayName !== undefined) body.displayName = news.displayName;
178 if (news.gcpOracleZone !== undefined) body.gcpOracleZone = news.gcpOracleZone;
179 if (news.exadataInfrastructure !== undefined) {
180 body.exadataInfrastructure = news.exadataInfrastructure;
181 }
182 return body;
183};
184
185const toAttrs = (vault: oracle.ExascaleDbStorageVault, project: string) => {
186 const name = vault.name ?? "";
187 const parsed = parseName(name, COLLECTION);
188 return {
189 name,
190 exascaleDbStorageVaultId: parsed.id,
191 project: parsed.project || project,
192 location: parsed.location,
193 displayName: vault.displayName,
194 gcpOracleZone: vault.gcpOracleZone,
195 exadataInfrastructure: vault.exadataInfrastructure,
196 labels: userLabels(vault.labels),
197 entitlementId: vault.entitlementId,
198 state: vault.properties?.state,
199 totalSizeGbs: vault.properties?.exascaleDbStorageDetails?.totalSizeGbs,
200 availableSizeGbs:
201 vault.properties?.exascaleDbStorageDetails?.availableSizeGbs,
202 description: vault.properties?.description,
203 ocid: vault.properties?.ocid,
204 createTime: vault.createTime,
205 };
206};
207
208const getByName = (name: string) =>
209 retryQuota(oracle.getProjectsLocationsExascaleDbStorageVaults({ name })).pipe(
210 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
211 );
212
213const listVaults = (project: string, region: string) => {
214 const collect = (parent: string) =>
215 collectPages(
216 oracle.listProjectsLocationsExascaleDbStorageVaults.pages({
217 parent,
218 pageSize: 1000,
219 }),
220 (page) => page.exascaleDbStorageVaults,
221 ).pipe(
222 Effect.map((items) =>
223 items.filter((item) => hasAlchemyLabelMap(item.labels)),
224 ),
225 );
226 return listAtLocation(project, region, collect).pipe(
227 Effect.catchTag("NotFound", () => Effect.succeed([])),
228 );
229};
230
231export const ExascaleDbStorageVaultProvider = () =>
232 Provider.succeed(ExascaleDbStorageVault, {
233 stables: [
234 "name",
235 "exascaleDbStorageVaultId",
236 "project",
237 "location",
238 "createTime",
239 "ocid",
240 ],
241
242 diff: Effect.fn(function* ({ news, olds, output }) {
243 if (!isResolved(news)) return undefined;
244 const env = yield* GcpEnvironment.current;
245 const previousInfra =
246 olds?.exadataInfrastructure ?? output?.exadataInfrastructure ?? "";
247 const nextInfra = news.exadataInfrastructure ?? previousInfra;
248 const previousZone = olds?.gcpOracleZone ?? output?.gcpOracleZone ?? "";
249 const nextZone = news.gcpOracleZone ?? previousZone;
250 return replaceOnIdentity({
251 previousId:
252 olds?.exascaleDbStorageVaultId ?? output?.exascaleDbStorageVaultId,
253 nextId:
254 news.exascaleDbStorageVaultId ??
255 olds?.exascaleDbStorageVaultId ??
256 output?.exascaleDbStorageVaultId,
257 previousLocation: normalizeLocation(
258 olds?.location ?? output?.location ?? env.region,
259 ),
260 nextLocation: normalizeLocation(
261 news.location ?? olds?.location ?? output?.location ?? env.region,
262 ),
263 extra: nextInfra !== previousInfra || nextZone !== previousZone,
264 });
265 }),
266
267 read: Effect.fn(function* ({ id, olds, output }) {
268 const env = yield* GcpEnvironment.current;
269 const exascaleDbStorageVaultId = yield* toPhysicalId(
270 id,
271 olds?.exascaleDbStorageVaultId,
272 output?.exascaleDbStorageVaultId,
273 FALLBACK_ID,
274 );
275 const location = normalizeLocation(
276 olds?.location ?? output?.location ?? env.region,
277 );
278 const name =
279 output?.name ??
280 resourceNameOf(
281 env.project,
282 location,
283 COLLECTION,
284 exascaleDbStorageVaultId,
285 );
286 const existing = yield* getByName(name);
287 if (existing === undefined) return undefined;
288 const attrs = toAttrs(existing, env.project);
289 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
290 ? attrs
291 : Unowned(attrs);
292 }),
293
294 list: () =>
295 Effect.gen(function* () {
296 const env = yield* GcpEnvironment.current;
297 const items = yield* listVaults(env.project, env.region);
298 return items.map((item) => toAttrs(item, env.project));
299 }),
300
301 reconcile: Effect.fn(function* ({ id, news, output }) {
302 const env = yield* GcpEnvironment.current;
303 const exascaleDbStorageVaultId = yield* toPhysicalId(
304 id,
305 news.exascaleDbStorageVaultId,
306 output?.exascaleDbStorageVaultId,
307 FALLBACK_ID,
308 );
309 const location = normalizeLocation(
310 news.location ?? output?.location ?? env.region,
311 );
312 const name = resourceNameOf(
313 env.project,
314 location,
315 COLLECTION,
316 exascaleDbStorageVaultId,
317 );
318 const desiredLabels = {
319 ...toLabels(news.labels),
320 ...(yield* createInternalLabels(id)),
321 };
322
323 let current = yield* getByName(output?.name ?? name);
324
325 if (current === undefined) {
326 const created = yield* oracle
327 .createProjectsLocationsExascaleDbStorageVaults({
328 parent: parentOf(env.project, location),
329 exascaleDbStorageVaultId,
330 body: toCreateBody(news, desiredLabels),
331 })
332 .pipe(
333 retryQuota,
334 Effect.catchTag("Conflict", () => Effect.succeed(undefined)),
335 );
336 if (created !== undefined) {
337 yield* waitForOperation(created);
338 }
339 current = yield* waitUntilExists(getByName(name), name);
340 }
341
342 if (current === undefined) {
343 return yield* new OracleDatabaseNotResolved({ name });
344 }
345
346 const ready = yield* waitUntilReady(
347 getByName(current.name ?? name),
348 current.name ?? name,
349 (value) => value.properties?.state,
350 );
351
352 return toAttrs(ready, env.project);
353 }),
354
355 delete: Effect.fn(function* ({ output }) {
356 const operation = yield* oracle
357 .deleteProjectsLocationsExascaleDbStorageVaults({
358 name: output.name,
359 })
360 .pipe(
361 retryConflict,
362 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
363 );
364 if (operation !== undefined) {
365 yield* waitForOperation(operation, { notFoundOk: true });
366 }
367 yield* waitUntilGone(getByName(output.name), output.name);
368 }),
369 });