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 DEFAULT_ZONE,
13 VmwareengineNotResolved,
14 canonicalizeLink,
15 changedFields,
16 collectPages,
17 createInternalLabels,
18 encodeOwnership,
19 expandName,
20 hasAlchemyLabels,
21 hasOwnershipMarker,
22 listAcrossLocations,
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 = "managementDnsZoneBindings";
35const CLOUD_COLLECTION = "privateClouds";
36const VEN_COLLECTION = "vmwareEngineNetworks";
37
38export type PrivateCloudsManagementDnsZoneBindingProps = {
39 /**
40 * Parent private cloud. Full name
41 * `projects/{project}/locations/{location}/privateClouds/{privateCloud}`.
42 * Immutable — changing it replaces the binding.
43 */
44 privateCloud: string;
45 /**
46 * Binding id (the `{managementDnsZoneBinding}` segment). If omitted, a
47 * unique RFC1035 name is generated. Immutable — changing it replaces
48 * the binding.
49 */
50 managementDnsZoneBindingId?: string;
51 /**
52 * Consumer VPC to bind
53 * (`projects/{project}/global/networks/{network}`). Mutually exclusive
54 * with `vmwareEngineNetwork`. Immutable — changing it replaces the
55 * binding.
56 */
57 vpcNetwork?: string;
58 /**
59 * VMware Engine network to bind
60 * (`projects/{project}/locations/global/vmwareEngineNetworks/{id}`).
61 * Mutually exclusive with `vpcNetwork`. Immutable — changing it
62 * replaces the binding.
63 */
64 vmwareEngineNetwork?: string;
65 /**
66 * Human-readable description. Bindings have no labels field, so
67 * Alchemy ownership is stored in a `[alchemy …]` prefix and stripped
68 * from attributes.
69 */
70 description?: string;
71};
72
73export type PrivateCloudsManagementDnsZoneBinding = Resource<
74 "GCP.VMwareEngine.PrivateCloudsManagementDnsZoneBinding",
75 PrivateCloudsManagementDnsZoneBindingProps,
76 {
77 /** Full resource name `.../privateClouds/{cloud}/managementDnsZoneBindings/{id}`. */
78 name: string;
79 /** Binding id (last path segment). */
80 managementDnsZoneBindingId: string;
81 /** Parent private cloud resource name. */
82 privateCloud: string;
83 /** Project id. */
84 project: string;
85 /** Location id (typically a zone such as `us-central1-a`). */
86 location: string;
87 /** Bound consumer VPC, if any. */
88 vpcNetwork: string | undefined;
89 /** Bound VMware Engine network, if any. */
90 vmwareEngineNetwork: string | undefined;
91 /** User description with the Alchemy ownership prefix stripped. */
92 description: string | undefined;
93 /** Server-reported state (`ACTIVE`, `CREATING`, …). */
94 state: string | undefined;
95 /** Server-generated uid. */
96 uid: string | undefined;
97 /** RFC3339 creation timestamp. */
98 createTime: string | undefined;
99 /** RFC3339 last-update timestamp. */
100 updateTime: string | undefined;
101 },
102 never,
103 Providers
104>;
105
106/**
107 * A binding between a consumer VPC (or VMware Engine network) and a
108 * private cloud's management DNS zone.
109 *
110 * The management DNS zone holds FQDNs for ESXi hosts and management
111 * appliances (vCenter, NSX Manager). Changing the parent private cloud,
112 * binding id, or bound network replaces the binding. Description
113 * updates in place.
114 *
115 * ### Creating a Binding
116 * **Example:** Bind a consumer VPC
117 * ```typescript
118 * const binding = yield* GCP.VMwareEngine.PrivateCloudsManagementDnsZoneBinding(
119 * "VpcDns",
120 * {
121 * privateCloud: cloud.name,
122 * vpcNetwork: "projects/my-project/global/networks/default",
123 * description: "consumer vpc",
124 * },
125 * );
126 * ```
127 *
128 * **Example:** Bind a VMware Engine network
129 * ```typescript
130 * const binding = yield* GCP.VMwareEngine.PrivateCloudsManagementDnsZoneBinding(
131 * "VenDns",
132 * {
133 * privateCloud: cloud.name,
134 * vmwareEngineNetwork: ven.name,
135 * description: "standard ven",
136 * },
137 * );
138 * ```
139 *
140 * ### Updating a Binding
141 * Re-declare the same logical id with changed props; the engine keeps the
142 * physical resource and updates it in place.
143 *
144 * **Example:** Description
145 * ```typescript
146 * const binding = yield* GCP.VMwareEngine.PrivateCloudsManagementDnsZoneBinding(
147 * "VpcDns",
148 * {
149 * privateCloud: cloud.name,
150 * vpcNetwork: "projects/my-project/global/networks/default",
151 * description: "consumer vpc v2",
152 * },
153 * );
154 * ```
155 *
156 * @resource
157 * @category VMwareEngine
158 */
159export const PrivateCloudsManagementDnsZoneBinding =
160 Resource<PrivateCloudsManagementDnsZoneBinding>(
161 "GCP.VMwareEngine.PrivateCloudsManagementDnsZoneBinding",
162 );
163
164const expandCloud = (project: string, value: string) =>
165 expandName(value, project, DEFAULT_ZONE, CLOUD_COLLECTION);
166
167const expandVen = (project: string, value: string) =>
168 expandName(value, project, DEFAULT_GLOBAL, VEN_COLLECTION);
169
170const expandVpc = (project: string, value: string) => {
171 const canonical = canonicalizeLink(value);
172 if (canonical.includes("/")) return canonical;
173 return `projects/${project}/global/networks/${rfc1035(canonical, "network")}`;
174};
175
176const resourceName = (privateCloud: string, bindingId: string) =>
177 `${canonicalizeLink(privateCloud)}/${COLLECTION}/${bindingId}`;
178
179const toAttrs = (
180 binding: vmwareengine.ManagementDnsZoneBinding,
181 project: string,
182) => {
183 const name = binding.name ?? "";
184 const parsed = parseName(name, COLLECTION, DEFAULT_ZONE);
185 const ownership = parseOwnership(binding.description);
186 return {
187 name,
188 managementDnsZoneBindingId: parsed.id,
189 privateCloud: parsed.parent,
190 project: parsed.project || project,
191 location: parsed.location,
192 vpcNetwork: binding.vpcNetwork,
193 vmwareEngineNetwork: binding.vmwareEngineNetwork,
194 description: ownership.text,
195 state: binding.state,
196 uid: binding.uid,
197 createTime: binding.createTime,
198 updateTime: binding.updateTime,
199 };
200};
201
202const getByName = (name: string) =>
203 name.length === 0
204 ? Effect.succeed(undefined)
205 : vmwareengine
206 .getProjectsLocationsPrivateCloudsManagementDnsZoneBindings({ name })
207 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
208
209const listBindingsAt = (parent: string) =>
210 collectPages(
211 vmwareengine.listProjectsLocationsPrivateCloudsManagementDnsZoneBindings.pages(
212 {
213 parent,
214 pageSize: 1000,
215 },
216 ),
217 (page) => page.managementDnsZoneBindings,
218 );
219
220export const PrivateCloudsManagementDnsZoneBindingProvider = () =>
221 Provider.succeed(PrivateCloudsManagementDnsZoneBinding, {
222 stables: [
223 "name",
224 "managementDnsZoneBindingId",
225 "privateCloud",
226 "project",
227 "location",
228 "uid",
229 "createTime",
230 ],
231
232 diff: Effect.fn(function* ({ news, olds, output }) {
233 if (!isResolved(news)) return undefined;
234 const previousVpc = canonicalizeLink(
235 olds?.vpcNetwork ?? output?.vpcNetwork,
236 );
237 const nextVpc = canonicalizeLink(news.vpcNetwork);
238 const previousVen = canonicalizeLink(
239 olds?.vmwareEngineNetwork ?? output?.vmwareEngineNetwork,
240 );
241 const nextVen = canonicalizeLink(news.vmwareEngineNetwork);
242 const switchedToVen = nextVen.length > 0 && previousVpc.length > 0;
243 const switchedToVpc = nextVpc.length > 0 && previousVen.length > 0;
244 return replaceOnIdentity({
245 previousId:
246 olds?.managementDnsZoneBindingId ??
247 output?.managementDnsZoneBindingId,
248 nextId: news.managementDnsZoneBindingId
249 ? rfc1035(news.managementDnsZoneBindingId, "binding")
250 : (olds?.managementDnsZoneBindingId ??
251 output?.managementDnsZoneBindingId),
252 previousLocation: "",
253 nextLocation: "",
254 previousParent: olds?.privateCloud ?? output?.privateCloud,
255 nextParent: news.privateCloud,
256 extra:
257 switchedToVen ||
258 switchedToVpc ||
259 (previousVpc.length > 0 &&
260 nextVpc.length > 0 &&
261 previousVpc !== nextVpc) ||
262 (previousVen.length > 0 &&
263 nextVen.length > 0 &&
264 previousVen !== nextVen),
265 });
266 }),
267
268 read: Effect.fn(function* ({ id, olds, output }) {
269 const env = yield* GcpEnvironment.current;
270 const privateCloud = expandCloud(
271 env.project,
272 olds?.privateCloud ?? output?.privateCloud ?? "",
273 );
274 const bindingId = yield* toPhysicalId(
275 id,
276 olds?.managementDnsZoneBindingId,
277 output?.managementDnsZoneBindingId,
278 "binding",
279 );
280 const name =
281 output?.name ??
282 (privateCloud.length > 0 ? resourceName(privateCloud, bindingId) : "");
283 const existing = yield* getByName(name);
284 if (existing === undefined) return undefined;
285 const attrs = toAttrs(existing, env.project);
286 const { labels } = parseOwnership(existing.description);
287 return (yield* hasAlchemyLabels(id, labels)) ? attrs : Unowned(attrs);
288 }),
289
290 list: () =>
291 Effect.gen(function* () {
292 const env = yield* GcpEnvironment.current;
293 const clouds = yield* listAcrossLocations(
294 env.project,
295 env.region,
296 (parent) =>
297 collectPages(
298 vmwareengine.listProjectsLocationsPrivateClouds.pages({
299 parent,
300 pageSize: 1000,
301 }),
302 (page) => page.privateClouds,
303 ),
304 );
305 const nested = yield* Effect.forEach(
306 clouds.filter((cloud) => (cloud.name ?? "").length > 0),
307 (cloud) => listBindingsAt(cloud.name!),
308 { concurrency: 4 },
309 );
310 return nested
311 .flat()
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 privateCloud = expandCloud(env.project, news.privateCloud);
319 const bindingId = yield* toPhysicalId(
320 id,
321 news.managementDnsZoneBindingId,
322 output?.managementDnsZoneBindingId,
323 "binding",
324 );
325 const name = resourceName(privateCloud, bindingId);
326 const ownership = yield* createInternalLabels(id);
327 const desiredDescription = encodeOwnership(ownership, news.description);
328 const vpcNetwork =
329 news.vpcNetwork === undefined
330 ? undefined
331 : expandVpc(env.project, news.vpcNetwork);
332 const vmwareEngineNetwork =
333 news.vmwareEngineNetwork === undefined
334 ? undefined
335 : expandVen(env.project, news.vmwareEngineNetwork);
336
337 let current = yield* getByName(output?.name ?? name);
338
339 if (current === undefined) {
340 const created = yield* vmwareengine
341 .createProjectsLocationsPrivateCloudsManagementDnsZoneBindings({
342 parent: privateCloud,
343 managementDnsZoneBindingId: bindingId,
344 body: {
345 description: desiredDescription,
346 vpcNetwork,
347 vmwareEngineNetwork,
348 },
349 })
350 .pipe(
351 Effect.retry({
352 while: (error) => error._tag === "Conflict",
353 times: 5,
354 schedule: Schedule.spaced("2 seconds"),
355 }),
356 Effect.catchTag("Conflict", () => Effect.succeed(undefined)),
357 );
358 if (created !== undefined) {
359 yield* waitForOperation(created);
360 }
361 current = yield* waitUntilPresent(getByName(name), name);
362 }
363
364 if (current === undefined) {
365 return yield* new VmwareengineNotResolved({ name });
366 }
367
368 current = yield* waitUntilReady(
369 getByName(current.name ?? name),
370 current.name ?? name,
371 (item) => item.state,
372 );
373
374 const updateMask = changedFields([
375 ["description", (current.description ?? "") !== desiredDescription],
376 ]);
377 if (updateMask.length > 0) {
378 const operation =
379 yield* vmwareengine.patchProjectsLocationsPrivateCloudsManagementDnsZoneBindings(
380 {
381 name: current.name ?? name,
382 updateMask: updateMask.join(","),
383 body: {
384 name: current.name ?? name,
385 description: desiredDescription,
386 },
387 },
388 );
389 yield* waitForOperation(operation);
390 current = yield* waitUntilReady(
391 getByName(current.name ?? name),
392 current.name ?? name,
393 (item) => item.state,
394 );
395 }
396
397 return toAttrs(current, env.project);
398 }),
399
400 delete: Effect.fn(function* ({ output }) {
401 const operation = yield* vmwareengine
402 .deleteProjectsLocationsPrivateCloudsManagementDnsZoneBindings({
403 name: output.name,
404 })
405 .pipe(
406 Effect.retry({
407 while: (error) => error._tag === "Conflict",
408 times: 8,
409 schedule: Schedule.spaced("2 seconds"),
410 }),
411 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
412 );
413 if (operation !== undefined) {
414 yield* waitForOperation(operation, { notFoundOk: true });
415 }
416 yield* waitUntilGone(getByName(output.name), output.name);
417 }),
418 });