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 changedFields,
14 collectPages,
15 createInternalLabels,
16 encodeOwnership,
17 hasAlchemyLabels,
18 hasOwnershipMarker,
19 listAcrossLocations,
20 normalizeLocation,
21 parentOf,
22 parseName,
23 parseOwnership,
24 replaceOnIdentity,
25 rfc1035,
26 toPhysicalId,
27 waitForOperation,
28 waitUntilGone,
29 waitUntilPresent,
30 waitUntilReady,
31} from "./internal.ts";
32
33const COLLECTION = "vmwareEngineNetworks";
34const DEFAULT_TYPE =
35 "STANDARD" satisfies vmwareengine.VmwareEngineNetworkTypeEnum;
36
37export type VmwareEngineNetworkType =
38 | vmwareengine.VmwareEngineNetworkTypeEnum
39 | (string & {});
40
41export type VmwareEngineNetworkVpc = {
42 /** VPC network type (`INTRANET`, `INTERNET`, `GOOGLE_CLOUD`). */
43 type: string | undefined;
44 /** Service VPC resource name. */
45 network: string | undefined;
46};
47
48export type VmwareEngineNetworkProps = {
49 /**
50 * Network id (the `{vmwareEngineNetwork}` segment of
51 * `projects/{project}/locations/{location}/vmwareEngineNetworks/{id}`).
52 * If omitted, a unique RFC1035 name is generated. For `LEGACY`
53 * networks the API requires `{region}-default`. Immutable — changing
54 * it replaces the network.
55 */
56 vmwareEngineNetworkId?: string;
57 /**
58 * Location. `STANDARD` networks are global (`global`). `LEGACY`
59 * networks are regional (`us-central1`). Immutable — changing it
60 * replaces the network. `US-CENTRAL1` is accepted and normalized.
61 * @default "global" (`STANDARD`) or the stack's GCP region (`LEGACY`)
62 */
63 location?: string;
64 /**
65 * Network type. `STANDARD` is global; `LEGACY` is the per-region
66 * singleton `{region}-default`. Immutable — changing it replaces the
67 * network.
68 * @default "STANDARD"
69 */
70 type?: VmwareEngineNetworkType;
71 /**
72 * Human-readable description. VMware Engine networks have no labels
73 * field, so Alchemy ownership is stored in a `[alchemy …]` prefix and
74 * stripped from attributes.
75 */
76 description?: string;
77};
78
79export type VmwareEngineNetwork = Resource<
80 "GCP.VMwareEngine.VmwareEngineNetwork",
81 VmwareEngineNetworkProps,
82 {
83 /** Full resource name `projects/{project}/locations/{location}/vmwareEngineNetworks/{id}`. */
84 name: string;
85 /** Network id (last path segment). */
86 vmwareEngineNetworkId: string;
87 /** Project id. */
88 project: string;
89 /** Location id (`global` or a region). */
90 location: string;
91 /** Network type (`STANDARD`, `LEGACY`). */
92 type: string;
93 /** User description with the Alchemy ownership prefix stripped. */
94 description: string | undefined;
95 /** Server-reported state (`ACTIVE`, `CREATING`, …). */
96 state: string | undefined;
97 /** Server-generated uid. */
98 uid: string | undefined;
99 /** Server etag for update/delete. */
100 etag: string | undefined;
101 /** Attached service VPC networks. */
102 vpcNetworks: VmwareEngineNetworkVpc[];
103 /** RFC3339 creation timestamp. */
104 createTime: string | undefined;
105 /** RFC3339 last-update timestamp. */
106 updateTime: string | undefined;
107 },
108 never,
109 Providers
110>;
111
112/**
113 * A VMware Engine network that private clouds attach to for intranet,
114 * internet, and Google Cloud connectivity.
115 *
116 * `STANDARD` networks live at `locations/global`. `LEGACY` networks are
117 * regional singletons named `{region}-default`. Changing id, location,
118 * or type replaces the network. Description updates in place.
119 *
120 * ### Creating a Network
121 * **Example:** Generated STANDARD network
122 * ```typescript
123 * const network = yield* GCP.VMwareEngine.VmwareEngineNetwork("Ven", {
124 * type: "STANDARD",
125 * description: "app network",
126 * });
127 * ```
128 *
129 * **Example:** Named STANDARD network
130 * ```typescript
131 * const network = yield* GCP.VMwareEngine.VmwareEngineNetwork("Ven", {
132 * vmwareEngineNetworkId: "app-ven",
133 * location: "global",
134 * type: "STANDARD",
135 * description: "prod network",
136 * });
137 * ```
138 *
139 * ### Legacy regional network
140 * **Example:** `{region}-default` LEGACY network
141 * ```typescript
142 * const network = yield* GCP.VMwareEngine.VmwareEngineNetwork("Legacy", {
143 * location: "us-central1",
144 * type: "LEGACY",
145 * description: "legacy default",
146 * });
147 * ```
148 *
149 * ### Updating a Network
150 * Re-declare the same logical id with changed props; the engine keeps the
151 * physical resource and updates it in place.
152 *
153 * **Example:** Description
154 * ```typescript
155 * const network = yield* GCP.VMwareEngine.VmwareEngineNetwork("Ven", {
156 * location: "global",
157 * type: "STANDARD",
158 * description: "prod network v2",
159 * });
160 * ```
161 *
162 * @resource
163 * @category VMwareEngine
164 */
165export const VmwareEngineNetwork = Resource<VmwareEngineNetwork>(
166 "GCP.VMwareEngine.VmwareEngineNetwork",
167);
168
169const resourceName = (project: string, location: string, networkId: string) =>
170 `${parentOf(project, location)}/${COLLECTION}/${networkId}`;
171
172const typeOf = (value: string | undefined) =>
173 (value ?? DEFAULT_TYPE).toUpperCase();
174
175const fallbackLocation = (type: string, region: string) =>
176 type === "LEGACY" ? region : DEFAULT_GLOBAL;
177
178const toId = (
179 id: string,
180 networkId: string | undefined,
181 existing: string | undefined,
182 type: string,
183 location: string,
184) => {
185 if (networkId !== undefined) {
186 return Effect.succeed(rfc1035(networkId, "network"));
187 }
188 if (type === "LEGACY") {
189 return Effect.succeed(existing ?? `${location}-default`);
190 }
191 return toPhysicalId(id, undefined, existing, "network");
192};
193
194const toVpcs = (
195 networks: vmwareengine.VpcNetworkList | undefined,
196): VmwareEngineNetworkVpc[] =>
197 (networks ?? []).map((network) => ({
198 type: network.type,
199 network: network.network,
200 }));
201
202const toAttrs = (
203 network: vmwareengine.VmwareEngineNetwork,
204 project: string,
205) => {
206 const name = network.name ?? "";
207 const parsed = parseName(name, COLLECTION, DEFAULT_GLOBAL);
208 const ownership = parseOwnership(network.description);
209 return {
210 name,
211 vmwareEngineNetworkId: parsed.id,
212 project: parsed.project || project,
213 location: parsed.location,
214 type: typeOf(network.type),
215 description: ownership.text,
216 state: network.state,
217 uid: network.uid,
218 etag: network.etag,
219 vpcNetworks: toVpcs(network.vpcNetworks),
220 createTime: network.createTime,
221 updateTime: network.updateTime,
222 };
223};
224
225const getByName = (name: string) =>
226 name.length === 0
227 ? Effect.succeed(undefined)
228 : vmwareengine
229 .getProjectsLocationsVmwareEngineNetworks({ name })
230 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
231
232export const VmwareEngineNetworkProvider = () =>
233 Provider.succeed(VmwareEngineNetwork, {
234 stables: [
235 "name",
236 "vmwareEngineNetworkId",
237 "project",
238 "location",
239 "type",
240 "uid",
241 "createTime",
242 ],
243
244 diff: Effect.fn(function* ({ news, olds, output }) {
245 if (!isResolved(news)) return undefined;
246 const env = yield* GcpEnvironment.current;
247 const previousType = typeOf(olds?.type ?? output?.type);
248 const nextType = typeOf(news.type ?? olds?.type ?? output?.type);
249 const previousLocation = normalizeLocation(
250 olds?.location ?? output?.location,
251 fallbackLocation(previousType, env.region),
252 );
253 const nextLocation = normalizeLocation(
254 news.location ?? olds?.location ?? output?.location,
255 fallbackLocation(nextType, env.region),
256 );
257 const previousId =
258 olds?.vmwareEngineNetworkId ?? output?.vmwareEngineNetworkId;
259 const nextId = news.vmwareEngineNetworkId
260 ? rfc1035(news.vmwareEngineNetworkId, "network")
261 : nextType === "LEGACY"
262 ? `${nextLocation}-default`
263 : previousId;
264 return replaceOnIdentity({
265 previousId,
266 nextId,
267 previousLocation,
268 nextLocation,
269 extra: previousType !== nextType,
270 });
271 }),
272
273 read: Effect.fn(function* ({ id, olds, output }) {
274 const env = yield* GcpEnvironment.current;
275 const type = typeOf(olds?.type ?? output?.type);
276 const location = normalizeLocation(
277 olds?.location ?? output?.location,
278 fallbackLocation(type, env.region),
279 );
280 const networkId = yield* toId(
281 id,
282 olds?.vmwareEngineNetworkId,
283 output?.vmwareEngineNetworkId,
284 type,
285 location,
286 );
287 const name =
288 output?.name ?? resourceName(env.project, location, networkId);
289 const existing = yield* getByName(name);
290 if (existing === undefined) return undefined;
291 const attrs = toAttrs(existing, env.project);
292 const { labels } = parseOwnership(existing.description);
293 return (yield* hasAlchemyLabels(id, labels)) ? attrs : Unowned(attrs);
294 }),
295
296 list: () =>
297 Effect.gen(function* () {
298 const env = yield* GcpEnvironment.current;
299 const items = yield* listAcrossLocations(
300 env.project,
301 env.region,
302 (parent) =>
303 collectPages(
304 vmwareengine.listProjectsLocationsVmwareEngineNetworks.pages({
305 parent,
306 pageSize: 1000,
307 }),
308 (page) => page.vmwareEngineNetworks,
309 ),
310 );
311 return items
312 .filter((item) => hasOwnershipMarker(item.description))
313 .map((item) => toAttrs(item, env.project));
314 }),
315
316 reconcile: Effect.fn(function* ({ id, news, output }) {
317 const env = yield* GcpEnvironment.current;
318 const type = typeOf(news.type ?? output?.type);
319 const location = normalizeLocation(
320 news.location ?? output?.location,
321 fallbackLocation(type, env.region),
322 );
323 const networkId = yield* toId(
324 id,
325 news.vmwareEngineNetworkId,
326 output?.vmwareEngineNetworkId,
327 type,
328 location,
329 );
330 const name = resourceName(env.project, location, networkId);
331 const ownership = yield* createInternalLabels(id);
332 const desiredDescription = encodeOwnership(ownership, news.description);
333
334 let current = yield* getByName(output?.name ?? name);
335
336 if (current === undefined) {
337 const created = yield* vmwareengine
338 .createProjectsLocationsVmwareEngineNetworks({
339 parent: parentOf(env.project, location),
340 vmwareEngineNetworkId: networkId,
341 body: {
342 type,
343 description: desiredDescription,
344 },
345 })
346 .pipe(
347 Effect.retry({
348 while: (error) => error._tag === "Conflict",
349 times: 5,
350 schedule: Schedule.spaced("2 seconds"),
351 }),
352 Effect.catchTag("Conflict", () => Effect.succeed(undefined)),
353 );
354 if (created !== undefined) {
355 yield* waitForOperation(created);
356 }
357 current = yield* waitUntilPresent(getByName(name), name);
358 }
359
360 if (current === undefined) {
361 return yield* new VmwareengineNotResolved({ name });
362 }
363
364 current = yield* waitUntilReady(
365 getByName(current.name ?? name),
366 current.name ?? name,
367 (item) => item.state,
368 );
369
370 const updateMask = changedFields([
371 ["description", (current.description ?? "") !== desiredDescription],
372 ]);
373 if (updateMask.length > 0) {
374 const operation =
375 yield* vmwareengine.patchProjectsLocationsVmwareEngineNetworks({
376 name: current.name ?? name,
377 updateMask: updateMask.join(","),
378 body: {
379 name: current.name ?? name,
380 description: desiredDescription,
381 },
382 });
383 yield* waitForOperation(operation);
384 current = yield* waitUntilReady(
385 getByName(current.name ?? name),
386 current.name ?? name,
387 (item) => item.state,
388 );
389 }
390
391 return toAttrs(current, env.project);
392 }),
393
394 delete: Effect.fn(function* ({ output }) {
395 const operation = yield* vmwareengine
396 .deleteProjectsLocationsVmwareEngineNetworks({
397 name: output.name,
398 etag: output.etag,
399 })
400 .pipe(
401 Effect.retry({
402 while: (error) => error._tag === "Conflict",
403 times: 8,
404 schedule: Schedule.spaced("2 seconds"),
405 }),
406 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
407 );
408 if (operation !== undefined) {
409 yield* waitForOperation(operation, { notFoundOk: true });
410 }
411 yield* waitUntilGone(getByName(output.name), output.name);
412 }),
413 });