1import * as cnr from "@distilled.cloud/gcp/cloudnumberregistry_v1alpha";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import { tagRecord } from "../../Tags.ts";
9import { GcpEnvironment } from "../Environment.ts";
10import {
11 createInternalLabels,
12 diffLabels,
13 hasAlchemyLabels,
14 toLabels,
15} from "../Labels.ts";
16import type { Providers } from "../Providers.ts";
17import {
18 DEFAULT_LOCATION,
19 ResourceNotResolved,
20 expandName,
21 fieldMask,
22 listAtLocation,
23 listLabeledPages,
24 normalizeLocation,
25 parentOf,
26 parseName,
27 replaceOnIdentity,
28 resourceName,
29 sameRef,
30 toPhysicalId,
31 userLabels,
32 waitForDeleteOperation,
33 waitForOperation,
34 waitUntilExists,
35 waitUntilGone,
36} from "./internal.ts";
37
38const COLLECTION = "realms";
39const DEFAULT_TRAFFIC: cnr.RealmTrafficTypeEnum = "PRIVATE";
40const DEFAULT_MANAGEMENT: cnr.RealmManagementTypeEnum = "USER";
41const DEFAULT_IP_VERSION: cnr.RealmIpVersionEnum = "IPV4";
42
43export type RealmDiscoveryMetadata = {
44 eventTime: string | undefined;
45 createTime: string | undefined;
46 state: string | undefined;
47 updateTime: string | undefined;
48 resourceUri: string | undefined;
49 resource: string | undefined;
50 sourceId: string | undefined;
51 sourceSubId: string | undefined;
52};
53
54export type RealmAggregatedData = {
55 customRangesCount: number | undefined;
56 discoveredRangesCount: number | undefined;
57};
58
59export type RealmProps = {
60 /**
61 * Realm id (the `{realm}` segment of
62 * `projects/{project}/locations/{location}/realms/{realm}`). If omitted,
63 * a unique RFC1035 name is generated. Immutable — changing it
64 * replaces the realm.
65 */
66 realmId?: string;
67 /**
68 * Location of the realm. Cloud Number Registry is global — `global`
69 * is the only supported value. Immutable — changing it replaces the
70 * realm.
71 * @default "global"
72 */
73 location?: string;
74 /**
75 * Registry book that claims this realm. Accepts a book id or a full
76 * resource name. Immutable — changing it replaces the realm.
77 */
78 registryBook: string;
79 /**
80 * Traffic type of the realm (`PRIVATE`, `INTERNET`, `LINKLOCAL`,
81 * `UNSET`). Immutable — changing it replaces the realm.
82 * @default "PRIVATE"
83 */
84 trafficType?: cnr.RealmTrafficTypeEnum | (string & {});
85 /**
86 * Who manages the realm (`USER` for custom ranges, `CNR` for
87 * discovered resources). Immutable — changing it replaces the realm.
88 * @default "USER"
89 */
90 managementType?: cnr.RealmManagementTypeEnum | (string & {});
91 /**
92 * IP version (`IPV4` or `IPV6`). Immutable — changing it replaces
93 * the realm.
94 * @default "IPV4"
95 */
96 ipVersion?: cnr.RealmIpVersionEnum | (string & {});
97 /**
98 * User labels. Alchemy ownership labels are merged in automatically.
99 */
100 labels?: Record<string, string>;
101};
102
103export type Realm = Resource<
104 "GCP.CloudNumberRegistry.Realm",
105 RealmProps,
106 {
107 /** Full resource name `projects/{project}/locations/{location}/realms/{realm}`. */
108 name: string;
109 /** Realm id (last path segment). */
110 realmId: string;
111 /** Project id. */
112 project: string;
113 /** Location id of the resource. */
114 location: string;
115 /** Registry book that claims this realm. */
116 registryBook: string | undefined;
117 /** Traffic type. */
118 trafficType: string | undefined;
119 /** Management type. */
120 managementType: string | undefined;
121 /** IP version. */
122 ipVersion: string | undefined;
123 /** User labels (Alchemy ownership labels stripped). */
124 labels: Record<string, string>;
125 /** Discovery metadata, when the realm was imported from Compute Engine. */
126 discoveryMetadata: RealmDiscoveryMetadata | undefined;
127 /** Aggregated counts, populated when the view is AGGREGATE. */
128 aggregatedData: RealmAggregatedData | undefined;
129 /** RFC3339 creation timestamp. */
130 createTime: string | undefined;
131 /** RFC3339 last-update timestamp. */
132 updateTime: string | undefined;
133 },
134 never,
135 Providers
136>;
137
138/**
139 * A Cloud Number Registry realm — a routing domain of IP ranges that
140 * must not overlap (unless one range is the parent of another). Custom
141 * ranges can only be added to user-managed realms.
142 *
143 * `realmId`, `location`, `registryBook`, `trafficType`,
144 * `managementType`, and `ipVersion` replace the resource. Labels
145 * update in place.
146 *
147 * ### Creating a Realm
148 * **Example:** User-managed IPv4 realm
149 * ```typescript
150 * const book = yield* GCP.CloudNumberRegistry.RegistryBook("Inventory", {});
151 * const realm = yield* GCP.CloudNumberRegistry.Realm("Private", {
152 * registryBook: book.name,
153 * trafficType: "PRIVATE",
154 * managementType: "USER",
155 * ipVersion: "IPV4",
156 * });
157 * ```
158 *
159 * ### Updating a Realm
160 * **Example:** Labels
161 * ```typescript
162 * const realm = yield* GCP.CloudNumberRegistry.Realm("Private", {
163 * registryBook: book.name,
164 * labels: { env: "prod" },
165 * });
166 * ```
167 *
168 * @resource
169 * @category CloudNumberRegistry
170 */
171export const Realm = Resource<Realm>("GCP.CloudNumberRegistry.Realm");
172
173const toDiscovery = (
174 metadata: cnr.DiscoveryMetadata | undefined,
175): RealmDiscoveryMetadata | undefined =>
176 metadata === undefined
177 ? undefined
178 : {
179 eventTime: metadata.eventTime,
180 createTime: metadata.createTime,
181 state: metadata.state,
182 updateTime: metadata.updateTime,
183 resourceUri: metadata.resourceUri,
184 resource: metadata.resource,
185 sourceId: metadata.sourceId,
186 sourceSubId: metadata.sourceSubId,
187 };
188
189const toAttrs = (item: cnr.Realm, project: string) => {
190 const name = item.name ?? "";
191 const parsed = parseName(name, COLLECTION);
192 return {
193 name,
194 realmId: parsed.id,
195 project: parsed.project || project,
196 location: parsed.location || DEFAULT_LOCATION,
197 registryBook: item.registryBook,
198 trafficType: item.trafficType,
199 managementType: item.managementType,
200 ipVersion: item.ipVersion,
201 labels: userLabels(item.labels),
202 discoveryMetadata: toDiscovery(item.discoveryMetadata),
203 aggregatedData: item.aggregatedData
204 ? {
205 customRangesCount: item.aggregatedData.customRangesCount,
206 discoveredRangesCount: item.aggregatedData.discoveredRangesCount,
207 }
208 : undefined,
209 createTime: item.createTime,
210 updateTime: item.updateTime,
211 };
212};
213
214const getByName = (name: string) =>
215 name.length === 0
216 ? Effect.succeed(undefined)
217 : cnr
218 .getProjectsLocationsRealms({ name })
219 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
220
221const listOwned = (project: string) =>
222 listAtLocation(project, (parent) =>
223 listLabeledPages(
224 cnr.listProjectsLocationsRealms.pages({
225 parent,
226 pageSize: 1000,
227 }),
228 (page) => page.realms,
229 (item) => item.labels,
230 ),
231 );
232
233export const RealmProvider = () =>
234 Provider.succeed(Realm, {
235 stables: [
236 "name",
237 "realmId",
238 "project",
239 "location",
240 "registryBook",
241 "trafficType",
242 "managementType",
243 "ipVersion",
244 "createTime",
245 ],
246
247 diff: Effect.fn(function* ({ news, olds, output }) {
248 if (!isResolved(news)) return undefined;
249 const previousBook = olds?.registryBook ?? output?.registryBook;
250 const previousTraffic = olds?.trafficType ?? output?.trafficType;
251 const nextTraffic = news.trafficType ?? previousTraffic;
252 const previousManagement = olds?.managementType ?? output?.managementType;
253 const nextManagement = news.managementType ?? previousManagement;
254 const previousVersion = olds?.ipVersion ?? output?.ipVersion;
255 const nextVersion = news.ipVersion ?? previousVersion;
256 return replaceOnIdentity({
257 previousId: olds?.realmId ?? output?.realmId,
258 nextId: news.realmId ?? olds?.realmId ?? output?.realmId,
259 previousLocation: normalizeLocation(olds?.location ?? output?.location),
260 nextLocation: normalizeLocation(
261 news.location ?? olds?.location ?? output?.location,
262 ),
263 extra:
264 (previousBook !== undefined &&
265 !sameRef(previousBook, news.registryBook)) ||
266 (previousTraffic !== undefined &&
267 nextTraffic !== undefined &&
268 previousTraffic !== nextTraffic) ||
269 (previousManagement !== undefined &&
270 nextManagement !== undefined &&
271 previousManagement !== nextManagement) ||
272 (previousVersion !== undefined &&
273 nextVersion !== undefined &&
274 previousVersion !== nextVersion),
275 });
276 }),
277
278 read: Effect.fn(function* ({ id, olds, output }) {
279 const env = yield* GcpEnvironment.current;
280 const realmId = yield* toPhysicalId(
281 id,
282 olds?.realmId,
283 output?.realmId,
284 "realm",
285 );
286 const location = normalizeLocation(olds?.location ?? output?.location);
287 const name =
288 output?.name ??
289 resourceName(env.project, location, COLLECTION, realmId);
290 const existing = yield* getByName(name);
291 if (existing === undefined) return undefined;
292 const attrs = toAttrs(existing, env.project);
293 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
294 ? attrs
295 : Unowned(attrs);
296 }),
297
298 list: () =>
299 Effect.gen(function* () {
300 const env = yield* GcpEnvironment.current;
301 const items = yield* listOwned(env.project);
302 return items.map((item) => toAttrs(item, env.project));
303 }),
304
305 reconcile: Effect.fn(function* ({ id, news, output }) {
306 const env = yield* GcpEnvironment.current;
307 const realmId = yield* toPhysicalId(
308 id,
309 news.realmId,
310 output?.realmId,
311 "realm",
312 );
313 const location = normalizeLocation(news.location ?? output?.location);
314 const name = resourceName(env.project, location, COLLECTION, realmId);
315 const desiredLabels = {
316 ...toLabels(news.labels),
317 ...(yield* createInternalLabels(id)),
318 };
319 const registryBook = expandName(
320 news.registryBook,
321 env.project,
322 location,
323 "registryBooks",
324 );
325 const trafficType = news.trafficType ?? DEFAULT_TRAFFIC;
326 const managementType = news.managementType ?? DEFAULT_MANAGEMENT;
327 const ipVersion = news.ipVersion ?? DEFAULT_IP_VERSION;
328
329 let current = yield* getByName(output?.name ?? name);
330
331 if (current === undefined) {
332 const created = yield* cnr
333 .createProjectsLocationsRealms({
334 parent: parentOf(env.project, location),
335 realmId,
336 body: {
337 registryBook,
338 trafficType,
339 managementType,
340 ipVersion,
341 labels: desiredLabels,
342 },
343 })
344 .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
345 if (created !== undefined) {
346 yield* waitForOperation(created);
347 }
348 current = yield* waitUntilExists(getByName(name), name);
349 }
350
351 if (current === undefined) {
352 return yield* new ResourceNotResolved({ name });
353 }
354
355 const observedLabels = tagRecord(current.labels);
356 const { upsert, removed } = diffLabels(observedLabels, desiredLabels);
357 const mask = fieldMask([
358 (upsert.length > 0 || removed.length > 0) && "labels",
359 ]);
360
361 if (mask.length > 0) {
362 const operation = yield* cnr.patchProjectsLocationsRealms({
363 name: current.name ?? name,
364 updateMask: mask,
365 body: {
366 name: current.name ?? name,
367 labels: desiredLabels,
368 },
369 });
370 yield* waitForOperation(operation);
371 current = yield* waitUntilExists(
372 getByName(current.name ?? name),
373 current.name ?? name,
374 );
375 }
376
377 return toAttrs(current, env.project);
378 }),
379
380 delete: Effect.fn(function* ({ output }) {
381 const operation = yield* cnr
382 .deleteProjectsLocationsRealms({
383 name: output.name,
384 force: true,
385 })
386 .pipe(
387 Effect.retry({
388 while: (error) => error._tag === "Conflict",
389 times: 8,
390 schedule: Schedule.spaced("2 seconds"),
391 }),
392 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
393 );
394 if (operation !== undefined) {
395 yield* waitForDeleteOperation(operation);
396 }
397 yield* waitUntilGone(getByName(output.name), output.name);
398 }),
399 });