1import * as networkservices from "@distilled.cloud/gcp/networkservices_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 { tagRecord } from "../../Tags.ts";
9import { GcpEnvironment } from "../Environment.ts";
10import {
11 createInternalLabels,
12 diffLabels,
13 hasAlchemyLabels,
14 toLabels,
15} from "../Labels.ts";
16import type { Providers } from "../Providers.ts";
17import {
18 DEFAULT_ZONE,
19 MAX_MULTICAST_NAME_LENGTH,
20 changedFields,
21 collectPages,
22 hasAlchemyLabelKeys,
23 linkKey,
24 normalizeLocation,
25 parentOf,
26 parseName,
27 resourceName,
28 rfc1035,
29 sameJson,
30 toNamedResource,
31 toPhysicalId,
32 userLabels,
33 waitForOperation,
34 waitUntilGone,
35 waitUntilPresent,
36} from "./internal.ts";
37
38const COLLECTION = "multicastGroupConsumerActivations";
39
40export type MulticastLogConfig = {
41 /**
42 * Export multicast group-consumer activity to Cloud Logging.
43 * @default false
44 */
45 enabled?: boolean;
46};
47
48export type MulticastGroupConsumerActivationProps = {
49 /**
50 * Activation id (the `{multicastGroupConsumerActivation}` segment of
51 * `projects/{project}/locations/{location}/multicastGroupConsumerActivations/{multicastGroupConsumerActivation}`).
52 * If omitted, a unique name is generated from the stack, stage, and
53 * logical id. Must be at most 48 characters. Immutable — changing it
54 * replaces the activation.
55 */
56 multicastGroupConsumerActivationId?: string;
57 /**
58 * Zone of the activation (`us-central1-a`, …). Immutable — changing
59 * it replaces the activation. `US-CENTRAL1-A` is accepted and
60 * normalized to `us-central1-a`.
61 * @default "us-central1-a"
62 */
63 location?: string;
64 /**
65 * Multicast consumer association in the same zone
66 * (`projects/{project}/locations/{location}/multicastConsumerAssociations/{association}`).
67 * Immutable — changing it replaces the activation.
68 */
69 multicastConsumerAssociation: string;
70 /**
71 * Multicast group range activation in the same zone
72 * (`projects/{project}/locations/{location}/multicastGroupRangeActivations/{activation}`).
73 * Immutable — changing it replaces the activation.
74 */
75 multicastGroupRangeActivation: string;
76 /**
77 * Deprecated multicast group resource name. Prefer
78 * `multicastGroupRangeActivation`.
79 */
80 multicastGroup?: string;
81 /**
82 * Human-readable description.
83 */
84 description?: string;
85 /**
86 * Logging options for this activation.
87 */
88 logConfig?: MulticastLogConfig;
89 /**
90 * User labels. Alchemy ownership labels are merged in automatically.
91 */
92 labels?: Record<string, string>;
93};
94
95export type MulticastGroupConsumerActivation = Resource<
96 "GCP.NetworkServices.MulticastGroupConsumerActivation",
97 MulticastGroupConsumerActivationProps,
98 {
99 /** Full resource name `projects/{project}/locations/{location}/multicastGroupConsumerActivations/{multicastGroupConsumerActivation}`. */
100 name: string;
101 /** Activation id (last path segment). */
102 multicastGroupConsumerActivationId: string;
103 /** Project id. */
104 project: string;
105 /** Zone id (`us-central1-a`, …). */
106 location: string;
107 /** Attached multicast consumer association. */
108 multicastConsumerAssociation: string | undefined;
109 /** Attached multicast group range activation. */
110 multicastGroupRangeActivation: string | undefined;
111 /** Deprecated multicast group name, if set. */
112 multicastGroup: string | undefined;
113 /** User-provided description. */
114 description: string | undefined;
115 /** Logging configuration. */
116 logConfig: MulticastLogConfig | undefined;
117 /** Deprecated resource-state enum. */
118 resourceState: string | undefined;
119 /** Nested multicast resource state. */
120 state: string | undefined;
121 /** Google-generated UUID. */
122 uniqueId: string | undefined;
123 /** User labels (Alchemy ownership labels stripped). */
124 labels: Record<string, string>;
125 /** RFC3339 creation timestamp. */
126 createTime: string | undefined;
127 /** RFC3339 last-update timestamp. */
128 updateTime: string | undefined;
129 },
130 never,
131 Providers
132>;
133
134/**
135 * A multicast group consumer activation lets VMs in an associated
136 * consumer VPC join a multicast group range in the same zone.
137 *
138 * Changing `multicastGroupConsumerActivationId`, `location`,
139 * `multicastConsumerAssociation`, or `multicastGroupRangeActivation`
140 * replaces the activation. Description, labels, and log config update
141 * in place.
142 *
143 * ### Creating a MulticastGroupConsumerActivation
144 * **Example:** Activate a group range for a consumer VPC
145 * ```typescript
146 * const activation = yield* GCP.NetworkServices.MulticastGroupConsumerActivation(
147 * "Join",
148 * {
149 * location: association.location,
150 * multicastConsumerAssociation: association.name,
151 * multicastGroupRangeActivation: rangeActivation.name,
152 * logConfig: { enabled: true },
153 * labels: { env: "prod" },
154 * },
155 * );
156 * ```
157 *
158 * @resource
159 * @category NetworkServices
160 */
161export const MulticastGroupConsumerActivation =
162 Resource<MulticastGroupConsumerActivation>(
163 "GCP.NetworkServices.MulticastGroupConsumerActivation",
164 );
165
166const toAssociation = (
167 project: string,
168 location: string,
169 value: string | undefined,
170) =>
171 value
172 ? toNamedResource(project, location, "multicastConsumerAssociations", value)
173 : undefined;
174
175const toRangeActivation = (
176 project: string,
177 location: string,
178 value: string | undefined,
179) =>
180 value
181 ? toNamedResource(
182 project,
183 location,
184 "multicastGroupRangeActivations",
185 value,
186 )
187 : undefined;
188
189const toGroup = (
190 project: string,
191 location: string,
192 value: string | undefined,
193) =>
194 value
195 ? toNamedResource(project, location, "multicastGroups", value)
196 : undefined;
197
198const toLogConfig = (
199 config: MulticastLogConfig | networkservices.MulticastLogConfig | undefined,
200): MulticastLogConfig | undefined => {
201 if (config === undefined) return undefined;
202 return { enabled: config.enabled };
203};
204
205const toAttrs = (
206 activation: networkservices.MulticastGroupConsumerActivation,
207 project: string,
208) => {
209 const name = activation.name ?? "";
210 const parsed = parseName(name, COLLECTION, DEFAULT_ZONE);
211 const location = parsed.location || DEFAULT_ZONE;
212 const proj = parsed.project || project;
213 return {
214 name,
215 multicastGroupConsumerActivationId: parsed.id,
216 project: proj,
217 location,
218 multicastConsumerAssociation: toAssociation(
219 proj,
220 location,
221 activation.multicastConsumerAssociation,
222 ),
223 multicastGroupRangeActivation: toRangeActivation(
224 proj,
225 location,
226 activation.multicastGroupRangeActivation,
227 ),
228 multicastGroup: toGroup(proj, location, activation.multicastGroup),
229 description: activation.description,
230 logConfig: toLogConfig(activation.logConfig),
231 resourceState: activation.resourceState,
232 state: activation.state?.state,
233 uniqueId: activation.uniqueId,
234 labels: userLabels(activation.labels),
235 createTime: activation.createTime,
236 updateTime: activation.updateTime,
237 };
238};
239
240const getByName = (name: string) =>
241 networkservices
242 .getProjectsLocationsMulticastGroupConsumerActivations({ name })
243 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
244
245export const MulticastGroupConsumerActivationProvider = () =>
246 Provider.succeed(MulticastGroupConsumerActivation, {
247 stables: [
248 "name",
249 "multicastGroupConsumerActivationId",
250 "project",
251 "location",
252 "multicastConsumerAssociation",
253 "multicastGroupRangeActivation",
254 "uniqueId",
255 "createTime",
256 ],
257
258 diff: Effect.fn(function* ({ news, olds, output }) {
259 if (!isResolved(news)) return undefined;
260 const previousId =
261 olds?.multicastGroupConsumerActivationId ??
262 output?.multicastGroupConsumerActivationId;
263 const nextId = news.multicastGroupConsumerActivationId
264 ? rfc1035(
265 news.multicastGroupConsumerActivationId,
266 "mcast-gca",
267 MAX_MULTICAST_NAME_LENGTH,
268 )
269 : previousId;
270 const previousLocation = normalizeLocation(
271 olds?.location ?? output?.location,
272 DEFAULT_ZONE,
273 );
274 const nextLocation = normalizeLocation(
275 news.location ?? olds?.location ?? output?.location,
276 DEFAULT_ZONE,
277 );
278 const previousAssociation = linkKey(
279 olds?.multicastConsumerAssociation ??
280 output?.multicastConsumerAssociation,
281 );
282 const nextAssociation = linkKey(news.multicastConsumerAssociation);
283 const previousRange = linkKey(
284 olds?.multicastGroupRangeActivation ??
285 output?.multicastGroupRangeActivation,
286 );
287 const nextRange = linkKey(news.multicastGroupRangeActivation);
288 if (
289 (previousId !== undefined &&
290 nextId !== undefined &&
291 nextId !== previousId) ||
292 previousLocation !== nextLocation ||
293 (previousAssociation.length > 0 &&
294 previousAssociation !== nextAssociation) ||
295 (previousRange.length > 0 && previousRange !== nextRange)
296 ) {
297 return { action: "replace" as const };
298 }
299 return undefined;
300 }),
301
302 read: Effect.fn(function* ({ id, olds, output }) {
303 const env = yield* GcpEnvironment.current;
304 const multicastGroupConsumerActivationId = yield* toPhysicalId(
305 id,
306 olds?.multicastGroupConsumerActivationId,
307 output?.multicastGroupConsumerActivationId,
308 "mcast-gca",
309 MAX_MULTICAST_NAME_LENGTH,
310 );
311 const location = normalizeLocation(
312 olds?.location ?? output?.location,
313 DEFAULT_ZONE,
314 );
315 const name =
316 output?.name ??
317 resourceName(
318 env.project,
319 location,
320 COLLECTION,
321 multicastGroupConsumerActivationId,
322 );
323 const existing = yield* getByName(name);
324 if (existing === undefined) return undefined;
325 const attrs = toAttrs(existing, env.project);
326 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
327 ? attrs
328 : Unowned(attrs);
329 }),
330
331 list: () =>
332 Effect.gen(function* () {
333 const env = yield* GcpEnvironment.current;
334 const items = yield* collectPages(
335 networkservices.listProjectsLocationsMulticastGroupConsumerActivations.pages(
336 {
337 parent: parentOf(env.project, "-"),
338 pageSize: 1000,
339 },
340 ),
341 (page) => page.multicastGroupConsumerActivations,
342 );
343 return items
344 .filter((item) => hasAlchemyLabelKeys(item.labels))
345 .map((item) => toAttrs(item, env.project));
346 }),
347
348 reconcile: Effect.fn(function* ({ id, news, output }) {
349 const env = yield* GcpEnvironment.current;
350 const multicastGroupConsumerActivationId = yield* toPhysicalId(
351 id,
352 news.multicastGroupConsumerActivationId,
353 output?.multicastGroupConsumerActivationId,
354 "mcast-gca",
355 MAX_MULTICAST_NAME_LENGTH,
356 );
357 const location = normalizeLocation(
358 news.location ?? output?.location,
359 DEFAULT_ZONE,
360 );
361 const name = resourceName(
362 env.project,
363 location,
364 COLLECTION,
365 multicastGroupConsumerActivationId,
366 );
367 const desiredLabels = {
368 ...toLabels(news.labels),
369 ...(yield* createInternalLabels(id)),
370 };
371 const multicastConsumerAssociation = toAssociation(
372 env.project,
373 location,
374 news.multicastConsumerAssociation,
375 );
376 const multicastGroupRangeActivation = toRangeActivation(
377 env.project,
378 location,
379 news.multicastGroupRangeActivation,
380 );
381 const multicastGroup = toGroup(
382 env.project,
383 location,
384 news.multicastGroup,
385 );
386 const logConfig = toLogConfig(news.logConfig);
387
388 let current = yield* getByName(name);
389
390 if (current === undefined) {
391 const created = yield* networkservices
392 .createProjectsLocationsMulticastGroupConsumerActivations({
393 parent: parentOf(env.project, location),
394 multicastGroupConsumerActivationId,
395 body: {
396 description: news.description,
397 labels: desiredLabels,
398 multicastConsumerAssociation,
399 multicastGroupRangeActivation,
400 multicastGroup,
401 logConfig,
402 },
403 })
404 .pipe(
405 Effect.retry({
406 while: (error) => error._tag === "Conflict",
407 times: 5,
408 schedule: Schedule.spaced("2 seconds"),
409 }),
410 Effect.catchTag("Conflict", () => Effect.succeed(undefined)),
411 );
412 if (created !== undefined) {
413 yield* waitForOperation(created);
414 }
415 current = yield* waitUntilPresent(getByName(name), name);
416 }
417
418 const observed = toAttrs(current, env.project);
419 const observedLabels = tagRecord(current.labels);
420 const { upsert, removed } = diffLabels(observedLabels, desiredLabels);
421 const labelsChanged = upsert.length > 0 || removed.length > 0;
422 const updateMask = changedFields([
423 ["labels", labelsChanged],
424 [
425 "description",
426 (current.description ?? "") !== (news.description ?? ""),
427 ],
428 ["logConfig", !sameJson(observed.logConfig, logConfig)],
429 ]);
430
431 if (updateMask.length > 0) {
432 const operation =
433 yield* networkservices.patchProjectsLocationsMulticastGroupConsumerActivations(
434 {
435 name: current.name ?? name,
436 updateMask: updateMask.join(","),
437 body: {
438 name: current.name ?? name,
439 labels: desiredLabels,
440 description: news.description,
441 logConfig,
442 },
443 },
444 );
445 yield* waitForOperation(operation);
446 current = yield* waitUntilPresent(
447 getByName(current.name ?? name),
448 current.name ?? name,
449 );
450 }
451
452 return toAttrs(current, env.project);
453 }),
454
455 delete: Effect.fn(function* ({ output }) {
456 const operation = yield* networkservices
457 .deleteProjectsLocationsMulticastGroupConsumerActivations({
458 name: output.name,
459 })
460 .pipe(
461 Effect.retry({
462 while: (error) => error._tag === "Conflict",
463 times: 8,
464 schedule: Schedule.spaced("2 seconds"),
465 }),
466 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
467 );
468 if (operation !== undefined) {
469 yield* waitForOperation(operation, { notFoundOk: true });
470 }
471 yield* waitUntilGone(getByName(output.name), output.name);
472 }),
473 });