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 { 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 { GcpEnvironment } from "../Environment.ts";
12import { createInternalLabels, hasAlchemyLabels } from "../Labels.ts";
13import type { Providers } from "../Providers.ts";
14
15export type SubnetworkSecondaryRange = {
16 /** Name of this secondary range. Must be unique within the subnetwork. */
17 rangeName: string;
18 /** CIDR of this secondary range. */
19 ipCidrRange: string;
20};
21
22export type SubnetworkLogConfig = {
23 /** Whether VPC flow logs are enabled. */
24 enable?: boolean;
25 /** Aggregation interval for flow logs. */
26 aggregationInterval?: compute.SubnetworkLogConfigAggregationIntervalEnum;
27 /** Sampling rate in `[0, 1]`. */
28 flowSampling?: number;
29 /** Metadata included in flow logs. */
30 metadata?: compute.SubnetworkLogConfigMetadataEnum;
31 /** Extra metadata fields when `metadata` is `CUSTOM_METADATA`. */
32 metadataFields?: string[];
33 /** Filter expression for exported flow logs. */
34 filterExpr?: string;
35};
36
37export type SubnetworkProps = {
38 /**
39 * Subnetwork name (RFC1035, 1-63 chars). If omitted, a unique name is
40 * generated from the stack, stage, and logical id. Changing it replaces
41 * the subnetwork.
42 */
43 subnetworkName?: string;
44 /**
45 * Region of the subnetwork. Immutable — changing it replaces the
46 * subnetwork. `US-CENTRAL1` is accepted and normalized to `us-central1`.
47 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
48 */
49 region?: string;
50 /**
51 * Parent VPC network name or URL. Immutable — changing it replaces the
52 * subnetwork.
53 */
54 network: string;
55 /**
56 * Primary IPv4 CIDR. Changing it replaces the subnetwork (expansion via
57 * `expandIpCidrRange` is not applied in-place).
58 */
59 ipCidrRange: string;
60 /**
61 * User description. Alchemy ownership is stamped into the description
62 * (Compute Subnetwork has no `labels` field) so `list` / nuke can find
63 * the resource. The API only accepts description at create time —
64 * changing it replaces the subnetwork.
65 */
66 description?: string;
67 /**
68 * Whether VMs in this subnet can reach Google APIs without an external
69 * IP. Updated in place via `setPrivateIpGoogleAccess`.
70 * @default false
71 */
72 privateIpGoogleAccess?: boolean;
73 /**
74 * IP stack. `IPV4_ONLY` by default. Updated in place via patch.
75 * @default "IPV4_ONLY"
76 */
77 stackType?: compute.SubnetworkStackTypeEnum;
78 /**
79 * Subnet purpose (`PRIVATE`, `INTERNAL_HTTPS_LOAD_BALANCER`,
80 * `REGIONAL_MANAGED_PROXY`, …). Immutable — changing it replaces the
81 * subnetwork.
82 */
83 purpose?: compute.SubnetworkPurposeEnum;
84 /**
85 * Secondary ranges for alias IPs. Updated in place via patch.
86 */
87 secondaryIpRanges?: ReadonlyArray<SubnetworkSecondaryRange>;
88 /**
89 * VPC flow log options. Updated in place via patch.
90 */
91 logConfig?: SubnetworkLogConfig;
92 /**
93 * Deprecated flow-log toggle. Prefer `logConfig.enable`. Updated in
94 * place via patch when set.
95 */
96 enableFlowLogs?: boolean;
97 /**
98 * IPv6 Google access mode. Updated in place via patch when set.
99 */
100 privateIpv6GoogleAccess?: compute.SubnetworkPrivateIpv6GoogleAccessEnum;
101 /**
102 * IPv6 access type (`EXTERNAL` or `INTERNAL`). Immutable after the
103 * subnet is dual-stack — changing it replaces the subnetwork.
104 */
105 ipv6AccessType?: compute.SubnetworkIpv6AccessTypeEnum;
106};
107
108export type Subnetwork = Resource<
109 "GCP.Compute.Subnetwork",
110 SubnetworkProps,
111 {
112 /** RFC1035 subnetwork name. */
113 subnetworkName: string;
114 /** Region short name (`us-central1`). */
115 region: string;
116 /** Project id. */
117 project: string;
118 /** Parent VPC network URL. */
119 network: string;
120 /** Primary IPv4 CIDR. */
121 ipCidrRange: string;
122 /** User description (Alchemy ownership marker stripped). */
123 description: string | undefined;
124 /** Whether Private Google Access is enabled. */
125 privateIpGoogleAccess: boolean;
126 /** IP stack type. */
127 stackType: string | undefined;
128 /** Subnet purpose. */
129 purpose: string | undefined;
130 /** Secondary alias-IP ranges. */
131 secondaryIpRanges: ReadonlyArray<SubnetworkSecondaryRange>;
132 /** VPC flow log options. */
133 logConfig: SubnetworkLogConfig | undefined;
134 /** Deprecated flow-log toggle. */
135 enableFlowLogs: boolean | undefined;
136 /** IPv6 Google access mode. */
137 privateIpv6GoogleAccess: string | undefined;
138 /** IPv6 access type. */
139 ipv6AccessType: string | undefined;
140 /** Gateway address for default routes. */
141 gatewayAddress: string | undefined;
142 /** Server-reported state (`READY` or `DRAINING`). */
143 state: string | undefined;
144 /** Server-defined resource URL. */
145 selfLink: string | undefined;
146 /** Server-assigned numeric id. */
147 subnetworkId: string | undefined;
148 /** RFC3339 creation timestamp. */
149 creationTimestamp: string | undefined;
150 /** Optimistic-locking fingerprint. */
151 fingerprint: string | undefined;
152 },
153 never,
154 Providers
155>;
156
157/**
158 * A Google Compute Engine VPC subnetwork (subnet).
159 *
160 * Subnets are regional partitions of a VPC with one primary IPv4 range and
161 * optional secondary ranges. Compute Subnetwork has no `labels` field —
162 * Alchemy stamps `alchemy-stack` / `alchemy-stage` / `alchemy-id` into the
163 * description so `list` and `pnpm nuke:gcp` can identify owned subnets.
164 *
165 * The parent VPC must be custom mode (`autoCreateSubnetworks: false`)
166 * unless you are attaching to an auto-mode network's existing range.
167 *
168 * ### Creating a Subnetwork
169 * **Example:** Subnet in a custom-mode VPC
170 * ```typescript
171 * const network = yield* GCP.Compute.Network("Vpc", {
172 * autoCreateSubnetworks: false,
173 * });
174 * const subnet = yield* GCP.Compute.Subnetwork("Private", {
175 * network: network.networkName,
176 * ipCidrRange: "10.0.0.0/24",
177 * });
178 * ```
179 *
180 * **Example:** Explicit name, region, and Private Google Access
181 * ```typescript
182 * const subnet = yield* GCP.Compute.Subnetwork("Private", {
183 * subnetworkName: "app-private",
184 * region: "us-central1",
185 * network: "app-vpc",
186 * ipCidrRange: "10.10.0.0/24",
187 * privateIpGoogleAccess: true,
188 * description: "application private subnet",
189 * });
190 * ```
191 *
192 * ### Secondary ranges
193 * **Example:** Alias-IP secondary range
194 * ```typescript
195 * const subnet = yield* GCP.Compute.Subnetwork("Private", {
196 * network: network.networkName,
197 * ipCidrRange: "10.10.0.0/24",
198 * secondaryIpRanges: [
199 * { rangeName: "pods", ipCidrRange: "10.10.1.0/24" },
200 * ],
201 * });
202 * ```
203 *
204 * @resource
205 * @category Compute
206 */
207export const Subnetwork = Resource<Subnetwork>("GCP.Compute.Subnetwork");
208
209export class SubnetworkNotResolved extends Data.TaggedError(
210 "GCP.Compute.SubnetworkNotResolved",
211)<{
212 project: string;
213 region: string;
214 subnetworkName: string;
215}> {}
216
217const DEFAULT_STACK_TYPE = "IPV4_ONLY";
218const DEFAULT_PRIVATE_GOOGLE_ACCESS = false;
219
220const OWNERSHIP_KEYS = [
221 "alchemy-stack",
222 "alchemy-stage",
223 "alchemy-id",
224] as const;
225
226const lastSegment = (value: string) => {
227 const trimmed = value.replace(/\/+$/, "");
228 const parts = trimmed.split("/");
229 return parts[parts.length - 1] || trimmed;
230};
231
232const normalizeRegion = (region: string | undefined, defaultRegion: string) =>
233 lastSegment(region ?? defaultRegion).toLowerCase();
234
235const linkKey = (value: string | undefined) =>
236 value === undefined || value === "" ? "" : lastSegment(value).toLowerCase();
237
238const networkRef = (project: string, network: string) =>
239 network.includes("/")
240 ? network
241 : `projects/${project}/global/networks/${network}`;
242
243const encodeDescription = (
244 internal: Record<string, string>,
245 user?: string,
246): string => {
247 const marker = OWNERSHIP_KEYS.map(
248 (key) => `${key}=${internal[key] ?? ""}`,
249 ).join(" ");
250 return user && user.length > 0 ? `${marker}\n${user}` : marker;
251};
252
253const parseDescription = (description: string | undefined) => {
254 if (!description) {
255 return { labels: {} as Record<string, string>, description: undefined };
256 }
257 const newline = description.indexOf("\n");
258 const first = newline === -1 ? description : description.slice(0, newline);
259 const rest = newline === -1 ? undefined : description.slice(newline + 1);
260 if (!first.includes("alchemy-id=") || !first.includes("alchemy-stack=")) {
261 return { labels: {} as Record<string, string>, description };
262 }
263 const labels: Record<string, string> = {};
264 for (const part of first.split(/\s+/)) {
265 const eq = part.indexOf("=");
266 if (eq > 0) {
267 labels[part.slice(0, eq)] = part.slice(eq + 1);
268 }
269 }
270 return {
271 labels,
272 description: rest && rest.length > 0 ? rest : undefined,
273 };
274};
275
276const hasAlchemyMarker = (description: string | undefined) =>
277 Object.keys(parseDescription(description).labels).some((key) =>
278 key.startsWith("alchemy-"),
279 );
280
281const toSubnetworkName = (
282 id: string,
283 name: string | undefined,
284 existing?: string,
285) =>
286 Effect.gen(function* () {
287 if (name !== undefined) return name;
288 if (existing !== undefined) return existing;
289 const generated = yield* createPhysicalName({
290 id,
291 maxLength: 63,
292 lowercase: true,
293 });
294 const rfc = generated.replace(/^[^a-z]+/, "n").replace(/-+$/g, "");
295 return rfc.slice(0, 63);
296 });
297
298const toSecondaryRanges = (
299 ranges: compute.SubnetworkSecondaryRangeList | undefined,
300): SubnetworkSecondaryRange[] =>
301 (ranges ?? [])
302 .filter(
303 (
304 range,
305 ): range is compute.SubnetworkSecondaryRange & {
306 rangeName: string;
307 ipCidrRange: string;
308 } => !!range.rangeName && !!range.ipCidrRange,
309 )
310 .map((range) => ({
311 rangeName: range.rangeName,
312 ipCidrRange: range.ipCidrRange,
313 }));
314
315const rangesKey = (
316 ranges: ReadonlyArray<SubnetworkSecondaryRange> | undefined,
317) =>
318 JSON.stringify(
319 [...(ranges ?? [])]
320 .map((range) => ({
321 rangeName: range.rangeName,
322 ipCidrRange: range.ipCidrRange,
323 }))
324 .sort((a, b) => a.rangeName.localeCompare(b.rangeName)),
325 );
326
327const toAggregationInterval = (
328 value: string | undefined,
329): compute.SubnetworkLogConfigAggregationIntervalEnum | undefined => {
330 switch (value) {
331 case "INTERVAL_10_MIN":
332 case "INTERVAL_15_MIN":
333 case "INTERVAL_1_MIN":
334 case "INTERVAL_30_SEC":
335 case "INTERVAL_5_MIN":
336 case "INTERVAL_5_SEC":
337 return value;
338 default:
339 return undefined;
340 }
341};
342
343const toLogMetadata = (
344 value: string | undefined,
345): compute.SubnetworkLogConfigMetadataEnum | undefined => {
346 switch (value) {
347 case "CUSTOM_METADATA":
348 case "EXCLUDE_ALL_METADATA":
349 case "INCLUDE_ALL_METADATA":
350 return value;
351 default:
352 return undefined;
353 }
354};
355
356const toLogConfig = (
357 logConfig: compute.SubnetworkLogConfig | undefined,
358): SubnetworkLogConfig | undefined => {
359 if (logConfig === undefined) return undefined;
360 return {
361 enable: logConfig.enable,
362 aggregationInterval: toAggregationInterval(logConfig.aggregationInterval),
363 flowSampling: logConfig.flowSampling,
364 metadata: toLogMetadata(logConfig.metadata),
365 metadataFields: logConfig.metadataFields,
366 filterExpr: logConfig.filterExpr,
367 };
368};
369
370const toAttrs = (
371 subnetwork: compute.Subnetwork,
372 project: string,
373): Subnetwork["Attributes"] => {
374 const parsed = parseDescription(subnetwork.description);
375 return {
376 subnetworkName: subnetwork.name ?? "",
377 region: lastSegment(subnetwork.region ?? "").toLowerCase(),
378 project,
379 network: subnetwork.network ?? "",
380 ipCidrRange: subnetwork.ipCidrRange ?? "",
381 description: parsed.description,
382 privateIpGoogleAccess: subnetwork.privateIpGoogleAccess === true,
383 stackType: subnetwork.stackType,
384 purpose: subnetwork.purpose,
385 secondaryIpRanges: toSecondaryRanges(subnetwork.secondaryIpRanges),
386 logConfig: toLogConfig(subnetwork.logConfig),
387 enableFlowLogs: subnetwork.enableFlowLogs,
388 privateIpv6GoogleAccess: subnetwork.privateIpv6GoogleAccess,
389 ipv6AccessType: subnetwork.ipv6AccessType,
390 gatewayAddress: subnetwork.gatewayAddress,
391 state: subnetwork.state,
392 selfLink: subnetwork.selfLink,
393 subnetworkId: subnetwork.id,
394 creationTimestamp: subnetwork.creationTimestamp,
395 fingerprint: subnetwork.fingerprint,
396 };
397};
398
399const getByName = (project: string, region: string, subnetworkName: string) =>
400 compute
401 .getSubnetworks({ project, region, subnetwork: subnetworkName })
402 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
403
404const requireSubnetwork = (
405 project: string,
406 region: string,
407 subnetworkName: string,
408) =>
409 getByName(project, region, subnetworkName).pipe(
410 Effect.flatMap((subnetwork) =>
411 subnetwork
412 ? Effect.succeed(subnetwork)
413 : Effect.fail(
414 new SubnetworkNotResolved({ project, region, subnetworkName }),
415 ),
416 ),
417 Effect.retry({
418 while: (error) => error._tag === "GCP.Compute.SubnetworkNotResolved",
419 schedule: Schedule.spaced("1 second"),
420 times: 8,
421 }),
422 );
423
424export const SubnetworkProvider = () =>
425 Provider.succeed(Subnetwork, {
426 nuke: {
427 dependsOn: ["GCP.Compute.Network"],
428 },
429 stables: [
430 "subnetworkName",
431 "region",
432 "project",
433 "network",
434 "ipCidrRange",
435 "subnetworkId",
436 "selfLink",
437 "creationTimestamp",
438 "gatewayAddress",
439 "purpose",
440 "ipv6AccessType",
441 ],
442
443 diff: Effect.fn(function* ({ news, olds, output }) {
444 if (!isResolved(news)) return undefined;
445 const env = yield* GcpEnvironment.current;
446 const previousName = olds?.subnetworkName ?? output?.subnetworkName;
447 const nextName = news.subnetworkName ?? previousName;
448 const previousRegion = normalizeRegion(
449 olds?.region ?? output?.region,
450 env.region,
451 );
452 const nextRegion = normalizeRegion(
453 news.region ?? previousRegion,
454 env.region,
455 );
456 const identityChanged =
457 previousRegion !== nextRegion ||
458 (previousName !== undefined &&
459 nextName !== undefined &&
460 previousName !== nextName);
461 const replace =
462 identityChanged ||
463 linkKey(news.network) !== linkKey(olds?.network ?? output?.network) ||
464 news.ipCidrRange !== (olds?.ipCidrRange ?? output?.ipCidrRange ?? "") ||
465 (news.description ?? "") !==
466 (olds?.description ?? output?.description ?? "") ||
467 (news.purpose !== undefined &&
468 news.purpose !== (olds?.purpose ?? output?.purpose)) ||
469 (news.ipv6AccessType !== undefined &&
470 news.ipv6AccessType !==
471 (olds?.ipv6AccessType ?? output?.ipv6AccessType));
472 if (!replace) return undefined;
473 return {
474 action: "replace" as const,
475 deleteFirst:
476 !identityChanged &&
477 nextName !== undefined &&
478 previousName !== undefined &&
479 nextName === previousName,
480 };
481 }),
482
483 read: Effect.fn(function* ({ id, olds, output }) {
484 const env = yield* GcpEnvironment.current;
485 const subnetworkName = yield* toSubnetworkName(
486 id,
487 olds?.subnetworkName,
488 output?.subnetworkName,
489 );
490 const region = normalizeRegion(
491 olds?.region ?? output?.region,
492 env.region,
493 );
494 const existing = yield* getByName(env.project, region, subnetworkName);
495 if (existing === undefined) return undefined;
496 const attrs = toAttrs(existing, env.project);
497 const parsed = parseDescription(existing.description);
498 return (yield* hasAlchemyLabels(id, parsed.labels))
499 ? attrs
500 : Unowned(attrs);
501 }),
502
503 list: () =>
504 Effect.gen(function* () {
505 const env = yield* GcpEnvironment.current;
506 const found: Subnetwork["Attributes"][] = [];
507 let pageToken: string | undefined;
508 for (let page = 0; page < 10; page++) {
509 const response = yield* compute.aggregatedListSubnetworks({
510 project: env.project,
511 returnPartialSuccess: true,
512 maxResults: 500,
513 pageToken,
514 });
515 for (const scoped of Object.values(response.items ?? {})) {
516 for (const item of scoped?.subnetworks ?? []) {
517 if (!hasAlchemyMarker(item.description)) continue;
518 found.push(toAttrs(item, env.project));
519 }
520 }
521 pageToken = response.nextPageToken;
522 if (pageToken === undefined || pageToken === "") break;
523 }
524 return found;
525 }),
526
527 reconcile: Effect.fn(function* ({ id, news, output }) {
528 const env = yield* GcpEnvironment.current;
529 const subnetworkName = yield* toSubnetworkName(
530 id,
531 news.subnetworkName,
532 output?.subnetworkName,
533 );
534 const region = normalizeRegion(news.region ?? output?.region, env.region);
535 const network = networkRef(env.project, news.network);
536 const privateIpGoogleAccess =
537 news.privateIpGoogleAccess ?? DEFAULT_PRIVATE_GOOGLE_ACCESS;
538 const stackType = news.stackType ?? DEFAULT_STACK_TYPE;
539 const internal = yield* createInternalLabels(id);
540 const desiredDescription = encodeDescription(internal, news.description);
541
542 let current = yield* getByName(env.project, region, subnetworkName);
543
544 if (current === undefined) {
545 const body: compute.Subnetwork = {
546 name: subnetworkName,
547 network,
548 ipCidrRange: news.ipCidrRange,
549 description: desiredDescription,
550 privateIpGoogleAccess,
551 stackType,
552 };
553 if (news.purpose !== undefined) body.purpose = news.purpose;
554 if (news.secondaryIpRanges !== undefined) {
555 body.secondaryIpRanges = [...news.secondaryIpRanges];
556 }
557 if (news.logConfig !== undefined) {
558 body.logConfig = { ...news.logConfig };
559 }
560 if (news.enableFlowLogs !== undefined) {
561 body.enableFlowLogs = news.enableFlowLogs;
562 }
563 if (news.privateIpv6GoogleAccess !== undefined) {
564 body.privateIpv6GoogleAccess = news.privateIpv6GoogleAccess;
565 }
566 if (news.ipv6AccessType !== undefined) {
567 body.ipv6AccessType = news.ipv6AccessType;
568 }
569 yield* compute
570 .insertSubnetworks({
571 project: env.project,
572 region,
573 body,
574 })
575 .pipe(
576 Effect.flatMap((operation) =>
577 waitRegionOperation(env.project, region, operation, {
578 ignore: ["RESOURCE_ALREADY_EXISTS"],
579 }),
580 ),
581 Effect.catchTag("Conflict", () => Effect.void),
582 );
583 current = yield* requireSubnetwork(env.project, region, subnetworkName);
584 }
585
586 if ((current.privateIpGoogleAccess === true) !== privateIpGoogleAccess) {
587 const updated = yield* compute.setPrivateIpGoogleAccessSubnetworks({
588 project: env.project,
589 region,
590 subnetwork: subnetworkName,
591 body: { privateIpGoogleAccess },
592 });
593 yield* waitRegionOperation(env.project, region, updated);
594 current = yield* requireSubnetwork(env.project, region, subnetworkName);
595 }
596
597 const patchBody: compute.Subnetwork = {};
598 if ((current.stackType ?? DEFAULT_STACK_TYPE) !== stackType) {
599 patchBody.stackType = stackType;
600 }
601 if (
602 news.secondaryIpRanges !== undefined &&
603 rangesKey(news.secondaryIpRanges) !==
604 rangesKey(toSecondaryRanges(current.secondaryIpRanges))
605 ) {
606 patchBody.secondaryIpRanges = [...news.secondaryIpRanges];
607 }
608 if (news.logConfig !== undefined) {
609 const observed = toLogConfig(current.logConfig);
610 if (
611 (observed?.enable === true) !== (news.logConfig.enable === true) ||
612 (news.logConfig.aggregationInterval !== undefined &&
613 observed?.aggregationInterval !==
614 news.logConfig.aggregationInterval) ||
615 (news.logConfig.flowSampling !== undefined &&
616 observed?.flowSampling !== news.logConfig.flowSampling) ||
617 (news.logConfig.metadata !== undefined &&
618 observed?.metadata !== news.logConfig.metadata) ||
619 (news.logConfig.filterExpr !== undefined &&
620 observed?.filterExpr !== news.logConfig.filterExpr)
621 ) {
622 patchBody.logConfig = { ...news.logConfig };
623 }
624 }
625 if (
626 news.enableFlowLogs !== undefined &&
627 (current.enableFlowLogs === true) !== news.enableFlowLogs
628 ) {
629 patchBody.enableFlowLogs = news.enableFlowLogs;
630 }
631 if (
632 news.privateIpv6GoogleAccess !== undefined &&
633 current.privateIpv6GoogleAccess !== news.privateIpv6GoogleAccess
634 ) {
635 patchBody.privateIpv6GoogleAccess = news.privateIpv6GoogleAccess;
636 }
637
638 if (Object.keys(patchBody).length > 0) {
639 patchBody.fingerprint = current.fingerprint;
640 const patched = yield* compute.patchSubnetworks({
641 project: env.project,
642 region,
643 subnetwork: subnetworkName,
644 body: patchBody,
645 });
646 yield* waitRegionOperation(env.project, region, patched);
647 current = yield* requireSubnetwork(env.project, region, subnetworkName);
648 }
649
650 return toAttrs(current, env.project);
651 }),
652
653 delete: Effect.fn(function* ({ output }) {
654 const env = yield* GcpEnvironment.current;
655 const project = output.project || env.project;
656 const region = normalizeRegion(output.region, env.region);
657 const subnetworkName = output.subnetworkName;
658 if (!subnetworkName) return;
659 yield* compute
660 .deleteSubnetworks({
661 project,
662 region,
663 subnetwork: subnetworkName,
664 })
665 .pipe(
666 Effect.flatMap((operation) =>
667 waitRegionOperation(project, region, operation, {
668 ignore: ["RESOURCE_NOT_FOUND"],
669 }),
670 ),
671 Effect.catchTag("NotFound", () => Effect.void),
672 Effect.retry({
673 while: (error) =>
674 error._tag === "Conflict" ||
675 (error._tag === "GCP.OperationFailed" &&
676 error.reason === "RESOURCE_IN_USE_BY_ANOTHER_RESOURCE"),
677 times: 8,
678 schedule: Schedule.spaced("2 seconds"),
679 }),
680 // Compute rejects the delete while another operation is still in
681 // flight on the subnet (e.g. load balancer or packet-mirroring
682 // teardown on resources inside it).
683 Effect.retry({
684 while: (error) => error._tag === "ResourceNotReady",
685 times: 20,
686 schedule: Schedule.spaced("10 seconds"),
687 }),
688 );
689 }),
690 });