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 waitUntilReady,
33} from "./internal.ts";
34
35const COLLECTION = "privateConnections";
36const VEN_COLLECTION = "vmwareEngineNetworks";
37const DEFAULT_ROUTING_MODE =
38 "GLOBAL" satisfies vmwareengine.PrivateConnectionRoutingModeEnum;
39
40export type PrivateConnectionType =
41 | vmwareengine.PrivateConnectionTypeEnum
42 | (string & {});
43
44export type PrivateConnectionRoutingMode =
45 | vmwareengine.PrivateConnectionRoutingModeEnum
46 | (string & {});
47
48export type PrivateConnectionProps = {
49 /**
50 * Connection id (the `{privateConnection}` segment of
51 * `projects/{project}/locations/{location}/privateConnections/{id}`).
52 * If omitted, a unique RFC1035 name is generated. Immutable — changing
53 * it replaces the connection.
54 */
55 privateConnectionId?: string;
56 /**
57 * Region (`us-central1`, …). Immutable — changing it replaces the
58 * connection. `US-CENTRAL1` is accepted and normalized.
59 * @default the stack's GCP region (`GCP.Region`, else the profile
60 * region, else `us-central1`)
61 */
62 location?: string;
63 /**
64 * Connection type (`PRIVATE_SERVICE_ACCESS`, `NETAPP_CLOUD_VOLUMES`,
65 * `DELL_POWERSCALE`, `THIRD_PARTY_SERVICE`). Immutable — changing it
66 * replaces the connection.
67 */
68 type: PrivateConnectionType;
69 /**
70 * VMware Engine network to attach
71 * (`projects/{project}/locations/{location}/vmwareEngineNetworks/{id}`).
72 * Immutable — changing it replaces the connection.
73 */
74 vmwareEngineNetwork: string;
75 /**
76 * Service network to peer
77 * (`projects/{project}/global/networks/{network}`). For
78 * `PRIVATE_SERVICE_ACCESS` this is the Service Networking VPC. For
79 * `THIRD_PARTY_SERVICE` it may be a consumer or producer VPC.
80 * Immutable — changing it replaces the connection.
81 */
82 serviceNetwork: string;
83 /**
84 * Routing mode. `PRIVATE_SERVICE_ACCESS` accepts `GLOBAL` or
85 * `REGIONAL`; other types only support `GLOBAL`.
86 * @default "GLOBAL"
87 */
88 routingMode?: PrivateConnectionRoutingMode;
89 /**
90 * Human-readable description. Private connections have no labels
91 * field, so Alchemy ownership is stored in a `[alchemy …]` prefix and
92 * stripped from attributes.
93 */
94 description?: string;
95};
96
97export type PrivateConnection = Resource<
98 "GCP.VMwareEngine.PrivateConnection",
99 PrivateConnectionProps,
100 {
101 /** Full resource name `projects/{project}/locations/{location}/privateConnections/{id}`. */
102 name: string;
103 /** Connection id (last path segment). */
104 privateConnectionId: string;
105 /** Project id. */
106 project: string;
107 /** Region id. */
108 location: string;
109 /** Connection type. */
110 type: string;
111 /** Attached VMware Engine network. */
112 vmwareEngineNetwork: string | undefined;
113 /** Canonical VMware Engine network name (project number). */
114 vmwareEngineNetworkCanonical: string | undefined;
115 /** Peered service network. */
116 serviceNetwork: string | undefined;
117 /** Routing mode. */
118 routingMode: string | undefined;
119 /** User description with the Alchemy ownership prefix stripped. */
120 description: string | undefined;
121 /** Server-reported state (`ACTIVE`, `CREATING`, …). */
122 state: string | undefined;
123 /** VPC peering state (`PEERING_ACTIVE`, `PEERING_INACTIVE`). */
124 peeringState: string | undefined;
125 /** VPC network peering id. */
126 peeringId: string | undefined;
127 /** Server-generated uid. */
128 uid: string | undefined;
129 /** RFC3339 creation timestamp. */
130 createTime: string | undefined;
131 /** RFC3339 last-update timestamp. */
132 updateTime: string | undefined;
133 },
134 never,
135 Providers
136>;
137
138/**
139 * A private connection that peers a service VPC with a VMware Engine
140 * network so private clouds can reach Google Cloud services, NetApp,
141 * Dell PowerScale, or a third-party VPC.
142 *
143 * Changing id, location, type, `vmwareEngineNetwork`, or
144 * `serviceNetwork` replaces the connection. Description and routing
145 * mode update in place.
146 *
147 * ### Creating a Connection
148 * **Example:** Third-party VPC
149 * ```typescript
150 * const connection = yield* GCP.VMwareEngine.PrivateConnection("Peer", {
151 * type: "THIRD_PARTY_SERVICE",
152 * vmwareEngineNetwork: ven.name,
153 * serviceNetwork: "projects/my-project/global/networks/default",
154 * description: "consumer vpc",
155 * });
156 * ```
157 *
158 * **Example:** Private Service Access
159 * ```typescript
160 * const connection = yield* GCP.VMwareEngine.PrivateConnection("Psa", {
161 * location: "us-central1",
162 * type: "PRIVATE_SERVICE_ACCESS",
163 * vmwareEngineNetwork: ven.name,
164 * serviceNetwork:
165 * "projects/my-project/global/networks/servicenetworking",
166 * routingMode: "GLOBAL",
167 * });
168 * ```
169 *
170 * ### Updating a Connection
171 * Re-declare the same logical id with changed props; the engine keeps the
172 * physical resource and updates it in place.
173 *
174 * **Example:** Description and regional routing
175 * ```typescript
176 * const connection = yield* GCP.VMwareEngine.PrivateConnection("Psa", {
177 * location: "us-central1",
178 * type: "PRIVATE_SERVICE_ACCESS",
179 * vmwareEngineNetwork: ven.name,
180 * serviceNetwork:
181 * "projects/my-project/global/networks/servicenetworking",
182 * routingMode: "REGIONAL",
183 * description: "psa v2",
184 * });
185 * ```
186 *
187 * @resource
188 * @category VMwareEngine
189 */
190export const PrivateConnection = Resource<PrivateConnection>(
191 "GCP.VMwareEngine.PrivateConnection",
192);
193
194const resourceName = (
195 project: string,
196 location: string,
197 connectionId: string,
198) => `${parentOf(project, location)}/${COLLECTION}/${connectionId}`;
199
200const expandVen = (project: string, value: string) =>
201 expandName(value, project, DEFAULT_GLOBAL, VEN_COLLECTION);
202
203const expandVpc = (project: string, value: string) => {
204 const canonical = canonicalizeLink(value);
205 if (canonical.includes("/")) return canonical;
206 return `projects/${project}/global/networks/${rfc1035(canonical, "network")}`;
207};
208
209const routingOf = (value: string | undefined) =>
210 (value ?? DEFAULT_ROUTING_MODE).toUpperCase();
211
212const toAttrs = (
213 connection: vmwareengine.PrivateConnection,
214 project: string,
215 region: string,
216) => {
217 const name = connection.name ?? "";
218 const parsed = parseName(name, COLLECTION, region);
219 const ownership = parseOwnership(connection.description);
220 return {
221 name,
222 privateConnectionId: parsed.id,
223 project: parsed.project || project,
224 location: parsed.location,
225 type: (connection.type ?? "").toUpperCase(),
226 vmwareEngineNetwork: connection.vmwareEngineNetwork,
227 vmwareEngineNetworkCanonical: connection.vmwareEngineNetworkCanonical,
228 serviceNetwork: connection.serviceNetwork,
229 routingMode: connection.routingMode,
230 description: ownership.text,
231 state: connection.state,
232 peeringState: connection.peeringState,
233 peeringId: connection.peeringId,
234 uid: connection.uid,
235 createTime: connection.createTime,
236 updateTime: connection.updateTime,
237 };
238};
239
240const getByName = (name: string) =>
241 name.length === 0
242 ? Effect.succeed(undefined)
243 : vmwareengine
244 .getProjectsLocationsPrivateConnections({ name })
245 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
246
247export const PrivateConnectionProvider = () =>
248 Provider.succeed(PrivateConnection, {
249 stables: [
250 "name",
251 "privateConnectionId",
252 "project",
253 "location",
254 "type",
255 "uid",
256 "createTime",
257 ],
258
259 diff: Effect.fn(function* ({ news, olds, output }) {
260 if (!isResolved(news)) return undefined;
261 const env = yield* GcpEnvironment.current;
262 const previousVen = canonicalizeLink(
263 olds?.vmwareEngineNetwork ?? output?.vmwareEngineNetwork,
264 );
265 const nextVen = canonicalizeLink(news.vmwareEngineNetwork);
266 const previousService = canonicalizeLink(
267 olds?.serviceNetwork ?? output?.serviceNetwork,
268 );
269 const nextService = canonicalizeLink(news.serviceNetwork);
270 const previousType = (olds?.type ?? output?.type ?? "").toUpperCase();
271 const nextType = news.type.toUpperCase();
272 return replaceOnIdentity({
273 previousId: olds?.privateConnectionId ?? output?.privateConnectionId,
274 nextId: news.privateConnectionId
275 ? rfc1035(news.privateConnectionId, "connection")
276 : (olds?.privateConnectionId ?? output?.privateConnectionId),
277 previousLocation: normalizeLocation(
278 olds?.location ?? output?.location,
279 env.region,
280 ),
281 nextLocation: normalizeLocation(
282 news.location ?? olds?.location ?? output?.location,
283 env.region,
284 ),
285 extra:
286 (previousType.length > 0 && previousType !== nextType) ||
287 (previousVen.length > 0 && previousVen !== nextVen) ||
288 (previousService.length > 0 && previousService !== nextService),
289 });
290 }),
291
292 read: Effect.fn(function* ({ id, olds, output }) {
293 const env = yield* GcpEnvironment.current;
294 const location = normalizeLocation(
295 olds?.location ?? output?.location,
296 env.region,
297 );
298 const connectionId = yield* toPhysicalId(
299 id,
300 olds?.privateConnectionId,
301 output?.privateConnectionId,
302 "connection",
303 );
304 const name =
305 output?.name ?? resourceName(env.project, location, connectionId);
306 const existing = yield* getByName(name);
307 if (existing === undefined) return undefined;
308 const attrs = toAttrs(existing, env.project, env.region);
309 const { labels } = parseOwnership(existing.description);
310 return (yield* hasAlchemyLabels(id, labels)) ? attrs : Unowned(attrs);
311 }),
312
313 list: () =>
314 Effect.gen(function* () {
315 const env = yield* GcpEnvironment.current;
316 const items = yield* listAcrossLocations(
317 env.project,
318 env.region,
319 (parent) =>
320 collectPages(
321 vmwareengine.listProjectsLocationsPrivateConnections.pages({
322 parent,
323 pageSize: 1000,
324 }),
325 (page) => page.privateConnections,
326 ),
327 );
328 return items
329 .filter((item) => hasOwnershipMarker(item.description))
330 .map((item) => toAttrs(item, env.project, env.region));
331 }),
332
333 reconcile: Effect.fn(function* ({ id, news, output }) {
334 const env = yield* GcpEnvironment.current;
335 const location = normalizeLocation(
336 news.location ?? output?.location,
337 env.region,
338 );
339 const connectionId = yield* toPhysicalId(
340 id,
341 news.privateConnectionId,
342 output?.privateConnectionId,
343 "connection",
344 );
345 const name = resourceName(env.project, location, connectionId);
346 const ownership = yield* createInternalLabels(id);
347 const desiredDescription = encodeOwnership(ownership, news.description);
348 const type = news.type.toUpperCase();
349 const routingMode = routingOf(news.routingMode);
350 const vmwareEngineNetwork = expandVen(
351 env.project,
352 news.vmwareEngineNetwork,
353 );
354 const serviceNetwork = expandVpc(env.project, news.serviceNetwork);
355
356 let current = yield* getByName(output?.name ?? name);
357
358 if (current === undefined) {
359 const created = yield* vmwareengine
360 .createProjectsLocationsPrivateConnections({
361 parent: parentOf(env.project, location),
362 privateConnectionId: connectionId,
363 body: {
364 type,
365 vmwareEngineNetwork,
366 serviceNetwork,
367 routingMode,
368 description: desiredDescription,
369 },
370 })
371 .pipe(
372 Effect.retry({
373 while: (error) => error._tag === "Conflict",
374 times: 5,
375 schedule: Schedule.spaced("2 seconds"),
376 }),
377 Effect.catchTag("Conflict", () => Effect.succeed(undefined)),
378 );
379 if (created !== undefined) {
380 yield* waitForOperation(created);
381 }
382 current = yield* waitUntilPresent(getByName(name), name);
383 }
384
385 if (current === undefined) {
386 return yield* new VmwareengineNotResolved({ name });
387 }
388
389 current = yield* waitUntilReady(
390 getByName(current.name ?? name),
391 current.name ?? name,
392 (item) => item.state,
393 );
394
395 const updateMask = changedFields([
396 ["description", (current.description ?? "") !== desiredDescription],
397 ["routingMode", routingOf(current.routingMode) !== routingMode],
398 ]);
399 if (updateMask.length > 0) {
400 const operation =
401 yield* vmwareengine.patchProjectsLocationsPrivateConnections({
402 name: current.name ?? name,
403 updateMask: updateMask.join(","),
404 body: {
405 name: current.name ?? name,
406 description: desiredDescription,
407 routingMode,
408 },
409 });
410 yield* waitForOperation(operation);
411 current = yield* waitUntilReady(
412 getByName(current.name ?? name),
413 current.name ?? name,
414 (item) => item.state,
415 );
416 }
417
418 return toAttrs(current, env.project, env.region);
419 }),
420
421 delete: Effect.fn(function* ({ output }) {
422 const operation = yield* vmwareengine
423 .deleteProjectsLocationsPrivateConnections({ name: output.name })
424 .pipe(
425 Effect.retry({
426 while: (error) => error._tag === "Conflict",
427 times: 8,
428 schedule: Schedule.spaced("2 seconds"),
429 }),
430 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
431 );
432 if (operation !== undefined) {
433 yield* waitForOperation(operation, { notFoundOk: true });
434 }
435 yield* waitUntilGone(getByName(output.name), output.name);
436 }),
437 });