1import * as networkconnectivity from "@distilled.cloud/gcp/networkconnectivity_v1";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Schedule from "effect/Schedule";
5import * as Stream from "effect/Stream";
6import { Unowned } from "../../AdoptPolicy.ts";
7import { isResolved } from "../../Diff.ts";
8import { createPhysicalName } from "../../PhysicalName.ts";
9import * as Provider from "../../Provider.ts";
10import { Resource } from "../../Resource.ts";
11import { tagRecord } from "../../Tags.ts";
12import { GcpEnvironment } from "../Environment.ts";
13import {
14 createInternalLabels,
15 hasAlchemyLabels,
16 stripInternalLabels,
17 toLabels,
18} from "../Labels.ts";
19import type { Providers } from "../Providers.ts";
20import { waitForOperation } from "./internal.ts";
21
22const DEFAULT_LOCATION = "global";
23const DEFAULT_PRIORITY = 1000;
24const DEFAULT_IP_PROTOCOL = "ALL";
25const MAX_NAME_LENGTH = 63;
26const COMPUTE_SELF_LINK_PREFIX = /^https?:\/\/[^/]+\/compute\/v1\//;
27
28export type PolicyBasedRouteProtocolVersion =
29 | networkconnectivity.FilterProtocolVersionEnum
30 | (string & {});
31
32export type PolicyBasedRouteNextHopOtherRoutes =
33 | networkconnectivity.PolicyBasedRouteNextHopOtherRoutesEnum
34 | (string & {});
35
36export type PolicyBasedRouteFilter = {
37 /**
38 * Internet protocol version this route matches (`IPV4` or `IPV6`).
39 */
40 protocolVersion: PolicyBasedRouteProtocolVersion;
41 /**
42 * L4 protocol this route matches (`TCP`, `UDP`, or `ALL`).
43 * @default "ALL"
44 */
45 ipProtocol?: string;
46 /**
47 * Source CIDR of matching packets. Defaults to `0.0.0.0/0` for IPv4
48 * and `::/0` for IPv6.
49 */
50 srcRange?: string;
51 /**
52 * Destination CIDR of matching packets. Defaults to `0.0.0.0/0` for
53 * IPv4 and `::/0` for IPv6.
54 */
55 destRange?: string;
56};
57
58export type PolicyBasedRouteVirtualMachine = {
59 /**
60 * VM network tags this route installs on. A VM matching ANY tag
61 * receives the route.
62 */
63 tags: string[];
64};
65
66export type PolicyBasedRouteInterconnectAttachment = {
67 /**
68 * Region of interconnect attachments this route applies to. Use
69 * `"all"` to install it on every attachment.
70 */
71 region: string;
72};
73
74export type PolicyBasedRouteWarning = {
75 /** Server warning code (`RESOURCE_NOT_ACTIVE`, …). */
76 code: string | undefined;
77 /** Human-readable warning. */
78 warningMessage: string | undefined;
79};
80
81export type PolicyBasedRouteProps = {
82 /**
83 * Policy-based route id (the `{policyBasedRoute}` segment of
84 * `projects/{project}/locations/global/policyBasedRoutes/{policyBasedRoute}`).
85 * If omitted, a unique RFC1035 name is generated from the stack, stage,
86 * and logical id. Must be 1-63 characters and match
87 * `[a-z]([a-z0-9-]*[a-z0-9])?`. Immutable — changing it replaces the
88 * route.
89 */
90 policyBasedRouteId?: string;
91 /**
92 * VPC network this route applies to. Accepts a name (`default`), a
93 * resource path (`projects/{project}/global/networks/{network}`), or a
94 * Compute self-link. Immutable — changing it replaces the route.
95 */
96 network: string;
97 /**
98 * L4 match filter. Immutable — changing it replaces the route.
99 */
100 filter: PolicyBasedRouteFilter;
101 /**
102 * IP address of a global-access-enabled internal L4 load balancer used
103 * as the next hop. Exactly one of `nextHopIlbIp` or
104 * `nextHopOtherRoutes` must be set. Immutable — changing it replaces
105 * the route.
106 */
107 nextHopIlbIp?: string;
108 /**
109 * Other routes consulted for the next hop. `DEFAULT_ROUTING` skips
110 * remaining policy-based routes and uses the VPC routing tables.
111 * Exactly one of `nextHopIlbIp` or `nextHopOtherRoutes` must be set.
112 * Immutable — changing it replaces the route.
113 */
114 nextHopOtherRoutes?: PolicyBasedRouteNextHopOtherRoutes;
115 /**
116 * VM instances this route applies to, selected by network tags. Omit
117 * together with `interconnectAttachment` to install on every endpoint
118 * in the VPC. Immutable — changing it replaces the route.
119 */
120 virtualMachine?: PolicyBasedRouteVirtualMachine;
121 /**
122 * Interconnect attachments this route applies to. Mutually exclusive
123 * with `virtualMachine`. Immutable — changing it replaces the route.
124 */
125 interconnectAttachment?: PolicyBasedRouteInterconnectAttachment;
126 /**
127 * Priority used to break ties among matching policy-based routes.
128 * Lower wins. Range `1`–`65535`.
129 * @default 1000
130 */
131 priority?: number;
132 /**
133 * Human-readable description. Immutable — changing it replaces the
134 * route (the API has no update method).
135 */
136 description?: string;
137 /**
138 * User labels. Alchemy ownership labels are merged in automatically.
139 * Immutable — changing them replaces the route (the API has no update
140 * method).
141 */
142 labels?: Record<string, string>;
143};
144
145export type PolicyBasedRoute = Resource<
146 "GCP.NetworkConnectivity.PolicyBasedRoute",
147 PolicyBasedRouteProps,
148 {
149 /**
150 * Full resource name
151 * `projects/{project}/locations/global/policyBasedRoutes/{policyBasedRoute}`.
152 */
153 name: string;
154 /** Policy-based route id (last path segment). */
155 policyBasedRouteId: string;
156 /** Project id. */
157 project: string;
158 /** Location id. Policy-based routes are global — always `"global"`. */
159 location: string;
160 /** Fully-qualified VPC network path. */
161 network: string;
162 /** L4 match filter as stored by the API. */
163 filter: PolicyBasedRouteFilter;
164 /** Next-hop ILB IP, if set. */
165 nextHopIlbIp: string | undefined;
166 /** Next-hop other-routes enum, if set. */
167 nextHopOtherRoutes: string | undefined;
168 /** VM tag target, if set. */
169 virtualMachine: PolicyBasedRouteVirtualMachine | undefined;
170 /** Interconnect attachment target, if set. */
171 interconnectAttachment: PolicyBasedRouteInterconnectAttachment | undefined;
172 /** Route priority. */
173 priority: number;
174 /** User-provided description. */
175 description: string | undefined;
176 /** User labels (Alchemy ownership labels stripped). */
177 labels: Record<string, string>;
178 /** Server-reported configuration warnings. */
179 warnings: PolicyBasedRouteWarning[];
180 /** Server-defined fully-qualified URL. */
181 selfLink: string | undefined;
182 /** Resource kind (`networkconnectivity#policyBasedRoute`). */
183 kind: string | undefined;
184 /** RFC3339 creation timestamp. */
185 createTime: string | undefined;
186 /** RFC3339 last-update timestamp. */
187 updateTime: string | undefined;
188 },
189 never,
190 Providers
191>;
192
193/**
194 * A VPC policy-based route.
195 *
196 * Policy-based routes match L4 traffic by source, destination, and
197 * protocol — not just destination IP — and always take precedence over
198 * other route types. They live at `locations/global`. The API has create,
199 * get, list, and delete only, so every user-facing field is immutable and
200 * changing it replaces the resource.
201 *
202 * ### Creating a Policy-Based Route
203 * **Example:** Skip other policy-based routes
204 * ```typescript
205 * const network = yield* GCP.Compute.Network("Vpc", {
206 * autoCreateSubnetworks: false,
207 * });
208 * const route = yield* GCP.NetworkConnectivity.PolicyBasedRoute("Skip", {
209 * network: network.networkName,
210 * filter: { protocolVersion: "IPV4" },
211 * nextHopOtherRoutes: "DEFAULT_ROUTING",
212 * });
213 * ```
214 *
215 * **Example:** Named route with VM tags and an ILB next hop
216 * ```typescript
217 * const route = yield* GCP.NetworkConnectivity.PolicyBasedRoute("Inspect", {
218 * policyBasedRouteId: "app-inspect",
219 * network: network.networkName,
220 * filter: {
221 * protocolVersion: "IPV4",
222 * ipProtocol: "TCP",
223 * srcRange: "10.0.0.0/8",
224 * destRange: "0.0.0.0/0",
225 * },
226 * nextHopIlbIp: "10.10.10.20",
227 * virtualMachine: { tags: ["client"] },
228 * priority: 500,
229 * description: "send tagged VMs to the inspection ILB",
230 * labels: { env: "prod" },
231 * });
232 * ```
233 *
234 * ### Interconnect attachments
235 * **Example:** Apply to every VLAN attachment
236 * ```typescript
237 * const route = yield* GCP.NetworkConnectivity.PolicyBasedRoute("Vlan", {
238 * network: network.networkName,
239 * filter: { protocolVersion: "IPV4" },
240 * nextHopOtherRoutes: "DEFAULT_ROUTING",
241 * interconnectAttachment: { region: "all" },
242 * });
243 * ```
244 *
245 * @resource
246 * @category NetworkConnectivity
247 */
248export const PolicyBasedRoute = Resource<PolicyBasedRoute>(
249 "GCP.NetworkConnectivity.PolicyBasedRoute",
250);
251
252export class PolicyBasedRouteNotResolved extends Data.TaggedError(
253 "GCP.NetworkConnectivity.PolicyBasedRouteNotResolved",
254)<{
255 name: string;
256}> {}
257
258export class PolicyBasedRouteStillExists extends Data.TaggedError(
259 "GCP.NetworkConnectivity.PolicyBasedRouteStillExists",
260)<{
261 name: string;
262}> {}
263
264const lastSegment = (value: string) => {
265 const trimmed = value.replace(/\/+$/, "");
266 const parts = trimmed.split("/");
267 return parts[parts.length - 1] || trimmed;
268};
269
270const rfc1035 = (name: string): string => {
271 let next = name
272 .toLowerCase()
273 .replace(/[^a-z0-9-]/g, "-")
274 .replace(/-+/g, "-")
275 .replace(/^-+/, "")
276 .replace(/-+$/, "");
277 if (!/^[a-z]/.test(next)) {
278 next = `p${next}`;
279 }
280 next = next.slice(0, MAX_NAME_LENGTH).replace(/-+$/, "");
281 return next.length > 0 ? next : "policy-based-route";
282};
283
284const resourceName = (project: string, policyBasedRouteId: string) =>
285 `projects/${project}/locations/${DEFAULT_LOCATION}/policyBasedRoutes/${policyBasedRouteId}`;
286
287const parentOf = (project: string) =>
288 `projects/${project}/locations/${DEFAULT_LOCATION}`;
289
290const parseName = (name: string) => {
291 const parts = name.split("/").filter((part) => part.length > 0);
292 const routesAt = parts.lastIndexOf("policyBasedRoutes");
293 const locationsAt = parts.lastIndexOf("locations");
294 const projectsAt = parts.lastIndexOf("projects");
295 return {
296 project:
297 projectsAt >= 0 && parts[projectsAt + 1] ? parts[projectsAt + 1]! : "",
298 location:
299 locationsAt >= 0 && parts[locationsAt + 1]
300 ? parts[locationsAt + 1]!
301 : DEFAULT_LOCATION,
302 policyBasedRouteId:
303 routesAt >= 0 && parts[routesAt + 1]
304 ? parts[routesAt + 1]!
305 : lastSegment(name),
306 };
307};
308
309const toNetworkUrl = (project: string, network: string) => {
310 const stripped = network
311 .replace(COMPUTE_SELF_LINK_PREFIX, "")
312 .replace(/^\/+/, "");
313 const parts = stripped.split("/").filter((part) => part.length > 0);
314 const name = parts[parts.length - 1] ?? stripped;
315 const projectsAt = parts.lastIndexOf("projects");
316 const owner =
317 projectsAt >= 0 && parts[projectsAt + 1] ? parts[projectsAt + 1]! : project;
318 return `projects/${owner}/global/networks/${name}`;
319};
320
321const sameRef = (left: string | undefined, right: string | undefined) =>
322 lastSegment(left ?? "").toLowerCase() ===
323 lastSegment(right ?? "").toLowerCase();
324
325const protocolVersionOf = (filter: PolicyBasedRouteFilter | undefined) =>
326 (filter?.protocolVersion ?? "IPV4").toUpperCase();
327
328const defaultRangeOf = (filter: PolicyBasedRouteFilter | undefined) =>
329 protocolVersionOf(filter) === "IPV6" ? "::/0" : "0.0.0.0/0";
330
331const normalizeFilter = (filter: PolicyBasedRouteFilter | undefined) => {
332 const fallback = defaultRangeOf(filter);
333 return {
334 protocolVersion: protocolVersionOf(filter),
335 ipProtocol: (filter?.ipProtocol ?? DEFAULT_IP_PROTOCOL).toUpperCase(),
336 srcRange: filter?.srcRange ?? fallback,
337 destRange: filter?.destRange ?? fallback,
338 };
339};
340
341const sameFilter = (
342 left: PolicyBasedRouteFilter | undefined,
343 right: PolicyBasedRouteFilter | undefined,
344) => {
345 const a = normalizeFilter(left);
346 const b = normalizeFilter(right);
347 return (
348 a.protocolVersion === b.protocolVersion &&
349 a.ipProtocol === b.ipProtocol &&
350 a.srcRange === b.srcRange &&
351 a.destRange === b.destRange
352 );
353};
354
355const tagsKey = (tags: readonly string[] | undefined) =>
356 [...(tags ?? [])]
357 .map((tag) => tag.toLowerCase())
358 .sort()
359 .join("\0");
360
361const sameVirtualMachine = (
362 left: PolicyBasedRouteVirtualMachine | undefined,
363 right: PolicyBasedRouteVirtualMachine | undefined,
364) => tagsKey(left?.tags) === tagsKey(right?.tags);
365
366const sameInterconnectAttachment = (
367 left: PolicyBasedRouteInterconnectAttachment | undefined,
368 right: PolicyBasedRouteInterconnectAttachment | undefined,
369) => (left?.region ?? "").toLowerCase() === (right?.region ?? "").toLowerCase();
370
371const labelsKey = (labels: Record<string, string> | undefined) =>
372 Object.entries(toLabels(labels))
373 .sort(([left], [right]) => left.localeCompare(right))
374 .map(([key, value]) => `${key}=${value}`)
375 .join("\n");
376
377const userLabels = (
378 labels: Record<string, string | undefined> | null | undefined,
379): Record<string, string> => stripInternalLabels(tagRecord(labels));
380
381const labelsCovered = (
382 observed: Record<string, string | undefined> | null | undefined,
383 desired: Record<string, string>,
384) => {
385 const current = tagRecord(observed);
386 return Object.entries(desired).every(
387 ([key, value]) => current[key] === value,
388 );
389};
390
391const toId = (
392 id: string,
393 policyBasedRouteId: string | undefined,
394 existing?: string,
395) =>
396 Effect.gen(function* () {
397 if (policyBasedRouteId !== undefined) return policyBasedRouteId;
398 if (existing !== undefined) return existing;
399 return rfc1035(
400 yield* createPhysicalName({
401 id,
402 maxLength: MAX_NAME_LENGTH,
403 lowercase: true,
404 }),
405 );
406 });
407
408const toFilterAttr = (
409 filter: networkconnectivity.Filter | undefined,
410): PolicyBasedRouteFilter => ({
411 protocolVersion: filter?.protocolVersion ?? "IPV4",
412 ipProtocol: filter?.ipProtocol,
413 srcRange: filter?.srcRange,
414 destRange: filter?.destRange,
415});
416
417const toVirtualMachine = (
418 virtualMachine: networkconnectivity.VirtualMachine | undefined,
419): PolicyBasedRouteVirtualMachine | undefined => {
420 if (virtualMachine === undefined) return undefined;
421 return { tags: virtualMachine.tags ?? [] };
422};
423
424const toInterconnectAttachment = (
425 interconnectAttachment:
426 | networkconnectivity.InterconnectAttachment
427 | undefined,
428): PolicyBasedRouteInterconnectAttachment | undefined => {
429 if (interconnectAttachment?.region === undefined) return undefined;
430 return { region: interconnectAttachment.region };
431};
432
433const toWarnings = (
434 warnings: networkconnectivity.WarningsList | undefined,
435): PolicyBasedRouteWarning[] =>
436 (warnings ?? []).map((warning) => ({
437 code: warning.code,
438 warningMessage: warning.warningMessage,
439 }));
440
441const toAttrs = (
442 route: networkconnectivity.PolicyBasedRoute,
443 project: string,
444) => {
445 const name = route.name ?? "";
446 const parsed = parseName(name);
447 return {
448 name,
449 policyBasedRouteId: parsed.policyBasedRouteId,
450 project: parsed.project || project,
451 location: parsed.location || DEFAULT_LOCATION,
452 network: route.network ?? "",
453 filter: toFilterAttr(route.filter),
454 nextHopIlbIp: route.nextHopIlbIp,
455 nextHopOtherRoutes: route.nextHopOtherRoutes,
456 virtualMachine: toVirtualMachine(route.virtualMachine),
457 interconnectAttachment: toInterconnectAttachment(
458 route.interconnectAttachment,
459 ),
460 priority: route.priority ?? DEFAULT_PRIORITY,
461 description: route.description,
462 labels: userLabels(route.labels),
463 warnings: toWarnings(route.warnings),
464 selfLink: route.selfLink,
465 kind: route.kind,
466 createTime: route.createTime,
467 updateTime: route.updateTime,
468 };
469};
470
471const toBody = (
472 project: string,
473 news: PolicyBasedRouteProps,
474 labels: Record<string, string>,
475): networkconnectivity.PolicyBasedRoute => {
476 const body: networkconnectivity.PolicyBasedRoute = {
477 network: toNetworkUrl(project, news.network),
478 filter: {
479 protocolVersion: news.filter.protocolVersion,
480 ipProtocol: news.filter.ipProtocol,
481 srcRange: news.filter.srcRange,
482 destRange: news.filter.destRange,
483 },
484 priority: news.priority ?? DEFAULT_PRIORITY,
485 description: news.description,
486 labels,
487 };
488 if (news.nextHopIlbIp !== undefined) {
489 body.nextHopIlbIp = news.nextHopIlbIp;
490 }
491 if (news.nextHopOtherRoutes !== undefined) {
492 body.nextHopOtherRoutes = news.nextHopOtherRoutes;
493 }
494 if (news.virtualMachine !== undefined) {
495 body.virtualMachine = { tags: news.virtualMachine.tags };
496 }
497 if (news.interconnectAttachment !== undefined) {
498 body.interconnectAttachment = {
499 region: news.interconnectAttachment.region,
500 };
501 }
502 return body;
503};
504
505const getByName = (name: string) =>
506 networkconnectivity
507 .getProjectsLocationsGlobalPolicyBasedRoutes({ name })
508 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
509
510const waitUntilExists = (name: string) =>
511 getByName(name).pipe(
512 Effect.flatMap((route) =>
513 route
514 ? Effect.succeed(route)
515 : Effect.fail(new PolicyBasedRouteNotResolved({ name })),
516 ),
517 Effect.retry({
518 while: (error) =>
519 error._tag === "GCP.NetworkConnectivity.PolicyBasedRouteNotResolved",
520 times: 8,
521 schedule: Schedule.spaced("1 second"),
522 }),
523 );
524
525const waitUntilGone = (name: string) =>
526 getByName(name).pipe(
527 Effect.flatMap((route) =>
528 route === undefined
529 ? Effect.void
530 : Effect.fail(new PolicyBasedRouteStillExists({ name })),
531 ),
532 Effect.retry({
533 while: (error) =>
534 error._tag === "GCP.NetworkConnectivity.PolicyBasedRouteStillExists",
535 times: 10,
536 schedule: Schedule.spaced("1 second"),
537 }),
538 );
539
540const removeRoute = (name: string) =>
541 Effect.gen(function* () {
542 const operation = yield* networkconnectivity
543 .deleteProjectsLocationsGlobalPolicyBasedRoutes({ name })
544 .pipe(
545 Effect.retry({
546 while: (error) => error._tag === "Conflict",
547 times: 8,
548 schedule: Schedule.spaced("2 seconds"),
549 }),
550 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
551 );
552 if (operation !== undefined) {
553 yield* waitForOperation(operation, { notFoundOk: true });
554 }
555 yield* waitUntilGone(name);
556 });
557
558const listOwnedRoutes = (project: string) =>
559 networkconnectivity.listProjectsLocationsGlobalPolicyBasedRoutes
560 .pages({
561 parent: parentOf(project),
562 pageSize: 1000,
563 })
564 .pipe(
565 Stream.flatMap((page) =>
566 Stream.fromIterable(page.policyBasedRoutes ?? []),
567 ),
568 Stream.filter((route) =>
569 Object.keys(route.labels ?? {}).some((key) =>
570 key.startsWith("alchemy-"),
571 ),
572 ),
573 Stream.map((route) => toAttrs(route, project)),
574 Stream.runCollect,
575 Effect.map((chunk) => Array.from(chunk)),
576 Effect.catchTag("NotFound", () => Effect.succeed([])),
577 );
578
579const matchesDesired = (
580 route: networkconnectivity.PolicyBasedRoute,
581 project: string,
582 news: PolicyBasedRouteProps,
583 desiredLabels: Record<string, string>,
584) => {
585 if (!sameRef(route.network, toNetworkUrl(project, news.network))) {
586 return false;
587 }
588 if (!sameFilter(toFilterAttr(route.filter), news.filter)) {
589 return false;
590 }
591 if ((route.nextHopIlbIp ?? "") !== (news.nextHopIlbIp ?? "")) {
592 return false;
593 }
594 if ((route.nextHopOtherRoutes ?? "") !== (news.nextHopOtherRoutes ?? "")) {
595 return false;
596 }
597 if (
598 !sameVirtualMachine(
599 toVirtualMachine(route.virtualMachine),
600 news.virtualMachine,
601 )
602 ) {
603 return false;
604 }
605 if (
606 !sameInterconnectAttachment(
607 toInterconnectAttachment(route.interconnectAttachment),
608 news.interconnectAttachment,
609 )
610 ) {
611 return false;
612 }
613 if (
614 (route.priority ?? DEFAULT_PRIORITY) !== (news.priority ?? DEFAULT_PRIORITY)
615 ) {
616 return false;
617 }
618 if ((route.description ?? "") !== (news.description ?? "")) {
619 return false;
620 }
621 return labelsCovered(route.labels, desiredLabels);
622};
623
624const immutableChanged = (
625 news: PolicyBasedRouteProps,
626 olds: Partial<PolicyBasedRouteProps> | undefined,
627 output: PolicyBasedRoute["Attributes"] | undefined,
628) => {
629 const previousNetwork = olds?.network ?? output?.network;
630 if (
631 previousNetwork !== undefined &&
632 !sameRef(news.network, previousNetwork)
633 ) {
634 return true;
635 }
636 const previousFilter = olds?.filter ?? output?.filter;
637 if (
638 previousFilter !== undefined &&
639 !sameFilter(news.filter, previousFilter)
640 ) {
641 return true;
642 }
643 const previousIlb = olds?.nextHopIlbIp ?? output?.nextHopIlbIp ?? "";
644 if ((news.nextHopIlbIp ?? "") !== previousIlb) {
645 return true;
646 }
647 const previousOther =
648 olds?.nextHopOtherRoutes ?? output?.nextHopOtherRoutes ?? "";
649 if ((news.nextHopOtherRoutes ?? "") !== previousOther) {
650 return true;
651 }
652 if (
653 !sameVirtualMachine(
654 news.virtualMachine,
655 olds?.virtualMachine ?? output?.virtualMachine,
656 )
657 ) {
658 return true;
659 }
660 if (
661 !sameInterconnectAttachment(
662 news.interconnectAttachment,
663 olds?.interconnectAttachment ?? output?.interconnectAttachment,
664 )
665 ) {
666 return true;
667 }
668 const previousPriority =
669 olds?.priority ?? output?.priority ?? DEFAULT_PRIORITY;
670 if ((news.priority ?? DEFAULT_PRIORITY) !== previousPriority) {
671 return true;
672 }
673 const previousDescription = olds?.description ?? output?.description ?? "";
674 if ((news.description ?? "") !== previousDescription) {
675 return true;
676 }
677 if (labelsKey(news.labels) !== labelsKey(olds?.labels ?? output?.labels)) {
678 return true;
679 }
680 return false;
681};
682
683export const PolicyBasedRouteProvider = () =>
684 Provider.succeed(PolicyBasedRoute, {
685 stables: [
686 "name",
687 "policyBasedRouteId",
688 "project",
689 "location",
690 "createTime",
691 "selfLink",
692 ],
693
694 diff: Effect.fn(function* ({ news, olds, output }) {
695 if (!isResolved(news)) return undefined;
696 const previousId = olds?.policyBasedRouteId ?? output?.policyBasedRouteId;
697 const nextId = news.policyBasedRouteId ?? previousId;
698 if (previousId === undefined && output === undefined) {
699 return undefined;
700 }
701 const idChanged =
702 previousId !== undefined &&
703 nextId !== undefined &&
704 nextId !== previousId;
705 if (idChanged || immutableChanged(news, olds, output)) {
706 return {
707 action: "replace" as const,
708 deleteFirst: nextId !== undefined && nextId === previousId,
709 };
710 }
711 return undefined;
712 }),
713
714 read: Effect.fn(function* ({ id, olds, output }) {
715 const env = yield* GcpEnvironment.current;
716 const policyBasedRouteId = yield* toId(
717 id,
718 olds?.policyBasedRouteId,
719 output?.policyBasedRouteId,
720 );
721 const name =
722 output?.name ?? resourceName(env.project, policyBasedRouteId);
723 const existing = yield* getByName(name);
724 if (existing === undefined) return undefined;
725 const attrs = toAttrs(existing, env.project);
726 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
727 ? attrs
728 : Unowned(attrs);
729 }),
730
731 list: () =>
732 Effect.gen(function* () {
733 const env = yield* GcpEnvironment.current;
734 return yield* listOwnedRoutes(env.project);
735 }),
736
737 reconcile: Effect.fn(function* ({ id, news, output }) {
738 const env = yield* GcpEnvironment.current;
739 const policyBasedRouteId = yield* toId(
740 id,
741 news.policyBasedRouteId,
742 output?.policyBasedRouteId,
743 );
744 const name =
745 output?.name ?? resourceName(env.project, policyBasedRouteId);
746 const desiredLabels = {
747 ...toLabels(news.labels),
748 ...(yield* createInternalLabels(id)),
749 };
750
751 let current = yield* getByName(name);
752
753 if (
754 current !== undefined &&
755 !matchesDesired(current, env.project, news, desiredLabels)
756 ) {
757 yield* removeRoute(current.name ?? name);
758 current = undefined;
759 }
760
761 if (current === undefined) {
762 const created = yield* networkconnectivity
763 .createProjectsLocationsGlobalPolicyBasedRoutes({
764 parent: parentOf(env.project),
765 policyBasedRouteId,
766 body: toBody(env.project, news, desiredLabels),
767 })
768 .pipe(
769 Effect.retry({
770 while: (error) => error._tag === "Conflict",
771 times: 5,
772 schedule: Schedule.spaced("2 seconds"),
773 }),
774 Effect.catchTag("Conflict", () => Effect.succeed(undefined)),
775 );
776 if (created !== undefined) {
777 yield* waitForOperation(created);
778 }
779 current = yield* waitUntilExists(name);
780 }
781
782 if (current === undefined) {
783 return yield* new PolicyBasedRouteNotResolved({ name });
784 }
785
786 return toAttrs(current, env.project);
787 }),
788
789 delete: Effect.fn(function* ({ output }) {
790 if (!output.name) return;
791 yield* removeRoute(output.name);
792 }),
793 });