1import * as vmwareengine from "@distilled.cloud/gcp/vmwareengine_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 type { Providers } from "../Providers.ts";
10import {
11 VmwareengineNotResolved,
12 changedFields,
13 collectPages,
14 createInternalLabels,
15 encodeOwnership,
16 expandName,
17 hasAlchemyLabels,
18 hasOwnershipMarker,
19 listAcrossLocations,
20 locationFromName,
21 normalizeLocation,
22 parseName,
23 parseOwnership,
24 replaceOnIdentity,
25 rfc1035,
26 sameJson,
27 toPhysicalId,
28 waitForOperation,
29 waitUntilGone,
30 waitUntilPresent,
31 waitUntilReady,
32} from "./internal.ts";
33
34const COLLECTION = "externalAccessRules";
35const PARENT_COLLECTION = "networkPolicies";
36
37export type IpRange = {
38 /** A single IP address (`10.0.0.5`). */
39 ipAddress?: string;
40 /** A CIDR range (`10.0.0.0/24`). */
41 ipAddressRange?: string;
42 /**
43 * ExternalAddress resource name reserved in the parent network policy
44 * scope.
45 */
46 externalAddress?: string;
47};
48
49export type ExternalAccessRuleAction =
50 | vmwareengine.ExternalAccessRuleActionEnum
51 | (string & {});
52
53export type NetworkPoliciesExternalAccessRuleProps = {
54 /**
55 * Parent NetworkPolicy resource name
56 * (`projects/{project}/locations/{location}/networkPolicies/{networkPolicy}`)
57 * or the policy id. Immutable — changing it replaces the rule.
58 */
59 networkPolicy: string;
60 /**
61 * Rule id (the `{externalAccessRule}` segment of
62 * `.../networkPolicies/{networkPolicy}/externalAccessRules/{externalAccessRule}`).
63 * If omitted, a unique RFC1035 name is generated. Immutable.
64 */
65 externalAccessRuleId?: string;
66 /**
67 * Region of the parent policy. Inferred from `networkPolicy` when that
68 * value is a full resource name. Immutable.
69 * @default the stack's GCP region (`GCP.Region`, else the profile
70 * region, else `us-central1`)
71 */
72 location?: string;
73 /**
74 * Rule priority (`100`–`4096`). Lower integers take precedence.
75 * @default 1000
76 */
77 priority?: number;
78 /**
79 * Action (`ALLOW` or `DENY`).
80 * @default "ALLOW"
81 */
82 action?: ExternalAccessRuleAction;
83 /**
84 * IP protocol (`tcp`, `udp`, or `icmp`).
85 * @default "tcp"
86 */
87 ipProtocol?: string;
88 /**
89 * Source IP ranges. Use `0.0.0.0/0` to match all sources.
90 */
91 sourceIpRanges?: IpRange[];
92 /**
93 * Source ports (`["22"]`, `["80","443"]`, `["12345-12349"]`). Use
94 * `["0-65535"]` to match all. TCP/UDP only.
95 */
96 sourcePorts?: string[];
97 /**
98 * Destination IP ranges. Must be reserved external IPs in the parent
99 * policy, or `0.0.0.0/0` for all.
100 */
101 destinationIpRanges?: IpRange[];
102 /**
103 * Destination ports. TCP/UDP only.
104 */
105 destinationPorts?: string[];
106 /**
107 * Human-readable description. Rules have no labels field, so Alchemy
108 * stamps ownership into a `[alchemy …]` prefix and strips it from
109 * attributes.
110 */
111 description?: string;
112};
113
114export type NetworkPoliciesExternalAccessRule = Resource<
115 "GCP.VMwareEngine.NetworkPoliciesExternalAccessRule",
116 NetworkPoliciesExternalAccessRuleProps,
117 {
118 /** Full resource name. */
119 name: string;
120 /** Rule id (last path segment). */
121 externalAccessRuleId: string;
122 /** Parent NetworkPolicy resource name. */
123 networkPolicy: string;
124 /** Project id. */
125 project: string;
126 /** Location id. */
127 location: string;
128 /** Rule priority. */
129 priority: number | undefined;
130 /** Action (`ALLOW` or `DENY`). */
131 action: string | undefined;
132 /** IP protocol. */
133 ipProtocol: string | undefined;
134 /** Source IP ranges. */
135 sourceIpRanges: IpRange[];
136 /** Source ports. */
137 sourcePorts: string[];
138 /** Destination IP ranges. */
139 destinationIpRanges: IpRange[];
140 /** Destination ports. */
141 destinationPorts: string[];
142 /** User description with the Alchemy ownership prefix stripped. */
143 description: string | undefined;
144 /** Server-reported state. */
145 state: string | undefined;
146 /** System-generated unique identifier. */
147 uid: string | undefined;
148 /** RFC3339 creation timestamp. */
149 createTime: string | undefined;
150 /** RFC3339 last-update timestamp. */
151 updateTime: string | undefined;
152 },
153 never,
154 Providers
155>;
156
157/**
158 * An inbound firewall rule on a VMware Engine network policy that filters
159 * traffic destined to ExternalAddress resources.
160 *
161 * Rules have no labels field, so Alchemy stamps ownership into the
162 * description for `list` / nuke. Changing the parent policy, rule id, or
163 * location replaces the rule. Priority, action, protocol, ranges, ports,
164 * and description update in place.
165 *
166 * ### Creating a NetworkPoliciesExternalAccessRule
167 * **Example:** Allow HTTPS from anywhere
168 * ```typescript
169 * const rule = yield* GCP.VMwareEngine.NetworkPoliciesExternalAccessRule(
170 * "Https",
171 * {
172 * networkPolicy: policy.name,
173 * action: "ALLOW",
174 * ipProtocol: "tcp",
175 * priority: 1000,
176 * sourceIpRanges: [{ ipAddressRange: "0.0.0.0/0" }],
177 * sourcePorts: ["0-65535"],
178 * destinationIpRanges: [{ ipAddressRange: "0.0.0.0/0" }],
179 * destinationPorts: ["443"],
180 * description: "allow https",
181 * },
182 * );
183 * ```
184 *
185 * @resource
186 * @category VMwareEngine
187 */
188export const NetworkPoliciesExternalAccessRule =
189 Resource<NetworkPoliciesExternalAccessRule>(
190 "GCP.VMwareEngine.NetworkPoliciesExternalAccessRule",
191 );
192
193const parentPolicyName = (
194 project: string,
195 location: string,
196 networkPolicy: string,
197) => expandName(networkPolicy, project, location, PARENT_COLLECTION);
198
199const resourceNameOf = (parent: string, ruleId: string) =>
200 `${parent}/${COLLECTION}/${ruleId}`;
201
202const ipRangesOf = (
203 values: readonly vmwareengine.IpRange[] | readonly IpRange[] | undefined,
204): IpRange[] =>
205 (values ?? []).map((range) => ({
206 ipAddress: range.ipAddress,
207 ipAddressRange: range.ipAddressRange,
208 externalAddress: range.externalAddress,
209 }));
210
211const toAttrs = (
212 item: vmwareengine.ExternalAccessRule,
213 project: string,
214 region: string,
215) => {
216 const name = item.name ?? "";
217 const parsed = parseName(name, COLLECTION, region);
218 const ownership = parseOwnership(item.description);
219 return {
220 name,
221 externalAccessRuleId: parsed.id,
222 networkPolicy: parsed.parent,
223 project: parsed.project || project,
224 location: parsed.location,
225 priority: item.priority,
226 action: item.action,
227 ipProtocol: item.ipProtocol,
228 sourceIpRanges: ipRangesOf(item.sourceIpRanges),
229 sourcePorts: item.sourcePorts ?? [],
230 destinationIpRanges: ipRangesOf(item.destinationIpRanges),
231 destinationPorts: item.destinationPorts ?? [],
232 description: ownership.text,
233 state: item.state,
234 uid: item.uid,
235 createTime: item.createTime,
236 updateTime: item.updateTime,
237 };
238};
239
240const getByName = (name: string) =>
241 vmwareengine
242 .getProjectsLocationsNetworkPoliciesExternalAccessRules({ name })
243 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
244
245export const NetworkPoliciesExternalAccessRuleProvider = () =>
246 Provider.succeed(NetworkPoliciesExternalAccessRule, {
247 stables: [
248 "name",
249 "externalAccessRuleId",
250 "networkPolicy",
251 "project",
252 "location",
253 "uid",
254 "createTime",
255 ],
256
257 diff: Effect.fn(function* ({ news, olds, output }) {
258 if (!isResolved(news)) return undefined;
259 const env = yield* GcpEnvironment.current;
260 const previousLocation = normalizeLocation(
261 olds?.location ?? output?.location,
262 env.region,
263 );
264 return replaceOnIdentity({
265 previousId: olds?.externalAccessRuleId ?? output?.externalAccessRuleId,
266 nextId: news.externalAccessRuleId
267 ? rfc1035(news.externalAccessRuleId, "rule")
268 : (olds?.externalAccessRuleId ?? output?.externalAccessRuleId),
269 previousLocation,
270 nextLocation: normalizeLocation(
271 news.location ??
272 locationFromName(news.networkPolicy, previousLocation),
273 env.region,
274 ),
275 previousParent: olds?.networkPolicy ?? output?.networkPolicy,
276 nextParent: news.networkPolicy,
277 });
278 }),
279
280 read: Effect.fn(function* ({ id, olds, output }) {
281 const env = yield* GcpEnvironment.current;
282 const location = normalizeLocation(
283 olds?.location ??
284 output?.location ??
285 (olds?.networkPolicy
286 ? locationFromName(olds.networkPolicy, env.region)
287 : undefined),
288 env.region,
289 );
290 const parent = parentPolicyName(
291 env.project,
292 location,
293 olds?.networkPolicy ?? output?.networkPolicy ?? "",
294 );
295 const ruleId = yield* toPhysicalId(
296 id,
297 olds?.externalAccessRuleId,
298 output?.externalAccessRuleId,
299 "rule",
300 );
301 const name = output?.name ?? resourceNameOf(parent, ruleId);
302 const existing = yield* getByName(name);
303 if (existing === undefined) return undefined;
304 const attrs = toAttrs(existing, env.project, env.region);
305 const { labels } = parseOwnership(existing.description);
306 return (yield* hasAlchemyLabels(id, labels)) ? attrs : Unowned(attrs);
307 }),
308
309 list: () =>
310 Effect.gen(function* () {
311 const env = yield* GcpEnvironment.current;
312 const policies = yield* listAcrossLocations(
313 env.project,
314 env.region,
315 (parent) =>
316 collectPages(
317 vmwareengine.listProjectsLocationsNetworkPolicies.pages({
318 parent,
319 pageSize: 1000,
320 }),
321 (page) => page.networkPolicies,
322 ),
323 );
324 const nested = yield* Effect.forEach(
325 policies.filter((policy) => (policy.name ?? "").length > 0),
326 (policy) =>
327 collectPages(
328 vmwareengine.listProjectsLocationsNetworkPoliciesExternalAccessRules.pages(
329 {
330 parent: policy.name ?? "",
331 pageSize: 1000,
332 },
333 ),
334 (page) => page.externalAccessRules,
335 ),
336 { concurrency: 4 },
337 );
338 return nested
339 .flat()
340 .filter((item) => hasOwnershipMarker(item.description))
341 .map((item) => toAttrs(item, env.project, env.region));
342 }),
343
344 reconcile: Effect.fn(function* ({ id, news, output }) {
345 const env = yield* GcpEnvironment.current;
346 const location = normalizeLocation(
347 news.location ??
348 output?.location ??
349 locationFromName(news.networkPolicy, env.region),
350 env.region,
351 );
352 const parent = parentPolicyName(
353 env.project,
354 location,
355 news.networkPolicy,
356 );
357 const ruleId = yield* toPhysicalId(
358 id,
359 news.externalAccessRuleId,
360 output?.externalAccessRuleId,
361 "rule",
362 );
363 const name = resourceNameOf(parent, ruleId);
364 const ownership = yield* createInternalLabels(id);
365 const desiredDescription = encodeOwnership(ownership, news.description);
366 const priority = news.priority ?? 1000;
367 const action = news.action ?? "ALLOW";
368 const ipProtocol = news.ipProtocol ?? "tcp";
369 const sourceIpRanges = news.sourceIpRanges ?? [
370 { ipAddressRange: "0.0.0.0/0" },
371 ];
372 const destinationIpRanges = news.destinationIpRanges ?? [
373 { ipAddressRange: "0.0.0.0/0" },
374 ];
375 const sourcePorts = news.sourcePorts ?? ["0-65535"];
376 const destinationPorts = news.destinationPorts ?? ["0-65535"];
377
378 let current = yield* getByName(output?.name ?? name);
379
380 if (current === undefined) {
381 const created = yield* vmwareengine
382 .createProjectsLocationsNetworkPoliciesExternalAccessRules({
383 parent,
384 externalAccessRuleId: ruleId,
385 body: {
386 priority,
387 action,
388 ipProtocol,
389 sourceIpRanges,
390 destinationIpRanges,
391 sourcePorts,
392 destinationPorts,
393 description: desiredDescription,
394 },
395 })
396 .pipe(
397 Effect.retry({
398 while: (error) => error._tag === "Conflict",
399 times: 5,
400 schedule: Schedule.spaced("2 seconds"),
401 }),
402 Effect.catchTag("Conflict", () => Effect.succeed(undefined)),
403 );
404 if (created !== undefined) {
405 yield* waitForOperation(created);
406 }
407 current = yield* waitUntilPresent(getByName(name), name);
408 }
409
410 if (current === undefined) {
411 return yield* new VmwareengineNotResolved({ name });
412 }
413
414 current = yield* waitUntilReady(
415 getByName(current.name ?? name),
416 current.name ?? name,
417 (item) => item.state,
418 );
419
420 const descriptionChanged =
421 (current.description ?? "") !== desiredDescription;
422 const priorityChanged = (current.priority ?? 0) !== priority;
423 const actionChanged = (current.action ?? "") !== action;
424 const protocolChanged = (current.ipProtocol ?? "") !== ipProtocol;
425 const sourceRangesChanged = !sameJson(
426 ipRangesOf(current.sourceIpRanges),
427 sourceIpRanges,
428 );
429 const destRangesChanged = !sameJson(
430 ipRangesOf(current.destinationIpRanges),
431 destinationIpRanges,
432 );
433 const sourcePortsChanged = !sameJson(current.sourcePorts, sourcePorts);
434 const destPortsChanged = !sameJson(
435 current.destinationPorts,
436 destinationPorts,
437 );
438 const updateMask = changedFields([
439 ["description", descriptionChanged],
440 ["priority", priorityChanged],
441 ["action", actionChanged],
442 ["ipProtocol", protocolChanged],
443 ["sourceIpRanges", sourceRangesChanged],
444 ["destinationIpRanges", destRangesChanged],
445 ["sourcePorts", sourcePortsChanged],
446 ["destinationPorts", destPortsChanged],
447 ]);
448
449 if (updateMask.length > 0) {
450 const operation =
451 yield* vmwareengine.patchProjectsLocationsNetworkPoliciesExternalAccessRules(
452 {
453 name: current.name ?? name,
454 updateMask: updateMask.join(","),
455 body: {
456 name: current.name ?? name,
457 description: desiredDescription,
458 priority,
459 action,
460 ipProtocol,
461 sourceIpRanges,
462 destinationIpRanges,
463 sourcePorts,
464 destinationPorts,
465 },
466 },
467 );
468 yield* waitForOperation(operation);
469 current = yield* waitUntilReady(
470 getByName(current.name ?? name),
471 current.name ?? name,
472 (item) => item.state,
473 );
474 }
475
476 return toAttrs(current, env.project, env.region);
477 }),
478
479 delete: Effect.fn(function* ({ output }) {
480 const operation = yield* vmwareengine
481 .deleteProjectsLocationsNetworkPoliciesExternalAccessRules({
482 name: output.name,
483 })
484 .pipe(
485 Effect.retry({
486 while: (error) => error._tag === "Conflict",
487 times: 8,
488 schedule: Schedule.spaced("2 seconds"),
489 }),
490 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
491 );
492 if (operation !== undefined) {
493 yield* waitForOperation(operation, { notFoundOk: true });
494 }
495 yield* waitUntilGone(getByName(output.name), output.name);
496 }),
497 });