1import * as netapp from "@distilled.cloud/gcp/netapp_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 expandParent,
19 fieldMask,
20 gibOf,
21 listAtLocation,
22 listLabeledPages,
23 networkName,
24 normalizeLocation,
25 parentOf,
26 parseName,
27 replaceOnIdentity,
28 ResourceNotResolved,
29 toPhysicalId,
30 userLabels,
31 waitForDeleteOperation,
32 waitForOperation,
33 waitUntilExists,
34 waitUntilGone,
35 waitUntilReady,
36} from "./internal.ts";
37
38const DEFAULT_SERVICE_LEVEL: netapp.StoragePoolServiceLevelEnum = "STANDARD";
39const DEFAULT_CAPACITY_GIB = 2048;
40
41export type StoragePoolProps = {
42 /**
43 * Storage pool id (the `{storagePool}` segment of
44 * `projects/{project}/locations/{location}/storagePools/{storagePool}`).
45 * If omitted, a unique RFC1035 name is generated from the stack, stage,
46 * and logical id. Immutable — changing it replaces the pool.
47 */
48 storagePoolId?: string;
49 /**
50 * Region (`us-central1`, `us-east1`, …). Immutable — changing it
51 * replaces the pool. `US-CENTRAL1` is accepted and normalized to
52 * `us-central1`.
53 * @default the stack's GCP region (`GCP.Region`, profile region, `us-central1`)
54 */
55 location?: string;
56 /**
57 * VPC network name (`default`) or path
58 * `projects/{project}/global/networks/{network}`. Immutable — changing
59 * it replaces the pool.
60 * @default "default"
61 */
62 network?: string;
63 /**
64 * Service level. Immutable — changing it replaces the pool.
65 * @default "STANDARD"
66 */
67 serviceLevel?: netapp.StoragePoolServiceLevelEnum | (string & {});
68 /**
69 * Pool capacity in GiB. STANDARD/PREMIUM/EXTREME minimum is 2048.
70 * @default 2048
71 */
72 capacityGib?: number | string;
73 /**
74 * Active zone for a regional Flex pool. Immutable — changing it
75 * replaces the pool.
76 */
77 zone?: string;
78 /**
79 * Replica zone for a regional Flex pool. Immutable — changing it
80 * replaces the pool.
81 */
82 replicaZone?: string;
83 /**
84 * KMS config used to encrypt volumes. Full name or id. Immutable —
85 * changing it replaces the pool.
86 */
87 kmsConfig?: string;
88 /**
89 * Active Directory used for SMB volumes. Full name or id.
90 */
91 activeDirectory?: string;
92 /**
93 * Enable NFS LDAP. Immutable — changing it replaces the pool.
94 */
95 ldapEnabled?: boolean;
96 /**
97 * Allow auto-tiering on volumes. Can be enabled after create but not
98 * disabled.
99 */
100 allowAutoTiering?: boolean;
101 /**
102 * Pool type. Immutable — changing it replaces the pool.
103 */
104 type?: netapp.StoragePoolTypeEnum | (string & {});
105 /**
106 * Pool mode. Immutable — changing it replaces the pool.
107 */
108 mode?: netapp.StoragePoolModeEnum | (string & {});
109 /**
110 * QoS type.
111 */
112 qosType?: netapp.StoragePoolQosTypeEnum | (string & {});
113 /**
114 * Enable independent scaling of capacity and performance. Immutable —
115 * changing it replaces the pool.
116 */
117 customPerformanceEnabled?: boolean;
118 /**
119 * Custom total throughput in MiB/s.
120 */
121 totalThroughputMibps?: number | string;
122 /**
123 * Custom total IOPS.
124 */
125 totalIops?: number | string;
126 /**
127 * Hot-tier size in GiB (Flex only).
128 */
129 hotTierSizeGib?: number | string;
130 /**
131 * Auto-increase hot-tier by 10% when it hits 100%.
132 */
133 enableHotTierAutoResize?: boolean;
134 /**
135 * Scale type. Immutable — changing it replaces the pool.
136 */
137 scaleType?: netapp.StoragePoolScaleTypeEnum | (string & {});
138 /**
139 * Human-readable description.
140 */
141 description?: string;
142 /**
143 * User labels. Alchemy ownership labels are merged in automatically.
144 */
145 labels?: Record<string, string>;
146};
147
148export type StoragePool = Resource<
149 "GCP.NetApp.StoragePool",
150 StoragePoolProps,
151 {
152 /** Full resource name. */
153 name: string;
154 /** Storage pool id (last path segment). */
155 storagePoolId: string;
156 /** Project id. */
157 project: string;
158 /** Location id. */
159 location: string;
160 /** VPC network path. */
161 network: string | undefined;
162 /** Service level. */
163 serviceLevel: string | undefined;
164 /** Capacity in GiB. */
165 capacityGib: string | undefined;
166 /** Allocated volume capacity in GiB. */
167 volumeCapacityGib: string | undefined;
168 /** Number of volumes. */
169 volumeCount: number | undefined;
170 /** Active zone. */
171 zone: string | undefined;
172 /** Replica zone. */
173 replicaZone: string | undefined;
174 /** KMS config name. */
175 kmsConfig: string | undefined;
176 /** Active Directory name. */
177 activeDirectory: string | undefined;
178 /** Whether NFS LDAP is enabled. */
179 ldapEnabled: boolean | undefined;
180 /** Whether auto-tiering is allowed. */
181 allowAutoTiering: boolean | undefined;
182 /** Pool type. */
183 type: string | undefined;
184 /** Pool mode. */
185 mode: string | undefined;
186 /** QoS type. */
187 qosType: string | undefined;
188 /** Whether custom performance is enabled. */
189 customPerformanceEnabled: boolean | undefined;
190 /** Total throughput in MiB/s. */
191 totalThroughputMibps: string | undefined;
192 /** Total IOPS. */
193 totalIops: string | undefined;
194 /** Hot-tier size in GiB. */
195 hotTierSizeGib: string | undefined;
196 /** Whether hot-tier auto-resize is enabled. */
197 enableHotTierAutoResize: boolean | undefined;
198 /** Available throughput in MiB/s. */
199 availableThroughputMibps: number | undefined;
200 /** Cold-tier usage in GiB. */
201 coldTierSizeUsedGib: string | undefined;
202 /** Hot-tier usage in GiB. */
203 hotTierSizeUsedGib: string | undefined;
204 /** Encryption key source. */
205 encryptionType: string | undefined;
206 /** Scale type. */
207 scaleType: string | undefined;
208 /** Human-readable description. */
209 description: string | undefined;
210 /** User labels (Alchemy ownership labels stripped). */
211 labels: Record<string, string>;
212 /** Server-reported state. */
213 state: string | undefined;
214 /** State details. */
215 stateDetails: string | undefined;
216 /** RFC3339 creation timestamp. */
217 createTime: string | undefined;
218 },
219 never,
220 Providers
221>;
222
223/**
224 * A Cloud NetApp Volumes storage pool — billed capacity that hosts
225 * volumes at a service level.
226 *
227 * Changing `storagePoolId`, `location`, `network`, `serviceLevel`,
228 * `zone`, `replicaZone`, `kmsConfig`, `ldapEnabled`, `type`, `mode`,
229 * `customPerformanceEnabled`, or `scaleType` replaces the pool. Capacity,
230 * description, labels, QoS, and auto-tiering update in place.
231 *
232 * ### Creating a Storage Pool
233 * **Example:** Generated name
234 * ```typescript
235 * const pool = yield* GCP.NetApp.StoragePool("Pool", {
236 * network: "default",
237 * serviceLevel: "STANDARD",
238 * capacityGib: 2048,
239 * });
240 * ```
241 *
242 * **Example:** Explicit id and labels
243 * ```typescript
244 * const pool = yield* GCP.NetApp.StoragePool("Pool", {
245 * storagePoolId: "app-pool",
246 * network: "default",
247 * serviceLevel: "PREMIUM",
248 * capacityGib: 4096,
249 * description: "app volumes",
250 * labels: { env: "prod" },
251 * });
252 * ```
253 *
254 * ### Updating a Storage Pool
255 * **Example:** Capacity and labels
256 * ```typescript
257 * const pool = yield* GCP.NetApp.StoragePool("Pool", {
258 * network: "default",
259 * serviceLevel: "STANDARD",
260 * capacityGib: 4096,
261 * description: "app volumes v2",
262 * labels: { env: "prod", team: "storage" },
263 * });
264 * ```
265 *
266 * @resource
267 * @category NetApp
268 */
269export const StoragePool = Resource<StoragePool>("GCP.NetApp.StoragePool");
270
271const resourceName = (
272 project: string,
273 location: string,
274 storagePoolId: string,
275) => `projects/${project}/locations/${location}/storagePools/${storagePoolId}`;
276
277const toAttrs = (item: netapp.StoragePool, project: string, region: string) => {
278 const name = item.name ?? "";
279 const parsed = parseName(name, "storagePools", region);
280 return {
281 name,
282 storagePoolId: parsed.id,
283 project: parsed.project || project,
284 location: parsed.location,
285 network: item.network,
286 serviceLevel: item.serviceLevel,
287 capacityGib: item.capacityGib,
288 volumeCapacityGib: item.volumeCapacityGib,
289 volumeCount: item.volumeCount,
290 zone: item.zone,
291 replicaZone: item.replicaZone,
292 kmsConfig: item.kmsConfig,
293 activeDirectory: item.activeDirectory,
294 ldapEnabled: item.ldapEnabled,
295 allowAutoTiering: item.allowAutoTiering,
296 type: item.type,
297 mode: item.mode,
298 qosType: item.qosType,
299 customPerformanceEnabled: item.customPerformanceEnabled,
300 totalThroughputMibps: item.totalThroughputMibps,
301 totalIops: item.totalIops,
302 hotTierSizeGib: item.hotTierSizeGib,
303 enableHotTierAutoResize: item.enableHotTierAutoResize,
304 availableThroughputMibps: item.availableThroughputMibps,
305 coldTierSizeUsedGib: item.coldTierSizeUsedGib,
306 hotTierSizeUsedGib: item.hotTierSizeUsedGib,
307 encryptionType: item.encryptionType,
308 scaleType: item.scaleType,
309 description: item.description,
310 labels: userLabels(item.labels),
311 state: item.state,
312 stateDetails: item.stateDetails,
313 createTime: item.createTime,
314 };
315};
316
317const getByName = (name: string) =>
318 netapp
319 .getProjectsLocationsStoragePools({ name })
320 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
321
322const listOwned = (project: string) =>
323 listAtLocation(project, (parent) =>
324 listLabeledPages(
325 netapp.listProjectsLocationsStoragePools.pages({
326 parent,
327 pageSize: 1000,
328 }),
329 (page) => page.storagePools,
330 (item) => item.labels,
331 ),
332 );
333
334export const StoragePoolProvider = () =>
335 Provider.succeed(StoragePool, {
336 stables: ["name", "storagePoolId", "project", "location", "createTime"],
337
338 diff: Effect.fn(function* ({ news, olds, output }) {
339 if (!isResolved(news)) return undefined;
340 const env = yield* GcpEnvironment.current;
341 const previousNetwork = olds?.network ?? output?.network;
342 const previousLevel = olds?.serviceLevel ?? output?.serviceLevel;
343 const previousZone = olds?.zone ?? output?.zone;
344 const previousReplica = olds?.replicaZone ?? output?.replicaZone;
345 const previousKms = olds?.kmsConfig ?? output?.kmsConfig;
346 const previousLdap = olds?.ldapEnabled ?? output?.ldapEnabled;
347 const previousType = olds?.type ?? output?.type;
348 const previousMode = olds?.mode ?? output?.mode;
349 const previousCustom =
350 olds?.customPerformanceEnabled ?? output?.customPerformanceEnabled;
351 const previousScale = olds?.scaleType ?? output?.scaleType;
352 return replaceOnIdentity({
353 previousId: olds?.storagePoolId ?? output?.storagePoolId,
354 nextId:
355 news.storagePoolId ?? olds?.storagePoolId ?? output?.storagePoolId,
356 previousLocation: normalizeLocation(
357 olds?.location ?? output?.location,
358 env.region,
359 ),
360 nextLocation: normalizeLocation(
361 news.location ?? olds?.location ?? output?.location,
362 env.region,
363 ),
364 extra:
365 (previousNetwork !== undefined &&
366 news.network !== undefined &&
367 news.network !== previousNetwork &&
368 !previousNetwork.endsWith(`/${news.network}`)) ||
369 (previousLevel !== undefined &&
370 news.serviceLevel !== undefined &&
371 news.serviceLevel !== previousLevel) ||
372 (previousZone !== undefined &&
373 news.zone !== undefined &&
374 news.zone !== previousZone) ||
375 (previousReplica !== undefined &&
376 news.replicaZone !== undefined &&
377 news.replicaZone !== previousReplica) ||
378 (previousKms !== undefined &&
379 news.kmsConfig !== undefined &&
380 news.kmsConfig !== previousKms) ||
381 (previousLdap !== undefined &&
382 news.ldapEnabled !== undefined &&
383 news.ldapEnabled !== previousLdap) ||
384 (previousType !== undefined &&
385 news.type !== undefined &&
386 news.type !== previousType) ||
387 (previousMode !== undefined &&
388 news.mode !== undefined &&
389 news.mode !== previousMode) ||
390 (previousCustom !== undefined &&
391 news.customPerformanceEnabled !== undefined &&
392 news.customPerformanceEnabled !== previousCustom) ||
393 (previousScale !== undefined &&
394 news.scaleType !== undefined &&
395 news.scaleType !== previousScale),
396 });
397 }),
398
399 read: Effect.fn(function* ({ id, olds, output }) {
400 const env = yield* GcpEnvironment.current;
401 const storagePoolId = yield* toPhysicalId(
402 id,
403 olds?.storagePoolId,
404 output?.storagePoolId,
405 "storagepool",
406 );
407 const location = normalizeLocation(
408 olds?.location ?? output?.location,
409 env.region,
410 );
411 const name =
412 output?.name ?? resourceName(env.project, location, storagePoolId);
413 const existing = yield* getByName(name);
414 if (existing === undefined) return undefined;
415 const attrs = toAttrs(existing, env.project, env.region);
416 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
417 ? attrs
418 : Unowned(attrs);
419 }),
420
421 list: () =>
422 Effect.gen(function* () {
423 const env = yield* GcpEnvironment.current;
424 const items = yield* listOwned(env.project);
425 return items.map((item) => toAttrs(item, env.project, env.region));
426 }),
427
428 reconcile: Effect.fn(function* ({ id, news, output }) {
429 const env = yield* GcpEnvironment.current;
430 const storagePoolId = yield* toPhysicalId(
431 id,
432 news.storagePoolId,
433 output?.storagePoolId,
434 "storagepool",
435 );
436 const location = normalizeLocation(
437 news.location ?? output?.location,
438 env.region,
439 );
440 const name = resourceName(env.project, location, storagePoolId);
441 const network = networkName(env.project, news.network);
442 const serviceLevel = news.serviceLevel ?? DEFAULT_SERVICE_LEVEL;
443 const capacityGib = gibOf(news.capacityGib ?? DEFAULT_CAPACITY_GIB);
444 const kmsConfig =
445 news.kmsConfig === undefined
446 ? undefined
447 : expandParent(news.kmsConfig, env.project, location, "kmsConfigs");
448 const activeDirectory =
449 news.activeDirectory === undefined
450 ? undefined
451 : expandParent(
452 news.activeDirectory,
453 env.project,
454 location,
455 "activeDirectories",
456 );
457 const desiredLabels = {
458 ...toLabels(news.labels),
459 ...(yield* createInternalLabels(id)),
460 };
461
462 let current = yield* getByName(output?.name ?? name);
463
464 if (current === undefined) {
465 const created = yield* netapp
466 .createProjectsLocationsStoragePools({
467 parent: parentOf(env.project, location),
468 storagePoolId,
469 body: {
470 network,
471 serviceLevel,
472 capacityGib,
473 zone: news.zone,
474 replicaZone: news.replicaZone,
475 kmsConfig,
476 activeDirectory,
477 ldapEnabled: news.ldapEnabled,
478 allowAutoTiering: news.allowAutoTiering,
479 type: news.type,
480 mode: news.mode,
481 qosType: news.qosType,
482 customPerformanceEnabled: news.customPerformanceEnabled,
483 totalThroughputMibps: gibOf(news.totalThroughputMibps),
484 totalIops: gibOf(news.totalIops),
485 hotTierSizeGib: gibOf(news.hotTierSizeGib),
486 enableHotTierAutoResize: news.enableHotTierAutoResize,
487 scaleType: news.scaleType,
488 description: news.description,
489 labels: desiredLabels,
490 },
491 })
492 .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
493 if (created !== undefined) {
494 yield* waitForOperation(created);
495 }
496 current = yield* waitUntilExists(getByName(name), name);
497 }
498
499 if (current === undefined) {
500 return yield* new ResourceNotResolved({ name });
501 }
502
503 current = yield* waitUntilReady(
504 getByName(current.name ?? name),
505 current.name ?? name,
506 (item) => item.state,
507 (item) => item.stateDetails,
508 );
509
510 const observedLabels = tagRecord(current.labels);
511 const { upsert, removed } = diffLabels(observedLabels, desiredLabels);
512 const desiredAd = activeDirectory ?? current.activeDirectory;
513 const mask = fieldMask([
514 (upsert.length > 0 || removed.length > 0) && "labels",
515 (current.description ?? "") !== (news.description ?? "") &&
516 "description",
517 (current.capacityGib ?? "") !== (capacityGib ?? "") && "capacityGib",
518 (current.activeDirectory ?? "") !== (desiredAd ?? "") &&
519 "activeDirectory",
520 (current.allowAutoTiering ?? false) !==
521 (news.allowAutoTiering ?? current.allowAutoTiering ?? false) &&
522 "allowAutoTiering",
523 (current.qosType ?? "") !== (news.qosType ?? current.qosType ?? "") &&
524 "qosType",
525 (current.totalThroughputMibps ?? "") !==
526 (gibOf(news.totalThroughputMibps) ??
527 current.totalThroughputMibps ??
528 "") && "totalThroughputMibps",
529 (current.totalIops ?? "") !==
530 (gibOf(news.totalIops) ?? current.totalIops ?? "") && "totalIops",
531 (current.hotTierSizeGib ?? "") !==
532 (gibOf(news.hotTierSizeGib) ?? current.hotTierSizeGib ?? "") &&
533 "hotTierSizeGib",
534 (current.enableHotTierAutoResize ?? true) !==
535 (news.enableHotTierAutoResize ??
536 current.enableHotTierAutoResize ??
537 true) && "enableHotTierAutoResize",
538 ]);
539
540 if (mask.length > 0) {
541 const operation = yield* netapp.patchProjectsLocationsStoragePools({
542 name: current.name ?? name,
543 updateMask: mask,
544 body: {
545 name: current.name ?? name,
546 labels: desiredLabels,
547 description: news.description,
548 capacityGib,
549 activeDirectory: desiredAd,
550 allowAutoTiering: news.allowAutoTiering ?? current.allowAutoTiering,
551 qosType: news.qosType ?? current.qosType,
552 totalThroughputMibps:
553 gibOf(news.totalThroughputMibps) ?? current.totalThroughputMibps,
554 totalIops: gibOf(news.totalIops) ?? current.totalIops,
555 hotTierSizeGib:
556 gibOf(news.hotTierSizeGib) ?? current.hotTierSizeGib,
557 enableHotTierAutoResize:
558 news.enableHotTierAutoResize ?? current.enableHotTierAutoResize,
559 },
560 });
561 yield* waitForOperation(operation);
562 current = yield* waitUntilReady(
563 getByName(current.name ?? name),
564 current.name ?? name,
565 (item) => item.state,
566 (item) => item.stateDetails,
567 );
568 }
569
570 return toAttrs(current, env.project, env.region);
571 }),
572
573 delete: Effect.fn(function* ({ output }) {
574 const operation = yield* netapp
575 .deleteProjectsLocationsStoragePools({ name: output.name })
576 .pipe(
577 Effect.retry({
578 while: (error) => error._tag === "Conflict",
579 times: 8,
580 schedule: Schedule.spaced("2 seconds"),
581 }),
582 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
583 );
584 if (operation !== undefined) {
585 yield* waitForDeleteOperation(operation);
586 }
587 yield* waitUntilGone(getByName(output.name), output.name);
588 }),
589 });