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 DEFAULT_GLOBAL,
12 VmwareengineNotResolved,
13 canonicalizeLink,
14 changedFields,
15 collectPages,
16 createInternalLabels,
17 encodeOwnership,
18 expandName,
19 hasAlchemyLabels,
20 hasOwnershipMarker,
21 listAcrossLocations,
22 normalizeLocation,
23 parentOf,
24 parseName,
25 parseOwnership,
26 replaceOnIdentity,
27 rfc1035,
28 toPhysicalId,
29 waitForOperation,
30 waitUntilGone,
31 waitUntilPresent,
32} from "./internal.ts";
33
34const COLLECTION = "networkPolicies";
35const VEN_COLLECTION = "vmwareEngineNetworks";
36const DEFAULT_EDGE_CIDR = "192.168.100.0/26";
37
38export type NetworkService = {
39 /** Whether the service is enabled. */
40 enabled?: boolean;
41 /** Output-only service state. */
42 state?: string;
43};
44
45export type NetworkPolicyProps = {
46 /**
47 * Network policy id (the `{networkPolicy}` segment of
48 * `projects/{project}/locations/{location}/networkPolicies/{networkPolicy}`).
49 * If omitted, a unique RFC1035 name is generated. Immutable.
50 */
51 networkPolicyId?: string;
52 /**
53 * Region. Immutable — changing it replaces the policy.
54 * @default the stack's GCP region (`GCP.Region`, else the profile
55 * region, else `us-central1`)
56 */
57 location?: string;
58 /**
59 * IP range in CIDR notation used for internet and external IP access.
60 * Must be an RFC 1918 `/26`.
61 * @default "192.168.100.0/26"
62 */
63 edgeServicesCidr?: string;
64 /**
65 * VMware Engine network this policy governs
66 * (`projects/{project}/locations/{location}/vmwareEngineNetworks/{id}`
67 * or the network id). Immutable.
68 */
69 vmwareEngineNetwork?: string;
70 /**
71 * Internet access for VMware workloads.
72 */
73 internetAccess?: NetworkService;
74 /**
75 * External IP assignment. Can only be enabled when internet access is
76 * also enabled.
77 */
78 externalIp?: NetworkService;
79 /**
80 * Human-readable description. Network policies have no labels field, so
81 * Alchemy stamps ownership into a `[alchemy …]` prefix and strips it
82 * from attributes.
83 */
84 description?: string;
85};
86
87export type NetworkPolicy = Resource<
88 "GCP.VMwareEngine.NetworkPolicy",
89 NetworkPolicyProps,
90 {
91 /** Full resource name. */
92 name: string;
93 /** Network policy id (last path segment). */
94 networkPolicyId: string;
95 /** Project id. */
96 project: string;
97 /** Location id. */
98 location: string;
99 /** Edge services CIDR. */
100 edgeServicesCidr: string | undefined;
101 /** Attached VMware Engine network. */
102 vmwareEngineNetwork: string | undefined;
103 /** Canonical VMware Engine network name. */
104 vmwareEngineNetworkCanonical: string | undefined;
105 /** Internet access service. */
106 internetAccess: NetworkService | undefined;
107 /** External IP service. */
108 externalIp: NetworkService | undefined;
109 /** User description with the Alchemy ownership prefix stripped. */
110 description: string | undefined;
111 /** System-generated unique identifier. */
112 uid: string | undefined;
113 /** RFC3339 creation timestamp. */
114 createTime: string | undefined;
115 /** RFC3339 last-update timestamp. */
116 updateTime: string | undefined;
117 },
118 never,
119 Providers
120>;
121
122/**
123 * A regional VMware Engine network policy that controls internet and
124 * external IP access for private clouds on a VMware Engine network.
125 *
126 * Network policies have no labels field, so Alchemy stamps ownership into
127 * the description for `list` / nuke. Changing `networkPolicyId`,
128 * `location`, or `vmwareEngineNetwork` replaces the policy. CIDR,
129 * internet access, external IP, and description update in place.
130 *
131 * ### Creating a NetworkPolicy
132 * **Example:** Disable internet and external IP
133 * ```typescript
134 * const policy = yield* GCP.VMwareEngine.NetworkPolicy("Edge", {
135 * vmwareEngineNetwork: ven.name,
136 * edgeServicesCidr: "192.168.100.0/26",
137 * internetAccess: { enabled: false },
138 * externalIp: { enabled: false },
139 * description: "edge policy",
140 * });
141 * ```
142 *
143 * @resource
144 * @category VMwareEngine
145 */
146export const NetworkPolicy = Resource<NetworkPolicy>(
147 "GCP.VMwareEngine.NetworkPolicy",
148);
149
150const resourceName = (
151 project: string,
152 location: string,
153 networkPolicyId: string,
154) => `${parentOf(project, location)}/${COLLECTION}/${networkPolicyId}`;
155
156const serviceOf = (
157 value: vmwareengine.NetworkService | NetworkService | undefined,
158): NetworkService | undefined => {
159 if (value === undefined) return undefined;
160 return {
161 enabled: value.enabled === true,
162 state: value.state,
163 };
164};
165
166const toAttrs = (
167 item: vmwareengine.NetworkPolicy,
168 project: string,
169 region: string,
170) => {
171 const name = item.name ?? "";
172 const parsed = parseName(name, COLLECTION, region);
173 const ownership = parseOwnership(item.description);
174 return {
175 name,
176 networkPolicyId: parsed.id,
177 project: parsed.project || project,
178 location: parsed.location,
179 edgeServicesCidr: item.edgeServicesCidr,
180 vmwareEngineNetwork: item.vmwareEngineNetwork,
181 vmwareEngineNetworkCanonical: item.vmwareEngineNetworkCanonical,
182 internetAccess: serviceOf(item.internetAccess),
183 externalIp: serviceOf(item.externalIp),
184 description: ownership.text,
185 uid: item.uid,
186 createTime: item.createTime,
187 updateTime: item.updateTime,
188 };
189};
190
191const getByName = (name: string) =>
192 vmwareengine
193 .getProjectsLocationsNetworkPolicies({ name })
194 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
195
196export const NetworkPolicyProvider = () =>
197 Provider.succeed(NetworkPolicy, {
198 stables: [
199 "name",
200 "networkPolicyId",
201 "project",
202 "location",
203 "uid",
204 "createTime",
205 ],
206
207 diff: Effect.fn(function* ({ news, olds, output }) {
208 if (!isResolved(news)) return undefined;
209 const env = yield* GcpEnvironment.current;
210 const previousVen = canonicalizeLink(
211 olds?.vmwareEngineNetwork ?? output?.vmwareEngineNetwork,
212 );
213 const nextVen = canonicalizeLink(news.vmwareEngineNetwork);
214 return replaceOnIdentity({
215 previousId: olds?.networkPolicyId ?? output?.networkPolicyId,
216 nextId: news.networkPolicyId
217 ? rfc1035(news.networkPolicyId, "networkpolicy")
218 : (olds?.networkPolicyId ?? output?.networkPolicyId),
219 previousLocation: normalizeLocation(
220 olds?.location ?? output?.location,
221 env.region,
222 ),
223 nextLocation: normalizeLocation(
224 news.location ?? olds?.location ?? output?.location,
225 env.region,
226 ),
227 extra:
228 previousVen.length > 0 &&
229 nextVen.length > 0 &&
230 previousVen !== nextVen,
231 });
232 }),
233
234 read: Effect.fn(function* ({ id, olds, output }) {
235 const env = yield* GcpEnvironment.current;
236 const networkPolicyId = yield* toPhysicalId(
237 id,
238 olds?.networkPolicyId,
239 output?.networkPolicyId,
240 "networkpolicy",
241 );
242 const location = normalizeLocation(
243 olds?.location ?? output?.location,
244 env.region,
245 );
246 const name =
247 output?.name ?? resourceName(env.project, location, networkPolicyId);
248 const existing = yield* getByName(name);
249 if (existing === undefined) return undefined;
250 const attrs = toAttrs(existing, env.project, env.region);
251 const { labels } = parseOwnership(existing.description);
252 return (yield* hasAlchemyLabels(id, labels)) ? attrs : Unowned(attrs);
253 }),
254
255 list: () =>
256 Effect.gen(function* () {
257 const env = yield* GcpEnvironment.current;
258 const items = yield* listAcrossLocations(
259 env.project,
260 env.region,
261 (parent) =>
262 collectPages(
263 vmwareengine.listProjectsLocationsNetworkPolicies.pages({
264 parent,
265 pageSize: 1000,
266 }),
267 (page) => page.networkPolicies,
268 ),
269 );
270 return items
271 .filter((item) => hasOwnershipMarker(item.description))
272 .map((item) => toAttrs(item, env.project, env.region));
273 }),
274
275 reconcile: Effect.fn(function* ({ id, news, output }) {
276 const env = yield* GcpEnvironment.current;
277 const networkPolicyId = yield* toPhysicalId(
278 id,
279 news.networkPolicyId,
280 output?.networkPolicyId,
281 "networkpolicy",
282 );
283 const location = normalizeLocation(
284 news.location ?? output?.location,
285 env.region,
286 );
287 const name = resourceName(env.project, location, networkPolicyId);
288 const ownership = yield* createInternalLabels(id);
289 const desiredDescription = encodeOwnership(ownership, news.description);
290 const edgeServicesCidr = news.edgeServicesCidr ?? DEFAULT_EDGE_CIDR;
291 const vmwareEngineNetwork = news.vmwareEngineNetwork
292 ? expandName(
293 news.vmwareEngineNetwork,
294 env.project,
295 DEFAULT_GLOBAL,
296 VEN_COLLECTION,
297 )
298 : undefined;
299 const internetEnabled = news.internetAccess?.enabled === true;
300 const externalIpEnabled = news.externalIp?.enabled === true;
301
302 let current = yield* getByName(output?.name ?? name);
303
304 if (current === undefined) {
305 const created = yield* vmwareengine
306 .createProjectsLocationsNetworkPolicies({
307 parent: parentOf(env.project, location),
308 networkPolicyId,
309 body: {
310 edgeServicesCidr,
311 vmwareEngineNetwork,
312 internetAccess: { enabled: internetEnabled },
313 externalIp: { enabled: externalIpEnabled },
314 description: desiredDescription,
315 },
316 })
317 .pipe(
318 Effect.retry({
319 while: (error) => error._tag === "Conflict",
320 times: 5,
321 schedule: Schedule.spaced("2 seconds"),
322 }),
323 Effect.catchTag("Conflict", () => Effect.succeed(undefined)),
324 );
325 if (created !== undefined) {
326 yield* waitForOperation(created);
327 }
328 current = yield* waitUntilPresent(getByName(name), name);
329 }
330
331 if (current === undefined) {
332 return yield* new VmwareengineNotResolved({ name });
333 }
334
335 const descriptionChanged =
336 (current.description ?? "") !== desiredDescription;
337 const cidrChanged = (current.edgeServicesCidr ?? "") !== edgeServicesCidr;
338 const internetChanged =
339 (current.internetAccess?.enabled === true) !== internetEnabled;
340 const externalIpChanged =
341 (current.externalIp?.enabled === true) !== externalIpEnabled;
342 const updateMask = changedFields([
343 ["description", descriptionChanged],
344 ["edgeServicesCidr", cidrChanged],
345 ["internetAccess.enabled", internetChanged],
346 ["externalIp.enabled", externalIpChanged],
347 ]);
348
349 if (updateMask.length > 0) {
350 const operation =
351 yield* vmwareengine.patchProjectsLocationsNetworkPolicies({
352 name: current.name ?? name,
353 updateMask: updateMask.join(","),
354 body: {
355 name: current.name ?? name,
356 description: desiredDescription,
357 edgeServicesCidr,
358 internetAccess: { enabled: internetEnabled },
359 externalIp: { enabled: externalIpEnabled },
360 },
361 });
362 yield* waitForOperation(operation);
363 current = yield* waitUntilPresent(
364 getByName(current.name ?? name),
365 current.name ?? name,
366 );
367 }
368
369 return toAttrs(current, env.project, env.region);
370 }),
371
372 delete: Effect.fn(function* ({ output }) {
373 const operation = yield* vmwareengine
374 .deleteProjectsLocationsNetworkPolicies({ name: output.name })
375 .pipe(
376 Effect.retry({
377 while: (error) => error._tag === "Conflict",
378 times: 8,
379 schedule: Schedule.spaced("2 seconds"),
380 }),
381 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
382 );
383 if (operation !== undefined) {
384 yield* waitForOperation(operation, { notFoundOk: true });
385 }
386 yield* waitUntilGone(getByName(output.name), output.name);
387 }),
388 });