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 DEFAULT_NETWORK,
15 hasAlchemyLabelMap,
16 lastSegment,
17 listAtLocation,
18 networkName,
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
32export type OdbNetworkProps = {
33 /**
34 * ODB Network id (the `{odb_network}` segment of
35 * `projects/{project}/locations/{location}/odbNetworks/{odb_network}`).
36 * If omitted, a unique RFC1035 name is generated from the stack, stage,
37 * and logical id. Immutable — changing it replaces the network.
38 */
39 odbNetworkId?: string;
40 /**
41 * Region (`us-central1`, `us-east4`, …). Immutable — changing it
42 * replaces the network. `US-CENTRAL1` is accepted and normalized to
43 * `us-central1`.
44 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
45 */
46 location?: string;
47 /**
48 * VPC network used for Oracle Database@Google Cloud connectivity.
49 * Accepts a name (`default`) or
50 * `projects/{project}/global/networks/{network}`. Immutable —
51 * changing it replaces the network.
52 * @default "default"
53 */
54 network?: string;
55 /**
56 * GCP Oracle zone (e.g. `us-east4-b-r2`). If omitted, the service
57 * picks a zone. Immutable — changing it replaces the network.
58 */
59 gcpOracleZone?: string;
60 /**
61 * User labels. Alchemy ownership labels are merged in automatically.
62 */
63 labels?: Record<string, string>;
64};
65
66export type OdbNetwork = Resource<
67 "GCP.OracleDatabase.OdbNetwork",
68 OdbNetworkProps,
69 {
70 /** Full resource name. */
71 name: string;
72 /** ODB Network id (last path segment). */
73 odbNetworkId: string;
74 /** Project id. */
75 project: string;
76 /** Location id. */
77 location: string;
78 /** VPC network resource name. */
79 network: string | undefined;
80 /** GCP Oracle zone. */
81 gcpOracleZone: string | undefined;
82 /** User labels (Alchemy ownership labels stripped). */
83 labels: Record<string, string>;
84 /** Server-reported state. */
85 state: string | undefined;
86 /** Marketplace entitlement id. */
87 entitlementId: string | undefined;
88 /** RFC3339 creation timestamp. */
89 createTime: string | undefined;
90 },
91 never,
92 Providers
93>;
94
95/**
96 * An Oracle Database@Google Cloud ODB Network — the private network that
97 * connects a VPC to Oracle Database resources in a region.
98 *
99 * Changing `odbNetworkId`, `location`, `network`, or `gcpOracleZone`
100 * replaces the network. Labels are set at create; the API has no update.
101 * Requires an Oracle Database@Google Cloud marketplace entitlement.
102 *
103 * ### Creating an ODB Network
104 * **Example:** Generated name on the default VPC
105 * ```typescript
106 * const net = yield* GCP.OracleDatabase.OdbNetwork("OracleNet", {});
107 * ```
108 *
109 * **Example:** Explicit id, labels, and network
110 * ```typescript
111 * const net = yield* GCP.OracleDatabase.OdbNetwork("OracleNet", {
112 * odbNetworkId: "app-odb",
113 * location: "us-central1",
114 * network: "default",
115 * labels: { env: "prod" },
116 * });
117 * ```
118 *
119 * @resource
120 * @category OracleDatabase
121 */
122export const OdbNetwork = Resource<OdbNetwork>("GCP.OracleDatabase.OdbNetwork");
123
124const resourceName = (
125 project: string,
126 location: string,
127 odbNetworkId: string,
128) => `projects/${project}/locations/${location}/odbNetworks/${odbNetworkId}`;
129
130const toAttrs = (network: oracle.OdbNetwork, project: string) => {
131 const name = network.name ?? "";
132 const parsed = parseName(name, "odbNetworks");
133 return {
134 name,
135 odbNetworkId: parsed.id,
136 project: parsed.project || project,
137 location: parsed.location,
138 network: network.network,
139 gcpOracleZone: network.gcpOracleZone,
140 labels: userLabels(network.labels),
141 state: network.state,
142 entitlementId: network.entitlementId,
143 createTime: network.createTime,
144 };
145};
146
147const getByName = (name: string) =>
148 name.length === 0
149 ? Effect.succeed(undefined)
150 : oracle
151 .getProjectsLocationsOdbNetworks({ name })
152 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
153
154const listOwned = (project: string, region: string) =>
155 listAtLocation(project, region, (parent) =>
156 oracle.listProjectsLocationsOdbNetworks
157 .pages({ parent, pageSize: 1000 })
158 .pipe(
159 Stream.flatMap((page) => Stream.fromIterable(page.odbNetworks ?? [])),
160 Stream.filter((item) => hasAlchemyLabelMap(item.labels)),
161 Stream.runCollect,
162 Effect.map((chunk) => Array.from(chunk)),
163 Effect.catchTag("NotFound", () => Effect.succeed([])),
164 ),
165 );
166
167export const OdbNetworkProvider = () =>
168 Provider.succeed(OdbNetwork, {
169 stables: [
170 "name",
171 "odbNetworkId",
172 "project",
173 "location",
174 "network",
175 "gcpOracleZone",
176 "entitlementId",
177 "createTime",
178 ],
179
180 diff: Effect.fn(function* ({ news, olds, output }) {
181 if (!isResolved(news)) return undefined;
182 const env = yield* GcpEnvironment.current;
183 const previousNetwork = lastSegment(
184 olds?.network ?? output?.network ?? DEFAULT_NETWORK,
185 );
186 const nextNetwork = lastSegment(
187 news.network ?? olds?.network ?? output?.network ?? DEFAULT_NETWORK,
188 );
189 const previousZone = olds?.gcpOracleZone ?? output?.gcpOracleZone ?? "";
190 const nextZone = news.gcpOracleZone ?? previousZone;
191 return replaceOnIdentity({
192 previousId: olds?.odbNetworkId ?? output?.odbNetworkId,
193 nextId: news.odbNetworkId ?? olds?.odbNetworkId ?? output?.odbNetworkId,
194 previousLocation: normalizeLocation(
195 olds?.location ?? output?.location ?? env.region,
196 ),
197 nextLocation: normalizeLocation(
198 news.location ?? olds?.location ?? output?.location ?? env.region,
199 ),
200 extra: previousNetwork !== nextNetwork || previousZone !== nextZone,
201 });
202 }),
203
204 read: Effect.fn(function* ({ id, olds, output }) {
205 const env = yield* GcpEnvironment.current;
206 const odbNetworkId = yield* toPhysicalId(
207 id,
208 olds?.odbNetworkId,
209 output?.odbNetworkId,
210 "odbnetwork",
211 );
212 const location = normalizeLocation(
213 olds?.location ?? output?.location ?? env.region,
214 );
215 const name =
216 output?.name ?? resourceName(env.project, location, odbNetworkId);
217 const existing = yield* getByName(name);
218 if (existing === undefined) return undefined;
219 const attrs = toAttrs(existing, env.project);
220 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
221 ? attrs
222 : Unowned(attrs);
223 }),
224
225 list: () =>
226 Effect.gen(function* () {
227 const env = yield* GcpEnvironment.current;
228 const items = yield* listOwned(env.project, env.region);
229 return items.map((item) => toAttrs(item, env.project));
230 }),
231
232 reconcile: Effect.fn(function* ({ id, news, output }) {
233 const env = yield* GcpEnvironment.current;
234 const odbNetworkId = yield* toPhysicalId(
235 id,
236 news.odbNetworkId,
237 output?.odbNetworkId,
238 "odbnetwork",
239 );
240 const location = normalizeLocation(
241 news.location ?? output?.location ?? env.region,
242 );
243 const name = resourceName(env.project, location, odbNetworkId);
244 const network = networkName(env.project, news.network);
245 const desiredLabels = {
246 ...toLabels(news.labels),
247 ...(yield* createInternalLabels(id)),
248 };
249
250 let current = yield* getByName(output?.name ?? name);
251
252 if (current === undefined) {
253 const created = yield* oracle
254 .createProjectsLocationsOdbNetworks({
255 parent: parentOf(env.project, location),
256 odbNetworkId,
257 body: {
258 network,
259 gcpOracleZone: news.gcpOracleZone,
260 labels: desiredLabels,
261 },
262 })
263 .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
264 if (created !== undefined) {
265 yield* waitForOperation(created);
266 }
267 current = yield* waitUntilExists(getByName(name), name);
268 }
269
270 if (current === undefined) {
271 return yield* new ResourceNotResolved({ name });
272 }
273
274 const ready = yield* waitUntilReady(
275 getByName(current.name ?? name),
276 current.name ?? name,
277 (item) => item.state,
278 );
279
280 return toAttrs(ready, env.project);
281 }),
282
283 delete: Effect.fn(function* ({ output }) {
284 const operation = yield* oracle
285 .deleteProjectsLocationsOdbNetworks({ name: output.name })
286 .pipe(
287 Effect.retry({
288 while: (error) => error._tag === "Conflict",
289 times: 8,
290 schedule: Schedule.spaced("5 seconds"),
291 }),
292 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
293 );
294 if (operation !== undefined) {
295 yield* waitForOperation(operation, { notFoundOk: true });
296 }
297 yield* waitUntilGone(getByName(output.name), output.name);
298 }),
299 });