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 sameStringList,
27 toPhysicalId,
28 userLabels,
29 waitForDeleteOperation,
30 waitForOperation,
31 waitUntilExists,
32 waitUntilGone,
33 waitUntilReady,
34} from "./internal.ts";
35
36export type ActiveDirectoryProps = {
37 /**
38 * Active Directory id (the `{activeDirectory}` segment of
39 * `projects/{project}/locations/{location}/activeDirectories/{activeDirectory}`).
40 * If omitted, a unique RFC1035 name is generated from the stack, stage,
41 * and logical id. Immutable — changing it replaces the directory.
42 */
43 activeDirectoryId?: string;
44 /**
45 * Region (`us-central1`, `us-east1`, …). Immutable — changing it
46 * replaces the directory. `US-CENTRAL1` is accepted and normalized to
47 * `us-central1`.
48 * @default the stack's GCP region (`GCP.Region`, profile region, `us-central1`)
49 */
50 location?: string;
51 /**
52 * Active Directory domain name (`ad.example.com`).
53 */
54 domain: string;
55 /**
56 * Comma-separated DNS server IP addresses for the domain.
57 */
58 dns: string;
59 /**
60 * NetBIOS prefix used for the SMB server name. Max 15 characters.
61 */
62 netBiosPrefix: string;
63 /**
64 * Domain administrator username.
65 */
66 username: string;
67 /**
68 * Domain administrator password. Write-only — never returned on read.
69 */
70 password?: string;
71 /**
72 * Active Directory site used to limit Domain Controller discovery.
73 */
74 site?: string;
75 /**
76 * Organizational Unit (OU) the machine account is created in.
77 */
78 organizationalUnit?: string;
79 /**
80 * Users added to the Built-in Administrators group.
81 */
82 administrators?: string[];
83 /**
84 * Users added to the Built-in Backup Operators group.
85 */
86 backupOperators?: string[];
87 /**
88 * Domain users granted SeSecurityPrivilege.
89 */
90 securityOperators?: string[];
91 /**
92 * Encrypt SMB traffic to the Domain Controller.
93 */
94 encryptDcConnections?: boolean;
95 /**
96 * Allow local users and LDAP users. Disable for LDAP-only access.
97 */
98 nfsUsersWithLdap?: boolean;
99 /**
100 * Sign LDAP traffic.
101 */
102 ldapSigning?: boolean;
103 /**
104 * Enable AES encryption for SMB communication.
105 */
106 aesEncryption?: boolean;
107 /**
108 * KDC server IP address (Kerberos volumes).
109 */
110 kdcIp?: string;
111 /**
112 * KDC hostname (Kerberos volumes).
113 */
114 kdcHostname?: string;
115 /**
116 * Human-readable description.
117 */
118 description?: string;
119 /**
120 * User labels. Alchemy ownership labels are merged in automatically.
121 */
122 labels?: Record<string, string>;
123};
124
125export type ActiveDirectory = Resource<
126 "GCP.NetApp.ActiveDirectory",
127 ActiveDirectoryProps,
128 {
129 /** Full resource name. */
130 name: string;
131 /** Active Directory id (last path segment). */
132 activeDirectoryId: string;
133 /** Project id. */
134 project: string;
135 /** Location id. */
136 location: string;
137 /** Domain name. */
138 domain: string | undefined;
139 /** DNS server list. */
140 dns: string | undefined;
141 /** NetBIOS prefix. */
142 netBiosPrefix: string | undefined;
143 /** Administrator username. */
144 username: string | undefined;
145 /** Site name. */
146 site: string | undefined;
147 /** Organizational unit. */
148 organizationalUnit: string | undefined;
149 /** Built-in Administrators group members. */
150 administrators: string[];
151 /** Built-in Backup Operators group members. */
152 backupOperators: string[];
153 /** SeSecurityPrivilege users. */
154 securityOperators: string[];
155 /** Whether DC connections are encrypted. */
156 encryptDcConnections: boolean | undefined;
157 /** Whether local + LDAP users are allowed. */
158 nfsUsersWithLdap: boolean | undefined;
159 /** Whether LDAP signing is enabled. */
160 ldapSigning: boolean | undefined;
161 /** Whether AES encryption is enabled. */
162 aesEncryption: boolean | undefined;
163 /** KDC IP address. */
164 kdcIp: string | undefined;
165 /** KDC hostname. */
166 kdcHostname: string | undefined;
167 /** Human-readable description. */
168 description: string | undefined;
169 /** User labels (Alchemy ownership labels stripped). */
170 labels: Record<string, string>;
171 /** Server-reported state. */
172 state: string | undefined;
173 /** State details. */
174 stateDetails: string | undefined;
175 /** RFC3339 creation timestamp. */
176 createTime: string | undefined;
177 },
178 never,
179 Providers
180>;
181
182/**
183 * A Cloud NetApp Volumes Active Directory connection used by SMB and
184 * Kerberos volumes.
185 *
186 * Changing `activeDirectoryId`, `location`, `domain`, or `netBiosPrefix`
187 * replaces the directory. Credentials, operators, description, and labels
188 * update in place.
189 *
190 * ### Creating an Active Directory
191 * **Example:** Generated name
192 * ```typescript
193 * const ad = yield* GCP.NetApp.ActiveDirectory("Corp", {
194 * domain: "ad.example.com",
195 * dns: "10.0.0.2",
196 * netBiosPrefix: "netapp",
197 * username: "admin",
198 * password: secret,
199 * });
200 * ```
201 *
202 * **Example:** Operators and encryption
203 * ```typescript
204 * const ad = yield* GCP.NetApp.ActiveDirectory("Corp", {
205 * domain: "ad.example.com",
206 * dns: "10.0.0.2,10.0.0.3",
207 * netBiosPrefix: "netapp",
208 * username: "admin",
209 * password: secret,
210 * administrators: ["alice"],
211 * aesEncryption: true,
212 * labels: { env: "prod" },
213 * });
214 * ```
215 *
216 * ### Updating an Active Directory
217 * **Example:** Description and labels
218 * ```typescript
219 * const ad = yield* GCP.NetApp.ActiveDirectory("Corp", {
220 * domain: "ad.example.com",
221 * dns: "10.0.0.2",
222 * netBiosPrefix: "netapp",
223 * username: "admin",
224 * description: "corp ad v2",
225 * labels: { env: "prod", team: "storage" },
226 * });
227 * ```
228 *
229 * @resource
230 * @category NetApp
231 */
232export const ActiveDirectory = Resource<ActiveDirectory>(
233 "GCP.NetApp.ActiveDirectory",
234);
235
236const resourceName = (
237 project: string,
238 location: string,
239 activeDirectoryId: string,
240) =>
241 `projects/${project}/locations/${location}/activeDirectories/${activeDirectoryId}`;
242
243const toAttrs = (
244 item: netapp.ActiveDirectory,
245 project: string,
246 region: string,
247) => {
248 const name = item.name ?? "";
249 const parsed = parseName(name, "activeDirectories", region);
250 return {
251 name,
252 activeDirectoryId: parsed.id,
253 project: parsed.project || project,
254 location: parsed.location,
255 domain: item.domain,
256 dns: item.dns,
257 netBiosPrefix: item.netBiosPrefix,
258 username: item.username,
259 site: item.site,
260 organizationalUnit: item.organizationalUnit,
261 administrators: item.administrators ?? [],
262 backupOperators: item.backupOperators ?? [],
263 securityOperators: item.securityOperators ?? [],
264 encryptDcConnections: item.encryptDcConnections,
265 nfsUsersWithLdap: item.nfsUsersWithLdap,
266 ldapSigning: item.ldapSigning,
267 aesEncryption: item.aesEncryption,
268 kdcIp: item.kdcIp,
269 kdcHostname: item.kdcHostname,
270 description: item.description,
271 labels: userLabels(item.labels),
272 state: item.state,
273 stateDetails: item.stateDetails,
274 createTime: item.createTime,
275 };
276};
277
278const getByName = (name: string) =>
279 netapp
280 .getProjectsLocationsActiveDirectories({ name })
281 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
282
283const listOwned = (project: string) =>
284 listAtLocation(project, (parent) =>
285 listLabeledPages(
286 netapp.listProjectsLocationsActiveDirectories.pages({
287 parent,
288 pageSize: 1000,
289 }),
290 (page) => page.activeDirectories,
291 (item) => item.labels,
292 ),
293 );
294
295export const ActiveDirectoryProvider = () =>
296 Provider.succeed(ActiveDirectory, {
297 stables: ["name", "activeDirectoryId", "project", "location", "createTime"],
298
299 diff: Effect.fn(function* ({ news, olds, output }) {
300 if (!isResolved(news)) return undefined;
301 const env = yield* GcpEnvironment.current;
302 const previousDomain = olds?.domain ?? output?.domain;
303 const previousPrefix = olds?.netBiosPrefix ?? output?.netBiosPrefix;
304 return replaceOnIdentity({
305 previousId: olds?.activeDirectoryId ?? output?.activeDirectoryId,
306 nextId:
307 news.activeDirectoryId ??
308 olds?.activeDirectoryId ??
309 output?.activeDirectoryId,
310 previousLocation: normalizeLocation(
311 olds?.location ?? output?.location,
312 env.region,
313 ),
314 nextLocation: normalizeLocation(
315 news.location ?? olds?.location ?? output?.location,
316 env.region,
317 ),
318 extra:
319 (previousDomain !== undefined && news.domain !== previousDomain) ||
320 (previousPrefix !== undefined &&
321 news.netBiosPrefix !== previousPrefix),
322 });
323 }),
324
325 read: Effect.fn(function* ({ id, olds, output }) {
326 const env = yield* GcpEnvironment.current;
327 const activeDirectoryId = yield* toPhysicalId(
328 id,
329 olds?.activeDirectoryId,
330 output?.activeDirectoryId,
331 "activedirectory",
332 );
333 const location = normalizeLocation(
334 olds?.location ?? output?.location,
335 env.region,
336 );
337 const name =
338 output?.name ?? resourceName(env.project, location, activeDirectoryId);
339 const existing = yield* getByName(name);
340 if (existing === undefined) return undefined;
341 const attrs = toAttrs(existing, env.project, env.region);
342 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
343 ? attrs
344 : Unowned(attrs);
345 }),
346
347 list: () =>
348 Effect.gen(function* () {
349 const env = yield* GcpEnvironment.current;
350 const items = yield* listOwned(env.project);
351 return items.map((item) => toAttrs(item, env.project, env.region));
352 }),
353
354 reconcile: Effect.fn(function* ({ id, news, output, olds }) {
355 const env = yield* GcpEnvironment.current;
356 const activeDirectoryId = yield* toPhysicalId(
357 id,
358 news.activeDirectoryId,
359 output?.activeDirectoryId,
360 "activedirectory",
361 );
362 const location = normalizeLocation(
363 news.location ?? output?.location,
364 env.region,
365 );
366 const name = resourceName(env.project, location, activeDirectoryId);
367 const desiredLabels = {
368 ...toLabels(news.labels),
369 ...(yield* createInternalLabels(id)),
370 };
371 const body: netapp.ActiveDirectory = {
372 domain: news.domain,
373 dns: news.dns,
374 netBiosPrefix: news.netBiosPrefix,
375 username: news.username,
376 password: news.password,
377 site: news.site,
378 organizationalUnit: news.organizationalUnit,
379 administrators: news.administrators,
380 backupOperators: news.backupOperators,
381 securityOperators: news.securityOperators,
382 encryptDcConnections: news.encryptDcConnections,
383 nfsUsersWithLdap: news.nfsUsersWithLdap,
384 ldapSigning: news.ldapSigning,
385 aesEncryption: news.aesEncryption,
386 kdcIp: news.kdcIp,
387 kdcHostname: news.kdcHostname,
388 description: news.description,
389 labels: desiredLabels,
390 };
391
392 let current = yield* getByName(output?.name ?? name);
393
394 if (current === undefined) {
395 const created = yield* netapp
396 .createProjectsLocationsActiveDirectories({
397 parent: parentOf(env.project, location),
398 activeDirectoryId,
399 body,
400 })
401 .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
402 if (created !== undefined) {
403 yield* waitForOperation(created);
404 }
405 current = yield* waitUntilExists(getByName(name), name);
406 }
407
408 if (current === undefined) {
409 return yield* new ResourceNotResolved({ name });
410 }
411
412 current = yield* waitUntilReady(
413 getByName(current.name ?? name),
414 current.name ?? name,
415 (item) => item.state,
416 (item) => item.stateDetails,
417 );
418
419 const observedLabels = tagRecord(current.labels);
420 const { upsert, removed } = diffLabels(observedLabels, desiredLabels);
421 const passwordChanged =
422 news.password !== undefined && news.password !== olds?.password;
423 const mask = fieldMask([
424 (upsert.length > 0 || removed.length > 0) && "labels",
425 (current.description ?? "") !== (news.description ?? "") &&
426 "description",
427 (current.dns ?? "") !== news.dns && "dns",
428 (current.username ?? "") !== news.username && "username",
429 passwordChanged && "password",
430 (current.site ?? "") !== (news.site ?? "") && "site",
431 (current.organizationalUnit ?? "") !==
432 (news.organizationalUnit ?? "") && "organizationalUnit",
433 !sameStringList(current.administrators, news.administrators) &&
434 "administrators",
435 !sameStringList(current.backupOperators, news.backupOperators) &&
436 "backupOperators",
437 !sameStringList(current.securityOperators, news.securityOperators) &&
438 "securityOperators",
439 (current.encryptDcConnections ?? false) !==
440 (news.encryptDcConnections ?? false) && "encryptDcConnections",
441 (current.nfsUsersWithLdap ?? false) !==
442 (news.nfsUsersWithLdap ?? false) && "nfsUsersWithLdap",
443 (current.ldapSigning ?? false) !== (news.ldapSigning ?? false) &&
444 "ldapSigning",
445 (current.aesEncryption ?? false) !== (news.aesEncryption ?? false) &&
446 "aesEncryption",
447 (current.kdcIp ?? "") !== (news.kdcIp ?? "") && "kdcIp",
448 (current.kdcHostname ?? "") !== (news.kdcHostname ?? "") &&
449 "kdcHostname",
450 ]);
451
452 if (mask.length > 0) {
453 const operation = yield* netapp.patchProjectsLocationsActiveDirectories(
454 {
455 name: current.name ?? name,
456 updateMask: mask,
457 body: {
458 name: current.name ?? name,
459 ...body,
460 },
461 },
462 );
463 yield* waitForOperation(operation);
464 current = yield* waitUntilReady(
465 getByName(current.name ?? name),
466 current.name ?? name,
467 (item) => item.state,
468 (item) => item.stateDetails,
469 );
470 }
471
472 return toAttrs(current, env.project, env.region);
473 }),
474
475 delete: Effect.fn(function* ({ output }) {
476 const operation = yield* netapp
477 .deleteProjectsLocationsActiveDirectories({ name: output.name })
478 .pipe(
479 Effect.retry({
480 while: (error) => error._tag === "Conflict",
481 times: 8,
482 schedule: Schedule.spaced("2 seconds"),
483 }),
484 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
485 );
486 if (operation !== undefined) {
487 yield* waitForDeleteOperation(operation);
488 }
489 yield* waitUntilGone(getByName(output.name), output.name);
490 }),
491 });