1import { ignoredCodes } from "./internal.ts";
2import * as compute from "@distilled.cloud/gcp/compute_v1";
3import { waitRegionOperation } 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 { tagRecord } from "../../Tags.ts";
14import { GcpEnvironment } from "../Environment.ts";
15import {
16 createInternalLabels,
17 diffLabels,
18 hasAlchemyLabels,
19 stripInternalLabels,
20 toLabels,
21} from "../Labels.ts";
22import type { Providers } from "../Providers.ts";
23import type {
24 SecurityPolicyAdaptiveProtectionConfig,
25 SecurityPolicyAdvancedOptionsConfig,
26 SecurityPolicyDdosProtectionConfig,
27 SecurityPolicyRecaptchaOptionsConfig,
28 SecurityPolicyRule,
29 SecurityPolicyRuleMatcher,
30 SecurityPolicyType,
31 SecurityPolicyUserDefinedField,
32} from "./SecurityPolicy.ts";
33
34const DEFAULT_TYPE = "CLOUD_ARMOR";
35const DEFAULT_RULE_PRIORITY = 2147483647;
36const MAX_NAME_LENGTH = 63;
37
38export type RegionSecurityPolicyProps = {
39 /**
40 * Policy name (RFC1035, 1-63 characters). If omitted, a unique name is
41 * generated from the stack, stage, and logical id. Changing the name
42 * replaces the policy.
43 */
44 securityPolicyName?: string;
45 /**
46 * Region the policy lives in (e.g. `us-central1`). Immutable — changing
47 * it replaces the policy. `US-CENTRAL1` is accepted and normalized to
48 * `us-central1`.
49 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
50 */
51 region?: string;
52 /**
53 * Intended use of the policy. Set only at create time — changing it
54 * replaces the policy.
55 * @default "CLOUD_ARMOR"
56 */
57 type?: SecurityPolicyType;
58 /**
59 * Optional description. Mutable in place via `regionSecurityPolicies.patch`.
60 */
61 description?: string;
62 /**
63 * User labels. Alchemy ownership labels (`alchemy-stack` /
64 * `alchemy-stage` / `alchemy-id`) are merged in automatically and
65 * synced via `setLabels` (labels cannot be set on insert).
66 */
67 labels?: Record<string, string>;
68 /**
69 * Ordered Cloud Armor rules. There must always be a default rule at
70 * priority `2147483647` matching `*`. If omitted on create, GCP adds
71 * an `allow` default. When this field is set, Alchemy syncs rules with
72 * `addRule` / `patchRule` / `removeRule` (not `patch`). When omitted
73 * on later updates, observed rules are left in place.
74 */
75 rules?: SecurityPolicyRule[];
76 /**
77 * JSON parsing, log level, and related WAF options. Only applied when
78 * set; omitted values leave the observed config in place.
79 */
80 advancedOptionsConfig?: SecurityPolicyAdvancedOptionsConfig;
81 /**
82 * Cloud Armor Adaptive Protection (CAAP). Only applied when set.
83 */
84 adaptiveProtectionConfig?: SecurityPolicyAdaptiveProtectionConfig;
85 /**
86 * reCAPTCHA site key used by `redirect` rules of type
87 * `GOOGLE_RECAPTCHA`. Only applied when set.
88 */
89 recaptchaOptionsConfig?: SecurityPolicyRecaptchaOptionsConfig;
90 /**
91 * Network DDoS protection (`STANDARD` or `ADVANCED`). Only applied
92 * when set.
93 */
94 ddosProtectionConfig?: SecurityPolicyDdosProtectionConfig;
95 /**
96 * Packet field extractors for `CLOUD_ARMOR_NETWORK` policies. Only
97 * applied when set.
98 */
99 userDefinedFields?: SecurityPolicyUserDefinedField[];
100};
101
102export type RegionSecurityPolicy = Resource<
103 "GCP.Compute.RegionSecurityPolicy",
104 RegionSecurityPolicyProps,
105 {
106 /** Policy name. */
107 securityPolicyName: string;
108 /** Project id. */
109 project: string;
110 /** Region short name (`us-central1`). */
111 region: string;
112 /** Policy type (`CLOUD_ARMOR`, `CLOUD_ARMOR_EDGE`, …). */
113 type: string;
114 /** Description. */
115 description: string | undefined;
116 /** User labels (Alchemy ownership labels stripped). */
117 labels: Record<string, string>;
118 /** Rules currently attached to the policy (including the default). */
119 rules: SecurityPolicyRule[];
120 /** Advanced WAF options, if configured. */
121 advancedOptionsConfig: SecurityPolicyAdvancedOptionsConfig | undefined;
122 /** Adaptive Protection config, if configured. */
123 adaptiveProtectionConfig:
124 | SecurityPolicyAdaptiveProtectionConfig
125 | undefined;
126 /** reCAPTCHA options, if configured. */
127 recaptchaOptionsConfig: SecurityPolicyRecaptchaOptionsConfig | undefined;
128 /** DDoS protection config, if configured. */
129 ddosProtectionConfig: SecurityPolicyDdosProtectionConfig | undefined;
130 /** User-defined packet fields, if configured. */
131 userDefinedFields: SecurityPolicyUserDefinedField[];
132 /** Optimistic-locking fingerprint for metadata patches. */
133 fingerprint: string | undefined;
134 /** Label fingerprint for `setLabels`. */
135 labelFingerprint: string | undefined;
136 /** Server-defined URL. */
137 selfLink: string | undefined;
138 /** Server-assigned numeric id. */
139 securityPolicyId: string | undefined;
140 /** RFC3339 creation timestamp. */
141 creationTimestamp: string | undefined;
142 /** Resource kind. */
143 kind: string | undefined;
144 },
145 never,
146 Providers
147>;
148
149/**
150 * A regional Cloud Armor security policy that filters HTTP(S) requests
151 * targeting regional backend services.
152 *
153 * Type and region are immutable — changing either replaces the policy.
154 * Description, advanced options, Adaptive Protection, reCAPTCHA, DDoS
155 * config, and user-defined fields update in place via
156 * `regionSecurityPolicies.patch`. Rules are synced with `addRule` /
157 * `patchRule` / `removeRule` (not `patch`). Labels are applied with
158 * `setLabels` after the policy exists.
159 *
160 * ### Creating a Regional Security Policy
161 * **Example:** Generated name with a deny rule
162 * ```typescript
163 * const policy = yield* GCP.Compute.RegionSecurityPolicy("Armor", {
164 * region: "us-central1",
165 * description: "deny a scanner",
166 * rules: [
167 * {
168 * action: "deny(403)",
169 * priority: 1000,
170 * description: "block scanner",
171 * match: {
172 * versionedExpr: "SRC_IPS_V1",
173 * config: { srcIpRanges: ["9.9.9.0/24"] },
174 * },
175 * },
176 * ],
177 * });
178 * ```
179 *
180 * @resource
181 * @category Compute
182 */
183export const RegionSecurityPolicy = Resource<RegionSecurityPolicy>(
184 "GCP.Compute.RegionSecurityPolicy",
185);
186
187export class RegionSecurityPolicyNotResolved extends Data.TaggedError(
188 "GCP.Compute.RegionSecurityPolicyNotResolved",
189)<{
190 securityPolicyName: string;
191 region: string;
192}> {}
193
194export class RegionSecurityPolicyStillExists extends Data.TaggedError(
195 "GCP.Compute.RegionSecurityPolicyStillExists",
196)<{
197 securityPolicyName: string;
198 region: string;
199}> {}
200
201const lastSegment = (value: string | undefined): string => {
202 if (value === undefined || value.length === 0) return "";
203 const parts = value.split("/");
204 return parts[parts.length - 1] || value;
205};
206
207const normalizeRegion = (region: string | undefined, defaultRegion: string) =>
208 lastSegment(region ?? defaultRegion).toLowerCase();
209
210const rfc1035 = (name: string): string => {
211 let next = name
212 .toLowerCase()
213 .replace(/[^a-z0-9-]/g, "-")
214 .replace(/-+/g, "-")
215 .replace(/^-+/, "")
216 .replace(/-+$/, "");
217 if (!/^[a-z]/.test(next)) {
218 next = `s${next}`;
219 }
220 next = next.slice(0, MAX_NAME_LENGTH).replace(/-+$/, "");
221 return next.length > 0 ? next : "policy";
222};
223
224const toName = (id: string, name: string | undefined, existing?: string) =>
225 Effect.gen(function* () {
226 if (name !== undefined) return name;
227 if (existing !== undefined) return existing;
228 return rfc1035(
229 yield* createPhysicalName({
230 id,
231 maxLength: MAX_NAME_LENGTH,
232 lowercase: true,
233 }),
234 );
235 });
236
237const typeOf = (value: string | undefined) =>
238 (value ?? DEFAULT_TYPE).toUpperCase();
239
240const userLabels = (
241 labels: Record<string, string | undefined> | null | undefined,
242): Record<string, string> => stripInternalLabels(tagRecord(labels));
243
244const sorted = (values: readonly string[] | undefined) =>
245 [...(values ?? [])].slice().sort();
246
247const sameJson = (left: unknown, right: unknown) =>
248 JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
249
250const subsetEqual = (observed: unknown, desired: unknown): boolean => {
251 if (desired === undefined) return true;
252 if (typeof desired !== typeof observed) return false;
253 if (desired === null || observed === null) return desired === observed;
254 if (Array.isArray(desired)) {
255 if (!Array.isArray(observed) || desired.length !== observed.length) {
256 return false;
257 }
258 return desired.every((item, index) => subsetEqual(observed[index], item));
259 }
260 if (typeof desired === "object") {
261 if (typeof observed !== "object") return false;
262 const current = observed as Record<string, unknown>;
263 return Object.entries(desired as Record<string, unknown>).every(
264 ([key, value]) => value === undefined || subsetEqual(current[key], value),
265 );
266 }
267 return observed === desired;
268};
269
270const canonMatch = (match: SecurityPolicyRuleMatcher | undefined) => {
271 if (match === undefined) return undefined;
272 return {
273 versionedExpr: match.versionedExpr,
274 config: match.config
275 ? { srcIpRanges: sorted(match.config.srcIpRanges) }
276 : undefined,
277 expr: match.expr,
278 exprOptions: match.exprOptions,
279 };
280};
281
282const canonRule = (rule: SecurityPolicyRule) => ({
283 priority: rule.priority,
284 action: rule.action,
285 description: rule.description ?? "",
286 preview: rule.preview === true,
287 match: canonMatch(rule.match),
288 redirectOptions: rule.redirectOptions ?? null,
289 rateLimitOptions: rule.rateLimitOptions ?? null,
290 headerAction: rule.headerAction ?? null,
291 preconfiguredWafConfig: rule.preconfiguredWafConfig ?? null,
292 networkMatch: rule.networkMatch ?? null,
293});
294
295const ruleEquals = (left: SecurityPolicyRule, right: SecurityPolicyRule) =>
296 sameJson(canonRule(left), canonRule(right));
297
298const toRuleBody = (rule: SecurityPolicyRule): SecurityPolicyRule => ({
299 priority: rule.priority,
300 action: rule.action,
301 description: rule.description,
302 preview: rule.preview === true ? true : undefined,
303 match: rule.match,
304 redirectOptions: rule.redirectOptions,
305 rateLimitOptions: rule.rateLimitOptions,
306 headerAction: rule.headerAction,
307 preconfiguredWafConfig: rule.preconfiguredWafConfig,
308 networkMatch: rule.networkMatch,
309});
310
311const withDefaultRule = (
312 rules: readonly SecurityPolicyRule[] | undefined,
313): SecurityPolicyRule[] | undefined => {
314 if (rules === undefined) return undefined;
315 const bodies = rules.map(toRuleBody);
316 if (bodies.some((rule) => rule.priority === DEFAULT_RULE_PRIORITY)) {
317 return bodies;
318 }
319 return [...bodies, defaultAllowRule()];
320};
321
322const defaultAllowRule = (): SecurityPolicyRule => ({
323 action: "allow",
324 priority: DEFAULT_RULE_PRIORITY,
325 match: {
326 versionedExpr: "SRC_IPS_V1",
327 config: { srcIpRanges: ["*"] },
328 },
329});
330
331const desiredRules = (
332 news: RegionSecurityPolicyProps,
333 observed: readonly SecurityPolicyRule[],
334): SecurityPolicyRule[] | undefined => {
335 if (news.rules === undefined) return undefined;
336 const byPriority = new Map<number, SecurityPolicyRule>();
337 for (const rule of news.rules) {
338 if (rule.priority === undefined) continue;
339 byPriority.set(rule.priority, rule);
340 }
341 if (!byPriority.has(DEFAULT_RULE_PRIORITY)) {
342 const observedDefault = observed.find(
343 (rule) => rule.priority === DEFAULT_RULE_PRIORITY,
344 );
345 byPriority.set(
346 DEFAULT_RULE_PRIORITY,
347 observedDefault ?? defaultAllowRule(),
348 );
349 }
350 return [...byPriority.values()].sort(
351 (left, right) => (left.priority ?? 0) - (right.priority ?? 0),
352 );
353};
354
355const toAttrs = (policy: compute.SecurityPolicy, project: string) => ({
356 securityPolicyName: policy.name ?? policy.id ?? "",
357 project,
358 region: lastSegment(policy.region).toLowerCase(),
359 type: typeOf(policy.type),
360 description: policy.description,
361 labels: userLabels(policy.labels),
362 rules: policy.rules ?? [],
363 advancedOptionsConfig: policy.advancedOptionsConfig,
364 adaptiveProtectionConfig: policy.adaptiveProtectionConfig,
365 recaptchaOptionsConfig: policy.recaptchaOptionsConfig,
366 ddosProtectionConfig: policy.ddosProtectionConfig,
367 userDefinedFields: policy.userDefinedFields ?? [],
368 fingerprint: policy.fingerprint,
369 labelFingerprint: policy.labelFingerprint,
370 selfLink: policy.selfLink,
371 securityPolicyId: policy.id,
372 creationTimestamp: policy.creationTimestamp,
373 kind: policy.kind,
374});
375
376const getByName = (project: string, region: string, securityPolicy: string) =>
377 compute
378 .getRegionSecurityPolicies({ project, region, securityPolicy })
379 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
380
381const awaitResource = (
382 project: string,
383 region: string,
384 securityPolicyName: string,
385) =>
386 getByName(project, region, securityPolicyName).pipe(
387 Effect.flatMap((policy) =>
388 policy !== undefined
389 ? Effect.succeed(policy)
390 : Effect.fail(
391 new RegionSecurityPolicyNotResolved({
392 securityPolicyName,
393 region,
394 }),
395 ),
396 ),
397 Effect.retry({
398 while: (error) =>
399 error._tag === "GCP.Compute.RegionSecurityPolicyNotResolved",
400 times: 8,
401 schedule: Schedule.spaced("1 second"),
402 }),
403 );
404
405const waitUntilGone = (
406 project: string,
407 region: string,
408 securityPolicyName: string,
409) =>
410 getByName(project, region, securityPolicyName).pipe(
411 Effect.flatMap((policy) =>
412 policy === undefined
413 ? Effect.void
414 : Effect.fail(
415 new RegionSecurityPolicyStillExists({
416 securityPolicyName,
417 region,
418 }),
419 ),
420 ),
421 Effect.retry({
422 while: (error) =>
423 error._tag === "GCP.Compute.RegionSecurityPolicyStillExists",
424 times: 10,
425 schedule: Schedule.spaced("1 second"),
426 }),
427 Effect.catchTag(
428 "GCP.Compute.RegionSecurityPolicyStillExists",
429 () => Effect.void,
430 ),
431 );
432
433const runOp = <E extends { readonly _tag: string }, R>(
434 project: string,
435 region: string,
436 securityPolicyName: string,
437 start: Effect.Effect<compute.Operation, E, R>,
438 options?: { ignoreAlreadyExists?: boolean; ignoreNotFound?: boolean },
439) =>
440 start.pipe(
441 Effect.flatMap((operation) =>
442 waitRegionOperation(project, region, operation, {
443 ignore: ignoredCodes(options),
444 }),
445 ),
446 Effect.retry({
447 while: (error) => error._tag === "Conflict",
448 times: 5,
449 schedule: Schedule.spaced("1 second"),
450 }),
451 );
452
453const insertBody = (
454 securityPolicyName: string,
455 news: RegionSecurityPolicyProps,
456): compute.SecurityPolicy => ({
457 name: securityPolicyName,
458 description: news.description,
459 type: typeOf(news.type),
460 rules: withDefaultRule(news.rules),
461 advancedOptionsConfig: news.advancedOptionsConfig,
462 adaptiveProtectionConfig: news.adaptiveProtectionConfig,
463 recaptchaOptionsConfig: news.recaptchaOptionsConfig,
464 ddosProtectionConfig: news.ddosProtectionConfig,
465 userDefinedFields: news.userDefinedFields,
466});
467
468const syncRules = (
469 project: string,
470 region: string,
471 securityPolicyName: string,
472 observed: readonly SecurityPolicyRule[],
473 desired: readonly SecurityPolicyRule[],
474) =>
475 Effect.gen(function* () {
476 const observedByPriority = new Map<number, SecurityPolicyRule>();
477 for (const rule of observed) {
478 if (rule.priority !== undefined) {
479 observedByPriority.set(rule.priority, rule);
480 }
481 }
482 const desiredByPriority = new Map<number, SecurityPolicyRule>();
483 for (const rule of desired) {
484 if (rule.priority !== undefined) {
485 desiredByPriority.set(rule.priority, rule);
486 }
487 }
488
489 for (const [priority, rule] of desiredByPriority) {
490 const current = observedByPriority.get(priority);
491 if (current === undefined) {
492 yield* runOp(
493 project,
494 region,
495 securityPolicyName,
496 compute.addRuleRegionSecurityPolicies({
497 project,
498 region,
499 securityPolicy: securityPolicyName,
500 body: toRuleBody(rule),
501 }),
502 { ignoreAlreadyExists: true },
503 );
504 continue;
505 }
506 if (!ruleEquals(current, rule)) {
507 yield* runOp(
508 project,
509 region,
510 securityPolicyName,
511 compute.patchRuleRegionSecurityPolicies({
512 project,
513 region,
514 securityPolicy: securityPolicyName,
515 priority,
516 body: toRuleBody(rule),
517 }),
518 );
519 }
520 }
521
522 for (const priority of observedByPriority.keys()) {
523 if (desiredByPriority.has(priority)) continue;
524 if (priority === DEFAULT_RULE_PRIORITY) continue;
525 yield* runOp(
526 project,
527 region,
528 securityPolicyName,
529 compute.removeRuleRegionSecurityPolicies({
530 project,
531 region,
532 securityPolicy: securityPolicyName,
533 priority,
534 }),
535 { ignoreNotFound: true },
536 ).pipe(Effect.catchTag("NotFound", () => Effect.void));
537 }
538 });
539
540export const RegionSecurityPolicyProvider = () =>
541 Provider.succeed(RegionSecurityPolicy, {
542 stables: [
543 "securityPolicyName",
544 "project",
545 "region",
546 "type",
547 "securityPolicyId",
548 "selfLink",
549 "creationTimestamp",
550 ],
551
552 diff: Effect.fn(function* ({ news, olds, output }) {
553 if (!isResolved(news)) return undefined;
554 const env = yield* GcpEnvironment.current;
555
556 const previousName =
557 olds?.securityPolicyName ?? output?.securityPolicyName;
558 const nextName = news.securityPolicyName ?? previousName;
559 const nameChanged =
560 previousName !== undefined &&
561 nextName !== undefined &&
562 previousName !== nextName;
563
564 const previousRegion = normalizeRegion(
565 olds?.region ?? output?.region,
566 env.region,
567 );
568 const nextRegion = normalizeRegion(
569 news.region ?? output?.region,
570 env.region,
571 );
572 const regionChanged = previousRegion !== nextRegion;
573
574 const previousType = typeOf(olds?.type ?? output?.type);
575 const nextType = typeOf(news.type ?? output?.type);
576 const typeChanged = previousType !== nextType;
577
578 if (nameChanged) {
579 return { action: "replace" as const, deleteFirst: false };
580 }
581 if (regionChanged || typeChanged) {
582 return { action: "replace" as const, deleteFirst: !regionChanged };
583 }
584 return undefined;
585 }),
586
587 read: Effect.fn(function* ({ id, olds, output }) {
588 const env = yield* GcpEnvironment.current;
589 const securityPolicyName = yield* toName(
590 id,
591 olds?.securityPolicyName,
592 output?.securityPolicyName,
593 );
594 const region = normalizeRegion(
595 olds?.region ?? output?.region,
596 env.region,
597 );
598 const existing = yield* getByName(
599 env.project,
600 region,
601 securityPolicyName,
602 );
603 if (existing === undefined) return undefined;
604 const attrs = toAttrs(existing, env.project);
605 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
606 ? attrs
607 : Unowned(attrs);
608 }),
609
610 list: () =>
611 Effect.gen(function* () {
612 const env = yield* GcpEnvironment.current;
613 const pages = yield* compute.aggregatedListSecurityPolicies
614 .pages({
615 project: env.project,
616 maxResults: 500,
617 filter: "labels.alchemy-id:*",
618 returnPartialSuccess: true,
619 })
620 .pipe(Stream.runCollect);
621 return Array.from(pages).flatMap((page) =>
622 Object.values(page.items ?? {}).flatMap((scoped) =>
623 (scoped?.securityPolicies ?? [])
624 .filter((policy) => (policy.region ?? "").length > 0)
625 .filter((policy) =>
626 Object.keys(policy.labels ?? {}).some((key) =>
627 key.startsWith("alchemy-"),
628 ),
629 )
630 .map((policy) => toAttrs(policy, env.project)),
631 ),
632 );
633 }),
634
635 reconcile: Effect.fn(function* ({ id, news, output }) {
636 const env = yield* GcpEnvironment.current;
637 const securityPolicyName = yield* toName(
638 id,
639 news.securityPolicyName,
640 output?.securityPolicyName,
641 );
642 const region = normalizeRegion(news.region ?? output?.region, env.region);
643 const desiredLabels = {
644 ...toLabels(news.labels),
645 ...(yield* createInternalLabels(id)),
646 };
647
648 let current = yield* getByName(env.project, region, securityPolicyName);
649
650 if (current === undefined) {
651 yield* compute
652 .insertRegionSecurityPolicies({
653 project: env.project,
654 region,
655 body: insertBody(securityPolicyName, news),
656 })
657 .pipe(
658 Effect.flatMap((operation) =>
659 waitRegionOperation(env.project, region, operation, {
660 ignore: ["RESOURCE_ALREADY_EXISTS"],
661 }),
662 ),
663 Effect.catchTag("Conflict", () => Effect.void),
664 );
665 current = yield* awaitResource(env.project, region, securityPolicyName);
666 }
667
668 if (current === undefined) {
669 return yield* new RegionSecurityPolicyNotResolved({
670 securityPolicyName,
671 region,
672 });
673 }
674
675 const patch: compute.SecurityPolicy = {
676 fingerprint: current.fingerprint,
677 };
678 let needsPatch = false;
679
680 if ((current.description ?? "") !== (news.description ?? "")) {
681 patch.description = news.description ?? "";
682 needsPatch = true;
683 }
684 if (
685 news.advancedOptionsConfig !== undefined &&
686 !subsetEqual(current.advancedOptionsConfig, news.advancedOptionsConfig)
687 ) {
688 patch.advancedOptionsConfig = news.advancedOptionsConfig;
689 needsPatch = true;
690 }
691 if (
692 news.adaptiveProtectionConfig !== undefined &&
693 !subsetEqual(
694 current.adaptiveProtectionConfig,
695 news.adaptiveProtectionConfig,
696 )
697 ) {
698 patch.adaptiveProtectionConfig = news.adaptiveProtectionConfig;
699 needsPatch = true;
700 }
701 if (
702 news.recaptchaOptionsConfig !== undefined &&
703 !subsetEqual(
704 current.recaptchaOptionsConfig,
705 news.recaptchaOptionsConfig,
706 )
707 ) {
708 patch.recaptchaOptionsConfig = news.recaptchaOptionsConfig;
709 needsPatch = true;
710 }
711 if (
712 news.ddosProtectionConfig !== undefined &&
713 !subsetEqual(current.ddosProtectionConfig, news.ddosProtectionConfig)
714 ) {
715 patch.ddosProtectionConfig = news.ddosProtectionConfig;
716 needsPatch = true;
717 }
718 if (
719 news.userDefinedFields !== undefined &&
720 !sameJson(current.userDefinedFields ?? [], news.userDefinedFields)
721 ) {
722 patch.userDefinedFields = news.userDefinedFields;
723 needsPatch = true;
724 }
725
726 if (needsPatch) {
727 yield* runOp(
728 env.project,
729 region,
730 securityPolicyName,
731 compute.patchRegionSecurityPolicies({
732 project: env.project,
733 region,
734 securityPolicy: securityPolicyName,
735 body: patch,
736 }),
737 );
738 current =
739 (yield* getByName(env.project, region, securityPolicyName)) ??
740 current;
741 }
742
743 const nextRules = desiredRules(news, current.rules ?? []);
744 if (nextRules !== undefined) {
745 yield* syncRules(
746 env.project,
747 region,
748 securityPolicyName,
749 current.rules ?? [],
750 nextRules,
751 );
752 current =
753 (yield* getByName(env.project, region, securityPolicyName)) ??
754 current;
755 }
756
757 const observedLabels = tagRecord(current.labels);
758 const { upsert, removed } = diffLabels(observedLabels, desiredLabels);
759 if (upsert.length > 0 || removed.length > 0) {
760 yield* runOp(
761 env.project,
762 region,
763 securityPolicyName,
764 compute.setLabelsRegionSecurityPolicies({
765 project: env.project,
766 region,
767 resource: securityPolicyName,
768 body: {
769 labels: desiredLabels,
770 labelFingerprint: current.labelFingerprint,
771 },
772 }),
773 );
774 current =
775 (yield* getByName(env.project, region, securityPolicyName)) ??
776 current;
777 }
778
779 if (current === undefined) {
780 return yield* new RegionSecurityPolicyNotResolved({
781 securityPolicyName,
782 region,
783 });
784 }
785
786 return toAttrs(current, env.project);
787 }),
788
789 delete: Effect.fn(function* ({ output }) {
790 const env = yield* GcpEnvironment.current;
791 const project = output.project || env.project;
792 const region = normalizeRegion(output.region, env.region);
793 yield* compute
794 .deleteRegionSecurityPolicies({
795 project,
796 region,
797 securityPolicy: output.securityPolicyName,
798 })
799 .pipe(
800 Effect.flatMap((operation) =>
801 waitRegionOperation(project, region, operation, {
802 ignore: ["RESOURCE_NOT_FOUND"],
803 }),
804 ),
805 Effect.catchTag("NotFound", () => Effect.void),
806 Effect.retry({
807 while: (error) => error._tag === "Conflict",
808 times: 8,
809 schedule: Schedule.spaced("2 seconds"),
810 }),
811 );
812 yield* waitUntilGone(project, region, output.securityPolicyName);
813 }),
814 });