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";
19import type {
20 GrpcHealthCheck,
21 HealthCheckLogConfig,
22 HealthCheckPortSpecification,
23 HealthCheckProxyHeader,
24 HealthCheckType,
25 Http2HealthCheck,
26 HttpHealthCheck,
27 HttpsHealthCheck,
28 SslHealthCheck,
29 TcpHealthCheck,
30} from "./HealthCheck.ts";
31
32const DEFAULT_CHECK_INTERVAL = 5;
33const DEFAULT_TIMEOUT = 5;
34const DEFAULT_HEALTHY = 2;
35const DEFAULT_UNHEALTHY = 2;
36const MAX_NAME_LENGTH = 63;
37
38export type RegionHealthCheckProps = {
39 /**
40 * Health check name (RFC1035, 1-63 chars). If omitted, a unique name is
41 * generated from the stack, stage, and logical id. Changing it replaces
42 * the resource.
43 */
44 healthCheckName?: string;
45 /**
46 * Region the health check lives in. Immutable — changing it replaces
47 * the resource. `US-CENTRAL1` is accepted and normalized to
48 * `us-central1`.
49 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
50 */
51 region?: string;
52 /**
53 * Optional description. Alchemy ownership is stored in a `[alchemy …]`
54 * prefix so `list` / nuke can find resources (Compute HealthCheck has no
55 * labels field).
56 */
57 description?: string;
58 /**
59 * How often to probe, in seconds.
60 * @default 5
61 */
62 checkIntervalSec?: number;
63 /**
64 * Probe timeout in seconds. Must be ≤ `checkIntervalSec`.
65 * @default 5
66 */
67 timeoutSec?: number;
68 /**
69 * Consecutive successes before an instance is healthy.
70 * @default 2
71 */
72 healthyThreshold?: number;
73 /**
74 * Consecutive failures before an instance is unhealthy.
75 * @default 2
76 */
77 unhealthyThreshold?: number;
78 /**
79 * Protocol. Inferred from the nested `*HealthCheck` block when omitted.
80 * Changing type replaces the resource.
81 * @default "HTTP"
82 */
83 type?: HealthCheckType;
84 /** HTTP probe. Used when `type` is `HTTP` (the default). */
85 httpHealthCheck?: HttpHealthCheck;
86 /** HTTPS probe. */
87 httpsHealthCheck?: HttpsHealthCheck;
88 /** HTTP/2 probe. */
89 http2HealthCheck?: Http2HealthCheck;
90 /** TCP probe. */
91 tcpHealthCheck?: TcpHealthCheck;
92 /** SSL probe. */
93 sslHealthCheck?: SslHealthCheck;
94 /** gRPC probe. */
95 grpcHealthCheck?: GrpcHealthCheck;
96 /** gRPC-with-TLS probe. */
97 grpcTlsHealthCheck?: GrpcHealthCheck;
98 /** Health-check logging. */
99 logConfig?: HealthCheckLogConfig;
100};
101
102export type RegionHealthCheck = Resource<
103 "GCP.Compute.RegionHealthCheck",
104 RegionHealthCheckProps,
105 {
106 /** Health check name. */
107 healthCheckName: string;
108 /** Project id. */
109 project: string;
110 /** Region short name (`us-central1`). */
111 region: string;
112 /** Protocol. */
113 type: HealthCheckType;
114 /** User description with the Alchemy ownership prefix stripped. */
115 description: string | undefined;
116 /** Probe interval in seconds. */
117 checkIntervalSec: number;
118 /** Probe timeout in seconds. */
119 timeoutSec: number;
120 /** Consecutive successes before healthy. */
121 healthyThreshold: number;
122 /** Consecutive failures before unhealthy. */
123 unhealthyThreshold: number;
124 /** HTTP probe, if this is an HTTP health check. */
125 httpHealthCheck: HttpHealthCheck | undefined;
126 /** HTTPS probe, if this is an HTTPS health check. */
127 httpsHealthCheck: HttpsHealthCheck | undefined;
128 /** HTTP/2 probe, if this is an HTTP/2 health check. */
129 http2HealthCheck: Http2HealthCheck | undefined;
130 /** TCP probe, if this is a TCP health check. */
131 tcpHealthCheck: TcpHealthCheck | undefined;
132 /** SSL probe, if this is an SSL health check. */
133 sslHealthCheck: SslHealthCheck | undefined;
134 /** gRPC probe, if this is a gRPC health check. */
135 grpcHealthCheck: GrpcHealthCheck | undefined;
136 /** gRPC-with-TLS probe, if this is a GRPC_WITH_TLS health check. */
137 grpcTlsHealthCheck: GrpcHealthCheck | undefined;
138 /** Logging configuration. */
139 logConfig: HealthCheckLogConfig | undefined;
140 /** Server-defined URL. */
141 selfLink: string | undefined;
142 /** Server-assigned numeric id. */
143 healthCheckId: string | undefined;
144 /** RFC3339 creation timestamp. */
145 creationTimestamp: string | undefined;
146 /** Resource kind. */
147 kind: string | undefined;
148 },
149 never,
150 Providers
151>;
152
153/**
154 * A regional Compute Engine health check.
155 *
156 * Health checks probe backends for load balancing and managed-instance-group
157 * autohealing. This resource maps to the regional `regionHealthChecks`
158 * collection (the global `healthChecks` resource is
159 * `GCP.Compute.HealthCheck`). Compute HealthCheck has no labels field —
160 * Alchemy ownership is stored in the description so nuke can find leaked
161 * checks.
162 *
163 * Changing `healthCheckName`, `region`, or `type` replaces the resource.
164 *
165 * ### Creating a Region Health Check
166 * **Example:** Generated name (HTTP on port 80)
167 * ```typescript
168 * const check = yield* GCP.Compute.RegionHealthCheck("api", {});
169 * ```
170 *
171 * **Example:** HTTP path and thresholds
172 * ```typescript
173 * const check = yield* GCP.Compute.RegionHealthCheck("api", {
174 * region: "us-central1",
175 * description: "frontend /health",
176 * checkIntervalSec: 10,
177 * timeoutSec: 5,
178 * httpHealthCheck: { port: 80, requestPath: "/health" },
179 * });
180 * ```
181 *
182 * **Example:** TCP health check
183 * ```typescript
184 * const check = yield* GCP.Compute.RegionHealthCheck("tcp", {
185 * type: "TCP",
186 * tcpHealthCheck: { port: 8080 },
187 * });
188 * ```
189 *
190 * @resource
191 * @category Compute
192 */
193export const RegionHealthCheck = Resource<RegionHealthCheck>(
194 "GCP.Compute.RegionHealthCheck",
195);
196
197export class RegionHealthCheckNotResolved extends Data.TaggedError(
198 "GCP.Compute.RegionHealthCheckNotResolved",
199)<{
200 healthCheckName: string;
201 region: string;
202}> {}
203
204const lastSegment = (value: string | undefined): string => {
205 if (value === undefined || value.length === 0) return "";
206 const parts = value.split("/").filter((part) => part.length > 0);
207 return parts[parts.length - 1] ?? value;
208};
209
210const normalizeRegion = (region: string | undefined, defaultRegion: string) =>
211 lastSegment(region ?? defaultRegion).toLowerCase();
212
213const rfc1035 = (name: string): string => {
214 let next = name
215 .toLowerCase()
216 .replace(/[^a-z0-9-]/g, "-")
217 .replace(/-+/g, "-")
218 .replace(/^-+/, "")
219 .replace(/-+$/, "");
220 if (!/^[a-z]/.test(next)) {
221 next = `h${next}`;
222 }
223 next = next.slice(0, MAX_NAME_LENGTH).replace(/-+$/, "");
224 return next.length > 0 ? next : "healthcheck";
225};
226
227const toName = (id: string, name: string | undefined, existing?: string) =>
228 Effect.gen(function* () {
229 if (name !== undefined) return name;
230 if (existing !== undefined) return existing;
231 return rfc1035(
232 yield* createPhysicalName({
233 id,
234 maxLength: MAX_NAME_LENGTH,
235 lowercase: true,
236 }),
237 );
238 });
239
240const inferType = (props: RegionHealthCheckProps): HealthCheckType => {
241 if (props.type) return props.type;
242 if (props.httpsHealthCheck) return "HTTPS";
243 if (props.http2HealthCheck) return "HTTP2";
244 if (props.sslHealthCheck) return "SSL";
245 if (props.tcpHealthCheck) return "TCP";
246 if (props.grpcHealthCheck) return "GRPC";
247 if (props.grpcTlsHealthCheck) return "GRPC_WITH_TLS";
248 return "HTTP";
249};
250
251const encodeDescription = (
252 labels: Record<string, string>,
253 description: string | undefined,
254): string => {
255 const marker = `[alchemy ${alchemyLabelKeys.stack}=${labels[alchemyLabelKeys.stack]} ${alchemyLabelKeys.stage}=${labels[alchemyLabelKeys.stage]} ${alchemyLabelKeys.id}=${labels[alchemyLabelKeys.id]}]`;
256 return description ? `${marker}\n${description}` : marker;
257};
258
259const parseDescription = (
260 description: string | undefined,
261): {
262 labels: Record<string, string>;
263 description: string | undefined;
264} => {
265 if (!description?.startsWith("[alchemy ")) {
266 return { labels: {}, description };
267 }
268 const end = description.indexOf("]");
269 if (end < 0) return { labels: {}, description };
270 const labels: Record<string, string> = {};
271 for (const part of description.slice("[alchemy ".length, end).split(/\s+/)) {
272 const eq = part.indexOf("=");
273 if (eq > 0) {
274 labels[part.slice(0, eq)] = part.slice(eq + 1);
275 }
276 }
277 const rest = description.slice(end + 1).replace(/^\n/, "");
278 return { labels, description: rest.length > 0 ? rest : undefined };
279};
280
281const hasOwnershipMarker = (description: string | undefined) =>
282 Object.keys(parseDescription(description).labels).some((key) =>
283 key.startsWith("alchemy-"),
284 );
285
286const protocolBody = (
287 type: HealthCheckType,
288 props: RegionHealthCheckProps,
289): Pick<
290 compute.HealthCheck,
291 | "httpHealthCheck"
292 | "httpsHealthCheck"
293 | "http2HealthCheck"
294 | "tcpHealthCheck"
295 | "sslHealthCheck"
296 | "grpcHealthCheck"
297 | "grpcTlsHealthCheck"
298> => {
299 switch (type) {
300 case "HTTPS":
301 return {
302 httpsHealthCheck: {
303 port: 443,
304 requestPath: "/",
305 ...props.httpsHealthCheck,
306 },
307 };
308 case "HTTP2":
309 return {
310 http2HealthCheck: {
311 port: 443,
312 requestPath: "/",
313 ...props.http2HealthCheck,
314 },
315 };
316 case "TCP":
317 return { tcpHealthCheck: { port: 80, ...props.tcpHealthCheck } };
318 case "SSL":
319 return { sslHealthCheck: { port: 443, ...props.sslHealthCheck } };
320 case "GRPC":
321 return { grpcHealthCheck: { port: 443, ...props.grpcHealthCheck } };
322 case "GRPC_WITH_TLS":
323 return {
324 grpcTlsHealthCheck: { port: 443, ...props.grpcTlsHealthCheck },
325 };
326 default:
327 return {
328 httpHealthCheck: {
329 port: 80,
330 requestPath: "/",
331 ...props.httpHealthCheck,
332 },
333 };
334 }
335};
336
337const toBody = (
338 healthCheckName: string,
339 props: RegionHealthCheckProps,
340 ownership: Record<string, string>,
341): compute.HealthCheck => {
342 const type = inferType(props);
343 return {
344 name: healthCheckName,
345 description: encodeDescription(ownership, props.description),
346 checkIntervalSec: props.checkIntervalSec ?? DEFAULT_CHECK_INTERVAL,
347 timeoutSec: props.timeoutSec ?? DEFAULT_TIMEOUT,
348 healthyThreshold: props.healthyThreshold ?? DEFAULT_HEALTHY,
349 unhealthyThreshold: props.unhealthyThreshold ?? DEFAULT_UNHEALTHY,
350 type,
351 logConfig: { enable: props.logConfig?.enable === true },
352 ...protocolBody(type, props),
353 };
354};
355
356const asType = (type: string | undefined): HealthCheckType => {
357 switch (type) {
358 case "TCP":
359 case "SSL":
360 case "HTTP":
361 case "HTTPS":
362 case "HTTP2":
363 case "GRPC":
364 case "GRPC_WITH_TLS":
365 return type;
366 default:
367 return "HTTP";
368 }
369};
370
371const asPortSpecification = (
372 value: string | undefined,
373): HealthCheckPortSpecification | undefined => {
374 switch (value) {
375 case "USE_FIXED_PORT":
376 case "USE_NAMED_PORT":
377 case "USE_SERVING_PORT":
378 return value;
379 default:
380 return undefined;
381 }
382};
383
384const asProxyHeader = (
385 value: string | undefined,
386): HealthCheckProxyHeader | undefined => {
387 switch (value) {
388 case "NONE":
389 case "PROXY_V1":
390 return value;
391 default:
392 return undefined;
393 }
394};
395
396const toHttpProbe = (
397 check:
398 | compute.HTTPHealthCheck
399 | compute.HTTPSHealthCheck
400 | compute.HTTP2HealthCheck
401 | undefined,
402): HttpHealthCheck | undefined => {
403 if (check === undefined) return undefined;
404 return {
405 host: check.host,
406 port: check.port,
407 portName: check.portName,
408 portSpecification: asPortSpecification(check.portSpecification),
409 requestPath: check.requestPath,
410 response: check.response,
411 proxyHeader: asProxyHeader(check.proxyHeader),
412 };
413};
414
415const toTcpProbe = (
416 check: compute.TCPHealthCheck | compute.SSLHealthCheck | undefined,
417): TcpHealthCheck | undefined => {
418 if (check === undefined) return undefined;
419 return {
420 port: check.port,
421 portName: check.portName,
422 portSpecification: asPortSpecification(check.portSpecification),
423 request: check.request,
424 response: check.response,
425 proxyHeader: asProxyHeader(check.proxyHeader),
426 };
427};
428
429const toGrpcProbe = (
430 check: compute.GRPCHealthCheck | compute.GRPCTLSHealthCheck | undefined,
431): GrpcHealthCheck | undefined => {
432 if (check === undefined) return undefined;
433 return {
434 port: check.port,
435 portName: "portName" in check ? check.portName : undefined,
436 portSpecification: asPortSpecification(check.portSpecification),
437 grpcServiceName: check.grpcServiceName,
438 };
439};
440
441const toAttrs = (
442 check: compute.HealthCheck,
443 project: string,
444): RegionHealthCheck["Attributes"] => {
445 const parsed = parseDescription(check.description);
446 return {
447 healthCheckName: check.name ?? check.id ?? "",
448 project,
449 region: lastSegment(check.region).toLowerCase(),
450 type: asType(check.type),
451 description: parsed.description,
452 checkIntervalSec: check.checkIntervalSec ?? DEFAULT_CHECK_INTERVAL,
453 timeoutSec: check.timeoutSec ?? DEFAULT_TIMEOUT,
454 healthyThreshold: check.healthyThreshold ?? DEFAULT_HEALTHY,
455 unhealthyThreshold: check.unhealthyThreshold ?? DEFAULT_UNHEALTHY,
456 httpHealthCheck: toHttpProbe(check.httpHealthCheck),
457 httpsHealthCheck: toHttpProbe(check.httpsHealthCheck),
458 http2HealthCheck: toHttpProbe(check.http2HealthCheck),
459 tcpHealthCheck: toTcpProbe(check.tcpHealthCheck),
460 sslHealthCheck: toTcpProbe(check.sslHealthCheck),
461 grpcHealthCheck: toGrpcProbe(check.grpcHealthCheck),
462 grpcTlsHealthCheck: toGrpcProbe(check.grpcTlsHealthCheck),
463 logConfig:
464 check.logConfig === undefined
465 ? undefined
466 : { enable: check.logConfig.enable },
467 selfLink: check.selfLink,
468 healthCheckId: check.id,
469 creationTimestamp: check.creationTimestamp,
470 kind: check.kind,
471 };
472};
473
474const subsetEqual = (
475 observed: Record<string, unknown> | undefined,
476 desired: Record<string, unknown> | undefined,
477): boolean => {
478 if (desired === undefined) return true;
479 const current = observed ?? {};
480 for (const [key, value] of Object.entries(desired)) {
481 if (value === undefined) continue;
482 if (current[key] !== value) return false;
483 }
484 return true;
485};
486
487const asRecord = (
488 value: object | undefined,
489): Record<string, unknown> | undefined => {
490 if (value === undefined) return undefined;
491 const record: Record<string, unknown> = {};
492 for (const [key, entry] of Object.entries(value)) {
493 record[key] = entry;
494 }
495 return record;
496};
497
498const protocolConfig = (check: compute.HealthCheck, type: HealthCheckType) => {
499 switch (type) {
500 case "HTTPS":
501 return check.httpsHealthCheck;
502 case "HTTP2":
503 return check.http2HealthCheck;
504 case "TCP":
505 return check.tcpHealthCheck;
506 case "SSL":
507 return check.sslHealthCheck;
508 case "GRPC":
509 return check.grpcHealthCheck;
510 case "GRPC_WITH_TLS":
511 return check.grpcTlsHealthCheck;
512 default:
513 return check.httpHealthCheck;
514 }
515};
516
517const needsUpdate = (
518 current: compute.HealthCheck,
519 desired: compute.HealthCheck,
520) => {
521 const currentType = asType(current.type);
522 const desiredType = asType(desired.type);
523 if (currentType !== desiredType) return true;
524 if (
525 (current.checkIntervalSec ?? DEFAULT_CHECK_INTERVAL) !==
526 (desired.checkIntervalSec ?? DEFAULT_CHECK_INTERVAL)
527 ) {
528 return true;
529 }
530 if (
531 (current.timeoutSec ?? DEFAULT_TIMEOUT) !==
532 (desired.timeoutSec ?? DEFAULT_TIMEOUT)
533 ) {
534 return true;
535 }
536 if (
537 (current.healthyThreshold ?? DEFAULT_HEALTHY) !==
538 (desired.healthyThreshold ?? DEFAULT_HEALTHY)
539 ) {
540 return true;
541 }
542 if (
543 (current.unhealthyThreshold ?? DEFAULT_UNHEALTHY) !==
544 (desired.unhealthyThreshold ?? DEFAULT_UNHEALTHY)
545 ) {
546 return true;
547 }
548 if ((current.description ?? "") !== (desired.description ?? "")) return true;
549 if (
550 (current.logConfig?.enable === true) !==
551 (desired.logConfig?.enable === true)
552 ) {
553 return true;
554 }
555 return !subsetEqual(
556 asRecord(protocolConfig(current, desiredType)),
557 asRecord(protocolConfig(desired, desiredType)),
558 );
559};
560
561const getByName = (project: string, region: string, healthCheck: string) =>
562 compute
563 .getRegionHealthChecks({ project, region, healthCheck })
564 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
565
566const awaitResource = (
567 project: string,
568 region: string,
569 healthCheckName: string,
570) =>
571 getByName(project, region, healthCheckName).pipe(
572 Effect.repeat({
573 schedule: Schedule.spaced("1 second"),
574 until: (check) => check !== undefined,
575 times: 8,
576 }),
577 );
578
579export const RegionHealthCheckProvider = () =>
580 Provider.succeed(RegionHealthCheck, {
581 stables: [
582 "healthCheckName",
583 "project",
584 "region",
585 "healthCheckId",
586 "selfLink",
587 "creationTimestamp",
588 ],
589
590 diff: Effect.fn(function* ({ news, olds, output }) {
591 if (!isResolved(news)) return undefined;
592 const env = yield* GcpEnvironment.current;
593 const previousName = olds?.healthCheckName ?? output?.healthCheckName;
594 const nextName = news.healthCheckName;
595 const previousRegion = normalizeRegion(
596 olds?.region ?? output?.region,
597 env.region,
598 );
599 const nextRegion = normalizeRegion(
600 news.region ?? (previousRegion || env.region),
601 env.region,
602 );
603 const nameChanged =
604 previousName !== undefined &&
605 nextName !== undefined &&
606 previousName !== nextName;
607 const regionChanged =
608 previousRegion.length > 0 && previousRegion !== nextRegion;
609 if (nameChanged || regionChanged) {
610 return {
611 action: "replace" as const,
612 deleteFirst:
613 !nameChanged || nextName === undefined || nextName === previousName,
614 };
615 }
616 const previousType =
617 (olds !== undefined ? inferType(olds) : undefined) ?? output?.type;
618 const nextType = inferType(news);
619 if (previousType !== undefined && previousType !== nextType) {
620 return {
621 action: "replace" as const,
622 deleteFirst:
623 previousName !== undefined &&
624 (nextName === undefined || nextName === previousName),
625 };
626 }
627 return undefined;
628 }),
629
630 read: Effect.fn(function* ({ id, olds, output }) {
631 const env = yield* GcpEnvironment.current;
632 const healthCheckName = yield* toName(
633 id,
634 olds?.healthCheckName,
635 output?.healthCheckName,
636 );
637 const region = normalizeRegion(
638 olds?.region ?? output?.region,
639 env.region,
640 );
641 const existing = yield* getByName(env.project, region, healthCheckName);
642 if (existing === undefined) return undefined;
643 const attrs = toAttrs(existing, env.project);
644 const { labels } = parseDescription(existing.description);
645 return (yield* hasAlchemyLabels(id, labels)) ? attrs : Unowned(attrs);
646 }),
647
648 list: () =>
649 Effect.gen(function* () {
650 const env = yield* GcpEnvironment.current;
651 const pages = yield* compute.aggregatedListHealthChecks
652 .pages({
653 project: env.project,
654 maxResults: 500,
655 returnPartialSuccess: true,
656 })
657 .pipe(Stream.runCollect);
658 return Array.from(pages).flatMap((page) =>
659 Object.values(page.items ?? {}).flatMap((scoped) =>
660 (scoped?.healthChecks ?? [])
661 .filter(
662 (check) =>
663 lastSegment(check.region).length > 0 &&
664 hasOwnershipMarker(check.description),
665 )
666 .map((check) => toAttrs(check, env.project)),
667 ),
668 );
669 }),
670
671 reconcile: Effect.fn(function* ({ id, news, output }) {
672 const env = yield* GcpEnvironment.current;
673 const healthCheckName = yield* toName(
674 id,
675 news.healthCheckName,
676 output?.healthCheckName,
677 );
678 const region = normalizeRegion(news.region ?? output?.region, env.region);
679 const ownership = yield* createInternalLabels(id);
680 const desired = toBody(healthCheckName, news, ownership);
681
682 let current = yield* getByName(env.project, region, healthCheckName);
683
684 if (current === undefined) {
685 yield* compute
686 .insertRegionHealthChecks({
687 project: env.project,
688 region,
689 body: desired,
690 })
691 .pipe(
692 Effect.flatMap((operation) =>
693 waitRegionOperation(env.project, region, operation),
694 ),
695 Effect.catchTag("Conflict", () => Effect.succeed(undefined)),
696 );
697 current = yield* awaitResource(env.project, region, healthCheckName);
698 }
699
700 if (current === undefined) {
701 return yield* new RegionHealthCheckNotResolved({
702 healthCheckName,
703 region,
704 });
705 }
706
707 if (needsUpdate(current, desired)) {
708 yield* compute
709 .patchRegionHealthChecks({
710 project: env.project,
711 region,
712 healthCheck: healthCheckName,
713 body: desired,
714 })
715 .pipe(
716 Effect.flatMap((operation) =>
717 waitRegionOperation(env.project, region, operation),
718 ),
719 );
720 current = yield* getByName(env.project, region, healthCheckName);
721 if (current === undefined) {
722 return yield* new RegionHealthCheckNotResolved({
723 healthCheckName,
724 region,
725 });
726 }
727 }
728
729 return toAttrs(current, env.project);
730 }),
731
732 delete: Effect.fn(function* ({ output }) {
733 const env = yield* GcpEnvironment.current;
734 const region = normalizeRegion(output.region, env.region);
735 const operation = yield* compute
736 .deleteRegionHealthChecks({
737 project: env.project,
738 region,
739 healthCheck: output.healthCheckName,
740 })
741 .pipe(
742 Effect.retry({
743 while: (error) => error._tag === "Conflict",
744 schedule: Schedule.spaced("2 seconds"),
745 times: 8,
746 }),
747 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
748 );
749 if (operation !== undefined) {
750 yield* waitRegionOperation(env.project, region, operation).pipe(
751 Effect.catchTag("NotFound", () => Effect.void),
752 );
753 }
754 }),
755 });