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 diffLabels,
16 hasAlchemyLabels,
17 stripInternalLabels,
18 toLabels,
19} from "../Labels.ts";
20import type { Providers } from "../Providers.ts";
21import { waitForOperation } from "./internal.ts";
22
23const DEFAULT_LOCATION = "global";
24const DEFAULT_USAGE =
25 "FOR_VPC" satisfies networkconnectivity.InternalRangeUsageEnum;
26const DEFAULT_PEERING =
27 "FOR_SELF" satisfies networkconnectivity.InternalRangePeeringEnum;
28const MAX_NAME_LENGTH = 63;
29
30export type InternalRangeUsage =
31 | networkconnectivity.InternalRangeUsageEnum
32 | (string & {});
33export type InternalRangePeering =
34 | networkconnectivity.InternalRangePeeringEnum
35 | (string & {});
36export type InternalRangeOverlap =
37 | networkconnectivity.InternalRangeOverlapsItemEnum
38 | (string & {});
39export type InternalRangeAllocationStrategy =
40 | networkconnectivity.AllocationOptionsAllocationStrategyEnum
41 | (string & {});
42
43export type InternalRangeAllocationOptions = {
44 /**
45 * Auto-allocation strategy used when `ipCidrRange` is omitted and
46 * `prefixLength` is set.
47 */
48 allocationStrategy?: InternalRangeAllocationStrategy;
49 /**
50 * Maximum expected parallelism of range-creation requests against the
51 * same peered space. Required when `allocationStrategy` is
52 * `RANDOM_FIRST_N_AVAILABLE`.
53 */
54 firstAvailableRangesLookupSize?: number;
55};
56
57export type InternalRangeMigration = {
58 /**
59 * Source subnet URI, e.g.
60 * `/projects/{project}/regions/{region}/subnetworks/{subnet}`.
61 * Required when `usage` is `FOR_MIGRATION`.
62 */
63 source: string;
64 /**
65 * Target subnet URI. The target project may differ (peer-network
66 * migration).
67 */
68 target: string;
69};
70
71export type InternalRangeProps = {
72 /**
73 * Internal range id (the `{internal_range}` segment of
74 * `projects/{project}/locations/{location}/internalRanges/{internal_range}`).
75 * If omitted, a unique RFC1035 name is generated from the stack, stage,
76 * and logical id. Must be 1-63 characters and match
77 * `[a-z]([-a-z0-9]*[a-z0-9])?`. Immutable — changing it replaces the
78 * range.
79 */
80 internalRangeId?: string;
81 /**
82 * Location of the range. Internal ranges are global — always `"global"`.
83 * Immutable — changing it replaces the range. `GLOBAL` is accepted and
84 * normalized to `global`.
85 * @default "global"
86 */
87 location?: string;
88 /**
89 * VPC network to reserve the range in. Accepts a name (`app-vpc`), a
90 * resource path (`projects/{project}/global/networks/{network}`), or a
91 * Compute self-link. Immutable — changing it replaces the range.
92 */
93 network: string;
94 /**
95 * How the range may be used. `FOR_VPC` can be associated with VPC
96 * resources; `EXTERNAL_TO_VPC` blocks space (on-prem, advertised
97 * routes); `FOR_MIGRATION` locks a subnet during peer-VPC migration.
98 * Immutable — changing it replaces the range.
99 * @default "FOR_VPC"
100 */
101 usage?: InternalRangeUsage;
102 /**
103 * Peering behavior. `FOR_SELF` (default) is used in this VPC and
104 * visible to peers; `FOR_PEER` donates the range to peers;
105 * `NOT_SHARED` keeps it local. Immutable — changing it replaces the
106 * range.
107 * @default "FOR_SELF"
108 */
109 peering?: InternalRangePeering;
110 /**
111 * Explicit CIDR this range reserves (e.g. `"10.0.0.0/24"`). Required
112 * unless `prefixLength` is set. IPv6 ranges must set this field.
113 */
114 ipCidrRange?: string;
115 /**
116 * Auto-allocate a free block of this prefix length when
117 * `ipCidrRange` is omitted. If both are set, the sizes must match.
118 * Updates may resize the range.
119 */
120 prefixLength?: number;
121 /**
122 * Address spaces to search when auto-allocating. Defaults to RFC1918
123 * (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`).
124 */
125 targetCidrRange?: string[];
126 /**
127 * CIDR blocks to skip during auto-allocation, without reserving them.
128 */
129 excludeCidrRanges?: string[];
130 /**
131 * Overlaps to allow (`OVERLAP_ROUTE_RANGE`,
132 * `OVERLAP_EXISTING_SUBNET_RANGE`).
133 */
134 overlaps?: InternalRangeOverlap[];
135 /**
136 * Human-readable description of the range.
137 */
138 description?: string;
139 /**
140 * User labels. Alchemy ownership labels are merged in automatically.
141 */
142 labels?: Record<string, string>;
143 /**
144 * When true, only `labels` and `description` can change after create.
145 * Immutable — changing it replaces the range.
146 * @default false
147 */
148 immutable?: boolean;
149 /**
150 * Auto-allocation options. Set only when allocating by `prefixLength`.
151 */
152 allocationOptions?: InternalRangeAllocationOptions;
153 /**
154 * Source and target subnet URIs. Required (and only valid) when
155 * `usage` is `FOR_MIGRATION`. Immutable — changing it replaces the
156 * range.
157 */
158 migration?: InternalRangeMigration;
159};
160
161export type InternalRange = Resource<
162 "GCP.NetworkConnectivity.InternalRange",
163 InternalRangeProps,
164 {
165 /** Full resource name `projects/{project}/locations/{location}/internalRanges/{internal_range}`. */
166 name: string;
167 /** Internal range id (last path segment). */
168 internalRangeId: string;
169 /** Project id. */
170 project: string;
171 /** Location id. Internal ranges are global — always `"global"`. */
172 location: string;
173 /** Network URI as reported by the API. */
174 network: string | undefined;
175 /** VPC network id (last path segment). */
176 networkName: string | undefined;
177 /** Usage mode. */
178 usage: string | undefined;
179 /** Peering mode. */
180 peering: string | undefined;
181 /** Allocated CIDR. */
182 ipCidrRange: string | undefined;
183 /** Prefix length used for allocation or reported size. */
184 prefixLength: number | undefined;
185 /** Auto-allocation search spaces. */
186 targetCidrRange: ReadonlyArray<string>;
187 /** CIDRs excluded from auto-allocation. */
188 excludeCidrRanges: ReadonlyArray<string>;
189 /** Allowed overlap types. */
190 overlaps: ReadonlyArray<string>;
191 /** User-provided description. */
192 description: string | undefined;
193 /** User labels (Alchemy ownership labels stripped). */
194 labels: Record<string, string>;
195 /** Whether the range is immutable besides labels/description. */
196 immutable: boolean;
197 /** Auto-allocation options, if set. */
198 allocationOptions: InternalRangeAllocationOptions | undefined;
199 /** Migration endpoints, if set. */
200 migration: InternalRangeMigration | undefined;
201 /** Resources currently using this range (output-only). */
202 users: ReadonlyArray<string>;
203 /** RFC3339 creation timestamp. */
204 createTime: string | undefined;
205 /** RFC3339 last-update timestamp. */
206 updateTime: string | undefined;
207 },
208 never,
209 Providers
210>;
211
212/**
213 * A Network Connectivity internal range — an IPAM reservation inside a
214 * VPC, with usage and peering behavior.
215 *
216 * Ranges live at `locations/global`. `internalRangeId`, `location`,
217 * `network`, `usage`, `peering`, `immutable`, and `migration` are
218 * immutable. Description, labels, CIDR, prefix length, overlaps, target
219 * CIDRs, exclude CIDRs, and allocation options update in place.
220 *
221 * ### Creating an Internal Range
222 * **Example:** Generated name on a custom VPC
223 * ```typescript
224 * const network = yield* GCP.Compute.Network("Vpc", {
225 * autoCreateSubnetworks: false,
226 * });
227 * const range = yield* GCP.NetworkConnectivity.InternalRange("Reserved", {
228 * network: network.selfLink ?? network.networkName,
229 * usage: "FOR_VPC",
230 * peering: "FOR_SELF",
231 * ipCidrRange: "10.0.0.0/24",
232 * });
233 * ```
234 *
235 * **Example:** Explicit id, labels, and description
236 * ```typescript
237 * const range = yield* GCP.NetworkConnectivity.InternalRange("Reserved", {
238 * internalRangeId: "app-reserved",
239 * network: "projects/{project}/global/networks/app-vpc",
240 * usage: "FOR_VPC",
241 * peering: "FOR_SELF",
242 * ipCidrRange: "10.0.0.0/24",
243 * description: "app subnet space",
244 * labels: { env: "prod" },
245 * });
246 * ```
247 *
248 * ### Auto-allocation
249 * **Example:** Free /24 from a target space
250 * ```typescript
251 * const range = yield* GCP.NetworkConnectivity.InternalRange("Reserved", {
252 * network: network.networkName,
253 * usage: "FOR_VPC",
254 * peering: "FOR_SELF",
255 * prefixLength: 24,
256 * targetCidrRange: ["192.168.0.0/16"],
257 * allocationOptions: { allocationStrategy: "FIRST_SMALLEST_FITTING" },
258 * });
259 * ```
260 *
261 * ### External reservation
262 * **Example:** Block on-prem space
263 * ```typescript
264 * const range = yield* GCP.NetworkConnectivity.InternalRange("OnPrem", {
265 * network: network.networkName,
266 * usage: "EXTERNAL_TO_VPC",
267 * peering: "FOR_SELF",
268 * ipCidrRange: "172.16.0.0/24",
269 * labels: { role: "on-prem" },
270 * });
271 * ```
272 *
273 * ### Updating an Internal Range
274 * **Example:** Description and labels
275 * ```typescript
276 * const range = yield* GCP.NetworkConnectivity.InternalRange("Reserved", {
277 * internalRangeId: "app-reserved",
278 * network: "app-vpc",
279 * usage: "FOR_VPC",
280 * peering: "FOR_SELF",
281 * ipCidrRange: "10.0.0.0/24",
282 * description: "app subnet space v2",
283 * labels: { env: "prod", role: "ipam" },
284 * });
285 * ```
286 *
287 * @resource
288 * @category NetworkConnectivity
289 */
290export const InternalRange = Resource<InternalRange>(
291 "GCP.NetworkConnectivity.InternalRange",
292);
293
294export class InternalRangeNotResolved extends Data.TaggedError(
295 "GCP.NetworkConnectivity.InternalRangeNotResolved",
296)<{
297 name: string;
298}> {}
299
300export class InternalRangeRangeMissing extends Data.TaggedError(
301 "GCP.NetworkConnectivity.InternalRangeRangeMissing",
302)<{
303 name: string;
304 message: string;
305}> {}
306
307export class InternalRangeStillExists extends Data.TaggedError(
308 "GCP.NetworkConnectivity.InternalRangeStillExists",
309)<{
310 name: string;
311}> {}
312
313const lastSegment = (value: string) => {
314 const trimmed = value.replace(/\/+$/, "");
315 const parts = trimmed.split("/");
316 return parts[parts.length - 1] || trimmed;
317};
318
319const rfc1035 = (name: string): string => {
320 let next = name
321 .toLowerCase()
322 .replace(/[^a-z0-9-]/g, "-")
323 .replace(/-+/g, "-")
324 .replace(/^-+/, "")
325 .replace(/-+$/, "");
326 if (!/^[a-z]/.test(next)) {
327 next = `r${next}`;
328 }
329 next = next.slice(0, MAX_NAME_LENGTH).replace(/-+$/, "");
330 return next.length > 0 ? next : "internalrange";
331};
332
333const normalizeLocation = (location: string | undefined) =>
334 lastSegment(location ?? DEFAULT_LOCATION).toLowerCase();
335
336const normalizeUsage = (usage: string | undefined) =>
337 (usage ?? DEFAULT_USAGE).toUpperCase();
338
339const normalizePeering = (peering: string | undefined) =>
340 (peering ?? DEFAULT_PEERING).toUpperCase();
341
342const networkNameOf = (network: string | undefined) =>
343 network === undefined || network.length === 0
344 ? undefined
345 : lastSegment(network);
346
347const toNetworkResource = (project: string, network: string) => {
348 const trimmed = network.trim();
349 if (trimmed.includes("/")) return trimmed;
350 return `projects/${project}/global/networks/${trimmed}`;
351};
352
353const resourceName = (
354 project: string,
355 location: string,
356 internalRangeId: string,
357) =>
358 `projects/${project}/locations/${location}/internalRanges/${internalRangeId}`;
359
360const parentOf = (project: string, location: string) =>
361 `projects/${project}/locations/${location}`;
362
363const parseName = (name: string) => {
364 const parts = name.split("/").filter((part) => part.length > 0);
365 const rangesAt = parts.lastIndexOf("internalRanges");
366 const locationsAt = parts.lastIndexOf("locations");
367 const projectsAt = parts.lastIndexOf("projects");
368 return {
369 project:
370 projectsAt >= 0 && parts[projectsAt + 1] ? parts[projectsAt + 1]! : "",
371 location:
372 locationsAt >= 0 && parts[locationsAt + 1]
373 ? parts[locationsAt + 1]!
374 : DEFAULT_LOCATION,
375 internalRangeId:
376 rangesAt >= 0 && parts[rangesAt + 1]
377 ? parts[rangesAt + 1]!
378 : lastSegment(name),
379 };
380};
381
382const userLabels = (
383 labels: Record<string, string | undefined> | null | undefined,
384): Record<string, string> => stripInternalLabels(tagRecord(labels));
385
386const toId = (
387 id: string,
388 internalRangeId: string | undefined,
389 existing?: string,
390) =>
391 Effect.gen(function* () {
392 if (internalRangeId !== undefined) return internalRangeId;
393 if (existing !== undefined) return existing;
394 return rfc1035(
395 yield* createPhysicalName({
396 id,
397 maxLength: MAX_NAME_LENGTH,
398 lowercase: true,
399 }),
400 );
401 });
402
403const toAllocationOptions = (
404 options: networkconnectivity.AllocationOptions | undefined,
405): InternalRangeAllocationOptions | undefined => {
406 if (options === undefined) return undefined;
407 if (
408 options.allocationStrategy === undefined &&
409 options.firstAvailableRangesLookupSize === undefined
410 ) {
411 return undefined;
412 }
413 return {
414 allocationStrategy: options.allocationStrategy,
415 firstAvailableRangesLookupSize: options.firstAvailableRangesLookupSize,
416 };
417};
418
419const toMigration = (
420 migration: networkconnectivity.Migration | undefined,
421): InternalRangeMigration | undefined => {
422 if (
423 migration === undefined ||
424 (migration.source === undefined && migration.target === undefined)
425 ) {
426 return undefined;
427 }
428 return {
429 source: migration.source ?? "",
430 target: migration.target ?? "",
431 };
432};
433
434const toAttrs = (range: networkconnectivity.InternalRange, project: string) => {
435 const name = range.name ?? "";
436 const parsed = parseName(name);
437 return {
438 name,
439 internalRangeId: parsed.internalRangeId,
440 project: parsed.project || project,
441 location: parsed.location || DEFAULT_LOCATION,
442 network: range.network,
443 networkName: networkNameOf(range.network),
444 usage: range.usage,
445 peering: range.peering,
446 ipCidrRange: range.ipCidrRange,
447 prefixLength: range.prefixLength,
448 targetCidrRange: range.targetCidrRange ?? [],
449 excludeCidrRanges: range.excludeCidrRanges ?? [],
450 overlaps: range.overlaps ?? [],
451 description: range.description,
452 labels: userLabels(range.labels),
453 immutable: range.immutable === true,
454 allocationOptions: toAllocationOptions(range.allocationOptions),
455 migration: toMigration(range.migration),
456 users: range.users ?? [],
457 createTime: range.createTime,
458 updateTime: range.updateTime,
459 };
460};
461
462const getByName = (name: string) =>
463 networkconnectivity
464 .getProjectsLocationsInternalRanges({ name })
465 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
466
467const waitUntilExists = (name: string) =>
468 getByName(name).pipe(
469 Effect.flatMap((range) =>
470 range
471 ? Effect.succeed(range)
472 : Effect.fail(new InternalRangeNotResolved({ name })),
473 ),
474 Effect.retry({
475 while: (error) =>
476 error._tag === "GCP.NetworkConnectivity.InternalRangeNotResolved",
477 times: 10,
478 schedule: Schedule.spaced("1 second"),
479 }),
480 );
481
482const waitUntilGone = (name: string) =>
483 getByName(name).pipe(
484 Effect.flatMap((range) =>
485 range === undefined
486 ? Effect.void
487 : Effect.fail(new InternalRangeStillExists({ name })),
488 ),
489 Effect.retry({
490 while: (error) =>
491 error._tag === "GCP.NetworkConnectivity.InternalRangeStillExists",
492 times: 10,
493 schedule: Schedule.spaced("1 second"),
494 }),
495 );
496
497const listOwnedInternalRanges = (parent: string, project: string) =>
498 networkconnectivity.listProjectsLocationsInternalRanges
499 .pages({
500 parent,
501 pageSize: 1000,
502 })
503 .pipe(
504 Stream.flatMap((page) => Stream.fromIterable(page.internalRanges ?? [])),
505 Stream.filter((range) =>
506 Object.keys(range.labels ?? {}).some((key) =>
507 key.startsWith("alchemy-"),
508 ),
509 ),
510 Stream.map((range) => toAttrs(range, project)),
511 Stream.runCollect,
512 Effect.map((chunk) => Array.from(chunk)),
513 Effect.catchTag("NotFound", () => Effect.succeed([])),
514 );
515
516const sameList = (left?: readonly string[], right?: readonly string[]) =>
517 [...(left ?? [])].sort().join("\0") === [...(right ?? [])].sort().join("\0");
518
519const sameAllocation = (
520 left: InternalRangeAllocationOptions | undefined,
521 right: networkconnectivity.AllocationOptions | undefined,
522) =>
523 (left?.allocationStrategy ?? "") === (right?.allocationStrategy ?? "") &&
524 (left?.firstAvailableRangesLookupSize ?? undefined) ===
525 (right?.firstAvailableRangesLookupSize ?? undefined);
526
527const sameMigration = (
528 left: InternalRangeMigration | undefined,
529 right: InternalRangeMigration | undefined,
530) =>
531 (left?.source ?? "") === (right?.source ?? "") &&
532 (left?.target ?? "") === (right?.target ?? "");
533
534const toCreateBody = (
535 news: InternalRangeProps,
536 project: string,
537 desiredLabels: Record<string, string>,
538): networkconnectivity.InternalRange => ({
539 description: news.description,
540 labels: desiredLabels,
541 network: toNetworkResource(project, news.network),
542 usage: news.usage ?? DEFAULT_USAGE,
543 peering: news.peering ?? DEFAULT_PEERING,
544 ipCidrRange: news.ipCidrRange,
545 prefixLength: news.prefixLength,
546 targetCidrRange: news.targetCidrRange,
547 excludeCidrRanges: news.excludeCidrRanges,
548 overlaps: news.overlaps,
549 immutable: news.immutable,
550 allocationOptions: news.allocationOptions,
551 migration: news.migration,
552});
553
554export const InternalRangeProvider = () =>
555 Provider.succeed(InternalRange, {
556 stables: [
557 "name",
558 "internalRangeId",
559 "project",
560 "location",
561 "networkName",
562 "createTime",
563 ],
564
565 diff: Effect.fn(function* ({ news, olds, output }) {
566 if (!isResolved(news)) return undefined;
567
568 const previousId = olds?.internalRangeId ?? output?.internalRangeId;
569 const nextId = news.internalRangeId ?? previousId;
570 const idChanged =
571 previousId !== undefined &&
572 nextId !== undefined &&
573 nextId !== previousId;
574
575 const previousLocation = normalizeLocation(
576 olds?.location ?? output?.location,
577 );
578 const nextLocation = normalizeLocation(
579 news.location ?? olds?.location ?? output?.location,
580 );
581 const locationChanged = previousLocation !== nextLocation;
582
583 const previousNetwork = networkNameOf(
584 olds?.network ?? output?.networkName ?? output?.network,
585 );
586 const nextNetwork = networkNameOf(news.network ?? previousNetwork);
587 const networkChanged =
588 previousNetwork !== undefined &&
589 nextNetwork !== undefined &&
590 previousNetwork !== nextNetwork;
591
592 const previousUsage = normalizeUsage(olds?.usage ?? output?.usage);
593 const nextUsage = normalizeUsage(
594 news.usage ?? olds?.usage ?? output?.usage,
595 );
596 const usageChanged = previousUsage !== nextUsage;
597
598 const previousPeering = normalizePeering(
599 olds?.peering ?? output?.peering,
600 );
601 const nextPeering = normalizePeering(
602 news.peering ?? olds?.peering ?? output?.peering,
603 );
604 const peeringChanged = previousPeering !== nextPeering;
605
606 const previousImmutable = olds?.immutable ?? output?.immutable ?? false;
607 const nextImmutable = news.immutable ?? previousImmutable;
608 const immutableChanged = previousImmutable !== nextImmutable;
609
610 const previousMigration = olds?.migration ?? output?.migration;
611 const nextMigration = news.migration ?? previousMigration;
612 const migrationChanged = !sameMigration(previousMigration, nextMigration);
613
614 if (
615 !idChanged &&
616 !locationChanged &&
617 !networkChanged &&
618 !usageChanged &&
619 !peeringChanged &&
620 !immutableChanged &&
621 !migrationChanged
622 ) {
623 return undefined;
624 }
625
626 return {
627 action: "replace" as const,
628 deleteFirst:
629 !idChanged &&
630 !locationChanged &&
631 previousId !== undefined &&
632 nextId === previousId,
633 };
634 }),
635
636 read: Effect.fn(function* ({ id, olds, output }) {
637 const env = yield* GcpEnvironment.current;
638 const internalRangeId = yield* toId(
639 id,
640 olds?.internalRangeId,
641 output?.internalRangeId,
642 );
643 const location = normalizeLocation(olds?.location ?? output?.location);
644 const name =
645 output?.name ?? resourceName(env.project, location, internalRangeId);
646 const existing = yield* getByName(name);
647 if (existing === undefined) return undefined;
648 const attrs = toAttrs(existing, env.project);
649 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
650 ? attrs
651 : Unowned(attrs);
652 }),
653
654 list: () =>
655 Effect.gen(function* () {
656 const env = yield* GcpEnvironment.current;
657 const aggregated = yield* listOwnedInternalRanges(
658 parentOf(env.project, "-"),
659 env.project,
660 );
661 if (aggregated.length > 0) return aggregated;
662 return yield* listOwnedInternalRanges(
663 parentOf(env.project, DEFAULT_LOCATION),
664 env.project,
665 );
666 }),
667
668 reconcile: Effect.fn(function* ({ id, news, output }) {
669 const env = yield* GcpEnvironment.current;
670 const internalRangeId = yield* toId(
671 id,
672 news.internalRangeId,
673 output?.internalRangeId,
674 );
675 const location = normalizeLocation(news.location ?? output?.location);
676 const name = resourceName(env.project, location, internalRangeId);
677 const desiredLabels = {
678 ...toLabels(news.labels),
679 ...(yield* createInternalLabels(id)),
680 };
681
682 let current = yield* getByName(name);
683
684 if (current === undefined) {
685 if (news.ipCidrRange === undefined && news.prefixLength === undefined) {
686 return yield* new InternalRangeRangeMissing({
687 name,
688 message:
689 "InternalRange requires ipCidrRange or prefixLength on create",
690 });
691 }
692 const created = yield* networkconnectivity
693 .createProjectsLocationsInternalRanges({
694 parent: parentOf(env.project, location),
695 internalRangeId,
696 body: toCreateBody(news, env.project, desiredLabels),
697 })
698 .pipe(
699 Effect.retry({
700 while: (error) => error._tag === "Conflict",
701 times: 5,
702 schedule: Schedule.spaced("2 seconds"),
703 }),
704 Effect.catchTag("Conflict", () => Effect.succeed(undefined)),
705 );
706 if (created !== undefined) {
707 yield* waitForOperation(created);
708 }
709 current = yield* waitUntilExists(name);
710 }
711
712 if (current === undefined) {
713 return yield* new InternalRangeNotResolved({ name });
714 }
715
716 const observedLabels = tagRecord(current.labels);
717 const { upsert, removed } = diffLabels(observedLabels, desiredLabels);
718 const labelsChanged = upsert.length > 0 || removed.length > 0;
719 const descriptionChanged =
720 (current.description ?? "") !== (news.description ?? "");
721 const cidrChanged =
722 news.ipCidrRange !== undefined &&
723 (current.ipCidrRange ?? "") !== news.ipCidrRange;
724 const prefixChanged =
725 news.prefixLength !== undefined &&
726 current.prefixLength !== news.prefixLength;
727 const targetChanged =
728 news.targetCidrRange !== undefined &&
729 !sameList(news.targetCidrRange, current.targetCidrRange);
730 const excludeChanged =
731 news.excludeCidrRanges !== undefined &&
732 !sameList(news.excludeCidrRanges, current.excludeCidrRanges);
733 const overlapsChanged =
734 news.overlaps !== undefined &&
735 !sameList(news.overlaps, current.overlaps);
736 const allocationChanged =
737 news.allocationOptions !== undefined &&
738 !sameAllocation(news.allocationOptions, current.allocationOptions);
739
740 const locked = current.immutable === true;
741 const mutableChanged =
742 !locked &&
743 (cidrChanged ||
744 prefixChanged ||
745 targetChanged ||
746 excludeChanged ||
747 overlapsChanged ||
748 allocationChanged);
749
750 if (labelsChanged || descriptionChanged || mutableChanged) {
751 const updateMask = [
752 labelsChanged ? "labels" : undefined,
753 descriptionChanged ? "description" : undefined,
754 !locked && cidrChanged ? "ipCidrRange" : undefined,
755 !locked && prefixChanged ? "prefixLength" : undefined,
756 !locked && targetChanged ? "targetCidrRange" : undefined,
757 !locked && excludeChanged ? "excludeCidrRanges" : undefined,
758 !locked && overlapsChanged ? "overlaps" : undefined,
759 !locked && allocationChanged ? "allocationOptions" : undefined,
760 ].filter((field): field is string => field !== undefined);
761
762 const operation =
763 yield* networkconnectivity.patchProjectsLocationsInternalRanges({
764 name: current.name ?? name,
765 updateMask: updateMask.join(","),
766 body: {
767 name: current.name ?? name,
768 labels: desiredLabels,
769 description: news.description,
770 ipCidrRange: news.ipCidrRange,
771 prefixLength: news.prefixLength,
772 targetCidrRange: news.targetCidrRange,
773 excludeCidrRanges: news.excludeCidrRanges,
774 overlaps: news.overlaps,
775 allocationOptions: news.allocationOptions,
776 },
777 });
778 yield* waitForOperation(operation);
779 current = yield* waitUntilExists(current.name ?? name);
780 }
781
782 return toAttrs(current, env.project);
783 }),
784
785 delete: Effect.fn(function* ({ output }) {
786 const operation = yield* networkconnectivity
787 .deleteProjectsLocationsInternalRanges({ name: output.name })
788 .pipe(
789 Effect.retry({
790 while: (error) => error._tag === "Conflict",
791 times: 8,
792 schedule: Schedule.spaced("2 seconds"),
793 }),
794 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
795 );
796 if (operation !== undefined) {
797 yield* waitForOperation(operation, { notFoundOk: true });
798 }
799 yield* waitUntilGone(output.name);
800 }),
801 });