1import * as networksecurity from "@distilled.cloud/gcp/networksecurity_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 { GcpEnvironment } from "../Environment.ts";
9import { createInternalLabels, hasAlchemyLabels } from "../Labels.ts";
10import type { Providers } from "../Providers.ts";
11import {
12 NetworksecurityNotResolved,
13 canonicalizeLink,
14 changedFields,
15 collectPages,
16 encodeOwnership,
17 hasOwnershipMarker,
18 normalizeLocation,
19 parentOf,
20 parseName,
21 parseOwnership,
22 rfc1035,
23 toPhysicalId,
24 waitForOperation,
25 waitUntilGone,
26 waitUntilPresent,
27} from "./internal.ts";
28
29const COLLECTION = "gatewaySecurityPolicies";
30
31export type GatewaySecurityPolicyProps = {
32 /**
33 * GatewaySecurityPolicy id (the `{gatewaySecurityPolicy}` segment of
34 * `projects/{project}/locations/{location}/gatewaySecurityPolicies/{gatewaySecurityPolicy}`).
35 * If omitted, a unique RFC1035 name is generated from the stack, stage,
36 * and logical id. Immutable — changing it replaces the policy.
37 */
38 gatewaySecurityPolicyId?: string;
39 /**
40 * Region of the policy (`us-central1`, …). Immutable — changing it
41 * replaces the policy. `US-CENTRAL1` is accepted and normalized to
42 * `us-central1`.
43 * @default the stack's GCP region (`GCP.Region`, profile region, `us-central1`)
44 */
45 location?: string;
46 /**
47 * Human-readable description. GatewaySecurityPolicy has no labels
48 * field, so Alchemy stamps ownership into a `[alchemy …]` prefix and
49 * strips it from attributes.
50 */
51 description?: string;
52 /**
53 * TlsInspectionPolicy resource name used by rules that enable TLS
54 * inspection.
55 */
56 tlsInspectionPolicy?: string;
57};
58
59export type GatewaySecurityPolicy = Resource<
60 "GCP.NetworkSecurity.GatewaySecurityPolicy",
61 GatewaySecurityPolicyProps,
62 {
63 /** Full resource name `projects/{project}/locations/{location}/gatewaySecurityPolicies/{gatewaySecurityPolicy}`. */
64 name: string;
65 /** GatewaySecurityPolicy id (last path segment). */
66 gatewaySecurityPolicyId: string;
67 /** Project id. */
68 project: string;
69 /** Location id (`us-central1`). */
70 location: string;
71 /** User description with the Alchemy ownership prefix stripped. */
72 description: string | undefined;
73 /** Attached TlsInspectionPolicy resource name, if any. */
74 tlsInspectionPolicy: string | undefined;
75 /** RFC3339 creation timestamp. */
76 createTime: string | undefined;
77 /** RFC3339 last-update timestamp. */
78 updateTime: string | undefined;
79 },
80 never,
81 Providers
82>;
83
84/**
85 * A Gateway Security Policy — a collection of
86 * GatewaySecurityPolicyRules used by Secure Web Proxy.
87 *
88 * The API has no labels field, so Alchemy stamps ownership into the
89 * description for `list` / nuke. Changing `gatewaySecurityPolicyId` or
90 * `location` replaces the policy. Description and `tlsInspectionPolicy`
91 * update in place.
92 *
93 * ### Creating a GatewaySecurityPolicy
94 * **Example:** Generated name
95 * ```typescript
96 * const policy = yield* GCP.NetworkSecurity.GatewaySecurityPolicy("Swp", {
97 * description: "secure web proxy",
98 * });
99 * ```
100 *
101 * **Example:** Named policy with TLS inspection
102 * ```typescript
103 * const policy = yield* GCP.NetworkSecurity.GatewaySecurityPolicy("Swp", {
104 * gatewaySecurityPolicyId: "app-swp",
105 * location: "us-central1",
106 * tlsInspectionPolicy: inspection.name,
107 * });
108 * ```
109 *
110 * @resource
111 * @category NetworkSecurity
112 */
113export const GatewaySecurityPolicy = Resource<GatewaySecurityPolicy>(
114 "GCP.NetworkSecurity.GatewaySecurityPolicy",
115);
116
117const resourceName = (
118 project: string,
119 location: string,
120 gatewaySecurityPolicyId: string,
121) =>
122 `projects/${project}/locations/${location}/gatewaySecurityPolicies/${gatewaySecurityPolicyId}`;
123
124const toAttrs = (
125 policy: networksecurity.GatewaySecurityPolicy,
126 project: string,
127 region: string,
128) => {
129 const name = policy.name ?? "";
130 const parsed = parseName(name, COLLECTION, region);
131 const ownership = parseOwnership(policy.description);
132 return {
133 name,
134 gatewaySecurityPolicyId: parsed.id,
135 project: parsed.project || project,
136 location: parsed.location || region,
137 description: ownership.text,
138 tlsInspectionPolicy: policy.tlsInspectionPolicy
139 ? canonicalizeLink(policy.tlsInspectionPolicy)
140 : undefined,
141 createTime: policy.createTime,
142 updateTime: policy.updateTime,
143 };
144};
145
146const getByName = (name: string) =>
147 networksecurity
148 .getProjectsLocationsGatewaySecurityPolicies({ name })
149 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
150
151export const GatewaySecurityPolicyProvider = () =>
152 Provider.succeed(GatewaySecurityPolicy, {
153 stables: [
154 "name",
155 "gatewaySecurityPolicyId",
156 "project",
157 "location",
158 "createTime",
159 ],
160
161 diff: Effect.fn(function* ({ news, olds, output }) {
162 if (!isResolved(news)) return undefined;
163 const env = yield* GcpEnvironment.current;
164 const previousId =
165 olds?.gatewaySecurityPolicyId ?? output?.gatewaySecurityPolicyId;
166 const nextId = news.gatewaySecurityPolicyId
167 ? rfc1035(news.gatewaySecurityPolicyId, "gateway-security-policy")
168 : previousId;
169 const previousLocation = normalizeLocation(
170 olds?.location ?? output?.location,
171 env.region,
172 );
173 const nextLocation = normalizeLocation(
174 news.location ?? olds?.location ?? output?.location,
175 env.region,
176 );
177 if (
178 (previousId !== undefined &&
179 nextId !== undefined &&
180 nextId !== previousId) ||
181 previousLocation !== nextLocation
182 ) {
183 return { action: "replace" as const };
184 }
185 return undefined;
186 }),
187
188 read: Effect.fn(function* ({ id, olds, output }) {
189 const env = yield* GcpEnvironment.current;
190 const gatewaySecurityPolicyId = yield* toPhysicalId(
191 id,
192 olds?.gatewaySecurityPolicyId,
193 output?.gatewaySecurityPolicyId,
194 "gateway-security-policy",
195 );
196 const location = normalizeLocation(
197 olds?.location ?? output?.location,
198 env.region,
199 );
200 const name =
201 output?.name ??
202 resourceName(env.project, location, gatewaySecurityPolicyId);
203 const existing = yield* getByName(name);
204 if (existing === undefined) return undefined;
205 const attrs = toAttrs(existing, env.project, env.region);
206 const { labels } = parseOwnership(existing.description);
207 return (yield* hasAlchemyLabels(id, labels)) ? attrs : Unowned(attrs);
208 }),
209
210 list: () =>
211 Effect.gen(function* () {
212 const env = yield* GcpEnvironment.current;
213 const items = yield* collectPages(
214 networksecurity.listProjectsLocationsGatewaySecurityPolicies.pages({
215 parent: parentOf(env.project, "-"),
216 pageSize: 1000,
217 }),
218 (page) => page.gatewaySecurityPolicies,
219 );
220 return items
221 .filter((item) => hasOwnershipMarker(item.description))
222 .map((item) => toAttrs(item, env.project, env.region));
223 }),
224
225 reconcile: Effect.fn(function* ({ id, news, output }) {
226 const env = yield* GcpEnvironment.current;
227 const gatewaySecurityPolicyId = yield* toPhysicalId(
228 id,
229 news.gatewaySecurityPolicyId,
230 output?.gatewaySecurityPolicyId,
231 "gateway-security-policy",
232 );
233 const location = normalizeLocation(
234 news.location ?? output?.location,
235 env.region,
236 );
237 const name = resourceName(env.project, location, gatewaySecurityPolicyId);
238 const ownership = yield* createInternalLabels(id);
239 const desiredDescription = encodeOwnership(ownership, news.description);
240 const tlsInspectionPolicy = news.tlsInspectionPolicy
241 ? canonicalizeLink(news.tlsInspectionPolicy)
242 : undefined;
243
244 let current = yield* getByName(name);
245
246 if (current === undefined) {
247 const created = yield* networksecurity
248 .createProjectsLocationsGatewaySecurityPolicies({
249 parent: parentOf(env.project, location),
250 gatewaySecurityPolicyId,
251 body: {
252 description: desiredDescription,
253 tlsInspectionPolicy,
254 },
255 })
256 .pipe(
257 Effect.retry({
258 while: (error) => error._tag === "Conflict",
259 times: 5,
260 schedule: Schedule.spaced("2 seconds"),
261 }),
262 Effect.catchTag("Conflict", () => Effect.succeed(undefined)),
263 );
264 if (created !== undefined) {
265 yield* waitForOperation(created);
266 }
267 current = yield* waitUntilPresent(getByName(name), name);
268 }
269
270 if (current === undefined) {
271 return yield* new NetworksecurityNotResolved({ name });
272 }
273
274 const descriptionChanged =
275 (current.description ?? "") !== desiredDescription;
276 const tlsChanged =
277 canonicalizeLink(current.tlsInspectionPolicy) !==
278 (tlsInspectionPolicy ?? "");
279 const updateMask = changedFields([
280 ["description", descriptionChanged],
281 ["tlsInspectionPolicy", tlsChanged],
282 ]);
283
284 if (updateMask.length > 0) {
285 const operation =
286 yield* networksecurity.patchProjectsLocationsGatewaySecurityPolicies({
287 name: current.name ?? name,
288 updateMask: updateMask.join(","),
289 body: {
290 name: current.name ?? name,
291 description: desiredDescription,
292 tlsInspectionPolicy,
293 },
294 });
295 yield* waitForOperation(operation);
296 current = yield* waitUntilPresent(
297 getByName(current.name ?? name),
298 current.name ?? name,
299 );
300 }
301
302 return toAttrs(current, env.project, env.region);
303 }),
304
305 delete: Effect.fn(function* ({ output }) {
306 const operation = yield* networksecurity
307 .deleteProjectsLocationsGatewaySecurityPolicies({ name: output.name })
308 .pipe(
309 Effect.retry({
310 while: (error) => error._tag === "Conflict",
311 times: 8,
312 schedule: Schedule.spaced("2 seconds"),
313 }),
314 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
315 );
316 if (operation !== undefined) {
317 yield* waitForOperation(operation, { notFoundOk: true });
318 }
319 yield* waitUntilGone(getByName(output.name), output.name);
320 }),
321 });