1import * as networkservices from "@distilled.cloud/gcp/networkservices_v1";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import { tagRecord } from "../../Tags.ts";
9import { GcpEnvironment } from "../Environment.ts";
10import {
11 createInternalLabels,
12 diffLabels,
13 hasAlchemyLabels,
14 toLabels,
15} from "../Labels.ts";
16import type { Providers } from "../Providers.ts";
17import type {
18 GrpcRouteFaultInjectionPolicy,
19 GrpcRouteStatefulSessionAffinityPolicy,
20} from "./GrpcRoute.ts";
21import {
22 DEFAULT_GLOBAL,
23 changedFields,
24 collectPages,
25 hasAlchemyLabelKeys,
26 normalizeLocation,
27 parentOf,
28 parseName,
29 resourceName,
30 rfc1035,
31 sameJson,
32 sameStringList,
33 toPhysicalId,
34 userLabels,
35 waitForOperation,
36 waitUntilGone,
37 waitUntilPresent,
38} from "./internal.ts";
39
40const COLLECTION = "httpRoutes";
41
42export type HttpRouteHeaderMatchIntegerRange = {
43 /** Inclusive start of the range. */
44 start?: number;
45 /** Exclusive end of the range. */
46 end?: number;
47};
48
49export type HttpRouteHeaderMatch = {
50 /** Exact header value. */
51 exactMatch?: string;
52 /** RE2 regular expression. */
53 regexMatch?: string;
54 /** Prefix of the header value. */
55 prefixMatch?: string;
56 /** Suffix of the header value. */
57 suffixMatch?: string;
58 /** Header is present regardless of value. */
59 presentMatch?: boolean;
60 /** Integer range match. */
61 rangeMatch?: HttpRouteHeaderMatchIntegerRange;
62 /** HTTP header name. */
63 header?: string;
64 /** Invert the match result. */
65 invertMatch?: boolean;
66};
67
68export type HttpRouteQueryParameterMatch = {
69 /** Exact query parameter value. */
70 exactMatch?: string;
71 /** RE2 regular expression. */
72 regexMatch?: string;
73 /** Parameter is present regardless of value. */
74 presentMatch?: boolean;
75 /** Query parameter name. */
76 queryParameter?: string;
77};
78
79export type HttpRouteRouteMatch = {
80 /** Exact request path. Mutually exclusive with prefix/regex. */
81 fullPathMatch?: string;
82 /** Path prefix. Must start with `/`. */
83 prefixMatch?: string;
84 /** RE2 regular expression against the path. */
85 regexMatch?: string;
86 /** Case-insensitive prefix/full path matching. */
87 ignoreCase?: boolean;
88 /** Header matchers. All must match. */
89 headers?: HttpRouteHeaderMatch[];
90 /** Query parameter matchers. All must match. */
91 queryParameters?: HttpRouteQueryParameterMatch[];
92};
93
94export type HttpRouteHeaderModifier = {
95 /** Replace headers by name. */
96 set?: Record<string, string>;
97 /** Append headers by name. */
98 add?: Record<string, string>;
99 /** Header names to remove. */
100 remove?: string[];
101};
102
103export type HttpRouteDestination = {
104 /** BackendService URL. */
105 serviceName?: string;
106 /** Traffic weight relative to other destinations. */
107 weight?: number;
108 /** Request header modifiers applied before forwarding. */
109 requestHeaderModifier?: HttpRouteHeaderModifier;
110 /** Response header modifiers applied before returning. */
111 responseHeaderModifier?: HttpRouteHeaderModifier;
112};
113
114export type HttpRouteRedirect = {
115 /** Host used in the redirect. */
116 hostRedirect?: string;
117 /** Path used in the redirect. Mutually exclusive with `prefixRewrite`. */
118 pathRedirect?: string;
119 /** Replace the matched prefix with this value. */
120 prefixRewrite?: string;
121 /** HTTP status (`MOVED_PERMANENTLY_DEFAULT`, `FOUND`, …). */
122 responseCode?: string;
123 /** Redirect to HTTPS. */
124 httpsRedirect?: boolean;
125 /** Strip the original query string. */
126 stripQuery?: boolean;
127 /** Port used in the redirect. */
128 portRedirect?: number;
129};
130
131export type HttpRouteURLRewrite = {
132 /** Replace the matching path prefix. */
133 pathPrefixRewrite?: string;
134 /** Replace the Host header. */
135 hostRewrite?: string;
136};
137
138export type HttpRouteRetryPolicy = {
139 /**
140 * Conditions that trigger a retry (`5xx`, `gateway-error`, `reset`,
141 * `connect-failure`, `retriable-4xx`, `refused-stream`).
142 */
143 retryConditions?: string[];
144 /** Allowed retry count. Defaults to 1. */
145 numRetries?: number;
146 /** Timeout per retry attempt. */
147 perTryTimeout?: string;
148};
149
150export type HttpRouteRequestMirrorPolicy = {
151 /** Shadow destination. Weight is ignored. */
152 destination?: HttpRouteDestination;
153 /** Percentage of requests mirrored. */
154 mirrorPercent?: number;
155};
156
157export type HttpRouteCorsPolicy = {
158 /** Allowed origins. */
159 allowOrigins?: string[];
160 /** Allowed origin RE2 patterns. */
161 allowOriginRegexes?: string[];
162 /** `Access-Control-Allow-Methods`. */
163 allowMethods?: string[];
164 /** `Access-Control-Allow-Headers`. */
165 allowHeaders?: string[];
166 /** `Access-Control-Expose-Headers`. */
167 exposeHeaders?: string[];
168 /** `Access-Control-Max-Age` in seconds. */
169 maxAge?: string;
170 /** `Access-Control-Allow-Credentials`. */
171 allowCredentials?: boolean;
172 /** Disable the CORS policy. */
173 disabled?: boolean;
174};
175
176export type HttpRouteHttpDirectResponse = {
177 /** Response body as a string. Max 1024 characters. */
178 stringBody?: string;
179 /** Response body as bytes. Max 4096B. */
180 bytesBody?: string;
181 /** HTTP status. */
182 status?: number;
183};
184
185export type HttpRouteRouteAction = {
186 /** Destinations that receive matched traffic. */
187 destinations?: HttpRouteDestination[];
188 /** Redirect instead of forwarding. */
189 redirect?: HttpRouteRedirect;
190 /** Fault injection. */
191 faultInjectionPolicy?: GrpcRouteFaultInjectionPolicy;
192 /** Request header modifiers. */
193 requestHeaderModifier?: HttpRouteHeaderModifier;
194 /** Response header modifiers. */
195 responseHeaderModifier?: HttpRouteHeaderModifier;
196 /** URL rewrite before forwarding. */
197 urlRewrite?: HttpRouteURLRewrite;
198 /** End-to-end timeout including retries. */
199 timeout?: string;
200 /** Retry policy. */
201 retryPolicy?: HttpRouteRetryPolicy;
202 /** Request mirroring. */
203 requestMirrorPolicy?: HttpRouteRequestMirrorPolicy;
204 /** CORS policy. */
205 corsPolicy?: HttpRouteCorsPolicy;
206 /** Cookie-based session affinity. */
207 statefulSessionAffinity?: GrpcRouteStatefulSessionAffinityPolicy;
208 /** Static HTTP response. */
209 directResponse?: HttpRouteHttpDirectResponse;
210 /** Idle timeout. `0s` disables it. */
211 idleTimeout?: string;
212};
213
214export type HttpRouteRouteRule = {
215 /**
216 * Matchers. The rule matches if any matcher matches. Omitted matches
217 * every request.
218 */
219 matches?: HttpRouteRouteMatch[];
220 /** How to route matched traffic. */
221 action?: HttpRouteRouteAction;
222};
223
224export type HttpRouteProps = {
225 /**
226 * HttpRoute id (the `{httpRoute}` segment of
227 * `projects/{project}/locations/{location}/httpRoutes/{httpRoute}`).
228 * If omitted, a unique name is generated from the stack, stage, and
229 * logical id. Must be 1-63 characters and not start with a number.
230 * Immutable — changing it replaces the route.
231 */
232 httpRouteId?: string;
233 /**
234 * Location (`global`, `us-central1`, …). Immutable — changing it
235 * replaces the route. `US-CENTRAL1` is accepted and normalized to
236 * `us-central1`.
237 * @default "global"
238 */
239 location?: string;
240 /**
241 * Hostnames this route matches against the HTTP Host header, e.g.
242 * `foo.example.com` or `*.example.com`.
243 */
244 hostnames: string[];
245 /**
246 * Routing rules matched in order. At least one rule is required.
247 */
248 rules: HttpRouteRouteRule[];
249 /**
250 * Mesh resource names this route attaches to. Attached meshes must be
251 * sidecar meshes.
252 */
253 meshes?: string[];
254 /**
255 * Gateway resource names this route attaches to.
256 */
257 gateways?: string[];
258 /**
259 * Human-readable description. Max length 1024 characters.
260 */
261 description?: string;
262 /**
263 * User labels. Alchemy ownership labels are merged in automatically.
264 */
265 labels?: Record<string, string>;
266};
267
268export type HttpRoute = Resource<
269 "GCP.NetworkServices.HttpRoute",
270 HttpRouteProps,
271 {
272 /** Full resource name `projects/{project}/locations/{location}/httpRoutes/{httpRoute}`. */
273 name: string;
274 /** HttpRoute id (last path segment). */
275 httpRouteId: string;
276 /** Project id. */
277 project: string;
278 /** Location id (`global`, `us-central1`, …). */
279 location: string;
280 /** Server-defined resource URL. */
281 selfLink: string | undefined;
282 /** Hostnames this route matches. */
283 hostnames: string[];
284 /** Routing rules. */
285 rules: HttpRouteRouteRule[];
286 /** Attached Mesh resource names. */
287 meshes: string[];
288 /** Attached Gateway resource names. */
289 gateways: string[];
290 /** User-provided description. */
291 description: string | undefined;
292 /** User labels (Alchemy ownership labels stripped). */
293 labels: Record<string, string>;
294 /** RFC3339 creation timestamp. */
295 createTime: string | undefined;
296 /** RFC3339 last-update timestamp. */
297 updateTime: string | undefined;
298 },
299 never,
300 Providers
301>;
302
303/**
304 * A Network Services HttpRoute — how a Mesh or Gateway routes HTTP
305 * traffic for a set of hostnames.
306 *
307 * Changing `httpRouteId` or `location` replaces the route. Hostnames,
308 * rules, mesh/gateway attachments, description, and labels update in
309 * place.
310 *
311 * ### Creating an HttpRoute
312 * **Example:** Redirect
313 * ```typescript
314 * const route = yield* GCP.NetworkServices.HttpRoute("Web", {
315 * hostnames: ["www.example.com"],
316 * rules: [
317 * {
318 * matches: [{ prefixMatch: "/" }],
319 * action: {
320 * redirect: { hostRedirect: "example.com", httpsRedirect: true },
321 * },
322 * },
323 * ],
324 * });
325 * ```
326 *
327 * **Example:** Direct response
328 * ```typescript
329 * const route = yield* GCP.NetworkServices.HttpRoute("Health", {
330 * httpRouteId: "app-http",
331 * hostnames: ["health.example.com"],
332 * labels: { env: "prod" },
333 * rules: [
334 * {
335 * matches: [{ fullPathMatch: "/healthz" }],
336 * action: { directResponse: { status: 200, stringBody: "ok" } },
337 * },
338 * ],
339 * });
340 * ```
341 *
342 * @resource
343 * @category NetworkServices
344 */
345export const HttpRoute = Resource<HttpRoute>("GCP.NetworkServices.HttpRoute");
346
347const toStringMap = (
348 value: Record<string, string | undefined> | undefined,
349): Record<string, string> | undefined => {
350 if (value === undefined) return undefined;
351 return Object.fromEntries(
352 Object.entries(value).filter(
353 (entry): entry is [string, string] => entry[1] !== undefined,
354 ),
355 );
356};
357
358const toHeaderModifier = (
359 value:
360 | HttpRouteHeaderModifier
361 | networkservices.HttpRouteHeaderModifier
362 | undefined,
363): HttpRouteHeaderModifier | undefined => {
364 if (value === undefined) return undefined;
365 return {
366 set: toStringMap(value.set),
367 add: toStringMap(value.add),
368 remove: value.remove ?? [],
369 };
370};
371
372const toHeaderMatch = (
373 value: HttpRouteHeaderMatch | networkservices.HttpRouteHeaderMatch,
374): HttpRouteHeaderMatch => ({
375 exactMatch: value.exactMatch,
376 regexMatch: value.regexMatch,
377 prefixMatch: value.prefixMatch,
378 suffixMatch: value.suffixMatch,
379 presentMatch: value.presentMatch,
380 rangeMatch: value.rangeMatch
381 ? { start: value.rangeMatch.start, end: value.rangeMatch.end }
382 : undefined,
383 header: value.header,
384 invertMatch: value.invertMatch,
385});
386
387const toQueryMatch = (
388 value:
389 | HttpRouteQueryParameterMatch
390 | networkservices.HttpRouteQueryParameterMatch,
391): HttpRouteQueryParameterMatch => ({
392 exactMatch: value.exactMatch,
393 regexMatch: value.regexMatch,
394 presentMatch: value.presentMatch,
395 queryParameter: value.queryParameter,
396});
397
398const toMatch = (
399 value: HttpRouteRouteMatch | networkservices.HttpRouteRouteMatch,
400): HttpRouteRouteMatch => ({
401 fullPathMatch: value.fullPathMatch,
402 prefixMatch: value.prefixMatch,
403 regexMatch: value.regexMatch,
404 ignoreCase: value.ignoreCase,
405 headers: (value.headers ?? []).map(toHeaderMatch),
406 queryParameters: (value.queryParameters ?? []).map(toQueryMatch),
407});
408
409const toDestination = (
410 value: HttpRouteDestination | networkservices.HttpRouteDestination,
411): HttpRouteDestination => ({
412 serviceName: value.serviceName,
413 weight: value.weight,
414 requestHeaderModifier: toHeaderModifier(value.requestHeaderModifier),
415 responseHeaderModifier: toHeaderModifier(value.responseHeaderModifier),
416});
417
418const toRedirect = (
419 value: HttpRouteRedirect | networkservices.HttpRouteRedirect | undefined,
420): HttpRouteRedirect | undefined => {
421 if (value === undefined) return undefined;
422 return {
423 hostRedirect: value.hostRedirect,
424 pathRedirect: value.pathRedirect,
425 prefixRewrite: value.prefixRewrite,
426 responseCode: value.responseCode,
427 httpsRedirect: value.httpsRedirect,
428 stripQuery: value.stripQuery,
429 portRedirect: value.portRedirect,
430 };
431};
432
433const toFault = (
434 value:
435 | GrpcRouteFaultInjectionPolicy
436 | networkservices.GrpcRouteFaultInjectionPolicy
437 | undefined,
438): GrpcRouteFaultInjectionPolicy | undefined => {
439 if (value === undefined) return undefined;
440 return {
441 delay: value.delay
442 ? {
443 fixedDelay: value.delay.fixedDelay,
444 percentage: value.delay.percentage,
445 }
446 : undefined,
447 abort: value.abort
448 ? {
449 httpStatus: value.abort.httpStatus,
450 percentage: value.abort.percentage,
451 }
452 : undefined,
453 };
454};
455
456const toRetry = (
457 value:
458 | HttpRouteRetryPolicy
459 | networkservices.HttpRouteRetryPolicy
460 | undefined,
461): HttpRouteRetryPolicy | undefined => {
462 if (value === undefined) return undefined;
463 return {
464 retryConditions: value.retryConditions ?? [],
465 numRetries: value.numRetries,
466 perTryTimeout: value.perTryTimeout,
467 };
468};
469
470const toMirror = (
471 value:
472 | HttpRouteRequestMirrorPolicy
473 | networkservices.HttpRouteRequestMirrorPolicy
474 | undefined,
475): HttpRouteRequestMirrorPolicy | undefined => {
476 if (value === undefined) return undefined;
477 return {
478 destination: value.destination
479 ? toDestination(value.destination)
480 : undefined,
481 mirrorPercent: value.mirrorPercent,
482 };
483};
484
485const toCors = (
486 value: HttpRouteCorsPolicy | networkservices.HttpRouteCorsPolicy | undefined,
487): HttpRouteCorsPolicy | undefined => {
488 if (value === undefined) return undefined;
489 return {
490 allowOrigins: value.allowOrigins ?? [],
491 allowOriginRegexes: value.allowOriginRegexes ?? [],
492 allowMethods: value.allowMethods ?? [],
493 allowHeaders: value.allowHeaders ?? [],
494 exposeHeaders: value.exposeHeaders ?? [],
495 maxAge: value.maxAge,
496 allowCredentials: value.allowCredentials,
497 disabled: value.disabled,
498 };
499};
500
501const toDirect = (
502 value:
503 | HttpRouteHttpDirectResponse
504 | networkservices.HttpRouteHttpDirectResponse
505 | undefined,
506): HttpRouteHttpDirectResponse | undefined => {
507 if (value === undefined) return undefined;
508 return {
509 stringBody: value.stringBody,
510 bytesBody: value.bytesBody,
511 status: value.status,
512 };
513};
514
515const toAction = (
516 value:
517 | HttpRouteRouteAction
518 | networkservices.HttpRouteRouteAction
519 | undefined,
520): HttpRouteRouteAction | undefined => {
521 if (value === undefined) return undefined;
522 return {
523 destinations: (value.destinations ?? []).map(toDestination),
524 redirect: toRedirect(value.redirect),
525 faultInjectionPolicy: toFault(value.faultInjectionPolicy),
526 requestHeaderModifier: toHeaderModifier(value.requestHeaderModifier),
527 responseHeaderModifier: toHeaderModifier(value.responseHeaderModifier),
528 urlRewrite: value.urlRewrite
529 ? {
530 pathPrefixRewrite: value.urlRewrite.pathPrefixRewrite,
531 hostRewrite: value.urlRewrite.hostRewrite,
532 }
533 : undefined,
534 timeout: value.timeout,
535 retryPolicy: toRetry(value.retryPolicy),
536 requestMirrorPolicy: toMirror(value.requestMirrorPolicy),
537 corsPolicy: toCors(value.corsPolicy),
538 statefulSessionAffinity: value.statefulSessionAffinity
539 ? { cookieTtl: value.statefulSessionAffinity.cookieTtl }
540 : undefined,
541 directResponse: toDirect(value.directResponse),
542 idleTimeout: value.idleTimeout,
543 };
544};
545
546const toRule = (
547 value: HttpRouteRouteRule | networkservices.HttpRouteRouteRule,
548): HttpRouteRouteRule => ({
549 matches: (value.matches ?? []).map(toMatch),
550 action: toAction(value.action),
551});
552
553const toAttrs = (route: networkservices.HttpRoute, project: string) => {
554 const name = route.name ?? "";
555 const parsed = parseName(name, COLLECTION, DEFAULT_GLOBAL);
556 return {
557 name,
558 httpRouteId: parsed.id,
559 project: parsed.project || project,
560 location: parsed.location || DEFAULT_GLOBAL,
561 selfLink: route.selfLink,
562 hostnames: route.hostnames ?? [],
563 rules: (route.rules ?? []).map(toRule),
564 meshes: route.meshes ?? [],
565 gateways: route.gateways ?? [],
566 description: route.description,
567 labels: userLabels(route.labels),
568 createTime: route.createTime,
569 updateTime: route.updateTime,
570 };
571};
572
573const getByName = (name: string) =>
574 networkservices
575 .getProjectsLocationsHttpRoutes({ name })
576 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
577
578export const HttpRouteProvider = () =>
579 Provider.succeed(HttpRoute, {
580 stables: [
581 "name",
582 "httpRouteId",
583 "project",
584 "location",
585 "selfLink",
586 "createTime",
587 ],
588
589 diff: Effect.fn(function* ({ news, olds, output }) {
590 if (!isResolved(news)) return undefined;
591 const previousId = olds?.httpRouteId ?? output?.httpRouteId;
592 const nextId = news.httpRouteId
593 ? rfc1035(news.httpRouteId, "http-route")
594 : previousId;
595 const previousLocation = normalizeLocation(
596 olds?.location ?? output?.location,
597 DEFAULT_GLOBAL,
598 );
599 const nextLocation = normalizeLocation(
600 news.location ?? olds?.location ?? output?.location,
601 DEFAULT_GLOBAL,
602 );
603 if (
604 (previousId !== undefined &&
605 nextId !== undefined &&
606 nextId !== previousId) ||
607 previousLocation !== nextLocation
608 ) {
609 return { action: "replace" as const };
610 }
611 return undefined;
612 }),
613
614 read: Effect.fn(function* ({ id, olds, output }) {
615 const env = yield* GcpEnvironment.current;
616 const httpRouteId = yield* toPhysicalId(
617 id,
618 olds?.httpRouteId,
619 output?.httpRouteId,
620 "http-route",
621 );
622 const location = normalizeLocation(
623 olds?.location ?? output?.location,
624 DEFAULT_GLOBAL,
625 );
626 const name =
627 output?.name ??
628 resourceName(env.project, location, COLLECTION, httpRouteId);
629 const existing = yield* getByName(name);
630 if (existing === undefined) return undefined;
631 const attrs = toAttrs(existing, env.project);
632 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
633 ? attrs
634 : Unowned(attrs);
635 }),
636
637 list: () =>
638 Effect.gen(function* () {
639 const env = yield* GcpEnvironment.current;
640 const items = yield* collectPages(
641 networkservices.listProjectsLocationsHttpRoutes.pages({
642 parent: parentOf(env.project, "-"),
643 pageSize: 1000,
644 returnPartialSuccess: true,
645 }),
646 (page) => page.httpRoutes,
647 );
648 return items
649 .filter((item) => hasAlchemyLabelKeys(item.labels))
650 .map((item) => toAttrs(item, env.project));
651 }),
652
653 reconcile: Effect.fn(function* ({ id, news, output }) {
654 const env = yield* GcpEnvironment.current;
655 const httpRouteId = yield* toPhysicalId(
656 id,
657 news.httpRouteId,
658 output?.httpRouteId,
659 "http-route",
660 );
661 const location = normalizeLocation(
662 news.location ?? output?.location,
663 DEFAULT_GLOBAL,
664 );
665 const name = resourceName(env.project, location, COLLECTION, httpRouteId);
666 const desiredLabels = {
667 ...toLabels(news.labels),
668 ...(yield* createInternalLabels(id)),
669 };
670 const desiredRules = news.rules.map(toRule);
671 const desiredHostnames = news.hostnames;
672 const desiredMeshes = news.meshes ?? [];
673 const desiredGateways = news.gateways ?? [];
674
675 let current = yield* getByName(name);
676
677 if (current === undefined) {
678 const created = yield* networkservices
679 .createProjectsLocationsHttpRoutes({
680 parent: parentOf(env.project, location),
681 httpRouteId,
682 body: {
683 labels: desiredLabels,
684 description: news.description,
685 hostnames: desiredHostnames,
686 rules: desiredRules,
687 meshes: desiredMeshes,
688 gateways: desiredGateways,
689 },
690 })
691 .pipe(
692 Effect.retry({
693 while: (error) => error._tag === "Conflict",
694 times: 5,
695 schedule: Schedule.spaced("2 seconds"),
696 }),
697 Effect.catchTag("Conflict", () => Effect.succeed(undefined)),
698 );
699 if (created !== undefined) {
700 yield* waitForOperation(created);
701 }
702 current = yield* waitUntilPresent(getByName(name), name);
703 }
704
705 const observedLabels = tagRecord(current.labels);
706 const { upsert, removed } = diffLabels(observedLabels, desiredLabels);
707 const labelsChanged = upsert.length > 0 || removed.length > 0;
708 const descriptionChanged =
709 (current.description ?? "") !== (news.description ?? "");
710 const hostnamesChanged = !sameStringList(
711 current.hostnames,
712 desiredHostnames,
713 );
714 const rulesChanged = !sameJson(
715 (current.rules ?? []).map(toRule),
716 desiredRules,
717 );
718 const meshesChanged = !sameStringList(current.meshes, desiredMeshes);
719 const gatewaysChanged = !sameStringList(
720 current.gateways,
721 desiredGateways,
722 );
723
724 const updateMask = changedFields([
725 ["labels", labelsChanged],
726 ["description", descriptionChanged],
727 ["hostnames", hostnamesChanged],
728 ["rules", rulesChanged],
729 ["meshes", meshesChanged],
730 ["gateways", gatewaysChanged],
731 ]);
732
733 if (updateMask.length > 0) {
734 const operation =
735 yield* networkservices.patchProjectsLocationsHttpRoutes({
736 name: current.name ?? name,
737 updateMask: updateMask.join(","),
738 body: {
739 name: current.name ?? name,
740 labels: desiredLabels,
741 description: news.description,
742 hostnames: desiredHostnames,
743 rules: desiredRules,
744 meshes: desiredMeshes,
745 gateways: desiredGateways,
746 },
747 });
748 yield* waitForOperation(operation);
749 current = yield* waitUntilPresent(
750 getByName(current.name ?? name),
751 current.name ?? name,
752 );
753 }
754
755 return toAttrs(current, env.project);
756 }),
757
758 delete: Effect.fn(function* ({ output }) {
759 const operation = yield* networkservices
760 .deleteProjectsLocationsHttpRoutes({ name: output.name })
761 .pipe(
762 Effect.retry({
763 while: (error) => error._tag === "Conflict",
764 times: 8,
765 schedule: Schedule.spaced("2 seconds"),
766 }),
767 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
768 );
769 if (operation !== undefined) {
770 yield* waitForOperation(operation, { notFoundOk: true });
771 }
772 yield* waitUntilGone(getByName(output.name), output.name);
773 }),
774 });