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 { GcpEnvironment } from "../Environment.ts";
13import {
14 alchemyLabelKeys,
15 createInternalLabels,
16 hasAlchemyLabels,
17} from "../Labels.ts";
18import type { Providers } from "../Providers.ts";
19
20const MAX_NAME_LENGTH = 63;
21
22export type SnapshotSchedulePolicy =
23 compute.ResourcePolicySnapshotSchedulePolicy;
24export type InstanceSchedulePolicy =
25 compute.ResourcePolicyInstanceSchedulePolicy;
26export type GroupPlacementPolicy = compute.ResourcePolicyGroupPlacementPolicy;
27export type WorkloadPolicy = compute.ResourcePolicyWorkloadPolicy;
28export type DiskConsistencyGroupPolicy =
29 compute.ResourcePolicyDiskConsistencyGroupPolicy;
30
31export type ResourcePolicyProps = {
32 /**
33 * Resource policy name (RFC1035, 1-63 characters). If omitted, a unique
34 * name is generated from the stack, stage, and logical id. Changing the
35 * name replaces the policy.
36 */
37 resourcePolicyName?: string;
38 /**
39 * Region the policy lives in (e.g. `us-central1`). Immutable — changing
40 * it replaces the policy. `US-CENTRAL1` is accepted and normalized to
41 * `us-central1`.
42 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
43 */
44 region?: string;
45 /**
46 * Optional description. Compute resource policies have no labels field,
47 * so Alchemy ownership (`alchemy-stack` / `alchemy-stage` / `alchemy-id`)
48 * is stored in a `[alchemy …]` prefix for `list` / nuke.
49 */
50 description?: string;
51 /**
52 * Snapshot schedule attached to persistent disks. Mutable in place via
53 * `resourcePolicies.patch`. Mutually exclusive with the other policy
54 * kinds — switching kind replaces the resource.
55 */
56 snapshotSchedulePolicy?: SnapshotSchedulePolicy;
57 /**
58 * Start/stop schedule for VM instances. Mutable in place. Mutually
59 * exclusive with the other policy kinds.
60 */
61 instanceSchedulePolicy?: InstanceSchedulePolicy;
62 /**
63 * Compact / collocated placement for instances. Immutable — changing it
64 * replaces the policy.
65 */
66 groupPlacementPolicy?: GroupPlacementPolicy;
67 /**
68 * Workload placement for managed instance groups. Immutable — changing
69 * it replaces the policy.
70 */
71 workloadPolicy?: WorkloadPolicy;
72 /**
73 * Disk consistency-group policy. Immutable — changing it replaces the
74 * policy.
75 */
76 diskConsistencyGroupPolicy?: DiskConsistencyGroupPolicy;
77};
78
79export type ResourcePolicy = Resource<
80 "GCP.Compute.ResourcePolicy",
81 ResourcePolicyProps,
82 {
83 /** Resource policy name. */
84 resourcePolicyName: string;
85 /** Server-assigned numeric id. */
86 resourcePolicyId: string | undefined;
87 /** Project id. */
88 project: string;
89 /** Region short name (`us-central1`). */
90 region: string;
91 /** User description with the Alchemy ownership prefix stripped. */
92 description: string | undefined;
93 /** Server-reported status (`READY`, `CREATING`, `DELETING`, …). */
94 status: string | undefined;
95 /** Snapshot schedule, if this is a snapshot policy. */
96 snapshotSchedulePolicy: SnapshotSchedulePolicy | undefined;
97 /** Instance start/stop schedule, if this is an instance-schedule policy. */
98 instanceSchedulePolicy: InstanceSchedulePolicy | undefined;
99 /** Group placement policy, if any. */
100 groupPlacementPolicy: GroupPlacementPolicy | undefined;
101 /** Workload policy, if any. */
102 workloadPolicy: WorkloadPolicy | undefined;
103 /** Disk consistency-group policy, if any. */
104 diskConsistencyGroupPolicy: DiskConsistencyGroupPolicy | undefined;
105 /** Output-only system status (e.g. next instance-schedule run). */
106 resourceStatus: compute.ResourcePolicyResourceStatus | undefined;
107 /** Compute Engine self-link. */
108 selfLink: string | undefined;
109 /** RFC3339 creation timestamp. */
110 creationTimestamp: string | undefined;
111 },
112 never,
113 Providers
114>;
115
116/**
117 * A regional Compute Engine resource policy.
118 *
119 * Resource policies schedule snapshots, start/stop VMs, or describe
120 * placement. Compute Engine has no labels on this resource, so Alchemy
121 * stamps ownership into the description (`[alchemy alchemy-stack=…
122 * alchemy-stage=… alchemy-id=…]`) so `list` / `pnpm nuke:gcp` can find
123 * them.
124 *
125 * Name, region, and policy kind (`snapshotSchedulePolicy` vs
126 * `instanceSchedulePolicy` vs placement/workload/consistency) are
127 * immutable — changing them replaces the policy. Description and the
128 * nested snapshot / instance schedules update in place via
129 * `resourcePolicies.patch`.
130 *
131 * ### Creating a Resource Policy
132 * **Example:** Daily snapshot schedule
133 * ```typescript
134 * const nightly = yield* GCP.Compute.ResourcePolicy("Nightly", {
135 * snapshotSchedulePolicy: {
136 * schedule: {
137 * dailySchedule: { daysInCycle: 1, startTime: "04:00" },
138 * },
139 * retentionPolicy: { maxRetentionDays: 7 },
140 * },
141 * });
142 * ```
143 *
144 * **Example:** Named policy with labels in the snapshot properties
145 * ```typescript
146 * const backups = yield* GCP.Compute.ResourcePolicy("Backups", {
147 * resourcePolicyName: "app-disk-nightly",
148 * region: "us-central1",
149 * description: "nightly disk snapshots",
150 * snapshotSchedulePolicy: {
151 * schedule: {
152 * dailySchedule: { daysInCycle: 1, startTime: "04:00" },
153 * },
154 * retentionPolicy: {
155 * maxRetentionDays: 14,
156 * onSourceDiskDelete: "KEEP_AUTO_SNAPSHOTS",
157 * },
158 * snapshotProperties: {
159 * storageLocations: ["us"],
160 * labels: { env: "prod" },
161 * },
162 * },
163 * });
164 * ```
165 *
166 * ### Instance Schedules
167 * **Example:** Start and stop VMs on weekdays
168 * ```typescript
169 * const hours = yield* GCP.Compute.ResourcePolicy("OfficeHours", {
170 * instanceSchedulePolicy: {
171 * timeZone: "America/Chicago",
172 * vmStartSchedule: { schedule: "0 8 * * 1-5" },
173 * vmStopSchedule: { schedule: "0 18 * * 1-5" },
174 * },
175 * });
176 * ```
177 *
178 * @resource
179 * @category Compute
180 */
181export const ResourcePolicy = Resource<ResourcePolicy>(
182 "GCP.Compute.ResourcePolicy",
183);
184
185export class ResourcePolicyNotResolved extends Data.TaggedError(
186 "GCP.Compute.ResourcePolicyNotResolved",
187)<{
188 resourcePolicyName: string;
189 region: string;
190}> {}
191
192export class ResourcePolicyNotReady extends Data.TaggedError(
193 "GCP.Compute.ResourcePolicyNotReady",
194)<{
195 resourcePolicyName: string;
196 status: string;
197}> {}
198
199export class ResourcePolicyFailed extends Data.TaggedError(
200 "GCP.Compute.ResourcePolicyFailed",
201)<{
202 resourcePolicyName: string;
203 status: string;
204}> {}
205
206export class ResourcePolicyStillExists extends Data.TaggedError(
207 "GCP.Compute.ResourcePolicyStillExists",
208)<{
209 resourcePolicyName: string;
210 status: string;
211}> {}
212
213type PolicyKind =
214 | "snapshot"
215 | "instance"
216 | "placement"
217 | "workload"
218 | "consistency"
219 | "none";
220
221const lastSegment = (value: string | undefined): string => {
222 if (value === undefined || value.length === 0) return "";
223 const trimmed = value.replace(/\/+$/, "");
224 const parts = trimmed.split("/");
225 return parts[parts.length - 1] || trimmed;
226};
227
228const normalizeRegion = (region: string | undefined, defaultRegion: string) =>
229 lastSegment(region ?? defaultRegion).toLowerCase();
230
231const toName = (id: string, name: string | undefined, existing?: string) =>
232 Effect.gen(function* () {
233 if (name !== undefined) return name;
234 if (existing !== undefined) return existing;
235 const generated = yield* createPhysicalName({
236 id,
237 maxLength: MAX_NAME_LENGTH,
238 lowercase: true,
239 });
240 return /^[a-z]/.test(generated)
241 ? generated
242 : `p${generated}`.slice(0, MAX_NAME_LENGTH);
243 });
244
245const encodeDescription = (
246 labels: Record<string, string>,
247 description: string | undefined,
248): string => {
249 const marker = `[alchemy ${alchemyLabelKeys.stack}=${labels[alchemyLabelKeys.stack]} ${alchemyLabelKeys.stage}=${labels[alchemyLabelKeys.stage]} ${alchemyLabelKeys.id}=${labels[alchemyLabelKeys.id]}]`;
250 return description ? `${marker}\n${description}` : marker;
251};
252
253const parseDescription = (
254 description: string | undefined,
255): {
256 labels: Record<string, string>;
257 description: string | undefined;
258} => {
259 if (!description?.startsWith("[alchemy ")) {
260 return { labels: {}, description };
261 }
262 const end = description.indexOf("]");
263 if (end < 0) return { labels: {}, description };
264 const labels: Record<string, string> = {};
265 for (const part of description.slice("[alchemy ".length, end).split(/\s+/)) {
266 const eq = part.indexOf("=");
267 if (eq > 0) {
268 labels[part.slice(0, eq)] = part.slice(eq + 1);
269 }
270 }
271 const rest = description.slice(end + 1).replace(/^\n/, "");
272 return { labels, description: rest.length > 0 ? rest : undefined };
273};
274
275const hasOwnershipMarker = (description: string | undefined) =>
276 Object.keys(parseDescription(description).labels).some((key) =>
277 key.startsWith("alchemy-"),
278 );
279
280const policyKindOf = (props: {
281 snapshotSchedulePolicy?: unknown;
282 instanceSchedulePolicy?: unknown;
283 groupPlacementPolicy?: unknown;
284 workloadPolicy?: unknown;
285 diskConsistencyGroupPolicy?: unknown;
286}): PolicyKind => {
287 if (props.snapshotSchedulePolicy !== undefined) return "snapshot";
288 if (props.instanceSchedulePolicy !== undefined) return "instance";
289 if (props.groupPlacementPolicy !== undefined) return "placement";
290 if (props.workloadPolicy !== undefined) return "workload";
291 if (props.diskConsistencyGroupPolicy !== undefined) return "consistency";
292 return "none";
293};
294
295const toBody = (
296 resourcePolicyName: string,
297 props: ResourcePolicyProps,
298 ownership: Record<string, string>,
299): compute.ResourcePolicy => ({
300 name: resourcePolicyName,
301 description: encodeDescription(ownership, props.description),
302 snapshotSchedulePolicy: props.snapshotSchedulePolicy,
303 instanceSchedulePolicy: props.instanceSchedulePolicy,
304 groupPlacementPolicy: props.groupPlacementPolicy,
305 workloadPolicy: props.workloadPolicy,
306 diskConsistencyGroupPolicy: props.diskConsistencyGroupPolicy,
307});
308
309const toAttrs = (policy: compute.ResourcePolicy, project: string) => {
310 const parsed = parseDescription(policy.description);
311 return {
312 resourcePolicyName: policy.name ?? policy.id ?? "",
313 resourcePolicyId: policy.id,
314 project,
315 region: lastSegment(policy.region).toLowerCase(),
316 description: parsed.description,
317 status: policy.status,
318 snapshotSchedulePolicy: policy.snapshotSchedulePolicy,
319 instanceSchedulePolicy: policy.instanceSchedulePolicy,
320 groupPlacementPolicy: policy.groupPlacementPolicy,
321 workloadPolicy: policy.workloadPolicy,
322 diskConsistencyGroupPolicy: policy.diskConsistencyGroupPolicy,
323 resourceStatus: policy.resourceStatus,
324 selfLink: policy.selfLink,
325 creationTimestamp: policy.creationTimestamp,
326 };
327};
328
329const normalizeTime = (value: string | undefined) => {
330 if (value === undefined || value.length === 0) return undefined;
331 const match = value.match(/^(\d{1,2}):(\d{2})/);
332 if (!match) return value;
333 return `${match[1]!.padStart(2, "0")}:${match[2]}`;
334};
335
336const sorted = (values: readonly string[] | undefined) =>
337 [...(values ?? [])].map((value) => value.toLowerCase()).sort();
338
339const canonSnapshot = (policy: SnapshotSchedulePolicy | undefined) => {
340 if (policy === undefined) return undefined;
341 const weekly = [...(policy.schedule?.weeklySchedule?.dayOfWeeks ?? [])]
342 .map((day) => ({
343 day: day.day,
344 startTime: normalizeTime(day.startTime),
345 }))
346 .sort((left, right) => (left.day ?? "").localeCompare(right.day ?? ""));
347 return {
348 retention: policy.retentionPolicy
349 ? {
350 maxRetentionDays: policy.retentionPolicy.maxRetentionDays,
351 onSourceDiskDelete: policy.retentionPolicy.onSourceDiskDelete,
352 }
353 : undefined,
354 properties: policy.snapshotProperties
355 ? {
356 storageLocations: sorted(policy.snapshotProperties.storageLocations),
357 guestFlush: policy.snapshotProperties.guestFlush === true,
358 chainName: policy.snapshotProperties.chainName ?? "",
359 labels: policy.snapshotProperties.labels ?? {},
360 }
361 : undefined,
362 daily: policy.schedule?.dailySchedule
363 ? {
364 daysInCycle: policy.schedule.dailySchedule.daysInCycle,
365 startTime: normalizeTime(policy.schedule.dailySchedule.startTime),
366 }
367 : undefined,
368 weekly: weekly.length > 0 ? weekly : undefined,
369 hourly: policy.schedule?.hourlySchedule
370 ? {
371 hoursInCycle: policy.schedule.hourlySchedule.hoursInCycle,
372 startTime: normalizeTime(policy.schedule.hourlySchedule.startTime),
373 }
374 : undefined,
375 };
376};
377
378const canonInstance = (policy: InstanceSchedulePolicy | undefined) => {
379 if (policy === undefined) return undefined;
380 return {
381 timeZone: policy.timeZone,
382 startTime: policy.startTime,
383 expirationTime: policy.expirationTime,
384 vmStart: policy.vmStartSchedule?.schedule,
385 vmStop: policy.vmStopSchedule?.schedule,
386 };
387};
388
389const subsetEqual = (observed: unknown, desired: unknown): boolean => {
390 if (desired === undefined) return true;
391 if (typeof desired !== typeof observed) return false;
392 if (desired === null || observed === null) return desired === observed;
393 if (Array.isArray(desired)) {
394 if (!Array.isArray(observed) || desired.length !== observed.length) {
395 return false;
396 }
397 return desired.every((item, index) => subsetEqual(observed[index], item));
398 }
399 if (typeof desired === "object") {
400 if (typeof observed !== "object") return false;
401 const current = observed as Record<string, unknown>;
402 return Object.entries(desired as Record<string, unknown>).every(
403 ([key, value]) => value === undefined || subsetEqual(current[key], value),
404 );
405 }
406 return observed === desired;
407};
408
409const sameJson = (left: unknown, right: unknown) =>
410 JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
411
412const snapshotChanged = (
413 observed: SnapshotSchedulePolicy | undefined,
414 desired: SnapshotSchedulePolicy | undefined,
415) => {
416 if (desired === undefined) return false;
417 return !subsetEqual(canonSnapshot(observed), canonSnapshot(desired));
418};
419
420const instanceChanged = (
421 observed: InstanceSchedulePolicy | undefined,
422 desired: InstanceSchedulePolicy | undefined,
423) => {
424 if (desired === undefined) return false;
425 return !subsetEqual(canonInstance(observed), canonInstance(desired));
426};
427
428const immutablePolicyChanged = (
429 news: ResourcePolicyProps,
430 olds: Partial<ResourcePolicyProps> | undefined,
431 output: ResourcePolicy["Attributes"] | undefined,
432) => {
433 const previousKind = policyKindOf(olds ?? output ?? {});
434 const nextKind = policyKindOf(news);
435 if (
436 nextKind !== "none" &&
437 previousKind !== "none" &&
438 previousKind !== nextKind
439 ) {
440 return true;
441 }
442 if (nextKind === "placement") {
443 return !sameJson(
444 news.groupPlacementPolicy,
445 olds?.groupPlacementPolicy ?? output?.groupPlacementPolicy,
446 );
447 }
448 if (nextKind === "workload") {
449 return !sameJson(
450 news.workloadPolicy,
451 olds?.workloadPolicy ?? output?.workloadPolicy,
452 );
453 }
454 if (nextKind === "consistency") {
455 const previous =
456 olds?.diskConsistencyGroupPolicy ?? output?.diskConsistencyGroupPolicy;
457 const next = news.diskConsistencyGroupPolicy;
458 return (previous === undefined) !== (next === undefined);
459 }
460 return false;
461};
462
463const getByName = (project: string, region: string, resourcePolicy: string) =>
464 compute
465 .getResourcePolicies({ project, region, resourcePolicy })
466 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
467
468const waitPolicyReady = (
469 project: string,
470 region: string,
471 resourcePolicyName: string,
472) =>
473 getByName(project, region, resourcePolicyName).pipe(
474 Effect.flatMap((policy) =>
475 policy?.status === "INVALID"
476 ? Effect.fail(
477 new ResourcePolicyFailed({
478 resourcePolicyName,
479 status: "INVALID",
480 }),
481 )
482 : Effect.succeed(policy),
483 ),
484 Effect.filterOrFail(
485 (policy): policy is compute.ResourcePolicy =>
486 policy !== undefined && policy.status === "READY",
487 (policy) =>
488 new ResourcePolicyNotReady({
489 resourcePolicyName,
490 status: policy?.status ?? "MISSING",
491 }),
492 ),
493 Effect.retry({
494 while: (error) => error._tag === "GCP.Compute.ResourcePolicyNotReady",
495 times: 10,
496 schedule: Schedule.spaced("2 seconds"),
497 }),
498 );
499
500const waitPolicyGone = (
501 project: string,
502 region: string,
503 resourcePolicyName: string,
504) =>
505 getByName(project, region, resourcePolicyName).pipe(
506 Effect.flatMap((policy) =>
507 policy === undefined
508 ? Effect.void
509 : Effect.fail(
510 new ResourcePolicyStillExists({
511 resourcePolicyName,
512 status: policy.status ?? "UNKNOWN",
513 }),
514 ),
515 ),
516 Effect.retry({
517 while: (error) => error instanceof ResourcePolicyStillExists,
518 times: 10,
519 schedule: Schedule.spaced("2 seconds"),
520 }),
521 );
522
523export const ResourcePolicyProvider = () =>
524 Provider.succeed(ResourcePolicy, {
525 stables: [
526 "resourcePolicyName",
527 "resourcePolicyId",
528 "project",
529 "region",
530 "selfLink",
531 "creationTimestamp",
532 ],
533
534 diff: Effect.fn(function* ({ news, olds, output }) {
535 if (!isResolved(news)) return undefined;
536 const env = yield* GcpEnvironment.current;
537
538 const previousName =
539 olds?.resourcePolicyName ?? output?.resourcePolicyName;
540 const nextName = news.resourcePolicyName ?? previousName;
541 const nameChanged =
542 previousName !== undefined &&
543 nextName !== undefined &&
544 previousName !== nextName;
545
546 const previousRegion = normalizeRegion(
547 olds?.region ?? output?.region,
548 env.region,
549 );
550 const nextRegion = normalizeRegion(
551 news.region ?? output?.region,
552 env.region,
553 );
554 const regionChanged = previousRegion !== nextRegion;
555
556 if (nameChanged || regionChanged) {
557 return { action: "replace" as const, deleteFirst: false };
558 }
559 if (immutablePolicyChanged(news, olds, output)) {
560 return { action: "replace" as const, deleteFirst: true };
561 }
562 return undefined;
563 }),
564
565 read: Effect.fn(function* ({ id, olds, output }) {
566 const env = yield* GcpEnvironment.current;
567 const resourcePolicyName = yield* toName(
568 id,
569 olds?.resourcePolicyName,
570 output?.resourcePolicyName,
571 );
572 const region = normalizeRegion(
573 olds?.region ?? output?.region,
574 env.region,
575 );
576 const existing = yield* getByName(
577 env.project,
578 region,
579 resourcePolicyName,
580 );
581 if (existing === undefined) return undefined;
582 const attrs = toAttrs(existing, env.project);
583 const { labels } = parseDescription(existing.description);
584 return (yield* hasAlchemyLabels(id, labels)) ? attrs : Unowned(attrs);
585 }),
586
587 list: () =>
588 Effect.gen(function* () {
589 const env = yield* GcpEnvironment.current;
590 const pages = yield* compute.aggregatedListResourcePolicies
591 .pages({
592 project: env.project,
593 returnPartialSuccess: true,
594 maxResults: 500,
595 })
596 .pipe(Stream.runCollect);
597 return Array.from(pages).flatMap((page) =>
598 Object.values(page.items ?? {}).flatMap((scoped) =>
599 (scoped?.resourcePolicies ?? [])
600 .filter((policy) => hasOwnershipMarker(policy.description))
601 .map((policy) => toAttrs(policy, env.project)),
602 ),
603 );
604 }),
605
606 reconcile: Effect.fn(function* ({ id, news, output }) {
607 const env = yield* GcpEnvironment.current;
608 const resourcePolicyName = yield* toName(
609 id,
610 news.resourcePolicyName,
611 output?.resourcePolicyName,
612 );
613 const region = normalizeRegion(news.region ?? output?.region, env.region);
614 const ownership = yield* createInternalLabels(id);
615 const desired = toBody(resourcePolicyName, news, ownership);
616
617 let current = yield* getByName(env.project, region, resourcePolicyName);
618 if (current?.status === "DELETING") {
619 yield* waitPolicyGone(env.project, region, resourcePolicyName);
620 current = undefined;
621 }
622
623 if (current === undefined) {
624 const inserted = yield* compute
625 .insertResourcePolicies({
626 project: env.project,
627 region,
628 body: desired,
629 })
630 .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
631 if (inserted !== undefined) {
632 yield* waitRegionOperation(env.project, region, inserted, {
633 ignore: ["RESOURCE_ALREADY_EXISTS"],
634 });
635 }
636 current = yield* waitPolicyReady(
637 env.project,
638 region,
639 resourcePolicyName,
640 );
641 }
642
643 if (current === undefined) {
644 return yield* new ResourcePolicyNotResolved({
645 resourcePolicyName,
646 region,
647 });
648 }
649
650 if (current.status === "CREATING") {
651 current = yield* waitPolicyReady(
652 env.project,
653 region,
654 resourcePolicyName,
655 );
656 }
657
658 if (current === undefined) {
659 return yield* new ResourcePolicyNotResolved({
660 resourcePolicyName,
661 region,
662 });
663 }
664
665 const descriptionChanged =
666 (current.description ?? "") !== (desired.description ?? "");
667 const snapshotNeedsPatch = snapshotChanged(
668 current.snapshotSchedulePolicy,
669 news.snapshotSchedulePolicy,
670 );
671 const instanceNeedsPatch = instanceChanged(
672 current.instanceSchedulePolicy,
673 news.instanceSchedulePolicy,
674 );
675
676 if (descriptionChanged || snapshotNeedsPatch || instanceNeedsPatch) {
677 const patched = yield* compute.patchResourcePolicies({
678 project: env.project,
679 region,
680 resourcePolicy: resourcePolicyName,
681 body: desired,
682 });
683 yield* waitRegionOperation(env.project, region, patched);
684 current =
685 (yield* getByName(env.project, region, resourcePolicyName)) ??
686 (yield* waitPolicyReady(env.project, region, resourcePolicyName));
687 }
688
689 if (current === undefined) {
690 return yield* new ResourcePolicyNotResolved({
691 resourcePolicyName,
692 region,
693 });
694 }
695
696 return toAttrs(current, env.project);
697 }),
698
699 delete: Effect.fn(function* ({ output }) {
700 const env = yield* GcpEnvironment.current;
701 const project = output.project || env.project;
702 const region = normalizeRegion(output.region, env.region);
703 const deleted = yield* compute
704 .deleteResourcePolicies({
705 project,
706 region,
707 resourcePolicy: output.resourcePolicyName,
708 })
709 .pipe(
710 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
711 Effect.retry({
712 while: (error) => error._tag === "Conflict",
713 times: 8,
714 schedule: Schedule.spaced("2 seconds"),
715 }),
716 );
717 if (deleted !== undefined) {
718 yield* waitRegionOperation(project, region, deleted, {
719 ignore: ["RESOURCE_NOT_FOUND"],
720 });
721 }
722 yield* waitPolicyGone(project, region, output.resourcePolicyName);
723 }),
724 });