1import * as monitoring from "@distilled.cloud/gcp/monitoring_v3";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Stream from "effect/Stream";
5import { Unowned } from "../../AdoptPolicy.ts";
6import { createPhysicalName } from "../../PhysicalName.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { tagRecord } from "../../Tags.ts";
10import { GcpEnvironment } from "../Environment.ts";
11import {
12 createInternalLabels,
13 diffLabels,
14 hasAlchemyLabels,
15 stripInternalLabels,
16 toLabels,
17} from "../Labels.ts";
18import type { Providers } from "../Providers.ts";
19
20const MAX_DISPLAY_NAME_LENGTH = 512;
21const DEFAULT_TIMEOUT = "10s";
22const DEFAULT_PERIOD = "600s";
23const DEFAULT_HOST = "example.com";
24const DEFAULT_PATH = "/";
25const DEFAULT_SSL_PORT = 443;
26const DEFAULT_HTTP_PORT = 80;
27const DEFAULT_RESOURCE_TYPE = "uptime_url";
28
29export type BasicAuthentication = {
30 /** Username for HTTP basic auth. */
31 username?: string;
32 /** Password for HTTP basic auth. */
33 password?: string;
34};
35
36export type PingConfig = {
37 /** Number of ICMP pings to send with the check (max 3). */
38 pingsCount?: number;
39};
40
41export type ResponseStatusCode = {
42 /** Exact HTTP status code to accept. */
43 statusValue?: number;
44 /** HTTP status class to accept (`STATUS_CLASS_2XX`, …). */
45 statusClass?: string;
46};
47
48export type HttpCheck = {
49 /**
50 * Request path. Combined with the monitored-resource host.
51 * @default "/"
52 */
53 path?: string;
54 /**
55 * TCP port. Defaults to 443 when `useSsl` is true, otherwise 80.
56 */
57 port?: number;
58 /**
59 * When true, probe over HTTPS.
60 * @default true
61 */
62 useSsl?: boolean;
63 /**
64 * When true, validate the SSL certificate. Only applies when `useSsl`
65 * is true.
66 * @default true
67 */
68 validateSsl?: boolean;
69 /** HTTP method. Defaults to GET. */
70 requestMethod?: string;
71 /** Extra request headers. */
72 headers?: Record<string, string>;
73 /** When true, encrypt stored header values. */
74 maskHeaders?: boolean;
75 /** HTTP basic authentication. */
76 authInfo?: BasicAuthentication;
77 /** Attach an OIDC token from the Monitoring service agent. */
78 serviceAgentAuthentication?: { type?: string };
79 /** Content-Type for POST checks. */
80 contentType?: string;
81 /** Custom Content-Type when `contentType` is `USER_PROVIDED`. */
82 customContentType?: string;
83 /** POST body. Ignored for GET. */
84 body?: string;
85 /** ICMP pings sent alongside the HTTP probe. */
86 pingConfig?: PingConfig;
87 /** Accepted HTTP status codes. Empty means 2xx. */
88 acceptedResponseStatusCodes?: ResponseStatusCode[];
89};
90
91export type TcpCheck = {
92 /** TCP port to probe. Required for TCP checks. */
93 port?: number;
94 /** ICMP pings sent alongside the TCP probe. */
95 pingConfig?: PingConfig;
96};
97
98export type JsonPathMatcher = {
99 /** JSONPath into the response body. */
100 jsonPath?: string;
101 /** Match type (`EXACT_MATCH` or `REGEX_MATCH`). */
102 jsonMatcher?: string;
103};
104
105export type ContentMatcher = {
106 /** String, regex, or JSON value to match. */
107 content?: string;
108 /** Matcher (`CONTAINS_STRING`, `MATCHES_REGEX`, …). */
109 matcher?: string;
110 /** JSONPath matcher, used with `MATCHES_JSON_PATH`. */
111 jsonPathMatcher?: JsonPathMatcher;
112};
113
114export type MonitoredResource = {
115 /**
116 * Monitored resource type (`uptime_url`, `gce_instance`, `gae_app`,
117 * `k8s_service`, `cloud_run_revision`, …).
118 */
119 type?: string;
120 /**
121 * Resource labels. For `uptime_url`, set `host` (and optionally
122 * `project_id`).
123 */
124 labels?: Record<string, string>;
125};
126
127export type ResourceGroup = {
128 /** Group id (not the full resource name). */
129 groupId?: string;
130 /** Member resource type (`INSTANCE` or `AWS_ELB_LOAD_BALANCER`). */
131 resourceType?: string;
132};
133
134export type SyntheticMonitorTarget = {
135 /** Cloud Functions v2 target. */
136 cloudFunctionV2?: { name?: string };
137};
138
139export type UptimeCheckConfigProps = {
140 /**
141 * Human-readable display name. If omitted, a unique name is generated
142 * from the stack, stage, and logical id.
143 */
144 displayName?: string;
145 /**
146 * Probe timeout. Must be between 1s and 60s.
147 * @default "10s"
148 */
149 timeout?: string;
150 /**
151 * How often the check runs. Supported values: `60s`, `300s`, `600s`,
152 * `900s`.
153 * @default "600s"
154 */
155 period?: string;
156 /**
157 * HTTP/HTTPS probe. Defaults to HTTPS against `example.com` when no
158 * other target is set.
159 */
160 httpCheck?: HttpCheck;
161 /**
162 * TCP probe. Mutually exclusive with `httpCheck` and
163 * `syntheticMonitor`.
164 */
165 tcpCheck?: TcpCheck;
166 /**
167 * Synthetic monitor (Cloud Functions v2) target.
168 */
169 syntheticMonitor?: SyntheticMonitorTarget;
170 /**
171 * Monitored resource. Defaults to `uptime_url` with host
172 * `example.com` for HTTP/TCP checks.
173 */
174 monitoredResource?: MonitoredResource;
175 /**
176 * Resource group instead of a single monitored resource.
177 */
178 resourceGroup?: ResourceGroup;
179 /**
180 * Content matchers. Only the first entry is used by the API.
181 */
182 contentMatchers?: ContentMatcher[];
183 /**
184 * Regions to probe from. Omitted runs from every available region.
185 * When set, enough regions must be listed to cover at least 3
186 * locations.
187 */
188 selectedRegions?: string[];
189 /**
190 * Checker type (`STATIC_IP_CHECKERS` or `VPC_CHECKERS`).
191 */
192 checkerType?: string;
193 /**
194 * When true, the check exists but does not run.
195 * @default false
196 */
197 disabled?: boolean;
198 /**
199 * When true, failed probes are written to Cloud Logging.
200 * @default false
201 */
202 logCheckFailures?: boolean;
203 /**
204 * User labels. Alchemy ownership labels are merged in automatically.
205 */
206 labels?: Record<string, string>;
207};
208
209export type UptimeCheckConfig = Resource<
210 "GCP.Monitoring.UptimeCheckConfig",
211 UptimeCheckConfigProps,
212 {
213 /** Full resource name `projects/{project}/uptimeCheckConfigs/{config}`. */
214 name: string;
215 /** Server-assigned config id (last path segment). */
216 uptimeCheckConfigId: string;
217 /** Project id. */
218 project: string;
219 /** Human-readable display name. */
220 displayName: string | undefined;
221 /** Probe timeout. */
222 timeout: string | undefined;
223 /** Check period. */
224 period: string | undefined;
225 /** HTTP/HTTPS probe, if set. */
226 httpCheck: HttpCheck | undefined;
227 /** TCP probe, if set. */
228 tcpCheck: TcpCheck | undefined;
229 /** Synthetic monitor target, if set. */
230 syntheticMonitor: SyntheticMonitorTarget | undefined;
231 /** Monitored resource, if set. */
232 monitoredResource: MonitoredResource | undefined;
233 /** Resource group, if set. */
234 resourceGroup: ResourceGroup | undefined;
235 /** Content matchers. */
236 contentMatchers: ContentMatcher[];
237 /** Regions the check runs from. */
238 selectedRegions: ReadonlyArray<string>;
239 /** Checker type. */
240 checkerType: string | undefined;
241 /** Whether the check is disabled. */
242 disabled: boolean;
243 /** Whether failed probes are logged. */
244 logCheckFailures: boolean;
245 /** User labels (Alchemy ownership labels stripped). */
246 labels: Record<string, string>;
247 },
248 never,
249 Providers
250>;
251
252/**
253 * A Cloud Monitoring uptime check — an HTTP, HTTPS, TCP, or synthetic
254 * probe that measures availability.
255 *
256 * Config ids are assigned by the API. Alchemy stamps ownership into
257 * `userLabels` so `list` / `pnpm nuke:gcp` can find them. Display name,
258 * period, timeout, HTTP/TCP settings, matchers, regions, and labels
259 * update in place.
260 *
261 * ### Creating a Check
262 * **Example:** HTTPS probe of example.com
263 * ```typescript
264 * const check = yield* GCP.Monitoring.UptimeCheckConfig("Homepage", {
265 * httpCheck: { path: "/", useSsl: true, validateSsl: true },
266 * monitoredResource: {
267 * type: "uptime_url",
268 * labels: { host: "example.com" },
269 * },
270 * });
271 * ```
272 *
273 * **Example:** Generated name with labels
274 * ```typescript
275 * const check = yield* GCP.Monitoring.UptimeCheckConfig("Homepage", {
276 * timeout: "10s",
277 * period: "600s",
278 * labels: { env: "prod" },
279 * });
280 * ```
281 *
282 * ### Updating a Check
283 * **Example:** Change path and period
284 * ```typescript
285 * const check = yield* GCP.Monitoring.UptimeCheckConfig("Homepage", {
286 * period: "300s",
287 * httpCheck: { path: "/health", useSsl: true, validateSsl: true },
288 * monitoredResource: {
289 * type: "uptime_url",
290 * labels: { host: "example.com" },
291 * },
292 * });
293 * ```
294 *
295 * @resource
296 * @category Monitoring
297 */
298export const UptimeCheckConfig = Resource<UptimeCheckConfig>(
299 "GCP.Monitoring.UptimeCheckConfig",
300);
301
302export class UptimeCheckConfigNotResolved extends Data.TaggedError(
303 "GCP.Monitoring.UptimeCheckConfigNotResolved",
304)<{
305 name: string;
306}> {}
307
308const lastSegment = (value: string) => {
309 const trimmed = value.replace(/\/+$/, "");
310 const parts = trimmed.split("/");
311 return parts[parts.length - 1] || trimmed;
312};
313
314const parentOf = (project: string) => `projects/${project}`;
315
316const userFacingLabels = (
317 labels: Record<string, string | undefined> | null | undefined,
318): Record<string, string> => stripInternalLabels(tagRecord(labels));
319
320const jsonEqual = (left: unknown, right: unknown) =>
321 JSON.stringify(canonicalize(left)) === JSON.stringify(canonicalize(right));
322
323const canonicalize = (value: unknown): unknown => {
324 if (Array.isArray(value)) return value.map(canonicalize);
325 if (value !== null && typeof value === "object") {
326 return Object.fromEntries(
327 Object.entries(value as Record<string, unknown>)
328 .filter(([, item]) => item !== undefined)
329 .sort(([left], [right]) => left.localeCompare(right))
330 .map(([key, item]) => [key, canonicalize(item)]),
331 );
332 }
333 return value;
334};
335
336const subsetEqual = (observed: unknown, desired: unknown): boolean => {
337 if (desired === undefined) return true;
338 if (Array.isArray(desired)) {
339 if (!Array.isArray(observed) || observed.length !== desired.length) {
340 return false;
341 }
342 return desired.every((item, index) => subsetEqual(observed[index], item));
343 }
344 if (desired !== null && typeof desired === "object") {
345 if (observed === null || typeof observed !== "object") return false;
346 const current = observed as Record<string, unknown>;
347 return Object.entries(desired as Record<string, unknown>).every(
348 ([key, value]) => value === undefined || subsetEqual(current[key], value),
349 );
350 }
351 return observed === desired;
352};
353
354const sorted = (values: readonly string[] | undefined) =>
355 [...(values ?? [])].sort();
356
357const toDisplayName = (
358 id: string,
359 displayName: string | undefined,
360 existing?: string,
361) =>
362 Effect.gen(function* () {
363 return (
364 displayName ??
365 existing ??
366 (yield* createPhysicalName({
367 id,
368 maxLength: MAX_DISPLAY_NAME_LENGTH,
369 lowercase: true,
370 }))
371 );
372 });
373
374const hasExplicitTarget = (news: UptimeCheckConfigProps) =>
375 news.httpCheck !== undefined ||
376 news.tcpCheck !== undefined ||
377 news.syntheticMonitor !== undefined;
378
379const defaultHttpCheck = (): HttpCheck => ({
380 path: DEFAULT_PATH,
381 port: DEFAULT_SSL_PORT,
382 useSsl: true,
383 validateSsl: true,
384});
385
386const defaultMonitoredResource = (project: string): MonitoredResource => ({
387 type: DEFAULT_RESOURCE_TYPE,
388 labels: { project_id: project, host: DEFAULT_HOST },
389});
390
391const desiredHttpCheck = (
392 news: UptimeCheckConfigProps,
393): HttpCheck | undefined => {
394 if (news.tcpCheck !== undefined || news.syntheticMonitor !== undefined) {
395 return news.httpCheck;
396 }
397 const check =
398 news.httpCheck ??
399 (hasExplicitTarget(news) ? undefined : defaultHttpCheck());
400 if (check === undefined) return undefined;
401 const useSsl = check.useSsl !== false;
402 return {
403 ...check,
404 path: check.path && check.path.length > 0 ? check.path : DEFAULT_PATH,
405 useSsl,
406 validateSsl: useSsl ? check.validateSsl !== false : false,
407 port: check.port ?? (useSsl ? DEFAULT_SSL_PORT : DEFAULT_HTTP_PORT),
408 };
409};
410
411const desiredMonitoredResource = (
412 news: UptimeCheckConfigProps,
413 project: string,
414): MonitoredResource | undefined => {
415 if (news.syntheticMonitor !== undefined) return news.monitoredResource;
416 if (news.resourceGroup !== undefined) return news.monitoredResource;
417 const resource = news.monitoredResource ?? defaultMonitoredResource(project);
418 const labels = {
419 ...(resource.labels ?? {}),
420 };
421 if (labels.project_id === undefined) labels.project_id = project;
422 if (
423 labels.host === undefined &&
424 (resource.type ?? DEFAULT_RESOURCE_TYPE) === DEFAULT_RESOURCE_TYPE
425 ) {
426 labels.host = DEFAULT_HOST;
427 }
428 return {
429 type: resource.type ?? DEFAULT_RESOURCE_TYPE,
430 labels,
431 };
432};
433
434const toHttpCheck = (
435 check: monitoring.HttpCheck | undefined,
436): HttpCheck | undefined => {
437 if (check === undefined) return undefined;
438 const useSsl = check.useSsl !== false;
439 const headers = tagRecord(check.headers);
440 return {
441 path: check.path && check.path.length > 0 ? check.path : DEFAULT_PATH,
442 port: check.port ?? (useSsl ? DEFAULT_SSL_PORT : DEFAULT_HTTP_PORT),
443 useSsl,
444 validateSsl: useSsl ? check.validateSsl !== false : false,
445 requestMethod:
446 check.requestMethod && check.requestMethod !== "METHOD_UNSPECIFIED"
447 ? check.requestMethod
448 : undefined,
449 headers: Object.keys(headers).length > 0 ? headers : undefined,
450 maskHeaders: check.maskHeaders === true ? true : undefined,
451 authInfo: check.authInfo,
452 serviceAgentAuthentication: check.serviceAgentAuthentication,
453 contentType:
454 check.contentType && check.contentType !== "TYPE_UNSPECIFIED"
455 ? check.contentType
456 : undefined,
457 customContentType: check.customContentType,
458 body: check.body,
459 pingConfig: check.pingConfig,
460 acceptedResponseStatusCodes: check.acceptedResponseStatusCodes,
461 };
462};
463
464const toMonitoredResource = (
465 resource: monitoring.MonitoredResource | undefined,
466): MonitoredResource | undefined => {
467 if (resource === undefined) return undefined;
468 return {
469 type: resource.type,
470 labels: tagRecord(resource.labels),
471 };
472};
473
474const toAttrs = (config: monitoring.UptimeCheckConfig, project: string) => {
475 const name = config.name ?? "";
476 return {
477 name,
478 uptimeCheckConfigId: lastSegment(name),
479 project,
480 displayName: config.displayName,
481 timeout: config.timeout,
482 period: config.period,
483 httpCheck: toHttpCheck(config.httpCheck),
484 tcpCheck: config.tcpCheck,
485 syntheticMonitor: config.syntheticMonitor,
486 monitoredResource: toMonitoredResource(config.monitoredResource),
487 resourceGroup: config.resourceGroup,
488 contentMatchers: config.contentMatchers ?? [],
489 selectedRegions: config.selectedRegions ?? [],
490 checkerType: config.checkerType,
491 disabled: config.disabled === true,
492 logCheckFailures: config.logCheckFailures === true,
493 labels: userFacingLabels(config.userLabels),
494 };
495};
496
497const getByName = (name: string) =>
498 monitoring
499 .getProjectsUptimeCheckConfigs({ name })
500 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
501
502const listOwned = (project: string) =>
503 monitoring.listProjectsUptimeCheckConfigs
504 .pages({
505 parent: parentOf(project),
506 pageSize: 1000,
507 })
508 .pipe(
509 Stream.flatMap((page) =>
510 Stream.fromIterable(page.uptimeCheckConfigs ?? []),
511 ),
512 Stream.filter((config) =>
513 Object.keys(config.userLabels ?? {}).some((key) =>
514 key.startsWith("alchemy-"),
515 ),
516 ),
517 Stream.map((config) => toAttrs(config, project)),
518 Stream.runCollect,
519 Effect.map((chunk) => Array.from(chunk)),
520 Effect.catchTag("NotFound", () => Effect.succeed([])),
521 );
522
523const findOwned = (project: string, id: string) =>
524 Effect.gen(function* () {
525 const owned = yield* monitoring.listProjectsUptimeCheckConfigs
526 .pages({
527 parent: parentOf(project),
528 pageSize: 1000,
529 })
530 .pipe(
531 Stream.flatMap((page) =>
532 Stream.fromIterable(page.uptimeCheckConfigs ?? []),
533 ),
534 Stream.runCollect,
535 Effect.map((chunk) => Array.from(chunk)),
536 Effect.catchTag("NotFound", () => Effect.succeed([])),
537 );
538 for (const config of owned) {
539 if (yield* hasAlchemyLabels(id, tagRecord(config.userLabels))) {
540 return config;
541 }
542 }
543 return undefined;
544 });
545
546const observe = (project: string, id: string, name: string | undefined) =>
547 Effect.gen(function* () {
548 if (name !== undefined) {
549 const existing = yield* getByName(name);
550 if (existing !== undefined) return existing;
551 }
552 return yield* findOwned(project, id);
553 });
554
555const toApiHttpCheck = (
556 check: HttpCheck | undefined,
557): monitoring.HttpCheck | undefined => {
558 if (check === undefined) return undefined;
559 return {
560 path: check.path,
561 port: check.port,
562 useSsl: check.useSsl,
563 validateSsl: check.validateSsl,
564 requestMethod: check.requestMethod,
565 headers: check.headers,
566 maskHeaders: check.maskHeaders,
567 authInfo: check.authInfo,
568 serviceAgentAuthentication: check.serviceAgentAuthentication,
569 contentType: check.contentType,
570 customContentType: check.customContentType,
571 body: check.body,
572 pingConfig: check.pingConfig,
573 acceptedResponseStatusCodes: check.acceptedResponseStatusCodes,
574 };
575};
576
577const toApiMonitoredResource = (
578 resource: MonitoredResource | undefined,
579): monitoring.MonitoredResource | undefined => {
580 if (resource === undefined) return undefined;
581 return {
582 type: resource.type,
583 labels: resource.labels,
584 };
585};
586
587export const UptimeCheckConfigProvider = () =>
588 Provider.succeed(UptimeCheckConfig, {
589 stables: ["name", "uptimeCheckConfigId", "project"],
590
591 read: Effect.fn(function* ({ id, output }) {
592 const env = yield* GcpEnvironment.current;
593 const existing = yield* observe(env.project, id, output?.name);
594 if (existing === undefined) return undefined;
595 const attrs = toAttrs(existing, env.project);
596 return (yield* hasAlchemyLabels(id, tagRecord(existing.userLabels)))
597 ? attrs
598 : Unowned(attrs);
599 }),
600
601 list: () =>
602 Effect.gen(function* () {
603 const env = yield* GcpEnvironment.current;
604 return yield* listOwned(env.project);
605 }),
606
607 reconcile: Effect.fn(function* ({ id, news, output }) {
608 const env = yield* GcpEnvironment.current;
609 const displayName = yield* toDisplayName(
610 id,
611 news.displayName,
612 output?.displayName,
613 );
614 const desiredLabels = {
615 ...toLabels(news.labels),
616 ...(yield* createInternalLabels(id)),
617 };
618 const timeout = news.timeout ?? DEFAULT_TIMEOUT;
619 const period = news.period ?? DEFAULT_PERIOD;
620 const httpCheck = desiredHttpCheck(news);
621 const monitoredResource = desiredMonitoredResource(news, env.project);
622 const desiredDisabled = news.disabled === true;
623 const desiredLogFailures = news.logCheckFailures === true;
624
625 const body: monitoring.UptimeCheckConfig = {
626 displayName,
627 timeout,
628 period,
629 userLabels: desiredLabels,
630 httpCheck: toApiHttpCheck(httpCheck),
631 tcpCheck: news.tcpCheck,
632 syntheticMonitor: news.syntheticMonitor,
633 monitoredResource: toApiMonitoredResource(monitoredResource),
634 resourceGroup: news.resourceGroup,
635 contentMatchers: news.contentMatchers,
636 selectedRegions: news.selectedRegions,
637 checkerType: news.checkerType,
638 disabled: desiredDisabled ? true : undefined,
639 logCheckFailures: desiredLogFailures ? true : undefined,
640 };
641
642 let current = yield* observe(env.project, id, output?.name);
643
644 if (current === undefined) {
645 const created = yield* monitoring
646 .createProjectsUptimeCheckConfigs({
647 parent: parentOf(env.project),
648 body,
649 })
650 .pipe(Effect.catchTag("Conflict", () => findOwned(env.project, id)));
651 current = created ?? undefined;
652 }
653
654 if (current === undefined) {
655 return yield* new UptimeCheckConfigNotResolved({
656 name: output?.name ?? displayName,
657 });
658 }
659
660 const name = current.name ?? output?.name ?? "";
661 const observedLabels = tagRecord(current.userLabels);
662 const { upsert, removed } = diffLabels(observedLabels, desiredLabels);
663 const labelsChanged = upsert.length > 0 || removed.length > 0;
664 const displayNameChanged = (current.displayName ?? "") !== displayName;
665 const timeoutChanged = (current.timeout ?? "") !== timeout;
666 const periodChanged = (current.period ?? DEFAULT_PERIOD) !== period;
667 const httpChanged =
668 httpCheck !== undefined &&
669 !subsetEqual(toHttpCheck(current.httpCheck), httpCheck);
670 const tcpChanged =
671 news.tcpCheck !== undefined &&
672 !subsetEqual(current.tcpCheck, news.tcpCheck);
673 const syntheticChanged =
674 news.syntheticMonitor !== undefined &&
675 !subsetEqual(current.syntheticMonitor, news.syntheticMonitor);
676 const resourceChanged =
677 monitoredResource !== undefined &&
678 !subsetEqual(
679 toMonitoredResource(current.monitoredResource),
680 monitoredResource,
681 );
682 const groupChanged =
683 news.resourceGroup !== undefined &&
684 !jsonEqual(current.resourceGroup, news.resourceGroup);
685 const matchersChanged =
686 news.contentMatchers !== undefined &&
687 !jsonEqual(current.contentMatchers ?? [], news.contentMatchers);
688 const regionsChanged =
689 news.selectedRegions !== undefined &&
690 !jsonEqual(
691 sorted(current.selectedRegions),
692 sorted(news.selectedRegions),
693 );
694 const checkerChanged =
695 news.checkerType !== undefined &&
696 (current.checkerType ?? "") !== news.checkerType;
697 const disabledChanged = (current.disabled === true) !== desiredDisabled;
698 const logChanged =
699 (current.logCheckFailures === true) !== desiredLogFailures;
700
701 const updateMask = [
702 displayNameChanged ? "display_name" : undefined,
703 timeoutChanged ? "timeout" : undefined,
704 periodChanged ? "period" : undefined,
705 labelsChanged ? "user_labels" : undefined,
706 httpChanged ? "http_check" : undefined,
707 tcpChanged ? "tcp_check" : undefined,
708 syntheticChanged ? "synthetic_monitor" : undefined,
709 resourceChanged ? "monitored_resource" : undefined,
710 groupChanged ? "resource_group" : undefined,
711 matchersChanged ? "content_matchers" : undefined,
712 regionsChanged ? "selected_regions" : undefined,
713 checkerChanged ? "checker_type" : undefined,
714 disabledChanged ? "disabled" : undefined,
715 logChanged ? "log_check_failures" : undefined,
716 ].filter((field): field is string => field !== undefined);
717
718 if (updateMask.length > 0) {
719 current = yield* monitoring.patchProjectsUptimeCheckConfigs({
720 name,
721 updateMask: updateMask.join(","),
722 body: { ...body, name },
723 });
724 }
725
726 return toAttrs(current, env.project);
727 }),
728
729 delete: Effect.fn(function* ({ output }) {
730 yield* monitoring
731 .deleteProjectsUptimeCheckConfigs({ name: output.name })
732 .pipe(Effect.catchTag("NotFound", () => Effect.void));
733 }),
734 });