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