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 { GcpEnvironment } from "../Environment.ts";
9import { alchemyLabelKeys, createInternalLabels } from "../Labels.ts";
10import type { Providers } from "../Providers.ts";
11import {
12 collectPages,
13 normalizeLocation,
14 parentOf,
15 parseName,
16 resourceName as qualifiedName,
17 rfc1035,
18 toPhysicalId,
19 waitForOperation,
20 waitUntilGone,
21 waitUntilPresent,
22 waitUntilReady,
23} from "./internal.ts";
24
25const COLLECTION = "networkMonitoringProviders";
26const DEFAULT_PROVIDER_TYPE =
27 "EXTERNAL" satisfies networkmanagement.NetworkMonitoringProviderProviderTypeEnum;
28
29export type NetworkMonitoringProviderType =
30 | networkmanagement.NetworkMonitoringProviderProviderTypeEnum
31 | (string & {});
32
33export type NetworkMonitoringProviderProps = {
34 /**
35 * Provider id (the `{network_monitoring_provider}` segment of
36 * `projects/{project}/locations/{location}/networkMonitoringProviders/{network_monitoring_provider}`).
37 * If omitted, a unique RFC1035 name is generated. Immutable — changing
38 * it replaces the provider.
39 */
40 networkMonitoringProviderId?: string;
41 /**
42 * Location. Network monitoring providers are only served from `global`
43 * (regional names are rejected as malformed). Immutable — changing it
44 * replaces the provider.
45 * @default "global"
46 */
47 location?: string;
48 /**
49 * Provider type. The API currently supports `EXTERNAL`. Immutable —
50 * changing it replaces the provider.
51 * @default "EXTERNAL"
52 */
53 providerType?: NetworkMonitoringProviderType;
54};
55
56export type NetworkMonitoringProvider = Resource<
57 "GCP.NetworkManagement.NetworkMonitoringProvider",
58 NetworkMonitoringProviderProps,
59 {
60 /** Full resource name `projects/{project}/locations/{location}/networkMonitoringProviders/{network_monitoring_provider}`. */
61 name: string;
62 /** Provider id (last path segment). */
63 networkMonitoringProviderId: string;
64 /** Project id. */
65 project: string;
66 /** Location id. */
67 location: string;
68 /** Provider type (`EXTERNAL`). */
69 providerType: string | undefined;
70 /** Link to the provider UI. */
71 providerUri: string | undefined;
72 /** Provider state (`ACTIVATING`, `ACTIVE`, …). */
73 state: string | undefined;
74 /** Server-reported error messages. */
75 errors: string[];
76 /** RFC3339 creation timestamp. */
77 createTime: string | undefined;
78 /** RFC3339 last-update timestamp. */
79 updateTime: string | undefined;
80 },
81 never,
82 Providers
83>;
84
85/**
86 * A Network Intelligence Center Network Monitoring Provider.
87 *
88 * The API has no labels, description, or patch method. Identity is the
89 * id, location, and provider type; changing any of them replaces the
90 * provider. Alchemy stamps ownership into the generated id so `list` /
91 * nuke can find leaked rows.
92 *
93 * ### Creating a NetworkMonitoringProvider
94 * **Example:** External provider
95 * ```typescript
96 * const provider = yield* GCP.NetworkManagement.NetworkMonitoringProvider(
97 * "AppNeta",
98 * { providerType: "EXTERNAL" },
99 * );
100 * ```
101 *
102 * **Example:** Named provider
103 * ```typescript
104 * const provider = yield* GCP.NetworkManagement.NetworkMonitoringProvider(
105 * "AppNeta",
106 * {
107 * networkMonitoringProviderId: "app-neta",
108 * location: "global",
109 * providerType: "EXTERNAL",
110 * },
111 * );
112 * ```
113 *
114 * @resource
115 * @category NetworkManagement
116 */
117export const NetworkMonitoringProvider = Resource<NetworkMonitoringProvider>(
118 "GCP.NetworkManagement.NetworkMonitoringProvider",
119);
120
121const resourceName = (
122 project: string,
123 location: string,
124 networkMonitoringProviderId: string,
125) => qualifiedName(project, location, COLLECTION, networkMonitoringProviderId);
126
127const typeOf = (value: string | undefined) =>
128 (value ?? DEFAULT_PROVIDER_TYPE).toUpperCase();
129
130const ownedPrefix = (labels: Record<string, string>) => {
131 const id = (labels[alchemyLabelKeys.id] ?? "x")
132 .toLowerCase()
133 .replace(/[^a-z0-9-]/g, "")
134 .slice(0, 8);
135 return `alch${id.length > 0 ? id : "x"}`;
136};
137
138const GLOBAL_LOCATION = "global";
139
140const isOwnedId = (id: string) => id.toLowerCase().startsWith("alch");
141
142const toOwnedId = (
143 id: string,
144 explicit: string | undefined,
145 existing: string | undefined,
146) =>
147 Effect.gen(function* () {
148 if (explicit !== undefined) {
149 return rfc1035(explicit, "nmp");
150 }
151 if (existing !== undefined) return existing;
152 const labels = yield* createInternalLabels(id);
153 const generated = yield* toPhysicalId(id, undefined, undefined, "nmp");
154 const prefix = ownedPrefix(labels);
155 if (generated.startsWith(prefix)) return generated;
156 return rfc1035(`${prefix}-${generated}`, "nmp");
157 });
158
159const toAttrs = (
160 provider: networkmanagement.NetworkMonitoringProvider,
161 project: string,
162 region: string,
163) => {
164 const name = provider.name ?? "";
165 const parsed = parseName(name, COLLECTION, region);
166 return {
167 name,
168 networkMonitoringProviderId: parsed.id,
169 project: parsed.project || project,
170 location: parsed.location || region,
171 providerType: provider.providerType,
172 providerUri: provider.providerUri,
173 state: provider.state,
174 errors: provider.errors ?? [],
175 createTime: provider.createTime,
176 updateTime: provider.updateTime,
177 };
178};
179
180const getByName = (name: string) =>
181 networkmanagement
182 .getProjectsLocationsNetworkMonitoringProviders({ name })
183 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
184
185export const NetworkMonitoringProviderProvider = () =>
186 Provider.succeed(NetworkMonitoringProvider, {
187 stables: [
188 "name",
189 "networkMonitoringProviderId",
190 "project",
191 "location",
192 "providerType",
193 "createTime",
194 ],
195
196 diff: Effect.fn(function* ({ news, olds, output }) {
197 if (!isResolved(news)) return undefined;
198 const previousId =
199 olds?.networkMonitoringProviderId ??
200 output?.networkMonitoringProviderId;
201 const nextId = news.networkMonitoringProviderId
202 ? rfc1035(news.networkMonitoringProviderId, "nmp")
203 : previousId;
204 const previousLocation = normalizeLocation(
205 olds?.location ?? output?.location,
206 GLOBAL_LOCATION,
207 );
208 const nextLocation = normalizeLocation(
209 news.location ?? olds?.location ?? output?.location,
210 GLOBAL_LOCATION,
211 );
212 const previousType = typeOf(olds?.providerType ?? output?.providerType);
213 const nextType = typeOf(
214 news.providerType ?? olds?.providerType ?? output?.providerType,
215 );
216 if (
217 (previousId !== undefined &&
218 nextId !== undefined &&
219 nextId !== previousId) ||
220 previousLocation !== nextLocation ||
221 previousType !== nextType
222 ) {
223 return { action: "replace" as const };
224 }
225 return undefined;
226 }),
227
228 read: Effect.fn(function* ({ id, olds, output }) {
229 const env = yield* GcpEnvironment.current;
230 const networkMonitoringProviderId = yield* toOwnedId(
231 id,
232 olds?.networkMonitoringProviderId,
233 output?.networkMonitoringProviderId,
234 );
235 const location = normalizeLocation(
236 olds?.location ?? output?.location,
237 GLOBAL_LOCATION,
238 );
239 const name =
240 output?.name ??
241 resourceName(env.project, location, networkMonitoringProviderId);
242 const existing = yield* getByName(name);
243 if (existing === undefined) return undefined;
244 const attrs = toAttrs(existing, env.project, GLOBAL_LOCATION);
245 return isOwnedId(attrs.networkMonitoringProviderId) ||
246 olds?.networkMonitoringProviderId !== undefined ||
247 output?.networkMonitoringProviderId !== undefined
248 ? attrs
249 : Unowned(attrs);
250 }),
251
252 list: () =>
253 Effect.gen(function* () {
254 const env = yield* GcpEnvironment.current;
255 const listed: NetworkMonitoringProvider["Attributes"][] = [];
256 const seen = new Set<string>();
257 const parents = [
258 parentOf(env.project, "-"),
259 ...[GLOBAL_LOCATION].map((location) =>
260 parentOf(env.project, location),
261 ),
262 ];
263 for (const parent of parents) {
264 const items = yield* collectPages(
265 networkmanagement.listProjectsLocationsNetworkMonitoringProviders.pages(
266 {
267 parent,
268 pageSize: 1000,
269 },
270 ),
271 (page) => page.networkMonitoringProviders,
272 );
273 for (const item of items) {
274 const attrs = toAttrs(item, env.project, GLOBAL_LOCATION);
275 if (!isOwnedId(attrs.networkMonitoringProviderId)) continue;
276 if (seen.has(attrs.name)) continue;
277 seen.add(attrs.name);
278 listed.push(attrs);
279 }
280 }
281 return listed;
282 }),
283
284 reconcile: Effect.fn(function* ({ id, news, output }) {
285 const env = yield* GcpEnvironment.current;
286 const networkMonitoringProviderId = yield* toOwnedId(
287 id,
288 news.networkMonitoringProviderId,
289 output?.networkMonitoringProviderId,
290 );
291 const location = normalizeLocation(
292 news.location ?? output?.location,
293 GLOBAL_LOCATION,
294 );
295 const name = resourceName(
296 env.project,
297 location,
298 networkMonitoringProviderId,
299 );
300 const providerType = typeOf(news.providerType);
301
302 let current = yield* getByName(name);
303
304 if (current === undefined) {
305 const created = yield* networkmanagement
306 .createProjectsLocationsNetworkMonitoringProviders({
307 parent: parentOf(env.project, location),
308 networkMonitoringProviderId,
309 body: { providerType },
310 })
311 .pipe(
312 Effect.retry({
313 while: (error) => error._tag === "Conflict",
314 times: 5,
315 schedule: Schedule.spaced("2 seconds"),
316 }),
317 Effect.catchTag("Conflict", () => Effect.succeed(undefined)),
318 );
319 if (created !== undefined) {
320 yield* waitForOperation(created);
321 }
322 current = yield* waitUntilPresent(getByName(name), name);
323 }
324
325 current = yield* waitUntilReady(
326 getByName(current.name ?? name),
327 current.name ?? name,
328 );
329
330 return toAttrs(current, env.project, GLOBAL_LOCATION);
331 }),
332
333 delete: Effect.fn(function* ({ output }) {
334 const operation = yield* networkmanagement
335 .deleteProjectsLocationsNetworkMonitoringProviders({
336 name: output.name,
337 })
338 .pipe(
339 Effect.retry({
340 while: (error) => error._tag === "Conflict",
341 times: 8,
342 schedule: Schedule.spaced("2 seconds"),
343 }),
344 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
345 );
346 if (operation !== undefined) {
347 yield* waitForOperation(operation, { notFoundOk: true });
348 }
349 yield* waitUntilGone(getByName(output.name), output.name);
350 }),
351 });