1import * as oracle from "@distilled.cloud/gcp/oracledatabase_v1";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import * as Stream from "effect/Stream";
5import { Unowned } from "../../AdoptPolicy.ts";
6import { isResolved } from "../../Diff.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { tagRecord } from "../../Tags.ts";
10import { GcpEnvironment } from "../Environment.ts";
11import { createInternalLabels, hasAlchemyLabels, toLabels } from "../Labels.ts";
12import type { Providers } from "../Providers.ts";
13import {
14 alphanumericId,
15 expandParent,
16 hasAlchemyLabelMap,
17 lastSegment,
18 listAtLocation,
19 normalizeLocation,
20 parentOf,
21 parseName,
22 replaceOnIdentity,
23 ResourceNotResolved,
24 toPhysicalId,
25 userLabels,
26 waitForOperation,
27 waitUntilExists,
28 waitUntilGone,
29 waitUntilReady,
30} from "./internal.ts";
31
32const DEFAULT_DEPLOYMENT_TYPE = "DATABASE_ORACLE";
33const DEFAULT_ADMIN_USERNAME = "oggadmin";
34
35export type GoldengateOggData = {
36 /**
37 * GoldenGate console username. Required on create.
38 */
39 adminUsername: string;
40 /**
41 * Name of the GoldenGate service deployment (1–32 alphanumeric
42 * characters, starting with a letter). Distinct from the GCP resource
43 * id.
44 */
45 deployment: string;
46 /**
47 * Console password in plain text. Create-only; omitted from attributes.
48 */
49 adminPassword?: string;
50 /**
51 * Secret Manager version holding the console password
52 * (`projects/{project}/secrets/{secret}/versions/{version}`).
53 */
54 adminPasswordSecretVersion?: string;
55 /**
56 * Oracle GoldenGate version. If omitted, the service picks a default.
57 */
58 oggVersion?: string;
59};
60
61export type GoldengateMaintenanceWindow = {
62 /** UTC start hour for the maintenance window. */
63 startHour?: number;
64 /** Day of week (`MONDAY` … `SUNDAY`). */
65 day?: oracle.GoldengateMaintenanceWindowDayEnum | (string & {});
66};
67
68export type GoldengateMaintenanceConfig = {
69 /** Auto-upgrade period for bundle releases, in days. */
70 bundleReleaseUpgradePeriodDays?: number;
71 /** Auto-upgrade period for major releases, in days. */
72 majorReleaseUpgradePeriodDays?: number;
73 /** Auto-upgrade period for security patches, in days. */
74 securityPatchUpgradePeriodDays?: number;
75 /** Enable auto-upgrade for interim releases. */
76 isInterimReleaseAutoUpgradeEnabled?: boolean;
77 /** Auto-upgrade period for interim releases, in days. */
78 interimReleaseUpgradePeriodDays?: number;
79};
80
81export type GoldengateDeploymentProps = {
82 /**
83 * Deployment id (the `{goldengate_deployment}` segment of
84 * `projects/{project}/locations/{location}/goldengateDeployments/{goldengate_deployment}`).
85 * If omitted, a unique RFC1035 name is generated. Immutable —
86 * changing it replaces the deployment.
87 */
88 goldengateDeploymentId?: string;
89 /**
90 * Region (`us-central1`, `us-east4`, …). Immutable — changing it
91 * replaces the deployment.
92 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
93 */
94 location?: string;
95 /**
96 * User-facing display name. Defaults to the generated resource id.
97 */
98 displayName?: string;
99 /**
100 * Parent ODB Network. Full name or id. Immutable — changing it
101 * replaces the deployment.
102 */
103 odbNetwork?: string;
104 /**
105 * ODB Subnet used for IP allocation. Full name
106 * `projects/{project}/locations/{location}/odbNetworks/{odb_network}/odbSubnets/{odb_subnet}`
107 * or a bare subnet id (requires `odbNetwork`). Immutable — changing
108 * it replaces the deployment.
109 */
110 odbSubnet: string;
111 /**
112 * GCP Oracle zone (e.g. `us-east4-b-r2`). Immutable — changing it
113 * replaces the deployment.
114 */
115 gcpOracleZone?: string;
116 /**
117 * Oracle GoldenGate deployment type (`DATABASE_ORACLE`, `BIGDATA`,
118 * `DATABASE_POSTGRESQL`, …). Immutable — changing it replaces the
119 * deployment.
120 * @default "DATABASE_ORACLE"
121 */
122 deploymentType?: string;
123 /**
124 * GoldenGate console and service identity. `deployment` is immutable.
125 */
126 oggData: GoldengateOggData;
127 /**
128 * Human-readable description stored on the Oracle properties.
129 */
130 description?: string;
131 /**
132 * Minimum OCPU count.
133 */
134 cpuCoreCount?: number;
135 /**
136 * Oracle license model (`LICENSE_INCLUDED`, `BRING_YOUR_OWN_LICENSE`).
137 */
138 licenseModel?:
139 | oracle.GoldengateDeploymentPropertiesLicenseModelEnum
140 | (string & {});
141 /**
142 * GoldenGate environment type (from `ListGoldengateDeploymentEnvironments`).
143 */
144 environmentType?: string;
145 /**
146 * Enable CPU auto-scaling.
147 */
148 isAutoScalingEnabled?: boolean;
149 /**
150 * Preferred weekly maintenance window.
151 */
152 maintenanceWindow?: GoldengateMaintenanceWindow;
153 /**
154 * Auto-upgrade periods for GoldenGate releases.
155 */
156 maintenanceConfig?: GoldengateMaintenanceConfig;
157 /**
158 * User labels. Alchemy ownership labels are merged in automatically.
159 */
160 labels?: Record<string, string>;
161};
162
163export type GoldengateDeployment = Resource<
164 "GCP.OracleDatabase.GoldengateDeployment",
165 GoldengateDeploymentProps,
166 {
167 /** Full resource name. */
168 name: string;
169 /** Deployment id (last path segment). */
170 goldengateDeploymentId: string;
171 /** Project id. */
172 project: string;
173 /** Location id. */
174 location: string;
175 /** Display name. */
176 displayName: string | undefined;
177 /** Parent ODB Network resource name. */
178 odbNetwork: string | undefined;
179 /** ODB Subnet resource name. */
180 odbSubnet: string | undefined;
181 /** GCP Oracle zone. */
182 gcpOracleZone: string | undefined;
183 /** GoldenGate deployment type. */
184 deploymentType: string | undefined;
185 /** GoldenGate service deployment name. */
186 oggDeployment: string | undefined;
187 /** GoldenGate console username. */
188 adminUsername: string | undefined;
189 /** GoldenGate version. */
190 oggVersion: string | undefined;
191 /** Description. */
192 description: string | undefined;
193 /** Minimum OCPU count. */
194 cpuCoreCount: number | undefined;
195 /** License model. */
196 licenseModel: string | undefined;
197 /** Environment type. */
198 environmentType: string | undefined;
199 /** Whether CPU auto-scaling is enabled. */
200 isAutoScalingEnabled: boolean | undefined;
201 /** User labels (Alchemy ownership labels stripped). */
202 labels: Record<string, string>;
203 /** Lifecycle state. */
204 lifecycleState: string | undefined;
205 /** Lifecycle details. */
206 lifecycleDetails: string | undefined;
207 /** GoldenGate FQDN. */
208 fqdn: string | undefined;
209 /** Deployment URL. */
210 deploymentUrl: string | undefined;
211 /** Private IP address. */
212 privateIpAddress: string | undefined;
213 /** Public IP address. */
214 publicIpAddress: string | undefined;
215 /** OCI console URL. */
216 ociUrl: string | undefined;
217 /** Marketplace entitlement id. */
218 entitlementId: string | undefined;
219 /** Oracle Cloud identifier. */
220 ocid: string | undefined;
221 /** RFC3339 creation timestamp. */
222 createTime: string | undefined;
223 },
224 never,
225 Providers
226>;
227
228/**
229 * An Oracle Database@Google Cloud GoldenGate deployment.
230 *
231 * Changing `goldengateDeploymentId`, `location`, `odbNetwork`,
232 * `odbSubnet`, `gcpOracleZone`, `deploymentType`, or
233 * `oggData.deployment` replaces the deployment. Labels and most
234 * properties are set at create; the API has no update. Requires an
235 * Oracle Database@Google Cloud entitlement and a client ODB Subnet.
236 *
237 * ### Creating a GoldenGate Deployment
238 * **Example:** Oracle GoldenGate on an ODB Subnet
239 * ```typescript
240 * const gg = yield* GCP.OracleDatabase.GoldengateDeployment("Replicat", {
241 * odbSubnet: subnet.name,
242 * odbNetwork: net.name,
243 * displayName: "app-gg",
244 * deploymentType: "DATABASE_ORACLE",
245 * oggData: {
246 * adminUsername: "oggadmin",
247 * deployment: "oggdeploy",
248 * adminPassword: "change-me",
249 * },
250 * });
251 * ```
252 *
253 * **Example:** Explicit id and labels
254 * ```typescript
255 * const gg = yield* GCP.OracleDatabase.GoldengateDeployment("Replicat", {
256 * goldengateDeploymentId: "app-gg",
257 * odbSubnet: subnet.name,
258 * deploymentType: "DATABASE_ORACLE",
259 * oggData: {
260 * adminUsername: "oggadmin",
261 * deployment: "oggdeploy",
262 * adminPasswordSecretVersion: secret.latestVersionName,
263 * },
264 * labels: { env: "prod" },
265 * });
266 * ```
267 *
268 * @resource
269 * @category OracleDatabase
270 */
271export const GoldengateDeployment = Resource<GoldengateDeployment>(
272 "GCP.OracleDatabase.GoldengateDeployment",
273);
274
275const resourceName = (
276 project: string,
277 location: string,
278 goldengateDeploymentId: string,
279) =>
280 `projects/${project}/locations/${location}/goldengateDeployments/${goldengateDeploymentId}`;
281
282const subnetOf = (
283 odbSubnet: string,
284 odbNetwork: string | undefined,
285 project: string,
286 location: string,
287) => {
288 if (odbSubnet.includes("/")) return odbSubnet.replace(/\/+$/, "");
289 const network = odbNetwork
290 ? expandParent(odbNetwork, project, location, "odbNetworks")
291 : `projects/${project}/locations/${location}/odbNetworks`;
292 return `${network}/odbSubnets/${odbSubnet}`;
293};
294
295const networkOf = (
296 odbNetwork: string | undefined,
297 project: string,
298 location: string,
299) =>
300 odbNetwork === undefined || odbNetwork.length === 0
301 ? undefined
302 : expandParent(odbNetwork, project, location, "odbNetworks");
303
304const desiredType = (type: string | undefined) =>
305 type ?? DEFAULT_DEPLOYMENT_TYPE;
306
307const toAttrs = (deployment: oracle.GoldengateDeployment, project: string) => {
308 const name = deployment.name ?? "";
309 const parsed = parseName(name, "goldengateDeployments");
310 const properties = deployment.properties;
311 return {
312 name,
313 goldengateDeploymentId: parsed.id,
314 project: parsed.project || project,
315 location: parsed.location,
316 displayName: deployment.displayName,
317 odbNetwork: deployment.odbNetwork,
318 odbSubnet: deployment.odbSubnet,
319 gcpOracleZone: deployment.gcpOracleZone,
320 deploymentType: properties?.deploymentType,
321 oggDeployment: properties?.oggData?.deployment,
322 adminUsername: properties?.oggData?.adminUsername,
323 oggVersion: properties?.oggData?.oggVersion,
324 description: properties?.description,
325 cpuCoreCount: properties?.cpuCoreCount,
326 licenseModel: properties?.licenseModel,
327 environmentType: properties?.environmentType,
328 isAutoScalingEnabled: properties?.isAutoScalingEnabled,
329 labels: userLabels(deployment.labels),
330 lifecycleState: properties?.lifecycleState,
331 lifecycleDetails: properties?.lifecycleDetails,
332 fqdn: properties?.fqdn,
333 deploymentUrl: properties?.deploymentUrl,
334 privateIpAddress: properties?.privateIpAddress,
335 publicIpAddress: properties?.publicIpAddress,
336 ociUrl: deployment.ociUrl,
337 entitlementId: deployment.entitlementId,
338 ocid: properties?.ocid,
339 createTime: deployment.createTime,
340 };
341};
342
343const getByName = (name: string) =>
344 name.length === 0
345 ? Effect.succeed(undefined)
346 : oracle
347 .getProjectsLocationsGoldengateDeployments({ name })
348 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
349
350const listOwned = (project: string, region: string) =>
351 listAtLocation(project, region, (parent) =>
352 oracle.listProjectsLocationsGoldengateDeployments
353 .pages({ parent, pageSize: 1000 })
354 .pipe(
355 Stream.flatMap((page) =>
356 Stream.fromIterable(page.goldengateDeployments ?? []),
357 ),
358 Stream.filter((item) => hasAlchemyLabelMap(item.labels)),
359 Stream.runCollect,
360 Effect.map((chunk) => Array.from(chunk)),
361 Effect.catchTag("NotFound", () => Effect.succeed([])),
362 ),
363 );
364
365const toCreateBody = (
366 news: GoldengateDeploymentProps,
367 desiredLabels: Record<string, string>,
368 displayName: string,
369 odbNetwork: string | undefined,
370 odbSubnet: string,
371): oracle.GoldengateDeployment => ({
372 displayName,
373 odbNetwork,
374 odbSubnet,
375 gcpOracleZone: news.gcpOracleZone,
376 labels: desiredLabels,
377 properties: {
378 deploymentType: desiredType(news.deploymentType),
379 description: news.description,
380 cpuCoreCount: news.cpuCoreCount,
381 licenseModel: news.licenseModel,
382 environmentType: news.environmentType,
383 isAutoScalingEnabled: news.isAutoScalingEnabled,
384 maintenanceWindow: news.maintenanceWindow,
385 maintenanceConfig: news.maintenanceConfig,
386 oggData: {
387 adminUsername: news.oggData.adminUsername || DEFAULT_ADMIN_USERNAME,
388 deployment: alphanumericId(news.oggData.deployment),
389 adminPassword: news.oggData.adminPassword,
390 adminPasswordSecretVersion: news.oggData.adminPasswordSecretVersion,
391 oggVersion: news.oggData.oggVersion,
392 },
393 },
394});
395
396export const GoldengateDeploymentProvider = () =>
397 Provider.succeed(GoldengateDeployment, {
398 stables: [
399 "name",
400 "goldengateDeploymentId",
401 "project",
402 "location",
403 "odbNetwork",
404 "odbSubnet",
405 "gcpOracleZone",
406 "deploymentType",
407 "oggDeployment",
408 "entitlementId",
409 "ocid",
410 "createTime",
411 ],
412
413 diff: Effect.fn(function* ({ news, olds, output }) {
414 if (!isResolved(news)) return undefined;
415 const env = yield* GcpEnvironment.current;
416 const previousType = desiredType(
417 olds?.deploymentType ?? output?.deploymentType,
418 );
419 const nextType = desiredType(news.deploymentType ?? previousType);
420 const previousOgg =
421 olds?.oggData?.deployment ?? output?.oggDeployment ?? "";
422 const nextOgg = news.oggData?.deployment ?? previousOgg;
423 const previousZone = olds?.gcpOracleZone ?? output?.gcpOracleZone ?? "";
424 const nextZone = news.gcpOracleZone ?? previousZone;
425 const previousSubnet = lastSegment(
426 olds?.odbSubnet ?? output?.odbSubnet ?? "",
427 );
428 const nextSubnet = lastSegment(news.odbSubnet ?? previousSubnet);
429 return replaceOnIdentity({
430 previousId:
431 olds?.goldengateDeploymentId ?? output?.goldengateDeploymentId,
432 nextId:
433 news.goldengateDeploymentId ??
434 olds?.goldengateDeploymentId ??
435 output?.goldengateDeploymentId,
436 previousLocation: normalizeLocation(
437 olds?.location ?? output?.location ?? env.region,
438 ),
439 nextLocation: normalizeLocation(
440 news.location ?? olds?.location ?? output?.location ?? env.region,
441 ),
442 previousParent: olds?.odbNetwork ?? output?.odbNetwork,
443 nextParent: news.odbNetwork ?? olds?.odbNetwork ?? output?.odbNetwork,
444 extra:
445 previousType !== nextType ||
446 previousOgg !== nextOgg ||
447 previousZone !== nextZone ||
448 previousSubnet !== nextSubnet,
449 });
450 }),
451
452 read: Effect.fn(function* ({ id, olds, output }) {
453 const env = yield* GcpEnvironment.current;
454 const goldengateDeploymentId = yield* toPhysicalId(
455 id,
456 olds?.goldengateDeploymentId,
457 output?.goldengateDeploymentId,
458 "goldengate",
459 );
460 const location = normalizeLocation(
461 olds?.location ?? output?.location ?? env.region,
462 );
463 const name =
464 output?.name ??
465 resourceName(env.project, location, goldengateDeploymentId);
466 const existing = yield* getByName(name);
467 if (existing === undefined) return undefined;
468 const attrs = toAttrs(existing, env.project);
469 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
470 ? attrs
471 : Unowned(attrs);
472 }),
473
474 list: () =>
475 Effect.gen(function* () {
476 const env = yield* GcpEnvironment.current;
477 const items = yield* listOwned(env.project, env.region);
478 return items.map((item) => toAttrs(item, env.project));
479 }),
480
481 reconcile: Effect.fn(function* ({ id, news, output }) {
482 const env = yield* GcpEnvironment.current;
483 const goldengateDeploymentId = yield* toPhysicalId(
484 id,
485 news.goldengateDeploymentId,
486 output?.goldengateDeploymentId,
487 "goldengate",
488 );
489 const location = normalizeLocation(
490 news.location ?? output?.location ?? env.region,
491 );
492 const name = resourceName(env.project, location, goldengateDeploymentId);
493 const odbNetwork = networkOf(news.odbNetwork, env.project, location);
494 const odbSubnet = subnetOf(
495 news.odbSubnet,
496 news.odbNetwork ?? odbNetwork,
497 env.project,
498 location,
499 );
500 const displayName = news.displayName ?? goldengateDeploymentId;
501 const desiredLabels = {
502 ...toLabels(news.labels),
503 ...(yield* createInternalLabels(id)),
504 };
505
506 let current = yield* getByName(output?.name ?? name);
507
508 if (current === undefined) {
509 const created = yield* oracle
510 .createProjectsLocationsGoldengateDeployments({
511 parent: parentOf(env.project, location),
512 goldengateDeploymentId,
513 body: toCreateBody(
514 news,
515 desiredLabels,
516 displayName,
517 odbNetwork,
518 odbSubnet,
519 ),
520 })
521 .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
522 if (created !== undefined) {
523 yield* waitForOperation(created);
524 }
525 current = yield* waitUntilExists(getByName(name), name);
526 }
527
528 if (current === undefined) {
529 return yield* new ResourceNotResolved({ name });
530 }
531
532 const ready = yield* waitUntilReady(
533 getByName(current.name ?? name),
534 current.name ?? name,
535 (item) => item.properties?.lifecycleState,
536 (item) => item.properties?.lifecycleDetails,
537 );
538
539 return toAttrs(ready, env.project);
540 }),
541
542 delete: Effect.fn(function* ({ output }) {
543 const operation = yield* oracle
544 .deleteProjectsLocationsGoldengateDeployments({ name: output.name })
545 .pipe(
546 Effect.retry({
547 while: (error) => error._tag === "Conflict",
548 times: 8,
549 schedule: Schedule.spaced("5 seconds"),
550 }),
551 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
552 );
553 if (operation !== undefined) {
554 yield* waitForOperation(operation, { notFoundOk: true });
555 }
556 yield* waitUntilGone(getByName(output.name), output.name);
557 }),
558 });