1import * as compute from "@distilled.cloud/gcp/compute_v1";
2import { waitRegionOperation } from "./operations.ts";
3import * as Data from "effect/Data";
4import * as Effect from "effect/Effect";
5import * as Schedule from "effect/Schedule";
6import * as Stream from "effect/Stream";
7import { Unowned } from "../../AdoptPolicy.ts";
8import { isResolved } from "../../Diff.ts";
9import { createPhysicalName } from "../../PhysicalName.ts";
10import * as Provider from "../../Provider.ts";
11import { Resource } from "../../Resource.ts";
12import { tagRecord } from "../../Tags.ts";
13import { GcpEnvironment } from "../Environment.ts";
14import {
15 alchemyLabelKeys,
16 createInternalLabels,
17 hasAlchemyLabels,
18} from "../Labels.ts";
19import type { Providers } from "../Providers.ts";
20
21const DEFAULT_NETWORK_ENDPOINT_TYPE = "SERVERLESS";
22const MAX_NAME_LENGTH = 63;
23
24export type RegionNetworkEndpointGroupType =
25 | "GCE_VM_IP"
26 | "GCE_VM_IP_PORT"
27 | "GCE_VM_IP_PORTMAP"
28 | "INTERNET_FQDN_PORT"
29 | "INTERNET_IP_PORT"
30 | "NON_GCP_PRIVATE_IP_PORT"
31 | "PRIVATE_SERVICE_CONNECT"
32 | "SERVERLESS";
33
34export type RegionNetworkEndpointGroupCloudRun = {
35 /** Cloud Run service name (RFC1035). Provide this or `urlMask`. */
36 service?: string;
37 /** Named Cloud Run revision tag. */
38 tag?: string;
39 /** URL mask used to parse `service` and `tag` from request URLs. */
40 urlMask?: string;
41};
42
43export type RegionNetworkEndpointGroupAppEngine = {
44 /** App Engine service name. */
45 service?: string;
46 /** App Engine version. */
47 version?: string;
48 /** URL mask used to parse `service` and `version` from request URLs. */
49 urlMask?: string;
50};
51
52export type RegionNetworkEndpointGroupCloudFunction = {
53 /** Cloud Function name. Provide this or `urlMask`. */
54 function?: string;
55 /** URL mask used to parse the function name from request URLs. */
56 urlMask?: string;
57};
58
59export type RegionNetworkEndpointGroupPscData = {
60 /**
61 * Producer port used to connect a PSC NEG to a specific port on the
62 * producer side. Only valid for `PRIVATE_SERVICE_CONNECT`.
63 */
64 producerPort?: number;
65 /** Address allocated from the given subnetwork for PSC. */
66 consumerPscAddress?: string;
67 /** PSC connection id. */
68 pscConnectionId?: string;
69 /** PSC forwarding-rule connection status. */
70 pscConnectionStatus?: string;
71};
72
73export type RegionNetworkEndpointGroupProps = {
74 /**
75 * NEG name. If omitted, a unique RFC1035 name is generated from the
76 * stack, stage, and logical id. Immutable — changing it replaces the
77 * group.
78 */
79 networkEndpointGroupName?: string;
80 /**
81 * Region the NEG lives in. Immutable — changing it replaces the group.
82 * `US-CENTRAL1` is accepted and normalized to `us-central1`.
83 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
84 */
85 region?: string;
86 /**
87 * Optional description. Compute region NEGs have no labels field and no
88 * update API, so Alchemy ownership (`alchemy-stack` / `alchemy-stage` /
89 * `alchemy-id`) is stored in a `[alchemy …]` prefix for `list` / nuke.
90 * Immutable — changing the user-facing description replaces the group.
91 */
92 description?: string;
93 /**
94 * Endpoint type. Inferred from `cloudRun` / `appEngine` /
95 * `cloudFunction` / `pscTargetService` when omitted.
96 * @default "SERVERLESS"
97 */
98 networkEndpointType?: RegionNetworkEndpointGroupType | (string & {});
99 /**
100 * Default port when an endpoint omits one. Must not be set for
101 * `SERVERLESS` or `PRIVATE_SERVICE_CONNECT`. Immutable.
102 */
103 defaultPort?: number;
104 /**
105 * VPC network URL or name. Required for internet NEGs. Must not be set
106 * for `SERVERLESS` or `PRIVATE_SERVICE_CONNECT` (except some PSC
107 * producer setups). Immutable.
108 */
109 network?: string;
110 /**
111 * Subnetwork URL or name. Immutable.
112 */
113 subnetwork?: string;
114 /**
115 * User annotations. Immutable — changing them replaces the group.
116 */
117 annotations?: Record<string, string>;
118 /**
119 * Cloud Run serverless config. Only valid when
120 * `networkEndpointType` is `SERVERLESS`. Immutable.
121 */
122 cloudRun?: RegionNetworkEndpointGroupCloudRun;
123 /**
124 * App Engine serverless config. Only valid when
125 * `networkEndpointType` is `SERVERLESS`. Immutable.
126 */
127 appEngine?: RegionNetworkEndpointGroupAppEngine;
128 /**
129 * Cloud Function serverless config. Only valid when
130 * `networkEndpointType` is `SERVERLESS`. Immutable.
131 */
132 cloudFunction?: RegionNetworkEndpointGroupCloudFunction;
133 /**
134 * Target service URL for a Private Service Connect NEG (Google API or
135 * producer service attachment). Immutable.
136 */
137 pscTargetService?: string;
138 /**
139 * PSC-specific settings (`producerPort`). Immutable.
140 */
141 pscData?: RegionNetworkEndpointGroupPscData;
142};
143
144export type RegionNetworkEndpointGroup = Resource<
145 "GCP.Compute.RegionNetworkEndpointGroup",
146 RegionNetworkEndpointGroupProps,
147 {
148 /** NEG name. */
149 networkEndpointGroupName: string;
150 /** Project id. */
151 project: string;
152 /** Region short name (`us-central1`). */
153 region: string;
154 /** Endpoint type. */
155 networkEndpointType: string | undefined;
156 /** User description with the Alchemy ownership prefix stripped. */
157 description: string | undefined;
158 /** Default port, if any. */
159 defaultPort: number | undefined;
160 /** Network URL, if any. */
161 network: string | undefined;
162 /** Subnetwork URL, if any. */
163 subnetwork: string | undefined;
164 /** User annotations. */
165 annotations: Record<string, string>;
166 /** Cloud Run config, if this is a Cloud Run serverless NEG. */
167 cloudRun: RegionNetworkEndpointGroupCloudRun | undefined;
168 /** App Engine config, if this is an App Engine serverless NEG. */
169 appEngine: RegionNetworkEndpointGroupAppEngine | undefined;
170 /** Cloud Function config, if this is a Cloud Function serverless NEG. */
171 cloudFunction: RegionNetworkEndpointGroupCloudFunction | undefined;
172 /** PSC target service, if any. */
173 pscTargetService: string | undefined;
174 /** PSC connection data, if any. */
175 pscData: RegionNetworkEndpointGroupPscData | undefined;
176 /** Number of endpoints in the group. */
177 size: number | undefined;
178 /** Server-assigned numeric id. */
179 networkEndpointGroupId: string | undefined;
180 /** Resource self-link. */
181 selfLink: string | undefined;
182 /** RFC3339 creation timestamp. */
183 creationTimestamp: string | undefined;
184 /** Resource kind. */
185 kind: string | undefined;
186 },
187 never,
188 Providers
189>;
190
191/**
192 * A regional Compute Engine network endpoint group.
193 *
194 * Regional NEGs back serverless (Cloud Run, App Engine, Cloud Functions),
195 * internet (`INTERNET_IP_PORT` / `INTERNET_FQDN_PORT`), and Private Service
196 * Connect load-balancing backends. The regional collection has no labels
197 * field and no update API — Alchemy stamps ownership into the description
198 * so `list` / nuke can find leaked groups. Name, region, type, network,
199 * serverless config, PSC target, port, annotations, and description are
200 * all immutable (changing any of them replaces the group).
201 *
202 * ### Creating a RegionNetworkEndpointGroup
203 * **Example:** Cloud Run serverless NEG with a URL mask
204 * ```typescript
205 * const neg = yield* GCP.Compute.RegionNetworkEndpointGroup("RunNeg", {
206 * region: "us-central1",
207 * networkEndpointType: "SERVERLESS",
208 * cloudRun: { urlMask: "<service>" },
209 * });
210 * ```
211 *
212 * **Example:** Cloud Run service backend
213 * ```typescript
214 * const neg = yield* GCP.Compute.RegionNetworkEndpointGroup("RunNeg", {
215 * cloudRun: { service: "api" },
216 * });
217 * ```
218 *
219 * ### Internet NEGs
220 * **Example:** Regional internet IP:port NEG
221 * ```typescript
222 * const neg = yield* GCP.Compute.RegionNetworkEndpointGroup("Internet", {
223 * network: "default",
224 * networkEndpointType: "INTERNET_IP_PORT",
225 * defaultPort: 443,
226 * });
227 * ```
228 *
229 * ### Private Service Connect
230 * **Example:** PSC NEG targeting a Google API
231 * ```typescript
232 * const neg = yield* GCP.Compute.RegionNetworkEndpointGroup("Kms", {
233 * networkEndpointType: "PRIVATE_SERVICE_CONNECT",
234 * pscTargetService: "us-central1-cloudkms.googleapis.com",
235 * subnetwork:
236 * "projects/{project}/regions/us-central1/subnetworks/default",
237 * });
238 * ```
239 *
240 * @resource
241 * @category Compute
242 */
243export const RegionNetworkEndpointGroup = Resource<RegionNetworkEndpointGroup>(
244 "GCP.Compute.RegionNetworkEndpointGroup",
245);
246
247export class RegionNetworkEndpointGroupNotResolved extends Data.TaggedError(
248 "GCP.Compute.RegionNetworkEndpointGroupNotResolved",
249)<{
250 networkEndpointGroupName: string;
251 region: string;
252}> {}
253
254export class RegionNetworkEndpointGroupPending extends Data.TaggedError(
255 "GCP.Compute.RegionNetworkEndpointGroupPending",
256)<{
257 networkEndpointGroupName: string;
258 status: string;
259}> {}
260
261const lastSegment = (value: string) => {
262 const trimmed = value.replace(/\/+$/, "");
263 const parts = trimmed.split("/");
264 return parts[parts.length - 1] || trimmed;
265};
266
267const normalizeRegion = (region: string | undefined, defaultRegion: string) =>
268 lastSegment(region ?? defaultRegion).toLowerCase();
269
270const resourceRefOf = (value: string | undefined) => {
271 if (!value) return "";
272 return lastSegment(value);
273};
274
275const networkUrl = (project: string, network: string) => {
276 if (network.includes("/")) {
277 return network.startsWith("projects/") || network.startsWith("http")
278 ? network
279 : `projects/${project}/${network.replace(/^\//, "")}`;
280 }
281 return `projects/${project}/global/networks/${network}`;
282};
283
284const subnetworkUrl = (project: string, region: string, subnetwork: string) => {
285 if (subnetwork.includes("/")) {
286 return subnetwork.startsWith("projects/") || subnetwork.startsWith("http")
287 ? subnetwork
288 : `projects/${project}/${subnetwork.replace(/^\//, "")}`;
289 }
290 return `projects/${project}/regions/${region}/subnetworks/${subnetwork}`;
291};
292
293const toName = (id: string, name: string | undefined, existing?: string) =>
294 Effect.gen(function* () {
295 if (name !== undefined) return name;
296 if (existing !== undefined) return existing;
297 const generated = yield* createPhysicalName({
298 id,
299 maxLength: MAX_NAME_LENGTH,
300 lowercase: true,
301 });
302 const rfc = generated.replace(/^[^a-z]+/, "n").replace(/-+$/g, "");
303 return rfc.slice(0, MAX_NAME_LENGTH);
304 });
305
306const encodeDescription = (
307 labels: Record<string, string>,
308 description: string | undefined,
309): string => {
310 const marker = `[alchemy ${alchemyLabelKeys.stack}=${labels[alchemyLabelKeys.stack]} ${alchemyLabelKeys.stage}=${labels[alchemyLabelKeys.stage]} ${alchemyLabelKeys.id}=${labels[alchemyLabelKeys.id]}]`;
311 return description ? `${marker}\n${description}` : marker;
312};
313
314const parseDescription = (
315 description: string | undefined,
316): {
317 labels: Record<string, string>;
318 description: string | undefined;
319} => {
320 if (!description?.startsWith("[alchemy ")) {
321 return { labels: {}, description };
322 }
323 const end = description.indexOf("]");
324 if (end < 0) return { labels: {}, description };
325 const labels: Record<string, string> = {};
326 for (const part of description.slice("[alchemy ".length, end).split(/\s+/)) {
327 const eq = part.indexOf("=");
328 if (eq > 0) {
329 labels[part.slice(0, eq)] = part.slice(eq + 1);
330 }
331 }
332 const rest = description.slice(end + 1).replace(/^\n/, "");
333 return { labels, description: rest.length > 0 ? rest : undefined };
334};
335
336const hasOwnershipMarker = (description: string | undefined) =>
337 Object.keys(parseDescription(description).labels).some((key) =>
338 key.startsWith("alchemy-"),
339 );
340
341const inferType = (props: RegionNetworkEndpointGroupProps): string => {
342 if (props.networkEndpointType) return props.networkEndpointType;
343 if (props.pscTargetService) return "PRIVATE_SERVICE_CONNECT";
344 if (props.cloudRun || props.appEngine || props.cloudFunction) {
345 return "SERVERLESS";
346 }
347 return DEFAULT_NETWORK_ENDPOINT_TYPE;
348};
349
350const annotationsOf = (
351 annotations: Record<string, string | undefined> | null | undefined,
352): Record<string, string> => tagRecord(annotations);
353
354const sameAnnotations = (
355 left: Record<string, string> | undefined,
356 right: Record<string, string> | undefined,
357) => {
358 const a = annotationsOf(left);
359 const b = annotationsOf(right);
360 const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
361 for (const key of keys) {
362 if (a[key] !== b[key]) return false;
363 }
364 return true;
365};
366
367const subsetEqual = (observed: unknown, desired: unknown): boolean => {
368 if (desired === undefined) return true;
369 if (desired === null || observed === null) return desired === observed;
370 if (Array.isArray(desired)) {
371 if (!Array.isArray(observed) || desired.length !== observed.length) {
372 return false;
373 }
374 return desired.every((item, index) => subsetEqual(observed[index], item));
375 }
376 if (typeof desired === "object") {
377 if (typeof observed !== "object" || observed === undefined) return false;
378 const current = observed as Record<string, unknown>;
379 return Object.entries(desired as Record<string, unknown>).every(
380 ([key, value]) => value === undefined || subsetEqual(current[key], value),
381 );
382 }
383 return observed === desired;
384};
385
386const toCloudRun = (
387 value: compute.NetworkEndpointGroupCloudRun | undefined,
388): RegionNetworkEndpointGroupCloudRun | undefined => {
389 if (value === undefined) return undefined;
390 if (
391 value.service === undefined &&
392 value.tag === undefined &&
393 value.urlMask === undefined
394 ) {
395 return undefined;
396 }
397 return {
398 service: value.service,
399 tag: value.tag,
400 urlMask: value.urlMask,
401 };
402};
403
404const toAppEngine = (
405 value: compute.NetworkEndpointGroupAppEngine | undefined,
406): RegionNetworkEndpointGroupAppEngine | undefined => {
407 if (value === undefined) return undefined;
408 if (
409 value.service === undefined &&
410 value.version === undefined &&
411 value.urlMask === undefined
412 ) {
413 return undefined;
414 }
415 return {
416 service: value.service,
417 version: value.version,
418 urlMask: value.urlMask,
419 };
420};
421
422const toCloudFunction = (
423 value: compute.NetworkEndpointGroupCloudFunction | undefined,
424): RegionNetworkEndpointGroupCloudFunction | undefined => {
425 if (value === undefined) return undefined;
426 if (value.function === undefined && value.urlMask === undefined) {
427 return undefined;
428 }
429 return {
430 function: value.function,
431 urlMask: value.urlMask,
432 };
433};
434
435const toPscData = (
436 value: compute.NetworkEndpointGroupPscData | undefined,
437): RegionNetworkEndpointGroupPscData | undefined => {
438 if (value === undefined) return undefined;
439 return {
440 producerPort: value.producerPort,
441 consumerPscAddress: value.consumerPscAddress,
442 pscConnectionId: value.pscConnectionId,
443 pscConnectionStatus: value.pscConnectionStatus,
444 };
445};
446
447const toBody = (
448 project: string,
449 region: string,
450 networkEndpointGroupName: string,
451 props: RegionNetworkEndpointGroupProps,
452 ownership: Record<string, string>,
453): compute.NetworkEndpointGroup => {
454 const networkEndpointType = inferType(props);
455 const body: compute.NetworkEndpointGroup = {
456 name: networkEndpointGroupName,
457 description: encodeDescription(ownership, props.description),
458 networkEndpointType,
459 defaultPort: props.defaultPort,
460 annotations:
461 props.annotations !== undefined &&
462 Object.keys(props.annotations).length > 0
463 ? props.annotations
464 : undefined,
465 cloudRun: props.cloudRun,
466 appEngine: props.appEngine,
467 cloudFunction: props.cloudFunction,
468 pscTargetService: props.pscTargetService,
469 };
470 if (props.network !== undefined) {
471 body.network = networkUrl(project, props.network);
472 }
473 if (props.subnetwork !== undefined) {
474 body.subnetwork = subnetworkUrl(project, region, props.subnetwork);
475 }
476 if (props.pscData?.producerPort !== undefined) {
477 body.pscData = { producerPort: props.pscData.producerPort };
478 }
479 return body;
480};
481
482const toAttrs = (
483 group: compute.NetworkEndpointGroup,
484 project: string,
485): RegionNetworkEndpointGroup["Attributes"] => {
486 const parsed = parseDescription(group.description);
487 return {
488 networkEndpointGroupName: group.name ?? "",
489 project,
490 region: lastSegment(group.region ?? "").toLowerCase(),
491 networkEndpointType: group.networkEndpointType,
492 description: parsed.description,
493 defaultPort: group.defaultPort,
494 network: group.network,
495 subnetwork: group.subnetwork,
496 annotations: annotationsOf(group.annotations),
497 cloudRun: toCloudRun(group.cloudRun),
498 appEngine: toAppEngine(group.appEngine),
499 cloudFunction: toCloudFunction(group.cloudFunction),
500 pscTargetService: group.pscTargetService,
501 pscData: toPscData(group.pscData),
502 size: group.size,
503 networkEndpointGroupId: group.id,
504 selfLink: group.selfLink,
505 creationTimestamp: group.creationTimestamp,
506 kind: group.kind,
507 };
508};
509
510const getByName = (
511 project: string,
512 region: string,
513 networkEndpointGroup: string,
514) =>
515 compute
516 .getRegionNetworkEndpointGroups({
517 project,
518 region,
519 networkEndpointGroup,
520 })
521 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
522
523const requireGroup = (
524 project: string,
525 region: string,
526 networkEndpointGroupName: string,
527) =>
528 getByName(project, region, networkEndpointGroupName).pipe(
529 Effect.flatMap((group) =>
530 group
531 ? Effect.succeed(group)
532 : Effect.fail(
533 new RegionNetworkEndpointGroupNotResolved({
534 networkEndpointGroupName,
535 region,
536 }),
537 ),
538 ),
539 Effect.retry({
540 while: (e) =>
541 e._tag === "GCP.Compute.RegionNetworkEndpointGroupNotResolved",
542 schedule: Schedule.spaced("1 second"),
543 times: 8,
544 }),
545 );
546
547const waitUntilGone = (
548 project: string,
549 region: string,
550 networkEndpointGroupName: string,
551) =>
552 getByName(project, region, networkEndpointGroupName).pipe(
553 Effect.flatMap((group) =>
554 group === undefined
555 ? Effect.void
556 : Effect.fail(
557 new RegionNetworkEndpointGroupPending({
558 networkEndpointGroupName,
559 status: "EXISTS",
560 }),
561 ),
562 ),
563 Effect.retry({
564 while: (e) => e._tag === "GCP.Compute.RegionNetworkEndpointGroupPending",
565 times: 10,
566 schedule: Schedule.spaced("2 seconds"),
567 }),
568 );
569
570const immutableChanged = (
571 news: RegionNetworkEndpointGroupProps,
572 olds: Partial<RegionNetworkEndpointGroupProps> | undefined,
573 output: RegionNetworkEndpointGroup["Attributes"] | undefined,
574) => {
575 const previousType =
576 (olds !== undefined ? inferType(olds) : undefined) ??
577 output?.networkEndpointType ??
578 DEFAULT_NETWORK_ENDPOINT_TYPE;
579 if (inferType(news) !== previousType) return true;
580
581 const previousDescription = olds?.description ?? output?.description ?? "";
582 if ((news.description ?? "") !== previousDescription) return true;
583
584 if (
585 news.defaultPort !== undefined &&
586 news.defaultPort !== (olds?.defaultPort ?? output?.defaultPort)
587 ) {
588 return true;
589 }
590
591 if (
592 news.network !== undefined &&
593 resourceRefOf(news.network) !==
594 resourceRefOf(olds?.network ?? output?.network)
595 ) {
596 return true;
597 }
598
599 if (
600 news.subnetwork !== undefined &&
601 resourceRefOf(news.subnetwork) !==
602 resourceRefOf(olds?.subnetwork ?? output?.subnetwork)
603 ) {
604 return true;
605 }
606
607 if (
608 news.pscTargetService !== undefined &&
609 news.pscTargetService !==
610 (olds?.pscTargetService ?? output?.pscTargetService)
611 ) {
612 return true;
613 }
614
615 if (
616 news.annotations !== undefined &&
617 !sameAnnotations(news.annotations, olds?.annotations ?? output?.annotations)
618 ) {
619 return true;
620 }
621
622 if (
623 news.cloudRun !== undefined &&
624 !subsetEqual(olds?.cloudRun ?? output?.cloudRun, news.cloudRun)
625 ) {
626 return true;
627 }
628 if (
629 news.appEngine !== undefined &&
630 !subsetEqual(olds?.appEngine ?? output?.appEngine, news.appEngine)
631 ) {
632 return true;
633 }
634 if (
635 news.cloudFunction !== undefined &&
636 !subsetEqual(
637 olds?.cloudFunction ?? output?.cloudFunction,
638 news.cloudFunction,
639 )
640 ) {
641 return true;
642 }
643 if (
644 news.pscData?.producerPort !== undefined &&
645 news.pscData.producerPort !==
646 (olds?.pscData?.producerPort ?? output?.pscData?.producerPort)
647 ) {
648 return true;
649 }
650 return false;
651};
652
653export const RegionNetworkEndpointGroupProvider = () =>
654 Provider.succeed(RegionNetworkEndpointGroup, {
655 stables: [
656 "networkEndpointGroupName",
657 "project",
658 "region",
659 "networkEndpointType",
660 "networkEndpointGroupId",
661 "selfLink",
662 "creationTimestamp",
663 ],
664
665 diff: Effect.fn(function* ({ news, olds, output }) {
666 if (!isResolved(news)) return undefined;
667 const env = yield* GcpEnvironment.current;
668 const previousName =
669 olds.networkEndpointGroupName ?? output?.networkEndpointGroupName;
670 const nextName = news.networkEndpointGroupName ?? previousName;
671 const nameChanged =
672 previousName !== undefined &&
673 nextName !== undefined &&
674 nextName !== previousName;
675
676 const previousRegion = normalizeRegion(
677 olds.region ?? output?.region,
678 env.region,
679 );
680 const nextRegion = normalizeRegion(
681 news.region ?? output?.region,
682 env.region,
683 );
684 const regionChanged = previousRegion !== nextRegion;
685
686 if (nameChanged || regionChanged) {
687 return { action: "replace" as const, deleteFirst: false };
688 }
689 if (immutableChanged(news, olds, output)) {
690 return { action: "replace" as const, deleteFirst: true };
691 }
692 return undefined;
693 }),
694
695 read: Effect.fn(function* ({ id, olds, output }) {
696 const env = yield* GcpEnvironment.current;
697 const networkEndpointGroupName = yield* toName(
698 id,
699 olds?.networkEndpointGroupName,
700 output?.networkEndpointGroupName,
701 );
702 const region = normalizeRegion(
703 olds?.region ?? output?.region,
704 env.region,
705 );
706 const existing = yield* getByName(
707 env.project,
708 region,
709 networkEndpointGroupName,
710 );
711 if (existing === undefined) return undefined;
712 const attrs = toAttrs(existing, env.project);
713 const { labels } = parseDescription(existing.description);
714 return (yield* hasAlchemyLabels(id, labels)) ? attrs : Unowned(attrs);
715 }),
716
717 list: () =>
718 Effect.gen(function* () {
719 const env = yield* GcpEnvironment.current;
720 const pages = yield* compute.aggregatedListNetworkEndpointGroups
721 .pages({
722 project: env.project,
723 returnPartialSuccess: true,
724 maxResults: 500,
725 })
726 .pipe(Stream.runCollect);
727 return Array.from(pages).flatMap((page) =>
728 Object.values(page.items ?? {}).flatMap((scoped) =>
729 (scoped?.networkEndpointGroups ?? [])
730 .filter(
731 (item) => item.region !== undefined && item.zone === undefined,
732 )
733 .filter((item) => hasOwnershipMarker(item.description))
734 .map((item) => toAttrs(item, env.project)),
735 ),
736 );
737 }),
738
739 reconcile: Effect.fn(function* ({ id, news, output }) {
740 const env = yield* GcpEnvironment.current;
741 const networkEndpointGroupName = yield* toName(
742 id,
743 news.networkEndpointGroupName,
744 output?.networkEndpointGroupName,
745 );
746 const region = normalizeRegion(news.region ?? output?.region, env.region);
747 const ownership = yield* createInternalLabels(id);
748 const desired = toBody(
749 env.project,
750 region,
751 networkEndpointGroupName,
752 news,
753 ownership,
754 );
755
756 let current = yield* getByName(
757 env.project,
758 region,
759 networkEndpointGroupName,
760 );
761
762 if (current === undefined) {
763 const created = yield* compute
764 .insertRegionNetworkEndpointGroups({
765 project: env.project,
766 region,
767 body: desired,
768 })
769 .pipe(
770 Effect.flatMap((operation) =>
771 waitRegionOperation(env.project, region, operation, {
772 ignore: ["RESOURCE_ALREADY_EXISTS"],
773 }).pipe(
774 Effect.flatMap(() =>
775 requireGroup(env.project, region, networkEndpointGroupName),
776 ),
777 ),
778 ),
779 Effect.catchTag("Conflict", () =>
780 getByName(env.project, region, networkEndpointGroupName),
781 ),
782 );
783 current = created ?? undefined;
784 }
785
786 if (current === undefined) {
787 return yield* new RegionNetworkEndpointGroupNotResolved({
788 networkEndpointGroupName,
789 region,
790 });
791 }
792
793 return toAttrs(current, env.project);
794 }),
795
796 delete: Effect.fn(function* ({ output }) {
797 const env = yield* GcpEnvironment.current;
798 const project = output.project || env.project;
799 const region = normalizeRegion(output.region, env.region);
800 if (!output.networkEndpointGroupName) return;
801 yield* compute
802 .deleteRegionNetworkEndpointGroups({
803 project,
804 region,
805 networkEndpointGroup: output.networkEndpointGroupName,
806 })
807 .pipe(
808 Effect.flatMap((operation) =>
809 waitRegionOperation(project, region, operation, {
810 ignore: ["RESOURCE_NOT_FOUND"],
811 }),
812 ),
813 Effect.catchTag("NotFound", () => Effect.void),
814 Effect.retry({
815 while: (e) => e._tag === "Conflict",
816 times: 8,
817 schedule: Schedule.spaced("2 seconds"),
818 }),
819 );
820 yield* waitUntilGone(project, region, output.networkEndpointGroupName);
821 }),
822 });