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 expandParent,
15 hasAlchemyLabelMap,
16 listAtLocation,
17 normalizeLocation,
18 parseName,
19 replaceOnIdentity,
20 ResourceNotResolved,
21 toPhysicalId,
22 userLabels,
23 waitForOperation,
24 waitUntilExists,
25 waitUntilGone,
26 waitUntilReady,
27} from "./internal.ts";
28
29const DEFAULT_PURPOSE = "CLIENT_SUBNET" satisfies oracle.OdbSubnetPurposeEnum;
30
31export type OdbNetworksOdbSubnetProps = {
32 /**
33 * Parent ODB Network. Full name
34 * `projects/{project}/locations/{location}/odbNetworks/{odb_network}`
35 * or the network id (combined with `location`). Immutable — changing
36 * it replaces the subnet.
37 */
38 odbNetwork: string;
39 /**
40 * Region used when `odbNetwork` is a bare id.
41 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
42 */
43 location?: string;
44 /**
45 * ODB Subnet id (the `{odb_subnet}` segment of
46 * `.../odbNetworks/{odb_network}/odbSubnets/{odb_subnet}`). If omitted,
47 * a unique RFC1035 name is generated. Immutable — changing it
48 * replaces the subnet.
49 */
50 odbSubnetId?: string;
51 /**
52 * CIDR range of the subnet. Must be RFC1918 and between `/27` and
53 * `/22`. Immutable — changing it replaces the subnet.
54 */
55 cidrRange: string;
56 /**
57 * Subnet purpose. `CLIENT_SUBNET` is used by databases and GoldenGate;
58 * `BACKUP_SUBNET` is used by Exadata VM Clusters. Immutable —
59 * changing it replaces the subnet.
60 * @default "CLIENT_SUBNET"
61 */
62 purpose?: oracle.OdbSubnetPurposeEnum | (string & {});
63 /**
64 * User labels. Alchemy ownership labels are merged in automatically.
65 */
66 labels?: Record<string, string>;
67};
68
69export type OdbNetworksOdbSubnet = Resource<
70 "GCP.OracleDatabase.OdbNetworksOdbSubnet",
71 OdbNetworksOdbSubnetProps,
72 {
73 /** Full resource name. */
74 name: string;
75 /** ODB Subnet id (last path segment). */
76 odbSubnetId: string;
77 /** Parent ODB Network resource name. */
78 odbNetwork: string;
79 /** Project id. */
80 project: string;
81 /** Location id. */
82 location: string;
83 /** CIDR range. */
84 cidrRange: string | undefined;
85 /** Subnet purpose. */
86 purpose: string | undefined;
87 /** User labels (Alchemy ownership labels stripped). */
88 labels: Record<string, string>;
89 /** Server-reported state. */
90 state: string | undefined;
91 /** RFC3339 creation timestamp. */
92 createTime: string | undefined;
93 },
94 never,
95 Providers
96>;
97
98/**
99 * An Oracle Database@Google Cloud ODB Subnet inside an ODB Network.
100 *
101 * Changing `odbNetwork`, `odbSubnetId`, `location`, `cidrRange`, or
102 * `purpose` replaces the subnet. Labels are set at create; the API has
103 * no update. Requires an Oracle Database@Google Cloud entitlement and
104 * a parent ODB Network.
105 *
106 * ### Creating an ODB Subnet
107 * **Example:** Client subnet under an ODB Network
108 * ```typescript
109 * const subnet = yield* GCP.OracleDatabase.OdbNetworksOdbSubnet("Client", {
110 * odbNetwork: net.name,
111 * cidrRange: "10.250.0.0/27",
112 * purpose: "CLIENT_SUBNET",
113 * });
114 * ```
115 *
116 * **Example:** Backup subnet with labels
117 * ```typescript
118 * const subnet = yield* GCP.OracleDatabase.OdbNetworksOdbSubnet("Backup", {
119 * odbNetwork: net.name,
120 * cidrRange: "10.250.0.32/27",
121 * purpose: "BACKUP_SUBNET",
122 * labels: { env: "prod" },
123 * });
124 * ```
125 *
126 * @resource
127 * @category OracleDatabase
128 */
129export const OdbNetworksOdbSubnet = Resource<OdbNetworksOdbSubnet>(
130 "GCP.OracleDatabase.OdbNetworksOdbSubnet",
131);
132
133const desiredPurpose = (purpose: string | undefined) =>
134 (purpose ?? DEFAULT_PURPOSE).toUpperCase();
135
136const networkOf = (odbNetwork: string, project: string, location: string) =>
137 expandParent(odbNetwork, project, location, "odbNetworks");
138
139const resourceName = (odbNetwork: string, odbSubnetId: string) =>
140 `${odbNetwork}/odbSubnets/${odbSubnetId}`;
141
142const toAttrs = (subnet: oracle.OdbSubnet, project: string) => {
143 const name = subnet.name ?? "";
144 const parsed = parseName(name, "odbSubnets");
145 return {
146 name,
147 odbSubnetId: parsed.id,
148 odbNetwork: parsed.parent,
149 project: parsed.project || project,
150 location: parsed.location,
151 cidrRange: subnet.cidrRange,
152 purpose: subnet.purpose,
153 labels: userLabels(subnet.labels),
154 state: subnet.state,
155 createTime: subnet.createTime,
156 };
157};
158
159const getByName = (name: string) =>
160 name.length === 0 || name.includes("//")
161 ? Effect.succeed(undefined)
162 : oracle
163 .getProjectsLocationsOdbNetworksOdbSubnets({ name })
164 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
165
166const listSubnets = (parent: string) =>
167 oracle.listProjectsLocationsOdbNetworksOdbSubnets
168 .pages({ parent, pageSize: 1000 })
169 .pipe(
170 Stream.flatMap((page) => Stream.fromIterable(page.odbSubnets ?? [])),
171 Stream.runCollect,
172 Effect.map((chunk) => Array.from(chunk)),
173 Effect.catchTag("NotFound", () =>
174 Effect.succeed([] as oracle.OdbSubnet[]),
175 ),
176 );
177
178const listOwned = (project: string, region: string) =>
179 Effect.gen(function* () {
180 const wildcard = yield* listSubnets(
181 `projects/${project}/locations/-/odbNetworks/-`,
182 );
183 const labeledWildcard = wildcard.filter((item) =>
184 hasAlchemyLabelMap(item.labels),
185 );
186 if (labeledWildcard.length > 0) return labeledWildcard;
187
188 const networks = yield* listAtLocation(project, region, (parent) =>
189 oracle.listProjectsLocationsOdbNetworks
190 .pages({ parent, pageSize: 1000 })
191 .pipe(
192 Stream.flatMap((page) => Stream.fromIterable(page.odbNetworks ?? [])),
193 Stream.runCollect,
194 Effect.map((chunk) => Array.from(chunk)),
195 Effect.catchTag("NotFound", () => Effect.succeed([])),
196 ),
197 );
198 const nested = yield* Effect.forEach(
199 networks,
200 (network) =>
201 network.name
202 ? listSubnets(network.name)
203 : Effect.succeed([] as oracle.OdbSubnet[]),
204 { concurrency: 4 },
205 );
206 return nested.flat().filter((item) => hasAlchemyLabelMap(item.labels));
207 });
208
209export const OdbNetworksOdbSubnetProvider = () =>
210 Provider.succeed(OdbNetworksOdbSubnet, {
211 stables: [
212 "name",
213 "odbSubnetId",
214 "odbNetwork",
215 "project",
216 "location",
217 "cidrRange",
218 "purpose",
219 "createTime",
220 ],
221
222 diff: Effect.fn(function* ({ news, olds, output }) {
223 if (!isResolved(news)) return undefined;
224 const env = yield* GcpEnvironment.current;
225 const previousPurpose = desiredPurpose(olds?.purpose ?? output?.purpose);
226 const nextPurpose = desiredPurpose(news.purpose ?? previousPurpose);
227 const previousCidr = olds?.cidrRange ?? output?.cidrRange ?? "";
228 const nextCidr = news.cidrRange ?? previousCidr;
229 return replaceOnIdentity({
230 previousId: olds?.odbSubnetId ?? output?.odbSubnetId,
231 nextId: news.odbSubnetId ?? olds?.odbSubnetId ?? output?.odbSubnetId,
232 previousLocation: normalizeLocation(
233 olds?.location ?? output?.location ?? env.region,
234 ),
235 nextLocation: normalizeLocation(
236 news.location ?? olds?.location ?? output?.location ?? env.region,
237 ),
238 previousParent: olds?.odbNetwork ?? output?.odbNetwork,
239 nextParent: news.odbNetwork ?? olds?.odbNetwork ?? output?.odbNetwork,
240 extra: previousPurpose !== nextPurpose || previousCidr !== nextCidr,
241 });
242 }),
243
244 read: Effect.fn(function* ({ id, olds, output }) {
245 const env = yield* GcpEnvironment.current;
246 const location = normalizeLocation(
247 olds?.location ?? output?.location ?? env.region,
248 );
249 const odbNetwork = networkOf(
250 olds?.odbNetwork ?? output?.odbNetwork ?? "",
251 env.project,
252 location,
253 );
254 const odbSubnetId = yield* toPhysicalId(
255 id,
256 olds?.odbSubnetId,
257 output?.odbSubnetId,
258 "odbsubnet",
259 );
260 const name = output?.name ?? resourceName(odbNetwork, odbSubnetId);
261 const existing = yield* getByName(name);
262 if (existing === undefined) return undefined;
263 const attrs = toAttrs(existing, env.project);
264 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
265 ? attrs
266 : Unowned(attrs);
267 }),
268
269 list: () =>
270 Effect.gen(function* () {
271 const env = yield* GcpEnvironment.current;
272 const items = yield* listOwned(env.project, env.region);
273 return items.map((item) => toAttrs(item, env.project));
274 }),
275
276 reconcile: Effect.fn(function* ({ id, news, output }) {
277 const env = yield* GcpEnvironment.current;
278 const location = normalizeLocation(
279 news.location ?? output?.location ?? env.region,
280 );
281 const odbNetwork = networkOf(news.odbNetwork, env.project, location);
282 const odbSubnetId = yield* toPhysicalId(
283 id,
284 news.odbSubnetId,
285 output?.odbSubnetId,
286 "odbsubnet",
287 );
288 const name = resourceName(odbNetwork, odbSubnetId);
289 const purpose = desiredPurpose(news.purpose);
290 const desiredLabels = {
291 ...toLabels(news.labels),
292 ...(yield* createInternalLabels(id)),
293 };
294
295 let current = yield* getByName(output?.name ?? name);
296
297 if (current === undefined) {
298 const created = yield* oracle
299 .createProjectsLocationsOdbNetworksOdbSubnets({
300 parent: odbNetwork,
301 odbSubnetId,
302 body: {
303 cidrRange: news.cidrRange,
304 purpose,
305 labels: desiredLabels,
306 },
307 })
308 .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
309 if (created !== undefined) {
310 yield* waitForOperation(created);
311 }
312 current = yield* waitUntilExists(getByName(name), name);
313 }
314
315 if (current === undefined) {
316 return yield* new ResourceNotResolved({ name });
317 }
318
319 const ready = yield* waitUntilReady(
320 getByName(current.name ?? name),
321 current.name ?? name,
322 (item) => item.state,
323 );
324
325 return toAttrs(ready, env.project);
326 }),
327
328 delete: Effect.fn(function* ({ output }) {
329 if (!output.name || output.name.includes("//")) return;
330 const operation = yield* oracle
331 .deleteProjectsLocationsOdbNetworksOdbSubnets({ name: output.name })
332 .pipe(
333 Effect.retry({
334 while: (error) => error._tag === "Conflict",
335 times: 8,
336 schedule: Schedule.spaced("5 seconds"),
337 }),
338 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
339 Effect.catchTag("BadRequest", () => Effect.succeed(undefined)),
340 );
341 if (operation !== undefined) {
342 yield* waitForOperation(operation, { notFoundOk: true });
343 }
344 yield* waitUntilGone(getByName(output.name), output.name);
345 }),
346 });