1import * as compute from "@distilled.cloud/gcp/compute_v1";
2import {
3 encodeDescription,
4 hasOwnershipMarker,
5 lastSegment,
6 parseDescription,
7 runGlobalOp,
8 toPhysicalName,
9} from "./internal.ts";
10import * as Data from "effect/Data";
11import * as Effect from "effect/Effect";
12import * as Schedule from "effect/Schedule";
13import * as Stream from "effect/Stream";
14import { Unowned } from "../../AdoptPolicy.ts";
15import { isResolved } from "../../Diff.ts";
16import * as Provider from "../../Provider.ts";
17import { Resource } from "../../Resource.ts";
18import { GcpEnvironment } from "../Environment.ts";
19import { createInternalLabels, hasAlchemyLabels } from "../Labels.ts";
20import type { Providers } from "../Providers.ts";
21
22export type PublicAdvertisedPrefixPdpScope =
23 | compute.PublicAdvertisedPrefixPdpScopeEnum
24 | (string & {});
25export type PublicAdvertisedPrefixIpv6AccessType =
26 | compute.PublicAdvertisedPrefixIpv6AccessTypeEnum
27 | (string & {});
28
29export type PublicAdvertisedPrefixProps = {
30 /**
31 * Prefix name (RFC1035, 1-63 characters). If omitted, a unique name is
32 * generated from the stack, stage, and logical id. Changing it replaces
33 * the prefix.
34 */
35 prefixName?: string;
36 /**
37 * The address range in CIDR format represented by this public advertised
38 * prefix. Immutable — changing it replaces the prefix.
39 */
40 ipCidrRange: string;
41 /**
42 * Address used for reverse DNS verification. Immutable — changing it
43 * replaces the prefix.
44 */
45 dnsVerificationIp?: string;
46 /**
47 * How child public delegated prefixes are scoped (`REGIONAL` or
48 * `GLOBAL`). Immutable — changing it replaces the prefix.
49 */
50 pdpScope?: PublicAdvertisedPrefixPdpScope;
51 /**
52 * Internet access type for IPv6 prefixes (`EXTERNAL` or `INTERNAL`).
53 * Immutable — changing it replaces the prefix.
54 */
55 ipv6AccessType?: PublicAdvertisedPrefixIpv6AccessType;
56 /**
57 * Optional description. Public advertised prefixes have no labels
58 * field, so Alchemy ownership is stored in a `[alchemy …]` prefix for
59 * `list` / nuke.
60 */
61 description?: string;
62};
63
64export type PublicAdvertisedPrefix = Resource<
65 "GCP.Compute.PublicAdvertisedPrefix",
66 PublicAdvertisedPrefixProps,
67 {
68 /** Prefix name. */
69 prefixName: string;
70 /** Project id. */
71 project: string;
72 /** Advertised CIDR range. */
73 ipCidrRange: string;
74 /** Reverse-DNS verification address. */
75 dnsVerificationIp: string | undefined;
76 /** Child PDP scope. */
77 pdpScope: string | undefined;
78 /** IPv6 access type. */
79 ipv6AccessType: string | undefined;
80 /** User description with the Alchemy ownership prefix stripped. */
81 description: string | undefined;
82 /** Provisioning status. */
83 status: string | undefined;
84 /** Shared secret for reverse DNS verification. */
85 sharedSecret: string | undefined;
86 /** Child public delegated prefixes. */
87 publicDelegatedPrefixs: compute.PublicAdvertisedPrefixPublicDelegatedPrefix[];
88 /** BYOIP API version. */
89 byoipApiVersion: string | undefined;
90 /** Optimistic-locking fingerprint. */
91 fingerprint: string | undefined;
92 /** Server-defined URL. */
93 selfLink: string | undefined;
94 /** Server-assigned numeric id. */
95 prefixId: string | undefined;
96 /** RFC3339 creation timestamp. */
97 creationTimestamp: string | undefined;
98 /** Resource kind. */
99 kind: string | undefined;
100 },
101 never,
102 Providers
103>;
104
105/**
106 * A global public advertised prefix (BYOIP).
107 *
108 * A public advertised prefix is an aggregated IP prefix you bring to
109 * Google Cloud. Creating one requires a prefix you own and reverse-DNS
110 * verification. Name, CIDR, verification IP, and PDP scope are immutable.
111 * Description updates in place via `publicAdvertisedPrefixes.patch`.
112 *
113 * ### Creating a Public Advertised Prefix
114 * **Example:** Regional-scope IPv4 prefix
115 * ```typescript
116 * const prefix = yield* GCP.Compute.PublicAdvertisedPrefix("Byoip", {
117 * ipCidrRange: "203.0.113.0/24",
118 * dnsVerificationIp: "203.0.113.1",
119 * pdpScope: "REGIONAL",
120 * description: "lab prefix",
121 * });
122 * ```
123 *
124 * **Example:** Explicit name
125 * ```typescript
126 * const prefix = yield* GCP.Compute.PublicAdvertisedPrefix("Byoip", {
127 * prefixName: "lab-pap",
128 * ipCidrRange: "203.0.113.0/24",
129 * pdpScope: "REGIONAL",
130 * });
131 * ```
132 *
133 * @resource
134 * @category Compute
135 */
136export const PublicAdvertisedPrefix = Resource<PublicAdvertisedPrefix>(
137 "GCP.Compute.PublicAdvertisedPrefix",
138);
139
140export class PublicAdvertisedPrefixNotResolved extends Data.TaggedError(
141 "GCP.Compute.PublicAdvertisedPrefixNotResolved",
142)<{
143 prefixName: string;
144}> {}
145
146const toAttrs = (
147 prefix: compute.PublicAdvertisedPrefix,
148 project: string,
149): PublicAdvertisedPrefix["Attributes"] => {
150 const parsed = parseDescription(prefix.description);
151 return {
152 prefixName: prefix.name ?? lastSegment(prefix.selfLink),
153 project,
154 ipCidrRange: prefix.ipCidrRange ?? "",
155 dnsVerificationIp: prefix.dnsVerificationIp,
156 pdpScope: prefix.pdpScope,
157 ipv6AccessType: prefix.ipv6AccessType,
158 description: parsed.description,
159 status: prefix.status,
160 sharedSecret: prefix.sharedSecret,
161 publicDelegatedPrefixs: prefix.publicDelegatedPrefixs ?? [],
162 byoipApiVersion: prefix.byoipApiVersion,
163 fingerprint: prefix.fingerprint,
164 selfLink: prefix.selfLink,
165 prefixId: prefix.id,
166 creationTimestamp: prefix.creationTimestamp,
167 kind: prefix.kind,
168 };
169};
170
171const getByName = (project: string, publicAdvertisedPrefix: string) =>
172 compute
173 .getPublicAdvertisedPrefixes({ project, publicAdvertisedPrefix })
174 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
175
176const awaitResource = (project: string, prefixName: string) =>
177 getByName(project, prefixName).pipe(
178 Effect.flatMap((prefix) =>
179 prefix !== undefined
180 ? Effect.succeed(prefix)
181 : Effect.fail(new PublicAdvertisedPrefixNotResolved({ prefixName })),
182 ),
183 Effect.retry({
184 while: (error) =>
185 error._tag === "GCP.Compute.PublicAdvertisedPrefixNotResolved",
186 times: 8,
187 schedule: Schedule.spaced("1 second"),
188 }),
189 );
190
191export const PublicAdvertisedPrefixProvider = () =>
192 Provider.succeed(PublicAdvertisedPrefix, {
193 stables: [
194 "prefixName",
195 "project",
196 "ipCidrRange",
197 "prefixId",
198 "selfLink",
199 "creationTimestamp",
200 ],
201
202 diff: Effect.fn(function* ({ news, olds, output }) {
203 if (!isResolved(news)) return undefined;
204 const previousName = olds?.prefixName ?? output?.prefixName;
205 const nextName = news.prefixName ?? previousName;
206 const nameChanged =
207 previousName !== undefined &&
208 nextName !== undefined &&
209 previousName !== nextName;
210 const cidrChanged =
211 (olds?.ipCidrRange ?? output?.ipCidrRange) !== undefined &&
212 news.ipCidrRange !== (olds?.ipCidrRange ?? output?.ipCidrRange);
213 const dnsChanged =
214 news.dnsVerificationIp !== undefined &&
215 (olds?.dnsVerificationIp ?? output?.dnsVerificationIp) !== undefined &&
216 news.dnsVerificationIp !==
217 (olds?.dnsVerificationIp ?? output?.dnsVerificationIp);
218 const scopeChanged =
219 news.pdpScope !== undefined &&
220 (olds?.pdpScope ?? output?.pdpScope) !== undefined &&
221 news.pdpScope !== (olds?.pdpScope ?? output?.pdpScope);
222 if (nameChanged || cidrChanged || dnsChanged || scopeChanged) {
223 return {
224 action: "replace" as const,
225 deleteFirst:
226 !nameChanged || nextName === undefined || nextName === previousName,
227 };
228 }
229 return undefined;
230 }),
231
232 read: Effect.fn(function* ({ id, olds, output }) {
233 const env = yield* GcpEnvironment.current;
234 const prefixName = yield* toPhysicalName(
235 id,
236 olds?.prefixName,
237 output?.prefixName,
238 "prefix",
239 );
240 const existing = yield* getByName(env.project, prefixName);
241 if (existing === undefined) return undefined;
242 const attrs = toAttrs(existing, env.project);
243 const { labels } = parseDescription(existing.description);
244 return (yield* hasAlchemyLabels(id, labels)) ? attrs : Unowned(attrs);
245 }),
246
247 list: () =>
248 Effect.gen(function* () {
249 const env = yield* GcpEnvironment.current;
250 return yield* compute.listPublicAdvertisedPrefixes
251 .items({
252 project: env.project,
253 maxResults: 500,
254 returnPartialSuccess: true,
255 })
256 .pipe(
257 Stream.filter((item) => hasOwnershipMarker(item.description)),
258 Stream.map((item) => toAttrs(item, env.project)),
259 Stream.runCollect,
260 Effect.map((chunk) => Array.from(chunk)),
261 Effect.catchTag("NotFound", () =>
262 Effect.succeed([] as PublicAdvertisedPrefix["Attributes"][]),
263 ),
264 );
265 }),
266
267 reconcile: Effect.fn(function* ({ id, news, output }) {
268 const env = yield* GcpEnvironment.current;
269 const prefixName = yield* toPhysicalName(
270 id,
271 news.prefixName,
272 output?.prefixName,
273 "prefix",
274 );
275 const ownership = yield* createInternalLabels(id);
276 const desiredDescription = encodeDescription(ownership, news.description);
277
278 let current = yield* getByName(env.project, prefixName);
279
280 if (current === undefined) {
281 yield* runGlobalOp(
282 env.project,
283 compute.insertPublicAdvertisedPrefixes({
284 project: env.project,
285 body: {
286 name: prefixName,
287 description: desiredDescription,
288 ipCidrRange: news.ipCidrRange,
289 dnsVerificationIp: news.dnsVerificationIp,
290 pdpScope: news.pdpScope,
291 ipv6AccessType: news.ipv6AccessType,
292 },
293 }),
294 { ignoreAlreadyExists: true },
295 ).pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
296 current = yield* awaitResource(env.project, prefixName);
297 }
298
299 if (current === undefined) {
300 return yield* new PublicAdvertisedPrefixNotResolved({ prefixName });
301 }
302
303 if ((current.description ?? "") !== desiredDescription) {
304 yield* runGlobalOp(
305 env.project,
306 compute.patchPublicAdvertisedPrefixes({
307 project: env.project,
308 publicAdvertisedPrefix: prefixName,
309 body: {
310 description: desiredDescription,
311 fingerprint: current.fingerprint,
312 },
313 }),
314 );
315 current = (yield* getByName(env.project, prefixName)) ?? current;
316 }
317
318 return toAttrs(current, env.project);
319 }),
320
321 delete: Effect.fn(function* ({ output }) {
322 const env = yield* GcpEnvironment.current;
323 yield* runGlobalOp(
324 env.project,
325 compute.deletePublicAdvertisedPrefixes({
326 project: env.project,
327 publicAdvertisedPrefix: output.prefixName,
328 }),
329 { ignoreNotFound: true },
330 ).pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
331 }),
332 });