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