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 normalizeLocation,
22 parentOf,
23 parseName,
24 parseOwnership,
25 replaceOnIdentity,
26 rfc1035,
27 toPhysicalId,
28 waitForOperation,
29 waitUntilGone,
30 waitUntilPresent,
31 waitUntilReady,
32} from "./internal.ts";
33
34const COLLECTION = "networkPeerings";
35const VEN_COLLECTION = "vmwareEngineNetworks";
36
37export type NetworkPeeringPeerNetworkType =
38 | vmwareengine.NetworkPeeringPeerNetworkTypeEnum
39 | (string & {});
40
41export type NetworkPeeringProps = {
42 /**
43 * Network peering id (the `{networkPeering}` segment of
44 * `projects/{project}/locations/global/networkPeerings/{networkPeering}`).
45 * If omitted, a unique RFC1035 name is generated. Immutable.
46 */
47 networkPeeringId?: string;
48 /**
49 * Location. Network peering is a global resource.
50 * @default "global"
51 */
52 location?: string;
53 /**
54 * Relative resource name of the network to peer with. VPC networks use
55 * `projects/{project}/global/networks/{network}`; VMware Engine
56 * networks use
57 * `projects/{project}/locations/global/vmwareEngineNetworks/{id}`.
58 * Immutable.
59 */
60 peerNetwork: string;
61 /**
62 * Type of the peer network (`STANDARD`, `VMWARE_ENGINE_NETWORK`, …).
63 * Immutable.
64 */
65 peerNetworkType: NetworkPeeringPeerNetworkType;
66 /**
67 * VMware Engine network to peer
68 * (`projects/{project}/locations/{location}/vmwareEngineNetworks/{id}`
69 * or the network id). Immutable.
70 */
71 vmwareEngineNetwork: string;
72 /**
73 * Export custom routes to the peer. Default true.
74 */
75 exportCustomRoutes?: boolean;
76 /**
77 * Import custom routes from the peer. Default true.
78 */
79 importCustomRoutes?: boolean;
80 /**
81 * Export custom routes that contain public IP ranges. Default true.
82 */
83 exportCustomRoutesWithPublicIp?: boolean;
84 /**
85 * Import custom routes that contain public IP ranges. Default true.
86 */
87 importCustomRoutesWithPublicIp?: boolean;
88 /**
89 * Automatically exchange subnet routes. Currently always true.
90 */
91 exchangeSubnetRoutes?: boolean;
92 /**
93 * Peer MTU in bytes. `0` uses the API default of 1500.
94 */
95 peerMtu?: number;
96 /**
97 * Human-readable description. Network peerings have no labels field, so
98 * Alchemy stamps ownership into a `[alchemy …]` prefix and strips it
99 * from attributes. Description is the only in-place update.
100 */
101 description?: string;
102};
103
104export type NetworkPeering = Resource<
105 "GCP.VMwareEngine.NetworkPeering",
106 NetworkPeeringProps,
107 {
108 /** Full resource name. */
109 name: string;
110 /** Network peering id (last path segment). */
111 networkPeeringId: string;
112 /** Project id. */
113 project: string;
114 /** Location id. Always `"global"`. */
115 location: string;
116 /** Peer network resource name. */
117 peerNetwork: string | undefined;
118 /** Peer network type. */
119 peerNetworkType: string | undefined;
120 /** VMware Engine network resource name. */
121 vmwareEngineNetwork: string | undefined;
122 /** Whether custom routes are exported. */
123 exportCustomRoutes: boolean;
124 /** Whether custom routes are imported. */
125 importCustomRoutes: boolean;
126 /** Whether public-IP custom routes are exported. */
127 exportCustomRoutesWithPublicIp: boolean;
128 /** Whether public-IP custom routes are imported. */
129 importCustomRoutesWithPublicIp: boolean;
130 /** Whether subnet routes are exchanged. */
131 exchangeSubnetRoutes: boolean;
132 /** Configured peer MTU. */
133 peerMtu: number | undefined;
134 /** User description with the Alchemy ownership prefix stripped. */
135 description: string | undefined;
136 /** Server-reported state. */
137 state: string | undefined;
138 /** Extra state details. */
139 stateDetails: string | undefined;
140 /** System-generated unique identifier. */
141 uid: string | undefined;
142 /** RFC3339 creation timestamp. */
143 createTime: string | undefined;
144 /** RFC3339 last-update timestamp. */
145 updateTime: string | undefined;
146 },
147 never,
148 Providers
149>;
150
151/**
152 * A peering between a VMware Engine network and a VPC or another VMware
153 * Engine network. NetworkPeering is a global resource.
154 *
155 * Network peerings have no labels field, so Alchemy stamps ownership into
156 * the description for `list` / nuke. Identity, peer network, peer type,
157 * and VMware Engine network are immutable. Description updates in place.
158 *
159 * ### Creating a NetworkPeering
160 * **Example:** Peer a VMware Engine network with the default VPC
161 * ```typescript
162 * const peering = yield* GCP.VMwareEngine.NetworkPeering("ToVpc", {
163 * vmwareEngineNetwork: ven.name,
164 * peerNetwork: "projects/my-project/global/networks/default",
165 * peerNetworkType: "STANDARD",
166 * description: "gcve to vpc",
167 * });
168 * ```
169 *
170 * @resource
171 * @category VMwareEngine
172 */
173export const NetworkPeering = Resource<NetworkPeering>(
174 "GCP.VMwareEngine.NetworkPeering",
175);
176
177const resourceName = (project: string, networkPeeringId: string) =>
178 `${parentOf(project, DEFAULT_GLOBAL)}/${COLLECTION}/${networkPeeringId}`;
179
180const expandVen = (project: string, value: string) =>
181 expandName(value, project, DEFAULT_GLOBAL, VEN_COLLECTION);
182
183const boolOrDefault = (value: boolean | undefined, fallback: boolean) =>
184 value ?? fallback;
185
186const toAttrs = (item: vmwareengine.NetworkPeering, project: string) => {
187 const name = item.name ?? "";
188 const parsed = parseName(name, COLLECTION, DEFAULT_GLOBAL);
189 const ownership = parseOwnership(item.description);
190 return {
191 name,
192 networkPeeringId: parsed.id,
193 project: parsed.project || project,
194 location: parsed.location || DEFAULT_GLOBAL,
195 peerNetwork: item.peerNetwork,
196 peerNetworkType: item.peerNetworkType,
197 vmwareEngineNetwork: item.vmwareEngineNetwork,
198 exportCustomRoutes: item.exportCustomRoutes !== false,
199 importCustomRoutes: item.importCustomRoutes !== false,
200 exportCustomRoutesWithPublicIp:
201 item.exportCustomRoutesWithPublicIp !== false,
202 importCustomRoutesWithPublicIp:
203 item.importCustomRoutesWithPublicIp !== false,
204 exchangeSubnetRoutes: item.exchangeSubnetRoutes !== false,
205 peerMtu: item.peerMtu,
206 description: ownership.text,
207 state: item.state,
208 stateDetails: item.stateDetails,
209 uid: item.uid,
210 createTime: item.createTime,
211 updateTime: item.updateTime,
212 };
213};
214
215const getByName = (name: string) =>
216 vmwareengine
217 .getProjectsLocationsNetworkPeerings({ name })
218 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
219
220export const NetworkPeeringProvider = () =>
221 Provider.succeed(NetworkPeering, {
222 stables: [
223 "name",
224 "networkPeeringId",
225 "project",
226 "location",
227 "uid",
228 "createTime",
229 ],
230
231 diff: Effect.fn(function* ({ news, olds, output }) {
232 if (!isResolved(news)) return undefined;
233 const previousPeer = canonicalizeLink(
234 olds?.peerNetwork ?? output?.peerNetwork,
235 );
236 const nextPeer = canonicalizeLink(news.peerNetwork);
237 const previousVen = canonicalizeLink(
238 olds?.vmwareEngineNetwork ?? output?.vmwareEngineNetwork,
239 );
240 const nextVen = canonicalizeLink(news.vmwareEngineNetwork);
241 const previousType =
242 olds?.peerNetworkType ?? output?.peerNetworkType ?? "";
243 return replaceOnIdentity({
244 previousId: olds?.networkPeeringId ?? output?.networkPeeringId,
245 nextId: news.networkPeeringId
246 ? rfc1035(news.networkPeeringId, "networkpeering")
247 : (olds?.networkPeeringId ?? output?.networkPeeringId),
248 previousLocation: DEFAULT_GLOBAL,
249 nextLocation: normalizeLocation(news.location, DEFAULT_GLOBAL),
250 extra:
251 (previousPeer.length > 0 && previousPeer !== nextPeer) ||
252 (previousVen.length > 0 && previousVen !== nextVen) ||
253 (previousType.length > 0 && previousType !== news.peerNetworkType),
254 });
255 }),
256
257 read: Effect.fn(function* ({ id, olds, output }) {
258 const env = yield* GcpEnvironment.current;
259 const networkPeeringId = yield* toPhysicalId(
260 id,
261 olds?.networkPeeringId,
262 output?.networkPeeringId,
263 "networkpeering",
264 );
265 const name = output?.name ?? resourceName(env.project, networkPeeringId);
266 const existing = yield* getByName(name);
267 if (existing === undefined) return undefined;
268 const attrs = toAttrs(existing, env.project);
269 const { labels } = parseOwnership(existing.description);
270 return (yield* hasAlchemyLabels(id, labels)) ? attrs : Unowned(attrs);
271 }),
272
273 list: () =>
274 Effect.gen(function* () {
275 const env = yield* GcpEnvironment.current;
276 const items = yield* collectPages(
277 vmwareengine.listProjectsLocationsNetworkPeerings.pages({
278 parent: parentOf(env.project, DEFAULT_GLOBAL),
279 pageSize: 1000,
280 }),
281 (page) => page.networkPeerings,
282 );
283 return items
284 .filter((item) => hasOwnershipMarker(item.description))
285 .map((item) => toAttrs(item, env.project));
286 }),
287
288 reconcile: Effect.fn(function* ({ id, news, output }) {
289 const env = yield* GcpEnvironment.current;
290 const networkPeeringId = yield* toPhysicalId(
291 id,
292 news.networkPeeringId,
293 output?.networkPeeringId,
294 "networkpeering",
295 );
296 const name = resourceName(env.project, networkPeeringId);
297 const ownership = yield* createInternalLabels(id);
298 const desiredDescription = encodeOwnership(ownership, news.description);
299 const vmwareEngineNetwork = expandVen(
300 env.project,
301 news.vmwareEngineNetwork,
302 );
303 const exportCustomRoutes = boolOrDefault(news.exportCustomRoutes, true);
304 const importCustomRoutes = boolOrDefault(news.importCustomRoutes, true);
305 const exportCustomRoutesWithPublicIp = boolOrDefault(
306 news.exportCustomRoutesWithPublicIp,
307 true,
308 );
309 const importCustomRoutesWithPublicIp = boolOrDefault(
310 news.importCustomRoutesWithPublicIp,
311 true,
312 );
313 const exchangeSubnetRoutes = boolOrDefault(
314 news.exchangeSubnetRoutes,
315 true,
316 );
317
318 let current = yield* getByName(output?.name ?? name);
319
320 if (current === undefined) {
321 const created = yield* vmwareengine
322 .createProjectsLocationsNetworkPeerings({
323 parent: parentOf(env.project, DEFAULT_GLOBAL),
324 networkPeeringId,
325 body: {
326 peerNetwork: news.peerNetwork,
327 peerNetworkType: news.peerNetworkType,
328 vmwareEngineNetwork,
329 exportCustomRoutes,
330 importCustomRoutes,
331 exportCustomRoutesWithPublicIp,
332 importCustomRoutesWithPublicIp,
333 exchangeSubnetRoutes,
334 peerMtu: news.peerMtu,
335 description: desiredDescription,
336 },
337 })
338 .pipe(
339 Effect.retry({
340 while: (error) => error._tag === "Conflict",
341 times: 5,
342 schedule: Schedule.spaced("2 seconds"),
343 }),
344 Effect.catchTag("Conflict", () => Effect.succeed(undefined)),
345 );
346 if (created !== undefined) {
347 yield* waitForOperation(created);
348 }
349 current = yield* waitUntilPresent(getByName(name), name);
350 }
351
352 if (current === undefined) {
353 return yield* new VmwareengineNotResolved({ name });
354 }
355
356 current = yield* waitUntilReady(
357 getByName(current.name ?? name),
358 current.name ?? name,
359 (item) => item.state,
360 );
361
362 const descriptionChanged =
363 (current.description ?? "") !== desiredDescription;
364 const updateMask = changedFields([["description", descriptionChanged]]);
365
366 if (updateMask.length > 0) {
367 const operation =
368 yield* vmwareengine.patchProjectsLocationsNetworkPeerings({
369 name: current.name ?? name,
370 updateMask: updateMask.join(","),
371 body: {
372 name: current.name ?? name,
373 description: desiredDescription,
374 },
375 });
376 yield* waitForOperation(operation);
377 current = yield* waitUntilReady(
378 getByName(current.name ?? name),
379 current.name ?? name,
380 (item) => item.state,
381 );
382 }
383
384 return toAttrs(current, env.project);
385 }),
386
387 delete: Effect.fn(function* ({ output }) {
388 const operation = yield* vmwareengine
389 .deleteProjectsLocationsNetworkPeerings({ name: output.name })
390 .pipe(
391 Effect.retry({
392 while: (error) => error._tag === "Conflict",
393 times: 8,
394 schedule: Schedule.spaced("2 seconds"),
395 }),
396 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
397 );
398 if (operation !== undefined) {
399 yield* waitForOperation(operation, { notFoundOk: true });
400 }
401 yield* waitUntilGone(getByName(output.name), output.name);
402 }),
403 });