1import * as networkmanagement from "@distilled.cloud/gcp/networkmanagement_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_GLOBAL,
19 changedFields,
20 collectPages,
21 hasAlchemyLabelKeys,
22 normalizeLocation,
23 parentOf,
24 parseName,
25 resourceName as qualifiedName,
26 rfc1035,
27 sameStringList,
28 toNetworkResource,
29 toPhysicalId,
30 toRegionalComputeResource,
31 toSubnetworkResource,
32 userLabels,
33 waitForOperation,
34 waitUntilGone,
35 waitUntilPresent,
36} from "./internal.ts";
37
38const COLLECTION = "vpcFlowLogsConfigs";
39const DEFAULT_STATE =
40 "ENABLED" satisfies networkmanagement.VpcFlowLogsConfigStateEnum;
41const DEFAULT_INTERVAL =
42 "INTERVAL_5_SEC" satisfies networkmanagement.VpcFlowLogsConfigAggregationIntervalEnum;
43const DEFAULT_METADATA =
44 "INCLUDE_ALL_METADATA" satisfies networkmanagement.VpcFlowLogsConfigMetadataEnum;
45const DEFAULT_SAMPLING = 1;
46
47export type VpcFlowLogsConfigState =
48 | networkmanagement.VpcFlowLogsConfigStateEnum
49 | (string & {});
50export type VpcFlowLogsConfigAggregationInterval =
51 | networkmanagement.VpcFlowLogsConfigAggregationIntervalEnum
52 | (string & {});
53export type VpcFlowLogsConfigMetadata =
54 | networkmanagement.VpcFlowLogsConfigMetadataEnum
55 | (string & {});
56
57export type VpcFlowLogsConfigProps = {
58 /**
59 * Config id (the `{vpc_flow_logs_config}` segment of
60 * `projects/{project}/locations/global/vpcFlowLogsConfigs/{vpc_flow_logs_config}`).
61 * If omitted, a unique RFC1035 name is generated. Immutable — changing
62 * it replaces the config.
63 */
64 vpcFlowLogsConfigId?: string;
65 /**
66 * Location. Must be `global`. Immutable — changing it replaces the
67 * config.
68 * @default "global"
69 */
70 location?: string;
71 /**
72 * Human-readable description (max 512 characters).
73 */
74 description?: string;
75 /**
76 * Whether log generation is enabled. Create requires `ENABLED`; a
77 * subsequent update may set `DISABLED` to pause generation.
78 * @default "ENABLED"
79 */
80 state?: VpcFlowLogsConfigState;
81 /**
82 * Log aggregation interval.
83 * @default "INTERVAL_5_SEC"
84 */
85 aggregationInterval?: VpcFlowLogsConfigAggregationInterval;
86 /**
87 * Sampling rate in (0, 1]. Use `state` to disable logs rather than
88 * setting this to 0.
89 * @default 1
90 */
91 flowSampling?: number;
92 /**
93 * Which metadata fields to include in reported logs.
94 * @default "INCLUDE_ALL_METADATA"
95 */
96 metadata?: VpcFlowLogsConfigMetadata;
97 /**
98 * Custom metadata fields. Only valid when `metadata` is
99 * `CUSTOM_METADATA`.
100 */
101 metadataFields?: string[];
102 /**
103 * CEL export filter selecting which VPC Flow Logs are recorded.
104 */
105 filterExpr?: string;
106 /**
107 * VPC network to log. Format
108 * `projects/{project}/global/networks/{name}` or a Compute self-link.
109 * Mutually exclusive with `subnet`, `interconnectAttachment`, and
110 * `vpnTunnel`. Changing the target resource replaces the config.
111 */
112 network?: string;
113 /**
114 * Subnetwork to log. Format
115 * `projects/{project}/regions/{region}/subnetworks/{name}` or a Compute
116 * self-link. Mutually exclusive with the other target fields. Changing
117 * the target resource replaces the config.
118 */
119 subnet?: string;
120 /**
121 * Interconnect attachment to log. Format
122 * `projects/{project}/regions/{region}/interconnectAttachments/{name}`.
123 * Mutually exclusive with the other target fields. Changing the target
124 * resource replaces the config.
125 */
126 interconnectAttachment?: string;
127 /**
128 * VPN tunnel to log. Format
129 * `projects/{project}/regions/{region}/vpnTunnels/{name}`. Mutually
130 * exclusive with the other target fields. Changing the target resource
131 * replaces the config.
132 */
133 vpnTunnel?: string;
134 /**
135 * User labels. Alchemy ownership labels are merged in automatically.
136 */
137 labels?: Record<string, string>;
138};
139
140export type VpcFlowLogsConfig = Resource<
141 "GCP.NetworkManagement.VpcFlowLogsConfig",
142 VpcFlowLogsConfigProps,
143 {
144 /** Full resource name `projects/{project}/locations/global/vpcFlowLogsConfigs/{vpc_flow_logs_config}`. */
145 name: string;
146 /** Config id (last path segment). */
147 vpcFlowLogsConfigId: string;
148 /** Project id. */
149 project: string;
150 /** Location id (`global`). */
151 location: string;
152 /** User-provided description. */
153 description: string | undefined;
154 /** Whether log generation is enabled. */
155 state: string | undefined;
156 /** Aggregation interval. */
157 aggregationInterval: string | undefined;
158 /** Sampling rate. */
159 flowSampling: number | undefined;
160 /** Metadata inclusion mode. */
161 metadata: string | undefined;
162 /** Custom metadata fields. */
163 metadataFields: string[];
164 /** CEL export filter. */
165 filterExpr: string | undefined;
166 /** Diagnostic state of the configured target resource. */
167 targetResourceState: string | undefined;
168 /** Target VPC network, if set. */
169 network: string | undefined;
170 /** Target subnetwork, if set. */
171 subnet: string | undefined;
172 /** Target interconnect attachment, if set. */
173 interconnectAttachment: string | undefined;
174 /** Target VPN tunnel, if set. */
175 vpnTunnel: string | undefined;
176 /** User labels (Alchemy ownership labels stripped). */
177 labels: Record<string, string>;
178 /** RFC3339 creation timestamp. */
179 createTime: string | undefined;
180 /** RFC3339 last-update timestamp. */
181 updateTime: string | undefined;
182 },
183 never,
184 Providers
185>;
186
187/**
188 * A project-level VPC Flow Logs configuration.
189 *
190 * Project configs require exactly one target (`network`, `subnet`,
191 * `interconnectAttachment`, or `vpnTunnel`). Changing the id, location,
192 * or target resource replaces the config. Description, labels, state,
193 * sampling, metadata, and the export filter update in place. Creating
194 * with `state=DISABLED` is rejected by the API, so Alchemy creates
195 * `ENABLED` and then patches.
196 *
197 * ### Creating a VpcFlowLogsConfig
198 * **Example:** Log an entire VPC
199 * ```typescript
200 * const network = yield* GCP.Compute.Network("AppVpc", {
201 * autoCreateSubnetworks: false,
202 * });
203 * const logs = yield* GCP.NetworkManagement.VpcFlowLogsConfig("VpcLogs", {
204 * network: network.selfLink ?? network.networkName,
205 * labels: { env: "prod" },
206 * });
207 * ```
208 *
209 * **Example:** Named config with sampling
210 * ```typescript
211 * const logs = yield* GCP.NetworkManagement.VpcFlowLogsConfig("VpcLogs", {
212 * vpcFlowLogsConfigId: "app-vpc-logs",
213 * network: "projects/my-project/global/networks/app-vpc",
214 * aggregationInterval: "INTERVAL_1_MIN",
215 * flowSampling: 0.5,
216 * description: "app vpc flow logs",
217 * });
218 * ```
219 *
220 * @resource
221 * @category NetworkManagement
222 */
223export const VpcFlowLogsConfig = Resource<VpcFlowLogsConfig>(
224 "GCP.NetworkManagement.VpcFlowLogsConfig",
225);
226
227const resourceName = (
228 project: string,
229 location: string,
230 vpcFlowLogsConfigId: string,
231) => qualifiedName(project, location, COLLECTION, vpcFlowLogsConfigId);
232
233const stateOf = (value: string | undefined) =>
234 (value ?? DEFAULT_STATE).toUpperCase();
235
236const intervalOf = (value: string | undefined) =>
237 (value ?? DEFAULT_INTERVAL).toUpperCase();
238
239const metadataOf = (value: string | undefined) =>
240 (value ?? DEFAULT_METADATA).toUpperCase();
241
242const samplingOf = (value: number | undefined) => value ?? DEFAULT_SAMPLING;
243
244const targetKey = (props: {
245 network?: string;
246 subnet?: string;
247 interconnectAttachment?: string;
248 vpnTunnel?: string;
249}) =>
250 JSON.stringify({
251 network: lastSegmentOf(props.network),
252 subnet: lastSegmentOf(props.subnet),
253 interconnectAttachment: lastSegmentOf(props.interconnectAttachment),
254 vpnTunnel: lastSegmentOf(props.vpnTunnel),
255 });
256
257const lastSegmentOf = (value: string | undefined) =>
258 (value ?? "")
259 .replace(/^https?:\/\/[^/]+\//, "")
260 .replace(/\/+$/, "")
261 .split("/")
262 .pop()
263 ?.toLowerCase() ?? "";
264
265const toTarget = (
266 project: string,
267 region: string,
268 news: {
269 network?: string;
270 subnet?: string;
271 interconnectAttachment?: string;
272 vpnTunnel?: string;
273 },
274) => ({
275 network:
276 news.network !== undefined
277 ? toNetworkResource(project, news.network)
278 : undefined,
279 subnet:
280 news.subnet !== undefined
281 ? toSubnetworkResource(project, region, news.subnet)
282 : undefined,
283 interconnectAttachment:
284 news.interconnectAttachment !== undefined
285 ? toRegionalComputeResource(
286 project,
287 region,
288 "interconnectAttachments",
289 news.interconnectAttachment,
290 )
291 : undefined,
292 vpnTunnel:
293 news.vpnTunnel !== undefined
294 ? toRegionalComputeResource(project, region, "vpnTunnels", news.vpnTunnel)
295 : undefined,
296});
297
298const toAttrs = (
299 config: networkmanagement.VpcFlowLogsConfig,
300 project: string,
301) => {
302 const name = config.name ?? "";
303 const parsed = parseName(name, COLLECTION, DEFAULT_GLOBAL);
304 return {
305 name,
306 vpcFlowLogsConfigId: parsed.id,
307 project: parsed.project || project,
308 location: parsed.location || DEFAULT_GLOBAL,
309 description: config.description,
310 state: config.state,
311 aggregationInterval: config.aggregationInterval,
312 flowSampling: config.flowSampling,
313 metadata: config.metadata,
314 metadataFields: config.metadataFields ?? [],
315 filterExpr: config.filterExpr,
316 targetResourceState: config.targetResourceState,
317 network: config.network,
318 subnet: config.subnet,
319 interconnectAttachment: config.interconnectAttachment,
320 vpnTunnel: config.vpnTunnel,
321 labels: userLabels(config.labels),
322 createTime: config.createTime,
323 updateTime: config.updateTime,
324 };
325};
326
327const getByName = (name: string) =>
328 networkmanagement
329 .getProjectsLocationsVpcFlowLogsConfigs({ name })
330 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
331
332export const VpcFlowLogsConfigProvider = () =>
333 Provider.succeed(VpcFlowLogsConfig, {
334 stables: [
335 "name",
336 "vpcFlowLogsConfigId",
337 "project",
338 "location",
339 "createTime",
340 ],
341
342 diff: Effect.fn(function* ({ news, olds, output }) {
343 if (!isResolved(news)) return undefined;
344 const previousId =
345 olds?.vpcFlowLogsConfigId ?? output?.vpcFlowLogsConfigId;
346 const nextId = news.vpcFlowLogsConfigId
347 ? rfc1035(news.vpcFlowLogsConfigId, "vpc-flow-logs")
348 : previousId;
349 const previousLocation = normalizeLocation(
350 olds?.location ?? output?.location,
351 DEFAULT_GLOBAL,
352 );
353 const nextLocation = normalizeLocation(
354 news.location ?? olds?.location ?? output?.location,
355 DEFAULT_GLOBAL,
356 );
357 const previousTarget = targetKey({
358 network: olds?.network ?? output?.network,
359 subnet: olds?.subnet ?? output?.subnet,
360 interconnectAttachment:
361 olds?.interconnectAttachment ?? output?.interconnectAttachment,
362 vpnTunnel: olds?.vpnTunnel ?? output?.vpnTunnel,
363 });
364 const nextTarget = targetKey({
365 network: news.network,
366 subnet: news.subnet,
367 interconnectAttachment: news.interconnectAttachment,
368 vpnTunnel: news.vpnTunnel,
369 });
370 if (
371 (previousId !== undefined &&
372 nextId !== undefined &&
373 nextId !== previousId) ||
374 previousLocation !== nextLocation ||
375 previousTarget !== nextTarget
376 ) {
377 return { action: "replace" as const };
378 }
379 return undefined;
380 }),
381
382 read: Effect.fn(function* ({ id, olds, output }) {
383 const env = yield* GcpEnvironment.current;
384 const vpcFlowLogsConfigId = yield* toPhysicalId(
385 id,
386 olds?.vpcFlowLogsConfigId,
387 output?.vpcFlowLogsConfigId,
388 "vpc-flow-logs",
389 );
390 const location = normalizeLocation(
391 olds?.location ?? output?.location,
392 DEFAULT_GLOBAL,
393 );
394 const name =
395 output?.name ??
396 resourceName(env.project, location, vpcFlowLogsConfigId);
397 const existing = yield* getByName(name);
398 if (existing === undefined) return undefined;
399 const attrs = toAttrs(existing, env.project);
400 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
401 ? attrs
402 : Unowned(attrs);
403 }),
404
405 list: () =>
406 Effect.gen(function* () {
407 const env = yield* GcpEnvironment.current;
408 const items = yield* collectPages(
409 networkmanagement.listProjectsLocationsVpcFlowLogsConfigs.pages({
410 parent: parentOf(env.project, DEFAULT_GLOBAL),
411 pageSize: 1000,
412 }),
413 (page) => page.vpcFlowLogsConfigs,
414 );
415 return items
416 .filter((item) => hasAlchemyLabelKeys(item.labels))
417 .map((item) => toAttrs(item, env.project));
418 }),
419
420 reconcile: Effect.fn(function* ({ id, news, output }) {
421 const env = yield* GcpEnvironment.current;
422 const vpcFlowLogsConfigId = yield* toPhysicalId(
423 id,
424 news.vpcFlowLogsConfigId,
425 output?.vpcFlowLogsConfigId,
426 "vpc-flow-logs",
427 );
428 const location = normalizeLocation(
429 news.location ?? output?.location,
430 DEFAULT_GLOBAL,
431 );
432 const name = resourceName(env.project, location, vpcFlowLogsConfigId);
433 const desiredLabels = {
434 ...toLabels(news.labels),
435 ...(yield* createInternalLabels(id)),
436 };
437 const target = toTarget(env.project, env.region, news);
438 const state = stateOf(news.state);
439 const createState = state === "DISABLED" ? DEFAULT_STATE : state;
440 const aggregationInterval = intervalOf(news.aggregationInterval);
441 const flowSampling = samplingOf(news.flowSampling);
442 const metadata = metadataOf(news.metadata);
443 const metadataFields = news.metadataFields ?? [];
444 const filterExpr = news.filterExpr;
445
446 let current = yield* getByName(name);
447
448 if (current === undefined) {
449 const created = yield* networkmanagement
450 .createProjectsLocationsVpcFlowLogsConfigs({
451 parent: parentOf(env.project, location),
452 vpcFlowLogsConfigId,
453 body: {
454 description: news.description,
455 labels: desiredLabels,
456 state: createState,
457 aggregationInterval,
458 flowSampling,
459 metadata,
460 metadataFields:
461 metadataFields.length > 0 ? metadataFields : undefined,
462 filterExpr,
463 ...target,
464 },
465 })
466 .pipe(
467 Effect.retry({
468 while: (error) => error._tag === "Conflict",
469 times: 5,
470 schedule: Schedule.spaced("2 seconds"),
471 }),
472 Effect.catchTag("Conflict", () => Effect.succeed(undefined)),
473 );
474 if (created !== undefined) {
475 yield* waitForOperation(created);
476 }
477 current = yield* waitUntilPresent(getByName(name), name);
478 }
479
480 const observedLabels = tagRecord(current.labels);
481 const { upsert, removed } = diffLabels(observedLabels, desiredLabels);
482 const labelsChanged = upsert.length > 0 || removed.length > 0;
483 const descriptionChanged =
484 (current.description ?? "") !== (news.description ?? "");
485 const stateChanged = (current.state ?? DEFAULT_STATE) !== state;
486 const intervalChanged =
487 (current.aggregationInterval ?? DEFAULT_INTERVAL) !==
488 aggregationInterval;
489 const samplingChanged =
490 (current.flowSampling ?? DEFAULT_SAMPLING) !== flowSampling;
491 const metadataChanged =
492 (current.metadata ?? DEFAULT_METADATA) !== metadata;
493 const metadataFieldsChanged = !sameStringList(
494 current.metadataFields,
495 metadataFields,
496 );
497 const filterChanged = (current.filterExpr ?? "") !== (filterExpr ?? "");
498
499 const updateMask = changedFields([
500 ["labels", labelsChanged],
501 ["description", descriptionChanged],
502 ["state", stateChanged],
503 ["aggregationInterval", intervalChanged],
504 ["flowSampling", samplingChanged],
505 ["metadata", metadataChanged],
506 ["metadataFields", metadataFieldsChanged],
507 ["filterExpr", filterChanged],
508 ]);
509
510 if (updateMask.length > 0) {
511 const operation =
512 yield* networkmanagement.patchProjectsLocationsVpcFlowLogsConfigs({
513 name: current.name ?? name,
514 updateMask: updateMask.join(","),
515 body: {
516 name: current.name ?? name,
517 labels: desiredLabels,
518 description: news.description,
519 state,
520 aggregationInterval,
521 flowSampling,
522 metadata,
523 metadataFields,
524 filterExpr,
525 },
526 });
527 yield* waitForOperation(operation);
528 current = yield* waitUntilPresent(
529 getByName(current.name ?? name),
530 current.name ?? name,
531 );
532 }
533
534 return toAttrs(current, env.project);
535 }),
536
537 delete: Effect.fn(function* ({ output }) {
538 const operation = yield* networkmanagement
539 .deleteProjectsLocationsVpcFlowLogsConfigs({ name: output.name })
540 .pipe(
541 Effect.retry({
542 while: (error) => error._tag === "Conflict",
543 times: 8,
544 schedule: Schedule.spaced("2 seconds"),
545 }),
546 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
547 );
548 if (operation !== undefined) {
549 yield* waitForOperation(operation, { notFoundOk: true });
550 }
551 yield* waitUntilGone(getByName(output.name), output.name);
552 }),
553 });