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 fingerprint,
21 listAtLocation,
22 listLabeledPages,
23 normalizeLocation,
24 parentOf,
25 parseName,
26 replaceOnIdentity,
27 ResourceNotResolved,
28 toPhysicalId,
29 userLabels,
30 waitForDeleteOperation,
31 waitForOperation,
32 waitUntilExists,
33 waitUntilGone,
34 waitUntilReady,
35} from "./internal.ts";
36
37export type BackupRetentionPolicy = {
38 /**
39 * Minimum retention duration in days for backups in the vault.
40 */
41 backupMinimumEnforcedRetentionDays?: number;
42 /**
43 * Whether daily backups are immutable.
44 */
45 dailyBackupImmutable?: boolean;
46 /**
47 * Whether weekly backups are immutable.
48 */
49 weeklyBackupImmutable?: boolean;
50 /**
51 * Whether monthly backups are immutable.
52 */
53 monthlyBackupImmutable?: boolean;
54 /**
55 * Whether manual backups are immutable.
56 */
57 manualBackupImmutable?: boolean;
58};
59
60export type BackupVaultProps = {
61 /**
62 * Backup vault id (the `{backupVault}` segment of
63 * `projects/{project}/locations/{location}/backupVaults/{backupVault}`).
64 * If omitted, a unique RFC1035 name is generated from the stack, stage,
65 * and logical id. Immutable — changing it replaces the vault.
66 */
67 backupVaultId?: string;
68 /**
69 * Region (`us-central1`, `us-east1`, …). Immutable — changing it
70 * replaces the vault. `US-CENTRAL1` is accepted and normalized to
71 * `us-central1`.
72 * @default the stack's GCP region (`GCP.Region`, profile region, `us-central1`)
73 */
74 location?: string;
75 /**
76 * Vault type. Immutable — changing it replaces the vault.
77 * @default "IN_REGION"
78 */
79 backupVaultType?: netapp.BackupVaultBackupVaultTypeEnum | (string & {});
80 /**
81 * Region that stores backups for a cross-region vault. Full location
82 * name or id. Immutable — changing it replaces the vault.
83 */
84 backupRegion?: string;
85 /**
86 * KMS config used to encrypt backups. Full name or id. Immutable —
87 * changing it replaces the vault.
88 */
89 kmsConfig?: string;
90 /**
91 * Backup retention policy. At least one immutable flag must be true
92 * when the policy is set.
93 */
94 backupRetentionPolicy?: BackupRetentionPolicy;
95 /**
96 * Human-readable description.
97 */
98 description?: string;
99 /**
100 * User labels. Alchemy ownership labels are merged in automatically.
101 */
102 labels?: Record<string, string>;
103};
104
105export type BackupVault = Resource<
106 "GCP.NetApp.BackupVault",
107 BackupVaultProps,
108 {
109 /** Full resource name. */
110 name: string;
111 /** Backup vault id (last path segment). */
112 backupVaultId: string;
113 /** Project id. */
114 project: string;
115 /** Location id. */
116 location: string;
117 /** Vault type. */
118 backupVaultType: string | undefined;
119 /** Backup region. */
120 backupRegion: string | undefined;
121 /** Source region. */
122 sourceRegion: string | undefined;
123 /** Source vault name. */
124 sourceBackupVault: string | undefined;
125 /** Destination vault name. */
126 destinationBackupVault: string | undefined;
127 /** KMS config name. */
128 kmsConfig: string | undefined;
129 /** Crypto key version used to encrypt backups. */
130 backupsCryptoKeyVersion: string | undefined;
131 /** Encryption state of CMEK backups. */
132 encryptionState: string | undefined;
133 /** Retention policy. */
134 backupRetentionPolicy: BackupRetentionPolicy | undefined;
135 /** Human-readable description. */
136 description: string | undefined;
137 /** User labels (Alchemy ownership labels stripped). */
138 labels: Record<string, string>;
139 /** Server-reported state. */
140 state: string | undefined;
141 /** RFC3339 creation timestamp. */
142 createTime: string | undefined;
143 },
144 never,
145 Providers
146>;
147
148/**
149 * A Cloud NetApp Volumes backup vault that stores volume backups.
150 *
151 * Changing `backupVaultId`, `location`, `backupVaultType`, `backupRegion`,
152 * or `kmsConfig` replaces the vault. Description, labels, and retention
153 * policy update in place.
154 *
155 * ### Creating a Backup Vault
156 * **Example:** Generated name
157 * ```typescript
158 * const vault = yield* GCP.NetApp.BackupVault("Vault", {});
159 * ```
160 *
161 * **Example:** Explicit id and description
162 * ```typescript
163 * const vault = yield* GCP.NetApp.BackupVault("Vault", {
164 * backupVaultId: "app-backups",
165 * description: "nightly backups",
166 * labels: { env: "prod" },
167 * });
168 * ```
169 *
170 * ### Updating a Backup Vault
171 * **Example:** Description and labels
172 * ```typescript
173 * const vault = yield* GCP.NetApp.BackupVault("Vault", {
174 * description: "nightly backups v2",
175 * labels: { env: "prod", team: "storage" },
176 * });
177 * ```
178 *
179 * @resource
180 * @category NetApp
181 */
182export const BackupVault = Resource<BackupVault>("GCP.NetApp.BackupVault");
183
184const resourceName = (
185 project: string,
186 location: string,
187 backupVaultId: string,
188) => `projects/${project}/locations/${location}/backupVaults/${backupVaultId}`;
189
190const toRetention = (
191 policy: netapp.BackupRetentionPolicy | undefined,
192): BackupRetentionPolicy | undefined =>
193 policy === undefined
194 ? undefined
195 : {
196 backupMinimumEnforcedRetentionDays:
197 policy.backupMinimumEnforcedRetentionDays,
198 dailyBackupImmutable: policy.dailyBackupImmutable,
199 weeklyBackupImmutable: policy.weeklyBackupImmutable,
200 monthlyBackupImmutable: policy.monthlyBackupImmutable,
201 manualBackupImmutable: policy.manualBackupImmutable,
202 };
203
204const toAttrs = (item: netapp.BackupVault, project: string, region: string) => {
205 const name = item.name ?? "";
206 const parsed = parseName(name, "backupVaults", region);
207 return {
208 name,
209 backupVaultId: parsed.id,
210 project: parsed.project || project,
211 location: parsed.location,
212 backupVaultType: item.backupVaultType,
213 backupRegion: item.backupRegion,
214 sourceRegion: item.sourceRegion,
215 sourceBackupVault: item.sourceBackupVault,
216 destinationBackupVault: item.destinationBackupVault,
217 kmsConfig: item.kmsConfig,
218 backupsCryptoKeyVersion: item.backupsCryptoKeyVersion,
219 encryptionState: item.encryptionState,
220 backupRetentionPolicy: toRetention(item.backupRetentionPolicy),
221 description: item.description,
222 labels: userLabels(item.labels),
223 state: item.state,
224 createTime: item.createTime,
225 };
226};
227
228const getByName = (name: string) =>
229 netapp
230 .getProjectsLocationsBackupVaults({ name })
231 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
232
233const listOwned = (project: string) =>
234 listAtLocation(project, (parent) =>
235 listLabeledPages(
236 netapp.listProjectsLocationsBackupVaults.pages({
237 parent,
238 pageSize: 1000,
239 }),
240 (page) => page.backupVaults,
241 (item) => item.labels,
242 ),
243 );
244
245export const BackupVaultProvider = () =>
246 Provider.succeed(BackupVault, {
247 stables: ["name", "backupVaultId", "project", "location", "createTime"],
248
249 diff: Effect.fn(function* ({ news, olds, output }) {
250 if (!isResolved(news)) return undefined;
251 const env = yield* GcpEnvironment.current;
252 const previousType = olds?.backupVaultType ?? output?.backupVaultType;
253 const previousRegion = olds?.backupRegion ?? output?.backupRegion;
254 const previousKms = olds?.kmsConfig ?? output?.kmsConfig;
255 return replaceOnIdentity({
256 previousId: olds?.backupVaultId ?? output?.backupVaultId,
257 nextId:
258 news.backupVaultId ?? olds?.backupVaultId ?? output?.backupVaultId,
259 previousLocation: normalizeLocation(
260 olds?.location ?? output?.location,
261 env.region,
262 ),
263 nextLocation: normalizeLocation(
264 news.location ?? olds?.location ?? output?.location,
265 env.region,
266 ),
267 extra:
268 (previousType !== undefined &&
269 news.backupVaultType !== undefined &&
270 news.backupVaultType !== previousType) ||
271 (previousRegion !== undefined &&
272 news.backupRegion !== undefined &&
273 news.backupRegion !== previousRegion) ||
274 (previousKms !== undefined &&
275 news.kmsConfig !== undefined &&
276 news.kmsConfig !== previousKms),
277 });
278 }),
279
280 read: Effect.fn(function* ({ id, olds, output }) {
281 const env = yield* GcpEnvironment.current;
282 const backupVaultId = yield* toPhysicalId(
283 id,
284 olds?.backupVaultId,
285 output?.backupVaultId,
286 "backupvault",
287 );
288 const location = normalizeLocation(
289 olds?.location ?? output?.location,
290 env.region,
291 );
292 const name =
293 output?.name ?? resourceName(env.project, location, backupVaultId);
294 const existing = yield* getByName(name);
295 if (existing === undefined) return undefined;
296 const attrs = toAttrs(existing, env.project, env.region);
297 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
298 ? attrs
299 : Unowned(attrs);
300 }),
301
302 list: () =>
303 Effect.gen(function* () {
304 const env = yield* GcpEnvironment.current;
305 const items = yield* listOwned(env.project);
306 return items.map((item) => toAttrs(item, env.project, env.region));
307 }),
308
309 reconcile: Effect.fn(function* ({ id, news, output }) {
310 const env = yield* GcpEnvironment.current;
311 const backupVaultId = yield* toPhysicalId(
312 id,
313 news.backupVaultId,
314 output?.backupVaultId,
315 "backupvault",
316 );
317 const location = normalizeLocation(
318 news.location ?? output?.location,
319 env.region,
320 );
321 const name = resourceName(env.project, location, backupVaultId);
322 const desiredLabels = {
323 ...toLabels(news.labels),
324 ...(yield* createInternalLabels(id)),
325 };
326 const backupRegion =
327 news.backupRegion === undefined
328 ? undefined
329 : news.backupRegion.includes("/")
330 ? news.backupRegion
331 : parentOf(env.project, news.backupRegion);
332 const kmsConfig =
333 news.kmsConfig === undefined
334 ? undefined
335 : expandParent(news.kmsConfig, env.project, location, "kmsConfigs");
336
337 let current = yield* getByName(output?.name ?? name);
338
339 if (current === undefined) {
340 const created = yield* netapp
341 .createProjectsLocationsBackupVaults({
342 parent: parentOf(env.project, location),
343 backupVaultId,
344 body: {
345 backupVaultType: news.backupVaultType,
346 backupRegion,
347 kmsConfig,
348 backupRetentionPolicy: news.backupRetentionPolicy,
349 description: news.description,
350 labels: desiredLabels,
351 },
352 })
353 .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
354 if (created !== undefined) {
355 yield* waitForOperation(created);
356 }
357 current = yield* waitUntilExists(getByName(name), name);
358 }
359
360 if (current === undefined) {
361 return yield* new ResourceNotResolved({ name });
362 }
363
364 current = yield* waitUntilReady(
365 getByName(current.name ?? name),
366 current.name ?? name,
367 (item) => item.state,
368 );
369
370 const observedLabels = tagRecord(current.labels);
371 const { upsert, removed } = diffLabels(observedLabels, desiredLabels);
372 const mask = fieldMask([
373 (upsert.length > 0 || removed.length > 0) && "labels",
374 (current.description ?? "") !== (news.description ?? "") &&
375 "description",
376 fingerprint(toRetention(current.backupRetentionPolicy)) !==
377 fingerprint(news.backupRetentionPolicy) && "backupRetentionPolicy",
378 ]);
379
380 if (mask.length > 0) {
381 const operation = yield* netapp.patchProjectsLocationsBackupVaults({
382 name: current.name ?? name,
383 updateMask: mask,
384 body: {
385 name: current.name ?? name,
386 labels: desiredLabels,
387 description: news.description,
388 backupRetentionPolicy: news.backupRetentionPolicy,
389 },
390 });
391 yield* waitForOperation(operation);
392 current = yield* waitUntilReady(
393 getByName(current.name ?? name),
394 current.name ?? name,
395 (item) => item.state,
396 );
397 }
398
399 return toAttrs(current, env.project, env.region);
400 }),
401
402 delete: Effect.fn(function* ({ output }) {
403 const operation = yield* netapp
404 .deleteProjectsLocationsBackupVaults({ name: output.name })
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* waitForDeleteOperation(operation);
415 }
416 yield* waitUntilGone(getByName(output.name), output.name);
417 }),
418 });