1import { ignoredCodes } from "./internal.ts";
2import * as compute from "@distilled.cloud/gcp/compute_v1";
3import { waitRegionOperation } from "./operations.ts";
4import * as Data from "effect/Data";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import * as Stream from "effect/Stream";
8import { Unowned } from "../../AdoptPolicy.ts";
9import { isResolved } from "../../Diff.ts";
10import { createPhysicalName } from "../../PhysicalName.ts";
11import * as Provider from "../../Provider.ts";
12import { Resource } from "../../Resource.ts";
13import { tagRecord } from "../../Tags.ts";
14import { GcpEnvironment } from "../Environment.ts";
15import {
16 createInternalLabels,
17 diffLabels,
18 hasAlchemyLabels,
19 stripInternalLabels,
20 toLabels,
21} from "../Labels.ts";
22import type { Providers } from "../Providers.ts";
23
24const DEFAULT_TYPE = "PARTNER";
25const DEFAULT_ENCRYPTION = "NONE";
26const DEFAULT_STACK = "IPV4_ONLY";
27const MAX_NAME_LENGTH = 63;
28
29export type InterconnectAttachmentType =
30 | compute.InterconnectAttachmentTypeEnum
31 | (string & {});
32export type InterconnectAttachmentBandwidth =
33 | compute.InterconnectAttachmentBandwidthEnum
34 | (string & {});
35export type InterconnectAttachmentEncryption =
36 | compute.InterconnectAttachmentEncryptionEnum
37 | (string & {});
38export type InterconnectAttachmentStackType =
39 | compute.InterconnectAttachmentStackTypeEnum
40 | (string & {});
41export type InterconnectAttachmentEdgeAvailabilityDomain =
42 | compute.InterconnectAttachmentEdgeAvailabilityDomainEnum
43 | (string & {});
44export type InterconnectAttachmentPartnerMetadata =
45 compute.InterconnectAttachmentPartnerMetadata;
46export type InterconnectAttachmentL2Forwarding =
47 compute.InterconnectAttachmentL2Forwarding;
48
49export type InterconnectAttachmentProps = {
50 /**
51 * Attachment name (RFC1035, 1-63 characters). If omitted, a unique name
52 * is generated from the stack, stage, and logical id. Immutable —
53 * changing it replaces the attachment.
54 */
55 interconnectAttachmentName?: string;
56 /**
57 * Region the attachment lives in. Immutable — changing it replaces the
58 * attachment. `US-CENTRAL1` is accepted and normalized to `us-central1`.
59 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
60 */
61 region?: string;
62 /**
63 * Cloud Router URL or name in the same region. Required. Immutable —
64 * changing it replaces the attachment.
65 */
66 router: string;
67 /**
68 * Attachment type (`DEDICATED`, `PARTNER`, `PARTNER_PROVIDER`,
69 * `L2_DEDICATED`). Immutable — changing it replaces the attachment.
70 * @default "PARTNER"
71 */
72 type?: InterconnectAttachmentType;
73 /**
74 * Underlying Interconnect URL or name. Required for `DEDICATED` and
75 * `L2_DEDICATED`. Immutable — changing it replaces the attachment.
76 */
77 interconnect?: string;
78 /**
79 * Desired availability domain for `PARTNER` attachments. Immutable —
80 * changing it replaces the attachment.
81 */
82 edgeAvailabilityDomain?: InterconnectAttachmentEdgeAvailabilityDomain;
83 /**
84 * Optional description. Updated in place via `patch`.
85 */
86 description?: string;
87 /**
88 * Administrative status. When `false`, the attachment carries no
89 * packets. Updated in place.
90 * @default true
91 */
92 adminEnabled?: boolean;
93 /**
94 * Provisioned bandwidth. Mutable for `DEDICATED` and
95 * `PARTNER_PROVIDER`.
96 */
97 bandwidth?: InterconnectAttachmentBandwidth;
98 /**
99 * Packet MTU in bytes (`1440`, `1460`, `1500`, `8896`). Updated in
100 * place.
101 */
102 mtu?: number;
103 /**
104 * IP stack (`IPV4_ONLY` or `IPV4_IPV6`). Updated in place.
105 * @default "IPV4_ONLY"
106 */
107 stackType?: InterconnectAttachmentStackType;
108 /**
109 * VLAN encryption. Immutable — changing it replaces the attachment.
110 * @default "NONE"
111 */
112 encryption?: InterconnectAttachmentEncryption;
113 /**
114 * IEEE 802.1Q VLAN tag (2-4093). Create-time only. Immutable —
115 * changing it replaces the attachment.
116 */
117 vlanTag8021q?: number;
118 /**
119 * Reserved internal addresses for IPsec-encrypted attachments.
120 * Immutable — changing them replaces the attachment.
121 */
122 ipsecInternalAddresses?: string[];
123 /**
124 * Partner metadata displayed to customers. Mutable for
125 * `PARTNER_PROVIDER`.
126 */
127 partnerMetadata?: InterconnectAttachmentPartnerMetadata;
128 /**
129 * L2 forwarding config. Required when `type` is `L2_DEDICATED`.
130 */
131 l2Forwarding?: InterconnectAttachmentL2Forwarding;
132 /**
133 * IPv4 subnet mask length (`29` or `30`). Create-time only.
134 */
135 subnetLength?: number;
136 /**
137 * Candidate link-local prefixes for Cloud Router IPs. Create-time
138 * only.
139 */
140 candidateSubnets?: string[];
141 /**
142 * User labels. Alchemy ownership labels are merged in automatically
143 * and synced via `setLabels` (labels cannot be set on insert).
144 */
145 labels?: Record<string, string>;
146};
147
148export type InterconnectAttachment = Resource<
149 "GCP.Compute.InterconnectAttachment",
150 InterconnectAttachmentProps,
151 {
152 /** Attachment name. */
153 interconnectAttachmentName: string;
154 /** Project id. */
155 project: string;
156 /** Region short name (`us-central1`). */
157 region: string;
158 /** Cloud Router URL. */
159 router: string | undefined;
160 /** Attachment type. */
161 type: string | undefined;
162 /** Underlying Interconnect URL. */
163 interconnect: string | undefined;
164 /** Description. */
165 description: string | undefined;
166 /** Administrative status. */
167 adminEnabled: boolean;
168 /** Provisioned bandwidth. */
169 bandwidth: string | undefined;
170 /** Packet MTU. */
171 mtu: number | undefined;
172 /** IP stack. */
173 stackType: string | undefined;
174 /** VLAN encryption. */
175 encryption: string | undefined;
176 /** VLAN tag. */
177 vlanTag8021q: number | undefined;
178 /** Availability domain. */
179 edgeAvailabilityDomain: string | undefined;
180 /** Partner pairing key (PARTNER type). */
181 pairingKey: string | undefined;
182 /** Attachment state. */
183 state: string | undefined;
184 /** Operational status. */
185 operationalStatus: string | undefined;
186 /** Cloud Router IPv4 address. */
187 cloudRouterIpAddress: string | undefined;
188 /** Customer router IPv4 address. */
189 customerRouterIpAddress: string | undefined;
190 /** Cloud Router IPv6 address. */
191 cloudRouterIpv6Address: string | undefined;
192 /** Customer router IPv6 address. */
193 customerRouterIpv6Address: string | undefined;
194 /** User labels (Alchemy ownership labels stripped). */
195 labels: Record<string, string>;
196 /** Partner metadata. */
197 partnerMetadata: InterconnectAttachmentPartnerMetadata | undefined;
198 /** L2 forwarding config. */
199 l2Forwarding: InterconnectAttachmentL2Forwarding | undefined;
200 /** Attachment group URL. */
201 attachmentGroup: string | undefined;
202 /** Server-assigned numeric id. */
203 interconnectAttachmentId: string | undefined;
204 /** Resource self-link. */
205 selfLink: string | undefined;
206 /** Label fingerprint for `setLabels`. */
207 labelFingerprint: string | undefined;
208 /** RFC3339 creation timestamp. */
209 creationTimestamp: string | undefined;
210 /** Resource kind. */
211 kind: string | undefined;
212 },
213 never,
214 Providers
215>;
216
217/**
218 * A regional Compute Engine Interconnect VLAN attachment.
219 *
220 * VLAN attachments connect a Cloud Router to a Dedicated or Partner
221 * Interconnect. Name, region, router, type, interconnect, encryption,
222 * VLAN tag, and availability domain are immutable. Description, admin
223 * status, bandwidth, MTU, and stack type update in place via
224 * `interconnectAttachments.patch`. Labels are applied with `setLabels`
225 * after the attachment exists.
226 *
227 * ### Creating an Interconnect Attachment
228 * **Example:** Partner attachment
229 * ```typescript
230 * const attachment = yield* GCP.Compute.InterconnectAttachment("Vlan", {
231 * region: "us-central1",
232 * router: router.routerName,
233 * type: "PARTNER",
234 * edgeAvailabilityDomain: "AVAILABILITY_DOMAIN_1",
235 * mtu: 1500,
236 * });
237 * ```
238 *
239 * **Example:** Dedicated attachment
240 * ```typescript
241 * const attachment = yield* GCP.Compute.InterconnectAttachment("Vlan", {
242 * interconnectAttachmentName: "app-vlan",
243 * router: router.selfLink,
244 * type: "DEDICATED",
245 * interconnect: interconnect.selfLink,
246 * vlanTag8021q: 100,
247 * bandwidth: "BPS_1G",
248 * });
249 * ```
250 *
251 * @resource
252 * @category Compute
253 */
254export const InterconnectAttachment = Resource<InterconnectAttachment>(
255 "GCP.Compute.InterconnectAttachment",
256);
257
258export class InterconnectAttachmentNotResolved extends Data.TaggedError(
259 "GCP.Compute.InterconnectAttachmentNotResolved",
260)<{
261 interconnectAttachmentName: string;
262 region: string;
263}> {}
264
265export class InterconnectAttachmentStillExists extends Data.TaggedError(
266 "GCP.Compute.InterconnectAttachmentStillExists",
267)<{
268 interconnectAttachmentName: string;
269}> {}
270
271const lastSegment = (value: string | undefined): string => {
272 if (value === undefined || value.length === 0) return "";
273 const trimmed = value.replace(/\/+$/, "");
274 const parts = trimmed.split("/");
275 return parts[parts.length - 1] || trimmed;
276};
277
278const normalizeRegion = (region: string | undefined, defaultRegion: string) =>
279 lastSegment(region ?? defaultRegion).toLowerCase();
280
281const rfc1035 = (name: string): string => {
282 let next = name
283 .toLowerCase()
284 .replace(/[^a-z0-9-]/g, "-")
285 .replace(/-+/g, "-")
286 .replace(/^-+/, "")
287 .replace(/-+$/, "");
288 if (!/^[a-z]/.test(next)) next = `a${next}`;
289 next = next.slice(0, MAX_NAME_LENGTH).replace(/-+$/, "");
290 return next.length > 0 ? next : "attachment";
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 return rfc1035(
298 yield* createPhysicalName({
299 id,
300 maxLength: MAX_NAME_LENGTH,
301 lowercase: true,
302 }),
303 );
304 });
305
306const typeOf = (value: string | undefined) =>
307 (value ?? DEFAULT_TYPE).toUpperCase();
308
309const encryptionOf = (value: string | undefined) =>
310 (value ?? DEFAULT_ENCRYPTION).toUpperCase();
311
312const stackOf = (value: string | undefined) =>
313 (value ?? DEFAULT_STACK).toUpperCase();
314
315const routerUrl = (project: string, region: string, value: string) => {
316 if (value.includes("/")) return value;
317 return `projects/${project}/regions/${region}/routers/${value}`;
318};
319
320const interconnectUrl = (project: string, value: string) => {
321 if (value.includes("/")) return value;
322 return `projects/${project}/global/interconnects/${value}`;
323};
324
325const userLabels = (
326 labels: Record<string, string | undefined> | null | undefined,
327): Record<string, string> => stripInternalLabels(tagRecord(labels));
328
329const toAttrs = (
330 attachment: compute.InterconnectAttachment,
331 project: string,
332): InterconnectAttachment["Attributes"] => ({
333 interconnectAttachmentName: attachment.name ?? "",
334 project,
335 region: lastSegment(attachment.region).toLowerCase(),
336 router: attachment.router,
337 type: attachment.type,
338 interconnect: attachment.interconnect,
339 description: attachment.description,
340 adminEnabled: attachment.adminEnabled !== false,
341 bandwidth: attachment.bandwidth,
342 mtu: attachment.mtu !== undefined ? Number(attachment.mtu) : undefined,
343 stackType: attachment.stackType,
344 encryption: attachment.encryption,
345 vlanTag8021q: attachment.vlanTag8021q,
346 edgeAvailabilityDomain: attachment.edgeAvailabilityDomain,
347 pairingKey: attachment.pairingKey,
348 state: attachment.state,
349 operationalStatus: attachment.operationalStatus,
350 cloudRouterIpAddress: attachment.cloudRouterIpAddress,
351 customerRouterIpAddress: attachment.customerRouterIpAddress,
352 cloudRouterIpv6Address: attachment.cloudRouterIpv6Address,
353 customerRouterIpv6Address: attachment.customerRouterIpv6Address,
354 labels: userLabels(attachment.labels),
355 partnerMetadata: attachment.partnerMetadata,
356 l2Forwarding: attachment.l2Forwarding,
357 attachmentGroup: attachment.attachmentGroup,
358 interconnectAttachmentId: attachment.id,
359 selfLink: attachment.selfLink,
360 labelFingerprint: attachment.labelFingerprint,
361 creationTimestamp: attachment.creationTimestamp,
362 kind: attachment.kind,
363});
364
365const getByName = (
366 project: string,
367 region: string,
368 interconnectAttachment: string,
369) =>
370 compute
371 .getInterconnectAttachments({
372 project,
373 region,
374 interconnectAttachment,
375 })
376 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
377
378const awaitResource = (
379 project: string,
380 region: string,
381 interconnectAttachmentName: string,
382) =>
383 getByName(project, region, interconnectAttachmentName).pipe(
384 Effect.flatMap((attachment) =>
385 attachment !== undefined
386 ? Effect.succeed(attachment)
387 : Effect.fail(
388 new InterconnectAttachmentNotResolved({
389 interconnectAttachmentName,
390 region,
391 }),
392 ),
393 ),
394 Effect.retry({
395 while: (error) =>
396 error._tag === "GCP.Compute.InterconnectAttachmentNotResolved",
397 times: 8,
398 schedule: Schedule.spaced("1 second"),
399 }),
400 );
401
402const waitUntilGone = (
403 project: string,
404 region: string,
405 interconnectAttachmentName: string,
406) =>
407 getByName(project, region, interconnectAttachmentName).pipe(
408 Effect.flatMap((attachment) =>
409 attachment === undefined
410 ? Effect.void
411 : Effect.fail(
412 new InterconnectAttachmentStillExists({
413 interconnectAttachmentName,
414 }),
415 ),
416 ),
417 Effect.retry({
418 while: (error) =>
419 error._tag === "GCP.Compute.InterconnectAttachmentStillExists",
420 times: 10,
421 schedule: Schedule.spaced("2 seconds"),
422 }),
423 Effect.catchTag(
424 "GCP.Compute.InterconnectAttachmentStillExists",
425 () => Effect.void,
426 ),
427 );
428
429const runOp = <E extends { readonly _tag: string }, R>(
430 project: string,
431 region: string,
432 interconnectAttachmentName: string,
433 start: Effect.Effect<compute.Operation, E, R>,
434 options?: { ignoreAlreadyExists?: boolean; ignoreNotFound?: boolean },
435) =>
436 start.pipe(
437 Effect.flatMap((operation) =>
438 waitRegionOperation(project, region, operation, {
439 ignore: ignoredCodes(options),
440 }),
441 ),
442 Effect.retry({
443 while: (error) => error._tag === "Conflict",
444 times: 5,
445 schedule: Schedule.spaced("1 second"),
446 }),
447 );
448
449export const InterconnectAttachmentProvider = () =>
450 Provider.succeed(InterconnectAttachment, {
451 stables: [
452 "interconnectAttachmentName",
453 "project",
454 "region",
455 "interconnectAttachmentId",
456 "selfLink",
457 "type",
458 "router",
459 "interconnect",
460 "creationTimestamp",
461 ],
462
463 diff: Effect.fn(function* ({ news, olds, output }) {
464 if (!isResolved(news)) return undefined;
465 const env = yield* GcpEnvironment.current;
466 const previousName =
467 olds?.interconnectAttachmentName ?? output?.interconnectAttachmentName;
468 const nextName = news.interconnectAttachmentName ?? previousName;
469 const nameChanged =
470 previousName !== undefined &&
471 nextName !== undefined &&
472 previousName !== nextName;
473 const previousRegion = normalizeRegion(
474 olds?.region ?? output?.region,
475 env.region,
476 );
477 const nextRegion = normalizeRegion(
478 news.region ?? previousRegion,
479 env.region,
480 );
481 const previousRouter = lastSegment(olds?.router ?? output?.router);
482 const nextRouter = lastSegment(news.router);
483 const previousType = typeOf(olds?.type ?? output?.type);
484 const nextType = typeOf(news.type ?? previousType);
485 const previousIx = lastSegment(
486 olds?.interconnect ?? output?.interconnect,
487 );
488 const nextIx = lastSegment(news.interconnect ?? previousIx);
489 const previousEnc = encryptionOf(olds?.encryption ?? output?.encryption);
490 const nextEnc = encryptionOf(news.encryption ?? previousEnc);
491 const previousVlan = olds?.vlanTag8021q ?? output?.vlanTag8021q;
492 const nextVlan = news.vlanTag8021q ?? previousVlan;
493 const previousDomain =
494 olds?.edgeAvailabilityDomain ?? output?.edgeAvailabilityDomain;
495 const nextDomain = news.edgeAvailabilityDomain ?? previousDomain;
496
497 const immutableChanged =
498 previousRouter !== nextRouter ||
499 previousType !== nextType ||
500 previousIx !== nextIx ||
501 previousEnc !== nextEnc ||
502 previousVlan !== nextVlan ||
503 previousDomain !== nextDomain;
504
505 if (nameChanged || previousRegion !== nextRegion) {
506 return { action: "replace" as const, deleteFirst: false };
507 }
508 if (immutableChanged) {
509 return { action: "replace" as const, deleteFirst: true };
510 }
511 return undefined;
512 }),
513
514 read: Effect.fn(function* ({ id, olds, output }) {
515 const env = yield* GcpEnvironment.current;
516 const interconnectAttachmentName = yield* toName(
517 id,
518 olds?.interconnectAttachmentName,
519 output?.interconnectAttachmentName,
520 );
521 const region = normalizeRegion(
522 olds?.region ?? output?.region,
523 env.region,
524 );
525 const existing = yield* getByName(
526 env.project,
527 region,
528 interconnectAttachmentName,
529 );
530 if (existing === undefined) return undefined;
531 const attrs = toAttrs(existing, env.project);
532 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
533 ? attrs
534 : Unowned(attrs);
535 }),
536
537 list: () =>
538 Effect.gen(function* () {
539 const env = yield* GcpEnvironment.current;
540 const pages = yield* compute.aggregatedListInterconnectAttachments
541 .pages({
542 project: env.project,
543 filter: "labels.alchemy-id:*",
544 returnPartialSuccess: true,
545 maxResults: 500,
546 })
547 .pipe(Stream.runCollect);
548 return Array.from(pages).flatMap((page) =>
549 Object.values(page.items ?? {}).flatMap((scoped) =>
550 (scoped?.interconnectAttachments ?? [])
551 .filter((item) =>
552 Object.keys(item.labels ?? {}).some((key) =>
553 key.startsWith("alchemy-"),
554 ),
555 )
556 .map((item) => toAttrs(item, env.project)),
557 ),
558 );
559 }),
560
561 reconcile: Effect.fn(function* ({ id, news, output }) {
562 const env = yield* GcpEnvironment.current;
563 const interconnectAttachmentName = yield* toName(
564 id,
565 news.interconnectAttachmentName,
566 output?.interconnectAttachmentName,
567 );
568 const region = normalizeRegion(news.region ?? output?.region, env.region);
569 const desiredLabels = {
570 ...toLabels(news.labels),
571 ...(yield* createInternalLabels(id)),
572 };
573 const router = routerUrl(env.project, region, news.router);
574 const type = typeOf(news.type);
575 const encryption = encryptionOf(news.encryption);
576 const stackType = stackOf(news.stackType);
577 const adminEnabled = news.adminEnabled !== false;
578
579 let current = yield* getByName(
580 env.project,
581 region,
582 interconnectAttachmentName,
583 );
584
585 if (current === undefined) {
586 yield* compute
587 .insertInterconnectAttachments({
588 project: env.project,
589 region,
590 body: {
591 name: interconnectAttachmentName,
592 router,
593 type,
594 interconnect: news.interconnect
595 ? interconnectUrl(env.project, news.interconnect)
596 : undefined,
597 edgeAvailabilityDomain: news.edgeAvailabilityDomain,
598 description: news.description,
599 adminEnabled,
600 bandwidth: news.bandwidth,
601 mtu: news.mtu,
602 stackType,
603 encryption,
604 vlanTag8021q: news.vlanTag8021q,
605 ipsecInternalAddresses: news.ipsecInternalAddresses,
606 partnerMetadata: news.partnerMetadata,
607 l2Forwarding: news.l2Forwarding,
608 subnetLength: news.subnetLength,
609 candidateSubnets: news.candidateSubnets,
610 },
611 })
612 .pipe(
613 Effect.flatMap((operation) =>
614 waitRegionOperation(env.project, region, operation, {
615 ignore: ["RESOURCE_ALREADY_EXISTS"],
616 }),
617 ),
618 Effect.catchTag("Conflict", () => Effect.void),
619 );
620 current = yield* awaitResource(
621 env.project,
622 region,
623 interconnectAttachmentName,
624 );
625 }
626
627 const observedMtu =
628 current.mtu !== undefined ? Number(current.mtu) : undefined;
629 const needsPatch =
630 (current.description ?? "") !== (news.description ?? "") ||
631 (current.adminEnabled !== false) !== adminEnabled ||
632 (news.bandwidth !== undefined &&
633 (current.bandwidth ?? "") !== news.bandwidth) ||
634 (news.mtu !== undefined && observedMtu !== news.mtu) ||
635 (news.stackType !== undefined &&
636 stackOf(current.stackType) !== stackType) ||
637 (news.partnerMetadata !== undefined &&
638 JSON.stringify(current.partnerMetadata ?? null) !==
639 JSON.stringify(news.partnerMetadata ?? null));
640
641 if (needsPatch) {
642 yield* runOp(
643 env.project,
644 region,
645 interconnectAttachmentName,
646 compute.patchInterconnectAttachments({
647 project: env.project,
648 region,
649 interconnectAttachment: interconnectAttachmentName,
650 body: {
651 description: news.description,
652 adminEnabled,
653 bandwidth: news.bandwidth,
654 mtu: news.mtu,
655 stackType,
656 partnerMetadata: news.partnerMetadata,
657 },
658 }),
659 );
660 current =
661 (yield* getByName(env.project, region, interconnectAttachmentName)) ??
662 current;
663 }
664
665 const observedLabels = tagRecord(current.labels);
666 const { upsert, removed } = diffLabels(observedLabels, desiredLabels);
667 if (upsert.length > 0 || removed.length > 0) {
668 yield* Effect.gen(function* () {
669 const latest =
670 (yield* getByName(
671 env.project,
672 region,
673 interconnectAttachmentName,
674 )) ?? current;
675 if (latest === undefined) {
676 return yield* new InterconnectAttachmentNotResolved({
677 interconnectAttachmentName,
678 region,
679 });
680 }
681 yield* runOp(
682 env.project,
683 region,
684 interconnectAttachmentName,
685 compute.setLabelsInterconnectAttachments({
686 project: env.project,
687 region,
688 resource: interconnectAttachmentName,
689 body: {
690 labels: desiredLabels,
691 labelFingerprint: latest.labelFingerprint,
692 },
693 }),
694 );
695 }).pipe(
696 Effect.retry({
697 while: (error) => error._tag === "Conflict",
698 times: 5,
699 schedule: Schedule.spaced("1 second"),
700 }),
701 );
702 current =
703 (yield* getByName(env.project, region, interconnectAttachmentName)) ??
704 current;
705 }
706
707 return toAttrs(current, env.project);
708 }),
709
710 delete: Effect.fn(function* ({ output }) {
711 if (!output.interconnectAttachmentName) return;
712 const env = yield* GcpEnvironment.current;
713 const project = output.project || env.project;
714 const region = normalizeRegion(output.region, env.region);
715 yield* compute
716 .deleteInterconnectAttachments({
717 project,
718 region,
719 interconnectAttachment: output.interconnectAttachmentName,
720 })
721 .pipe(
722 Effect.flatMap((operation) =>
723 waitRegionOperation(project, region, operation, {
724 ignore: ["RESOURCE_NOT_FOUND"],
725 }),
726 ),
727 Effect.catchTag("NotFound", () => Effect.void),
728 Effect.retry({
729 while: (error) => error._tag === "Conflict",
730 times: 8,
731 schedule: Schedule.spaced("2 seconds"),
732 }),
733 );
734 yield* waitUntilGone(project, region, output.interconnectAttachmentName);
735 }),
736 });