1import { ignoredCodes } from "./internal.ts";
2import * as compute from "@distilled.cloud/gcp/compute_v1";
3import { waitGlobalOperation } from "./operations.ts";
4import * as Data from "effect/Data";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import * as Stream from "effect/Stream";
8import { Unowned } from "../../AdoptPolicy.ts";
9import { isResolved } from "../../Diff.ts";
10import { createPhysicalName } from "../../PhysicalName.ts";
11import * as Provider from "../../Provider.ts";
12import { Resource } from "../../Resource.ts";
13import { GcpEnvironment } from "../Environment.ts";
14import {
15 alchemyLabelKeys,
16 createInternalLabels,
17 hasAlchemyLabels,
18} from "../Labels.ts";
19import type { Providers } from "../Providers.ts";
20
21const DEFAULT_POLICY_TYPE = "VPC_POLICY";
22const RESERVED_PRIORITY_MIN = 2147483548;
23const RESERVED_PRIORITY_MAX = 2147483647;
24const MAX_NAME_LENGTH = 63;
25
26const isReservedPriority = (priority: number | undefined) =>
27 priority !== undefined &&
28 priority >= RESERVED_PRIORITY_MIN &&
29 priority <= RESERVED_PRIORITY_MAX;
30
31export type NetworkFirewallPolicyType =
32 | compute.FirewallPolicyPolicyTypeEnum
33 | (string & {});
34export type NetworkFirewallPolicyRule = compute.FirewallPolicyRule;
35export type NetworkFirewallPolicyRuleMatcher =
36 compute.FirewallPolicyRuleMatcher;
37export type NetworkFirewallPolicyAssociation =
38 compute.FirewallPolicyAssociation;
39
40export type NetworkFirewallPolicyProps = {
41 /**
42 * Policy name (RFC1035, 1-63 characters). If omitted, a unique name is
43 * generated from the stack, stage, and logical id. Immutable — changing
44 * it replaces the policy.
45 */
46 networkFirewallPolicyName?: string;
47 /**
48 * Optional description. Network firewall policies have no labels field,
49 * so Alchemy ownership (`alchemy-stack` / `alchemy-stage` /
50 * `alchemy-id`) is stored in a `[alchemy …]` prefix for `list` / nuke.
51 * Updated in place via `patch`.
52 */
53 description?: string;
54 /**
55 * Policy type. Set only at create time — changing it replaces the
56 * policy.
57 * @default "VPC_POLICY"
58 */
59 policyType?: NetworkFirewallPolicyType;
60 /**
61 * Hierarchical firewall rules. GCP always keeps default `goto_next`
62 * rules in the reserved priority range `2147483548`–`2147483647`;
63 * Alchemy never add/patch/remove those. When this field is set, user
64 * rules are synced with `addRule` / `patchRule` / `removeRule`. When
65 * omitted on later updates, observed rules are left in place.
66 */
67 rules?: NetworkFirewallPolicyRule[];
68 /**
69 * VPC associations. When set, Alchemy syncs associations with
70 * `addAssociation` / `removeAssociation`. `attachmentTarget` accepts a
71 * network name or URL. When omitted on later updates, observed
72 * associations are left in place.
73 */
74 associations?: NetworkFirewallPolicyAssociation[];
75};
76
77export type NetworkFirewallPolicy = Resource<
78 "GCP.Compute.NetworkFirewallPolicy",
79 NetworkFirewallPolicyProps,
80 {
81 /** Policy name. */
82 networkFirewallPolicyName: string;
83 /** Project id. */
84 project: string;
85 /** User description with the Alchemy ownership prefix stripped. */
86 description: string | undefined;
87 /** Policy type. */
88 policyType: string;
89 /** Rules currently attached (including the default). */
90 rules: NetworkFirewallPolicyRule[];
91 /** Associations currently attached. */
92 associations: NetworkFirewallPolicyAssociation[];
93 /** Optimistic-locking fingerprint. */
94 fingerprint: string | undefined;
95 /** Server-assigned numeric id. */
96 networkFirewallPolicyId: string | undefined;
97 /** Resource self-link. */
98 selfLink: string | undefined;
99 /** Self-link including the numeric id. */
100 selfLinkWithId: string | undefined;
101 /** Total count of rule tuples. */
102 ruleTupleCount: number | undefined;
103 /** RFC3339 creation timestamp. */
104 creationTimestamp: string | undefined;
105 /** Resource kind. */
106 kind: string | undefined;
107 },
108 never,
109 Providers
110>;
111
112/**
113 * A global Compute Engine network firewall policy.
114 *
115 * Network firewall policies live at the project level under
116 * `global/firewallPolicies` and are identified by the user-provided
117 * `name`. Name and policy type are immutable. Description updates in
118 * place via `networkFirewallPolicies.patch`. Rules are synced with
119 * `addRule` / `patchRule` / `removeRule`. Associations are synced with
120 * `addAssociation` / `removeAssociation`. Compute network firewall
121 * policies have no labels field — Alchemy stamps ownership into the
122 * description so nuke can find leaked policies.
123 *
124 * ### Creating a Network Firewall Policy
125 * **Example:** Generated name with an allow rule
126 * ```typescript
127 * const policy = yield* GCP.Compute.NetworkFirewallPolicy("VpcFw", {
128 * description: "allow internal http",
129 * rules: [
130 * {
131 * action: "allow",
132 * priority: 1000,
133 * direction: "INGRESS",
134 * match: {
135 * srcIpRanges: ["10.0.0.0/8"],
136 * layer4Configs: [{ ipProtocol: "tcp", ports: ["80"] }],
137 * },
138 * },
139 * ],
140 * });
141 * ```
142 *
143 * **Example:** Associate with a VPC
144 * ```typescript
145 * const policy = yield* GCP.Compute.NetworkFirewallPolicy("VpcFw", {
146 * networkFirewallPolicyName: "app-nfw",
147 * associations: [{ name: "vpc", attachmentTarget: network.selfLink }],
148 * });
149 * ```
150 *
151 * @resource
152 * @category Compute
153 */
154export const NetworkFirewallPolicy = Resource<NetworkFirewallPolicy>(
155 "GCP.Compute.NetworkFirewallPolicy",
156);
157
158export class NetworkFirewallPolicyNotResolved extends Data.TaggedError(
159 "GCP.Compute.NetworkFirewallPolicyNotResolved",
160)<{
161 networkFirewallPolicyName: string;
162}> {}
163
164export class NetworkFirewallPolicyStillExists extends Data.TaggedError(
165 "GCP.Compute.NetworkFirewallPolicyStillExists",
166)<{
167 networkFirewallPolicyName: string;
168}> {}
169
170const lastSegment = (value: string | undefined): string => {
171 if (value === undefined || value.length === 0) return "";
172 const trimmed = value.replace(/\/+$/, "");
173 const parts = trimmed.split("/");
174 return parts[parts.length - 1] || trimmed;
175};
176
177const rfc1035 = (name: string): string => {
178 let next = name
179 .toLowerCase()
180 .replace(/[^a-z0-9-]/g, "-")
181 .replace(/-+/g, "-")
182 .replace(/^-+/, "")
183 .replace(/-+$/, "");
184 if (!/^[a-z]/.test(next)) next = `p${next}`;
185 next = next.slice(0, MAX_NAME_LENGTH).replace(/-+$/, "");
186 return next.length > 0 ? next : "policy";
187};
188
189const toName = (id: string, name: string | undefined, existing?: string) =>
190 Effect.gen(function* () {
191 if (name !== undefined) return rfc1035(name);
192 if (existing !== undefined) return existing;
193 return rfc1035(
194 yield* createPhysicalName({
195 id,
196 maxLength: MAX_NAME_LENGTH,
197 lowercase: true,
198 }),
199 );
200 });
201
202const typeOf = (value: string | undefined) =>
203 (value ?? DEFAULT_POLICY_TYPE).toUpperCase();
204
205const encodeDescription = (
206 labels: Record<string, string>,
207 description: string | undefined,
208): string => {
209 const marker = `[alchemy ${alchemyLabelKeys.stack}=${labels[alchemyLabelKeys.stack]} ${alchemyLabelKeys.stage}=${labels[alchemyLabelKeys.stage]} ${alchemyLabelKeys.id}=${labels[alchemyLabelKeys.id]}]`;
210 return description ? `${marker}\n${description}` : marker;
211};
212
213const parseDescription = (
214 description: string | undefined,
215): {
216 labels: Record<string, string>;
217 description: string | undefined;
218} => {
219 if (!description?.startsWith("[alchemy ")) {
220 return { labels: {}, description };
221 }
222 const end = description.indexOf("]");
223 if (end < 0) return { labels: {}, description };
224 const labels: Record<string, string> = {};
225 for (const part of description.slice("[alchemy ".length, end).split(/\s+/)) {
226 const eq = part.indexOf("=");
227 if (eq > 0) labels[part.slice(0, eq)] = part.slice(eq + 1);
228 }
229 const rest = description.slice(end + 1).replace(/^\n/, "");
230 return { labels, description: rest.length > 0 ? rest : undefined };
231};
232
233const hasOwnershipMarker = (description: string | undefined) =>
234 Object.keys(parseDescription(description).labels).some((key) =>
235 key.startsWith("alchemy-"),
236 );
237
238const networkUrl = (project: string, value: string) => {
239 if (value.includes("/")) return value;
240 return `projects/${project}/global/networks/${value}`;
241};
242
243const sorted = (values: readonly string[] | undefined) =>
244 [...(values ?? [])].slice().sort();
245
246const sameJson = (left: unknown, right: unknown) =>
247 JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
248
249const canonMatch = (match: NetworkFirewallPolicyRuleMatcher | undefined) => {
250 if (match === undefined) return undefined;
251 return {
252 srcIpRanges: sorted(match.srcIpRanges),
253 destIpRanges: sorted(match.destIpRanges),
254 srcFqdns: sorted(match.srcFqdns),
255 destFqdns: sorted(match.destFqdns),
256 srcRegionCodes: sorted(match.srcRegionCodes),
257 destRegionCodes: sorted(match.destRegionCodes),
258 srcAddressGroups: sorted(match.srcAddressGroups),
259 destAddressGroups: sorted(match.destAddressGroups),
260 srcThreatIntelligences: sorted(match.srcThreatIntelligences),
261 destThreatIntelligences: sorted(match.destThreatIntelligences),
262 srcNetworks: sorted(match.srcNetworks),
263 srcNetworkType: match.srcNetworkType,
264 destNetworkType: match.destNetworkType,
265 layer4Configs: [...(match.layer4Configs ?? [])]
266 .map((config) => ({
267 ipProtocol: (config.ipProtocol ?? "").toLowerCase(),
268 ports: sorted(config.ports),
269 }))
270 .sort(
271 (left, right) =>
272 left.ipProtocol.localeCompare(right.ipProtocol) ||
273 JSON.stringify(left.ports).localeCompare(JSON.stringify(right.ports)),
274 ),
275 srcSecureTags: [...(match.srcSecureTags ?? [])]
276 .map((tag) => tag.name ?? "")
277 .sort(),
278 };
279};
280
281const canonRule = (rule: NetworkFirewallPolicyRule) => ({
282 priority: rule.priority,
283 action: rule.action,
284 description: rule.description ?? "",
285 direction: (rule.direction ?? "INGRESS").toUpperCase(),
286 enableLogging: rule.enableLogging === true,
287 disabled: rule.disabled === true,
288 ruleName: rule.ruleName ?? "",
289 tlsInspect: rule.tlsInspect === true,
290 targetType: rule.targetType ?? "INSTANCES",
291 securityProfileGroup: rule.securityProfileGroup ?? "",
292 match: canonMatch(rule.match),
293 targetResources: sorted(rule.targetResources),
294 targetServiceAccounts: sorted(rule.targetServiceAccounts),
295 targetForwardingRules: sorted(rule.targetForwardingRules),
296 targetSecureTags: [...(rule.targetSecureTags ?? [])]
297 .map((tag) => tag.name ?? "")
298 .sort(),
299});
300
301const ruleEquals = (
302 left: NetworkFirewallPolicyRule,
303 right: NetworkFirewallPolicyRule,
304) => sameJson(canonRule(left), canonRule(right));
305
306const toRuleBody = (
307 rule: NetworkFirewallPolicyRule,
308): NetworkFirewallPolicyRule => ({
309 priority: rule.priority,
310 action: rule.action,
311 description: rule.description,
312 direction: rule.direction,
313 enableLogging: rule.enableLogging === true ? true : undefined,
314 disabled: rule.disabled === true ? true : undefined,
315 ruleName: rule.ruleName,
316 tlsInspect: rule.tlsInspect === true ? true : undefined,
317 targetType: rule.targetType,
318 securityProfileGroup: rule.securityProfileGroup,
319 match: rule.match,
320 targetResources: rule.targetResources,
321 targetServiceAccounts: rule.targetServiceAccounts,
322 targetForwardingRules: rule.targetForwardingRules,
323 targetSecureTags: rule.targetSecureTags,
324});
325
326const desiredRules = (
327 news: NetworkFirewallPolicyProps,
328): NetworkFirewallPolicyRule[] | undefined => {
329 if (news.rules === undefined) return undefined;
330 const byPriority = new Map<number, NetworkFirewallPolicyRule>();
331 for (const rule of news.rules) {
332 if (rule.priority === undefined) continue;
333 if (isReservedPriority(rule.priority)) continue;
334 byPriority.set(rule.priority, rule);
335 }
336 return [...byPriority.values()].sort(
337 (left, right) => (left.priority ?? 0) - (right.priority ?? 0),
338 );
339};
340
341const associationNameOf = (
342 association: NetworkFirewallPolicyAssociation,
343): string =>
344 association.name && association.name.length > 0
345 ? association.name
346 : lastSegment(association.attachmentTarget) || "assoc";
347
348const toAttrs = (
349 policy: compute.FirewallPolicy,
350 project: string,
351): NetworkFirewallPolicy["Attributes"] => {
352 const parsed = parseDescription(policy.description);
353 return {
354 networkFirewallPolicyName: policy.name ?? "",
355 project,
356 description: parsed.description,
357 policyType: typeOf(policy.policyType),
358 rules: policy.rules ?? [],
359 associations: policy.associations ?? [],
360 fingerprint: policy.fingerprint,
361 networkFirewallPolicyId: policy.id,
362 selfLink: policy.selfLink,
363 selfLinkWithId: policy.selfLinkWithId,
364 ruleTupleCount: policy.ruleTupleCount,
365 creationTimestamp: policy.creationTimestamp,
366 kind: policy.kind,
367 };
368};
369
370const getByName = (project: string, firewallPolicy: string) =>
371 compute
372 .getNetworkFirewallPolicies({ project, firewallPolicy })
373 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
374
375const awaitResource = (project: string, networkFirewallPolicyName: string) =>
376 getByName(project, networkFirewallPolicyName).pipe(
377 Effect.flatMap((policy) =>
378 policy !== undefined
379 ? Effect.succeed(policy)
380 : Effect.fail(
381 new NetworkFirewallPolicyNotResolved({
382 networkFirewallPolicyName,
383 }),
384 ),
385 ),
386 Effect.retry({
387 while: (error) =>
388 error._tag === "GCP.Compute.NetworkFirewallPolicyNotResolved",
389 times: 8,
390 schedule: Schedule.spaced("1 second"),
391 }),
392 );
393
394const waitUntilGone = (project: string, networkFirewallPolicyName: string) =>
395 getByName(project, networkFirewallPolicyName).pipe(
396 Effect.flatMap((policy) =>
397 policy === undefined
398 ? Effect.void
399 : Effect.fail(
400 new NetworkFirewallPolicyStillExists({
401 networkFirewallPolicyName,
402 }),
403 ),
404 ),
405 Effect.retry({
406 while: (error) =>
407 error._tag === "GCP.Compute.NetworkFirewallPolicyStillExists",
408 times: 10,
409 schedule: Schedule.spaced("1 second"),
410 }),
411 Effect.catchTag(
412 "GCP.Compute.NetworkFirewallPolicyStillExists",
413 () => Effect.void,
414 ),
415 );
416
417const runOp = <E extends { readonly _tag: string }, R>(
418 project: string,
419 networkFirewallPolicyName: string,
420 start: Effect.Effect<compute.Operation, E, R>,
421 options?: { ignoreAlreadyExists?: boolean; ignoreNotFound?: boolean },
422) =>
423 start.pipe(
424 Effect.flatMap((operation) =>
425 waitGlobalOperation(project, operation, {
426 ignore: ignoredCodes(options),
427 }),
428 ),
429 Effect.retry({
430 while: (error) => error._tag === "Conflict",
431 times: 5,
432 schedule: Schedule.spaced("1 second"),
433 }),
434 );
435
436const syncRules = (
437 project: string,
438 networkFirewallPolicyName: string,
439 observed: readonly NetworkFirewallPolicyRule[],
440 desired: readonly NetworkFirewallPolicyRule[],
441) =>
442 Effect.gen(function* () {
443 const observedByPriority = new Map<number, NetworkFirewallPolicyRule>();
444 for (const rule of observed) {
445 if (rule.priority !== undefined) {
446 observedByPriority.set(rule.priority, rule);
447 }
448 }
449 const desiredByPriority = new Map<number, NetworkFirewallPolicyRule>();
450 for (const rule of desired) {
451 if (rule.priority !== undefined) {
452 desiredByPriority.set(rule.priority, rule);
453 }
454 }
455
456 for (const [priority, rule] of desiredByPriority) {
457 if (isReservedPriority(priority)) continue;
458 const current = observedByPriority.get(priority);
459 if (current === undefined) {
460 yield* runOp(
461 project,
462 networkFirewallPolicyName,
463 compute.addRuleNetworkFirewallPolicies({
464 project,
465 firewallPolicy: networkFirewallPolicyName,
466 body: toRuleBody(rule),
467 }),
468 { ignoreAlreadyExists: true },
469 );
470 continue;
471 }
472 if (!ruleEquals(current, rule)) {
473 yield* runOp(
474 project,
475 networkFirewallPolicyName,
476 compute.patchRuleNetworkFirewallPolicies({
477 project,
478 firewallPolicy: networkFirewallPolicyName,
479 priority,
480 body: toRuleBody(rule),
481 }),
482 );
483 }
484 }
485
486 for (const priority of observedByPriority.keys()) {
487 if (desiredByPriority.has(priority)) continue;
488 if (isReservedPriority(priority)) continue;
489 yield* runOp(
490 project,
491 networkFirewallPolicyName,
492 compute.removeRuleNetworkFirewallPolicies({
493 project,
494 firewallPolicy: networkFirewallPolicyName,
495 priority,
496 }),
497 { ignoreNotFound: true },
498 ).pipe(Effect.catchTag("NotFound", () => Effect.void));
499 }
500 });
501
502const syncAssociations = (
503 project: string,
504 networkFirewallPolicyName: string,
505 observed: readonly NetworkFirewallPolicyAssociation[],
506 desired: readonly NetworkFirewallPolicyAssociation[],
507) =>
508 Effect.gen(function* () {
509 const observedByName = new Map<string, NetworkFirewallPolicyAssociation>();
510 for (const association of observed) {
511 observedByName.set(associationNameOf(association), association);
512 }
513 const desiredByName = new Map<string, NetworkFirewallPolicyAssociation>();
514 for (const association of desired) {
515 desiredByName.set(associationNameOf(association), association);
516 }
517
518 for (const [name, association] of desiredByName) {
519 const current = observedByName.get(name);
520 const target = association.attachmentTarget
521 ? networkUrl(project, association.attachmentTarget)
522 : undefined;
523 if (current === undefined) {
524 yield* runOp(
525 project,
526 networkFirewallPolicyName,
527 compute.addAssociationNetworkFirewallPolicies({
528 project,
529 firewallPolicy: networkFirewallPolicyName,
530 body: { name, attachmentTarget: target },
531 }),
532 { ignoreAlreadyExists: true },
533 );
534 continue;
535 }
536 if (
537 lastSegment(current.attachmentTarget) !== lastSegment(target) &&
538 target !== undefined
539 ) {
540 yield* runOp(
541 project,
542 networkFirewallPolicyName,
543 compute.addAssociationNetworkFirewallPolicies({
544 project,
545 firewallPolicy: networkFirewallPolicyName,
546 replaceExistingAssociation: true,
547 body: { name, attachmentTarget: target },
548 }),
549 );
550 }
551 }
552
553 for (const name of observedByName.keys()) {
554 if (desiredByName.has(name)) continue;
555 yield* runOp(
556 project,
557 networkFirewallPolicyName,
558 compute.removeAssociationNetworkFirewallPolicies({
559 project,
560 firewallPolicy: networkFirewallPolicyName,
561 name,
562 }),
563 { ignoreNotFound: true },
564 ).pipe(Effect.catchTag("NotFound", () => Effect.void));
565 }
566 });
567
568export const NetworkFirewallPolicyProvider = () =>
569 Provider.succeed(NetworkFirewallPolicy, {
570 stables: [
571 "networkFirewallPolicyName",
572 "project",
573 "networkFirewallPolicyId",
574 "policyType",
575 "selfLink",
576 "selfLinkWithId",
577 "creationTimestamp",
578 ],
579
580 diff: Effect.fn(function* ({ id, news, olds, output }) {
581 if (!isResolved(news)) return undefined;
582 // Name is immutable on GCP. Resolve the desired name the same way
583 // create does — do not fall back to previousName when news omits
584 // networkFirewallPolicyName, or a generated-name change looks like a no-op.
585 const previousName =
586 output?.networkFirewallPolicyName ?? olds?.networkFirewallPolicyName;
587 const nextName = yield* toName(id, news.networkFirewallPolicyName);
588 const nameChanged =
589 previousName !== undefined && previousName !== nextName;
590 const previousType = typeOf(olds?.policyType ?? output?.policyType);
591 const nextType = typeOf(news.policyType ?? previousType);
592 if (nameChanged) {
593 return { action: "replace" as const, deleteFirst: false };
594 }
595 if (previousType !== nextType) {
596 return { action: "replace" as const, deleteFirst: true };
597 }
598 return undefined;
599 }),
600
601 read: Effect.fn(function* ({ id, olds, output }) {
602 const env = yield* GcpEnvironment.current;
603 const networkFirewallPolicyName = yield* toName(
604 id,
605 olds?.networkFirewallPolicyName,
606 output?.networkFirewallPolicyName,
607 );
608 const existing = yield* getByName(env.project, networkFirewallPolicyName);
609 if (existing === undefined) return undefined;
610 const attrs = toAttrs(existing, env.project);
611 const { labels } = parseDescription(existing.description);
612 return (yield* hasAlchemyLabels(id, labels)) ? attrs : Unowned(attrs);
613 }),
614
615 list: () =>
616 Effect.gen(function* () {
617 const env = yield* GcpEnvironment.current;
618 return yield* compute.listNetworkFirewallPolicies
619 .items({
620 project: env.project,
621 maxResults: 500,
622 returnPartialSuccess: true,
623 })
624 .pipe(
625 Stream.filter((policy) => hasOwnershipMarker(policy.description)),
626 Stream.map((policy) => toAttrs(policy, env.project)),
627 Stream.runCollect,
628 Effect.map((items) => Array.from(items)),
629 Effect.catchTag("NotFound", () =>
630 Effect.succeed([] as NetworkFirewallPolicy["Attributes"][]),
631 ),
632 );
633 }),
634
635 reconcile: Effect.fn(function* ({ id, news, output }) {
636 const env = yield* GcpEnvironment.current;
637 const networkFirewallPolicyName = yield* toName(
638 id,
639 news.networkFirewallPolicyName,
640 output?.networkFirewallPolicyName,
641 );
642 const ownership = yield* createInternalLabels(id);
643 const desiredDescription = encodeDescription(ownership, news.description);
644 const policyType = typeOf(news.policyType);
645
646 let current = yield* getByName(env.project, networkFirewallPolicyName);
647
648 if (current === undefined) {
649 yield* compute
650 .insertNetworkFirewallPolicies({
651 project: env.project,
652 body: {
653 name: networkFirewallPolicyName,
654 description: desiredDescription,
655 policyType,
656 },
657 })
658 .pipe(
659 Effect.flatMap((operation) =>
660 waitGlobalOperation(env.project, operation, {
661 ignore: ["RESOURCE_ALREADY_EXISTS"],
662 }),
663 ),
664 Effect.catchTag("Conflict", () => Effect.void),
665 );
666 current = yield* awaitResource(env.project, networkFirewallPolicyName);
667 }
668
669 if ((current.description ?? "") !== desiredDescription) {
670 yield* runOp(
671 env.project,
672 networkFirewallPolicyName,
673 compute.patchNetworkFirewallPolicies({
674 project: env.project,
675 firewallPolicy: networkFirewallPolicyName,
676 body: {
677 description: desiredDescription,
678 fingerprint: current.fingerprint,
679 },
680 }),
681 );
682 current =
683 (yield* getByName(env.project, networkFirewallPolicyName)) ?? current;
684 }
685
686 const nextRules = desiredRules(news);
687 if (nextRules !== undefined) {
688 yield* syncRules(
689 env.project,
690 networkFirewallPolicyName,
691 current.rules ?? [],
692 nextRules,
693 );
694 current =
695 (yield* getByName(env.project, networkFirewallPolicyName)) ?? current;
696 }
697
698 if (news.associations !== undefined) {
699 yield* syncAssociations(
700 env.project,
701 networkFirewallPolicyName,
702 current.associations ?? [],
703 news.associations,
704 );
705 current =
706 (yield* getByName(env.project, networkFirewallPolicyName)) ?? current;
707 }
708
709 return toAttrs(current, env.project);
710 }),
711
712 delete: Effect.fn(function* ({ output }) {
713 if (!output.networkFirewallPolicyName) return;
714 const env = yield* GcpEnvironment.current;
715 const project = output.project || env.project;
716 const existing = yield* getByName(
717 project,
718 output.networkFirewallPolicyName,
719 );
720 for (const association of existing?.associations ?? []) {
721 const name = associationNameOf(association);
722 yield* compute
723 .removeAssociationNetworkFirewallPolicies({
724 project,
725 firewallPolicy: output.networkFirewallPolicyName,
726 name,
727 })
728 .pipe(
729 Effect.flatMap((operation) =>
730 waitGlobalOperation(project, operation, {
731 ignore: ["RESOURCE_NOT_FOUND"],
732 }),
733 ),
734 Effect.catchTag("NotFound", () => Effect.void),
735 );
736 }
737 yield* compute
738 .deleteNetworkFirewallPolicies({
739 project,
740 firewallPolicy: output.networkFirewallPolicyName,
741 })
742 .pipe(
743 Effect.flatMap((operation) =>
744 waitGlobalOperation(project, operation, {
745 ignore: ["RESOURCE_NOT_FOUND"],
746 }),
747 ),
748 Effect.catchTag("NotFound", () => Effect.void),
749 Effect.retry({
750 while: (error) => error._tag === "Conflict",
751 times: 8,
752 schedule: Schedule.spaced("2 seconds"),
753 }),
754 );
755 yield* waitUntilGone(project, output.networkFirewallPolicyName);
756 }),
757 });