1import * as compute from "@distilled.cloud/gcp/compute_v1";
2import {
3 encodeDescription,
4 hasOwnershipMarker,
5 lastSegment,
6 normalizeRegion,
7 parseDescription,
8 runRegionOp,
9 toPhysicalName,
10} from "./internal.ts";
11import * as Data from "effect/Data";
12import * as Effect from "effect/Effect";
13import * as Schedule from "effect/Schedule";
14import * as Stream from "effect/Stream";
15import { Unowned } from "../../AdoptPolicy.ts";
16import { isResolved } from "../../Diff.ts";
17import * as Provider from "../../Provider.ts";
18import { Resource } from "../../Resource.ts";
19import { GcpEnvironment } from "../Environment.ts";
20import { createInternalLabels, hasAlchemyLabels } from "../Labels.ts";
21import type { Providers } from "../Providers.ts";
22
23export type PublicDelegatedPrefixMode =
24 | compute.PublicDelegatedPrefixModeEnum
25 | (string & {});
26
27export type PublicDelegatedPrefixProps = {
28 /**
29 * Prefix name (RFC1035, 1-63 characters). If omitted, a unique name is
30 * generated from the stack, stage, and logical id. Changing it replaces
31 * the prefix.
32 */
33 prefixName?: string;
34 /**
35 * Region the prefix lives in. Immutable — changing it replaces the
36 * prefix. `US-CENTRAL1` is accepted and normalized to `us-central1`.
37 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
38 */
39 region?: string;
40 /**
41 * Parent public advertised prefix or public delegated prefix URL.
42 * Immutable — changing it replaces the prefix.
43 */
44 parentPrefix: string;
45 /**
46 * IP address range in CIDR format delegated from the parent. Immutable
47 * — changing it replaces the prefix.
48 */
49 ipCidrRange: string;
50 /**
51 * IPv6-only mode (`DELEGATION`, `EXTERNAL_IPV6_FORWARDING_RULE_CREATION`,
52 * `EXTERNAL_IPV6_SUBNETWORK_CREATION`, or
53 * `INTERNAL_IPV6_SUBNETWORK_CREATION`). Immutable.
54 */
55 mode?: PublicDelegatedPrefixMode;
56 /**
57 * Allocatable prefix length supported by this PDP. Optional; cannot be
58 * set for prefixes in `DELEGATION` mode or for IPv4 prefixes.
59 */
60 allocatablePrefixLength?: number;
61 /**
62 * If true, the prefix is a live-migration prefix. Immutable.
63 */
64 isLiveMigration?: boolean;
65 /**
66 * Optional description. Public delegated prefixes have no labels field,
67 * so Alchemy ownership is stored in a `[alchemy …]` prefix for `list` /
68 * nuke.
69 */
70 description?: string;
71};
72
73export type PublicDelegatedPrefix = Resource<
74 "GCP.Compute.PublicDelegatedPrefix",
75 PublicDelegatedPrefixProps,
76 {
77 /** Prefix name. */
78 prefixName: string;
79 /** Project id. */
80 project: string;
81 /** Region short name (`us-central1`). */
82 region: string;
83 /** Parent prefix URL. */
84 parentPrefix: string | undefined;
85 /** Delegated CIDR range. */
86 ipCidrRange: string;
87 /** IPv6 mode, if set. */
88 mode: string | undefined;
89 /** Allocatable prefix length. */
90 allocatablePrefixLength: number | undefined;
91 /** Whether this is a live-migration prefix. */
92 isLiveMigration: boolean;
93 /** User description with the Alchemy ownership prefix stripped. */
94 description: string | undefined;
95 /** Provisioning status. */
96 status: string | undefined;
97 /** Child sub-prefixes. */
98 publicDelegatedSubPrefixs: compute.PublicDelegatedPrefixPublicDelegatedSubPrefix[];
99 /** IPv6 access type inherited from the parent. */
100 ipv6AccessType: string | undefined;
101 /** Whether enhanced IPv4 allocations are enabled. */
102 enableEnhancedIpv4Allocation: boolean | undefined;
103 /** BYOIP API version. */
104 byoipApiVersion: string | undefined;
105 /** Optimistic-locking fingerprint. */
106 fingerprint: string | undefined;
107 /** Server-defined URL. */
108 selfLink: string | undefined;
109 /** Server-assigned numeric id. */
110 prefixId: string | undefined;
111 /** RFC3339 creation timestamp. */
112 creationTimestamp: string | undefined;
113 /** Resource kind. */
114 kind: string | undefined;
115 },
116 never,
117 Providers
118>;
119
120/**
121 * A regional public delegated prefix (BYOIP).
122 *
123 * A public delegated prefix is an IP block carved from a public advertised
124 * prefix and scoped to one region (or global). Creating one requires a
125 * parent advertised prefix. Name, region, parent, CIDR, and mode are
126 * immutable. Description and sub-prefixes update in place via
127 * `publicDelegatedPrefixes.patch`.
128 *
129 * ### Creating a Public Delegated Prefix
130 * **Example:** Regional IPv4 sub-prefix
131 * ```typescript
132 * const pap = yield* GCP.Compute.PublicAdvertisedPrefix("Byoip", {
133 * ipCidrRange: "203.0.113.0/24",
134 * pdpScope: "REGIONAL",
135 * });
136 * const pdp = yield* GCP.Compute.PublicDelegatedPrefix("Delegate", {
137 * parentPrefix: pap.selfLink,
138 * ipCidrRange: "203.0.113.0/26",
139 * description: "us-central1 block",
140 * });
141 * ```
142 *
143 * **Example:** Explicit name
144 * ```typescript
145 * const pdp = yield* GCP.Compute.PublicDelegatedPrefix("Delegate", {
146 * prefixName: "lab-pdp",
147 * region: "us-central1",
148 * parentPrefix: pap.selfLink,
149 * ipCidrRange: "203.0.113.0/26",
150 * });
151 * ```
152 *
153 * @resource
154 * @category Compute
155 */
156export const PublicDelegatedPrefix = Resource<PublicDelegatedPrefix>(
157 "GCP.Compute.PublicDelegatedPrefix",
158);
159
160export class PublicDelegatedPrefixNotResolved extends Data.TaggedError(
161 "GCP.Compute.PublicDelegatedPrefixNotResolved",
162)<{
163 prefixName: string;
164 region: string;
165}> {}
166
167const toAttrs = (
168 prefix: compute.PublicDelegatedPrefix,
169 project: string,
170): PublicDelegatedPrefix["Attributes"] => {
171 const parsed = parseDescription(prefix.description);
172 return {
173 prefixName: prefix.name ?? lastSegment(prefix.selfLink),
174 project,
175 region: lastSegment(prefix.region).toLowerCase(),
176 parentPrefix: prefix.parentPrefix,
177 ipCidrRange: prefix.ipCidrRange ?? "",
178 mode: prefix.mode,
179 allocatablePrefixLength: prefix.allocatablePrefixLength,
180 isLiveMigration: prefix.isLiveMigration === true,
181 description: parsed.description,
182 status: prefix.status,
183 publicDelegatedSubPrefixs: prefix.publicDelegatedSubPrefixs ?? [],
184 ipv6AccessType: prefix.ipv6AccessType,
185 enableEnhancedIpv4Allocation: prefix.enableEnhancedIpv4Allocation,
186 byoipApiVersion: prefix.byoipApiVersion,
187 fingerprint: prefix.fingerprint,
188 selfLink: prefix.selfLink,
189 prefixId: prefix.id,
190 creationTimestamp: prefix.creationTimestamp,
191 kind: prefix.kind,
192 };
193};
194
195const getByName = (project: string, region: string, name: string) =>
196 compute
197 .getPublicDelegatedPrefixes({
198 project,
199 region,
200 publicDelegatedPrefix: name,
201 })
202 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
203
204const awaitResource = (project: string, region: string, prefixName: string) =>
205 getByName(project, region, prefixName).pipe(
206 Effect.flatMap((prefix) =>
207 prefix !== undefined
208 ? Effect.succeed(prefix)
209 : Effect.fail(
210 new PublicDelegatedPrefixNotResolved({ prefixName, region }),
211 ),
212 ),
213 Effect.retry({
214 while: (error) =>
215 error._tag === "GCP.Compute.PublicDelegatedPrefixNotResolved",
216 times: 8,
217 schedule: Schedule.spaced("1 second"),
218 }),
219 );
220
221export const PublicDelegatedPrefixProvider = () =>
222 Provider.succeed(PublicDelegatedPrefix, {
223 stables: [
224 "prefixName",
225 "project",
226 "region",
227 "ipCidrRange",
228 "prefixId",
229 "selfLink",
230 "creationTimestamp",
231 ],
232
233 diff: Effect.fn(function* ({ news, olds, output }) {
234 if (!isResolved(news)) return undefined;
235 const env = yield* GcpEnvironment.current;
236 const previousName = olds?.prefixName ?? output?.prefixName;
237 const nextName = news.prefixName ?? previousName;
238 const previousRegion = normalizeRegion(
239 olds?.region ?? output?.region,
240 env.region,
241 );
242 const nextRegion = normalizeRegion(
243 news.region ?? (previousRegion || env.region),
244 env.region,
245 );
246 const nameChanged =
247 previousName !== undefined &&
248 nextName !== undefined &&
249 previousName !== nextName;
250 const regionChanged =
251 previousRegion.length > 0 && previousRegion !== nextRegion;
252 const parentChanged =
253 lastSegment(olds?.parentPrefix ?? output?.parentPrefix) !==
254 lastSegment(news.parentPrefix) &&
255 (olds?.parentPrefix ?? output?.parentPrefix) !== undefined;
256 const cidrChanged =
257 (olds?.ipCidrRange ?? output?.ipCidrRange) !== undefined &&
258 news.ipCidrRange !== (olds?.ipCidrRange ?? output?.ipCidrRange);
259 const modeChanged =
260 news.mode !== undefined &&
261 (olds?.mode ?? output?.mode) !== undefined &&
262 news.mode !== (olds?.mode ?? output?.mode);
263 if (
264 nameChanged ||
265 regionChanged ||
266 parentChanged ||
267 cidrChanged ||
268 modeChanged
269 ) {
270 return {
271 action: "replace" as const,
272 deleteFirst:
273 !nameChanged || nextName === undefined || nextName === previousName,
274 };
275 }
276 return undefined;
277 }),
278
279 read: Effect.fn(function* ({ id, olds, output }) {
280 const env = yield* GcpEnvironment.current;
281 const prefixName = yield* toPhysicalName(
282 id,
283 olds?.prefixName,
284 output?.prefixName,
285 "prefix",
286 );
287 const region = normalizeRegion(
288 olds?.region ?? output?.region,
289 env.region,
290 );
291 const existing = yield* getByName(env.project, region, prefixName);
292 if (existing === undefined) return undefined;
293 const attrs = toAttrs(existing, env.project);
294 const { labels } = parseDescription(existing.description);
295 return (yield* hasAlchemyLabels(id, labels)) ? attrs : Unowned(attrs);
296 }),
297
298 list: () =>
299 Effect.gen(function* () {
300 const env = yield* GcpEnvironment.current;
301 const pages = yield* compute.aggregatedListPublicDelegatedPrefixes
302 .pages({
303 project: env.project,
304 maxResults: 500,
305 returnPartialSuccess: true,
306 })
307 .pipe(
308 Stream.runCollect,
309 Effect.catchTag("NotFound", () => Effect.succeed([] as never[])),
310 );
311 return Array.from(
312 pages as readonly compute.PublicDelegatedPrefixAggregatedList[],
313 ).flatMap((page) =>
314 Object.entries(page.items ?? {}).flatMap(([scope, scoped]) => {
315 if (!scope.startsWith("regions/")) return [];
316 return (scoped?.publicDelegatedPrefixes ?? [])
317 .filter((item) => hasOwnershipMarker(item.description))
318 .map((item) => toAttrs(item, env.project));
319 }),
320 );
321 }),
322
323 reconcile: Effect.fn(function* ({ id, news, output }) {
324 const env = yield* GcpEnvironment.current;
325 const prefixName = yield* toPhysicalName(
326 id,
327 news.prefixName,
328 output?.prefixName,
329 "prefix",
330 );
331 const region = normalizeRegion(news.region ?? output?.region, env.region);
332 const ownership = yield* createInternalLabels(id);
333 const desiredDescription = encodeDescription(ownership, news.description);
334
335 let current = yield* getByName(env.project, region, prefixName);
336
337 if (current === undefined) {
338 yield* runRegionOp(
339 env.project,
340 region,
341 compute.insertPublicDelegatedPrefixes({
342 project: env.project,
343 region,
344 body: {
345 name: prefixName,
346 description: desiredDescription,
347 parentPrefix: news.parentPrefix,
348 ipCidrRange: news.ipCidrRange,
349 mode: news.mode,
350 allocatablePrefixLength: news.allocatablePrefixLength,
351 isLiveMigration: news.isLiveMigration,
352 },
353 }),
354 { ignoreAlreadyExists: true },
355 ).pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
356 current = yield* awaitResource(env.project, region, prefixName);
357 }
358
359 if (current === undefined) {
360 return yield* new PublicDelegatedPrefixNotResolved({
361 prefixName,
362 region,
363 });
364 }
365
366 if ((current.description ?? "") !== desiredDescription) {
367 yield* runRegionOp(
368 env.project,
369 region,
370 compute.patchPublicDelegatedPrefixes({
371 project: env.project,
372 region,
373 publicDelegatedPrefix: prefixName,
374 body: {
375 description: desiredDescription,
376 fingerprint: current.fingerprint,
377 },
378 }),
379 );
380 current =
381 (yield* getByName(env.project, region, prefixName)) ?? current;
382 }
383
384 return toAttrs(current, env.project);
385 }),
386
387 delete: Effect.fn(function* ({ output }) {
388 const env = yield* GcpEnvironment.current;
389 const region = normalizeRegion(output.region, env.region);
390 yield* runRegionOp(
391 env.project,
392 region,
393 compute.deletePublicDelegatedPrefixes({
394 project: env.project,
395 region,
396 publicDelegatedPrefix: output.prefixName,
397 }),
398 { ignoreNotFound: true },
399 ).pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
400 }),
401 });