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 fieldMask,
19 listAtLocation,
20 listLabeledPages,
21 normalizeLocation,
22 parentOf,
23 parseName,
24 replaceOnIdentity,
25 ResourceNotResolved,
26 toPhysicalId,
27 userLabels,
28 waitForDeleteOperation,
29 waitForOperation,
30 waitUntilExists,
31 waitUntilGone,
32 waitUntilReady,
33} from "./internal.ts";
34
35export type KmsConfigProps = {
36 /**
37 * KMS config id (the `{kmsConfig}` segment of
38 * `projects/{project}/locations/{location}/kmsConfigs/{kmsConfig}`).
39 * If omitted, a unique RFC1035 name is generated from the stack, stage,
40 * and logical id. Immutable — changing it replaces the config.
41 */
42 kmsConfigId?: string;
43 /**
44 * Region (`us-central1`, `us-east1`, …). Immutable — changing it
45 * replaces the config. `US-CENTRAL1` is accepted and normalized to
46 * `us-central1`.
47 * @default the stack's GCP region (`GCP.Region`, profile region, `us-central1`)
48 */
49 location?: string;
50 /**
51 * Customer-managed crypto key. Format
52 * `projects/{project}/locations/{location}/keyRings/{keyRing}/cryptoKeys/{cryptoKey}`.
53 * Immutable — changing it replaces the config.
54 */
55 cryptoKeyName: string;
56 /**
57 * Human-readable description.
58 */
59 description?: string;
60 /**
61 * User labels. Alchemy ownership labels are merged in automatically.
62 */
63 labels?: Record<string, string>;
64};
65
66export type KmsConfig = Resource<
67 "GCP.NetApp.KmsConfig",
68 KmsConfigProps,
69 {
70 /** Full resource name. */
71 name: string;
72 /** KMS config id (last path segment). */
73 kmsConfigId: string;
74 /** Project id. */
75 project: string;
76 /** Location id. */
77 location: string;
78 /** Customer-managed crypto key name. */
79 cryptoKeyName: string | undefined;
80 /** Service account that must be granted access to the key. */
81 serviceAccount: string | undefined;
82 /** Instructions for granting key access. */
83 instructions: string | undefined;
84 /** Human-readable description. */
85 description: string | undefined;
86 /** User labels (Alchemy ownership labels stripped). */
87 labels: Record<string, string>;
88 /** Server-reported state. */
89 state: string | undefined;
90 /** State details. */
91 stateDetails: string | undefined;
92 /** RFC3339 creation timestamp. */
93 createTime: string | undefined;
94 },
95 never,
96 Providers
97>;
98
99/**
100 * A Cloud NetApp Volumes CMEK configuration that encrypts volumes with a
101 * customer-managed Cloud KMS key.
102 *
103 * Changing `kmsConfigId`, `location`, or `cryptoKeyName` replaces the
104 * config. Description and labels update in place.
105 *
106 * ### Creating a KMS Config
107 * **Example:** Generated name
108 * ```typescript
109 * const kms = yield* GCP.NetApp.KmsConfig("Cmek", {
110 * cryptoKeyName: key.name,
111 * });
112 * ```
113 *
114 * **Example:** Explicit id and description
115 * ```typescript
116 * const kms = yield* GCP.NetApp.KmsConfig("Cmek", {
117 * kmsConfigId: "app-cmek",
118 * cryptoKeyName: key.name,
119 * description: "volume encryption",
120 * labels: { env: "prod" },
121 * });
122 * ```
123 *
124 * ### Updating a KMS Config
125 * **Example:** Description and labels
126 * ```typescript
127 * const kms = yield* GCP.NetApp.KmsConfig("Cmek", {
128 * cryptoKeyName: key.name,
129 * description: "volume encryption v2",
130 * labels: { env: "prod", team: "storage" },
131 * });
132 * ```
133 *
134 * @resource
135 * @category NetApp
136 */
137export const KmsConfig = Resource<KmsConfig>("GCP.NetApp.KmsConfig");
138
139const resourceName = (project: string, location: string, kmsConfigId: string) =>
140 `projects/${project}/locations/${location}/kmsConfigs/${kmsConfigId}`;
141
142const toAttrs = (item: netapp.KmsConfig, project: string, region: string) => {
143 const name = item.name ?? "";
144 const parsed = parseName(name, "kmsConfigs", region);
145 return {
146 name,
147 kmsConfigId: parsed.id,
148 project: parsed.project || project,
149 location: parsed.location,
150 cryptoKeyName: item.cryptoKeyName,
151 serviceAccount: item.serviceAccount,
152 instructions: item.instructions,
153 description: item.description,
154 labels: userLabels(item.labels),
155 state: item.state,
156 stateDetails: item.stateDetails,
157 createTime: item.createTime,
158 };
159};
160
161const getByName = (name: string) =>
162 netapp
163 .getProjectsLocationsKmsConfigs({ name })
164 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
165
166const listOwned = (project: string) =>
167 listAtLocation(project, (parent) =>
168 listLabeledPages(
169 netapp.listProjectsLocationsKmsConfigs.pages({
170 parent,
171 pageSize: 1000,
172 }),
173 (page) => page.kmsConfigs,
174 (item) => item.labels,
175 ),
176 );
177
178export const KmsConfigProvider = () =>
179 Provider.succeed(KmsConfig, {
180 stables: ["name", "kmsConfigId", "project", "location", "createTime"],
181
182 diff: Effect.fn(function* ({ news, olds, output }) {
183 if (!isResolved(news)) return undefined;
184 const env = yield* GcpEnvironment.current;
185 const previousKey = olds?.cryptoKeyName ?? output?.cryptoKeyName;
186 return replaceOnIdentity({
187 previousId: olds?.kmsConfigId ?? output?.kmsConfigId,
188 nextId: news.kmsConfigId ?? olds?.kmsConfigId ?? output?.kmsConfigId,
189 previousLocation: normalizeLocation(
190 olds?.location ?? output?.location,
191 env.region,
192 ),
193 nextLocation: normalizeLocation(
194 news.location ?? olds?.location ?? output?.location,
195 env.region,
196 ),
197 extra: previousKey !== undefined && news.cryptoKeyName !== previousKey,
198 });
199 }),
200
201 read: Effect.fn(function* ({ id, olds, output }) {
202 const env = yield* GcpEnvironment.current;
203 const kmsConfigId = yield* toPhysicalId(
204 id,
205 olds?.kmsConfigId,
206 output?.kmsConfigId,
207 "kmsconfig",
208 );
209 const location = normalizeLocation(
210 olds?.location ?? output?.location,
211 env.region,
212 );
213 const name =
214 output?.name ?? resourceName(env.project, location, kmsConfigId);
215 const existing = yield* getByName(name);
216 if (existing === undefined) return undefined;
217 const attrs = toAttrs(existing, env.project, env.region);
218 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
219 ? attrs
220 : Unowned(attrs);
221 }),
222
223 list: () =>
224 Effect.gen(function* () {
225 const env = yield* GcpEnvironment.current;
226 const items = yield* listOwned(env.project);
227 return items.map((item) => toAttrs(item, env.project, env.region));
228 }),
229
230 reconcile: Effect.fn(function* ({ id, news, output }) {
231 const env = yield* GcpEnvironment.current;
232 const kmsConfigId = yield* toPhysicalId(
233 id,
234 news.kmsConfigId,
235 output?.kmsConfigId,
236 "kmsconfig",
237 );
238 const location = normalizeLocation(
239 news.location ?? output?.location,
240 env.region,
241 );
242 const name = resourceName(env.project, location, kmsConfigId);
243 const desiredLabels = {
244 ...toLabels(news.labels),
245 ...(yield* createInternalLabels(id)),
246 };
247
248 let current = yield* getByName(output?.name ?? name);
249
250 if (current === undefined) {
251 const created = yield* netapp
252 .createProjectsLocationsKmsConfigs({
253 parent: parentOf(env.project, location),
254 kmsConfigId,
255 body: {
256 cryptoKeyName: news.cryptoKeyName,
257 description: news.description,
258 labels: desiredLabels,
259 },
260 })
261 .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
262 if (created !== undefined) {
263 yield* waitForOperation(created);
264 }
265 current = yield* waitUntilExists(getByName(name), name);
266 }
267
268 if (current === undefined) {
269 return yield* new ResourceNotResolved({ name });
270 }
271
272 current = yield* waitUntilReady(
273 getByName(current.name ?? name),
274 current.name ?? name,
275 (item) => item.state,
276 (item) => item.stateDetails,
277 );
278
279 const observedLabels = tagRecord(current.labels);
280 const { upsert, removed } = diffLabels(observedLabels, desiredLabels);
281 const mask = fieldMask([
282 (upsert.length > 0 || removed.length > 0) && "labels",
283 (current.description ?? "") !== (news.description ?? "") &&
284 "description",
285 ]);
286
287 if (mask.length > 0) {
288 const operation = yield* netapp.patchProjectsLocationsKmsConfigs({
289 name: current.name ?? name,
290 updateMask: mask,
291 body: {
292 name: current.name ?? name,
293 labels: desiredLabels,
294 description: news.description,
295 },
296 });
297 yield* waitForOperation(operation);
298 current = yield* waitUntilReady(
299 getByName(current.name ?? name),
300 current.name ?? name,
301 (item) => item.state,
302 (item) => item.stateDetails,
303 );
304 }
305
306 return toAttrs(current, env.project, env.region);
307 }),
308
309 delete: Effect.fn(function* ({ output }) {
310 const operation = yield* netapp
311 .deleteProjectsLocationsKmsConfigs({ name: output.name })
312 .pipe(
313 Effect.retry({
314 while: (error) => error._tag === "Conflict",
315 times: 8,
316 schedule: Schedule.spaced("2 seconds"),
317 }),
318 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
319 );
320 if (operation !== undefined) {
321 yield* waitForDeleteOperation(operation);
322 }
323 yield* waitUntilGone(getByName(output.name), output.name);
324 }),
325 });