1import * as managedidentities from "@distilled.cloud/gcp/managedidentities_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 domainResourceOf,
19 fieldMask,
20 listBackups,
21 parseName,
22 replaceOnIdentity,
23 ResourceNotResolved,
24 toPhysicalId,
25 userLabels,
26 waitForOperation,
27 waitUntilExists,
28 waitUntilGone,
29 waitUntilReady,
30} from "./internal.ts";
31
32export type DomainsBackupProps = {
33 /**
34 * Parent Managed Microsoft AD domain. Full name
35 * `projects/{project}/locations/global/domains/{domain}` or the domain
36 * FQDN. Immutable — changing it replaces the backup.
37 */
38 domain: string;
39 /**
40 * Backup id (the `{backup}` segment of
41 * `.../domains/{domain}/backups/{backup}`). If omitted, a unique
42 * RFC1035 name is generated from the stack, stage, and logical id.
43 * 1-63 characters, lowercase letters, numbers, and hyphens. Immutable
44 * — changing it replaces the backup.
45 */
46 backupId?: string;
47 /**
48 * User labels. Alchemy ownership labels are merged in automatically.
49 * Labels are the only mutable field.
50 */
51 labels?: Record<string, string>;
52};
53
54export type DomainsBackup = Resource<
55 "GCP.ManagedIdentities.DomainsBackup",
56 DomainsBackupProps,
57 {
58 /** Full resource name. */
59 name: string;
60 /** Backup id (last path segment). */
61 backupId: string;
62 /** Parent domain resource name. */
63 domain: string;
64 /** Project id. */
65 project: string;
66 /** Resource location (`global`). */
67 location: string;
68 /** User labels (Alchemy ownership labels stripped). */
69 labels: Record<string, string>;
70 /** Backup type (`ON_DEMAND` or `SCHEDULED`). */
71 type: string | undefined;
72 /** Server-reported state (`ACTIVE`, `CREATING`, …). */
73 state: string | undefined;
74 /** Additional status information, if available. */
75 statusMessage: string | undefined;
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 * An on-demand backup of a Managed Microsoft AD domain.
87 *
88 * Changing `backupId` or `domain` replaces the backup. Labels update in
89 * place. Create is on-demand; scheduled backups are produced by the
90 * domain and are not managed here.
91 *
92 * ### Creating a Backup
93 * **Example:** Generated name
94 * ```typescript
95 * const backup = yield* GCP.ManagedIdentities.DomainsBackup("Nightly", {
96 * domain: domain.name,
97 * });
98 * ```
99 *
100 * **Example:** Explicit id and labels
101 * ```typescript
102 * const backup = yield* GCP.ManagedIdentities.DomainsBackup("Nightly", {
103 * domain: domain.name,
104 * backupId: "app-nightly",
105 * labels: { env: "prod" },
106 * });
107 * ```
108 *
109 * ### Updating a Backup
110 * Re-declare the same logical id with changed props; the engine keeps the
111 * physical resource and updates it in place.
112 *
113 * **Example:** Labels
114 * ```typescript
115 * const backup = yield* GCP.ManagedIdentities.DomainsBackup("Nightly", {
116 * domain: domain.name,
117 * labels: { env: "prod", team: "identity" },
118 * });
119 * ```
120 *
121 * @resource
122 * @category ManagedIdentities
123 */
124export const DomainsBackup = Resource<DomainsBackup>(
125 "GCP.ManagedIdentities.DomainsBackup",
126);
127
128const resourceName = (domain: string, backupId: string) =>
129 `${domain}/backups/${backupId}`;
130
131const toAttrs = (item: managedidentities.Backup, project: string) => {
132 const name = item.name ?? "";
133 const parsed = parseName(name, "backups");
134 return {
135 name,
136 backupId: parsed.id,
137 domain: parsed.parent,
138 project: parsed.project || project,
139 location: parsed.location,
140 labels: userLabels(item.labels),
141 type: item.type,
142 state: item.state,
143 statusMessage: item.statusMessage,
144 createTime: item.createTime,
145 updateTime: item.updateTime,
146 };
147};
148
149const getByName = (name: string) =>
150 name.length === 0 || name.includes("//")
151 ? Effect.succeed(undefined)
152 : managedidentities
153 .getProjectsLocationsGlobalDomainsBackups({ name })
154 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
155
156export const DomainsBackupProvider = () =>
157 Provider.succeed(DomainsBackup, {
158 stables: [
159 "name",
160 "backupId",
161 "domain",
162 "project",
163 "location",
164 "createTime",
165 ],
166
167 diff: Effect.fn(function* ({ news, olds, output }) {
168 if (!isResolved(news)) return undefined;
169 const env = yield* GcpEnvironment.current;
170 const previousDomain = domainResourceOf(
171 olds?.domain ?? output?.domain ?? "",
172 env.project,
173 );
174 const nextDomain = domainResourceOf(
175 news.domain ?? olds?.domain ?? output?.domain ?? "",
176 env.project,
177 );
178 return replaceOnIdentity({
179 previousId: olds?.backupId ?? output?.backupId,
180 nextId: news.backupId ?? olds?.backupId ?? output?.backupId,
181 previousParent: previousDomain,
182 nextParent: nextDomain,
183 });
184 }),
185
186 read: Effect.fn(function* ({ id, olds, output }) {
187 const env = yield* GcpEnvironment.current;
188 const domain = domainResourceOf(
189 olds?.domain ?? output?.domain ?? "",
190 env.project,
191 );
192 const backupId = yield* toPhysicalId(
193 id,
194 olds?.backupId,
195 output?.backupId,
196 "backup",
197 );
198 const name = output?.name ?? resourceName(domain, backupId);
199 const existing = yield* getByName(name);
200 if (existing === undefined) return undefined;
201 const attrs = toAttrs(existing, env.project);
202 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
203 ? attrs
204 : Unowned(attrs);
205 }),
206
207 list: () =>
208 Effect.gen(function* () {
209 const env = yield* GcpEnvironment.current;
210 const items = yield* listBackups(env.project);
211 return items.map((item) => toAttrs(item, env.project));
212 }),
213
214 reconcile: Effect.fn(function* ({ id, news, output }) {
215 const env = yield* GcpEnvironment.current;
216 const domain = domainResourceOf(news.domain, env.project);
217 const backupId = yield* toPhysicalId(
218 id,
219 news.backupId,
220 output?.backupId,
221 "backup",
222 );
223 const name = output?.name ?? resourceName(domain, backupId);
224 const desiredLabels = {
225 ...toLabels(news.labels),
226 ...(yield* createInternalLabels(id)),
227 };
228
229 let current = yield* getByName(output?.name ?? name);
230
231 if (current === undefined) {
232 const created = yield* managedidentities
233 .createProjectsLocationsGlobalDomainsBackups({
234 parent: domain,
235 backupId,
236 body: {
237 labels: desiredLabels,
238 },
239 })
240 .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
241 if (created !== undefined) {
242 yield* waitForOperation(created);
243 }
244 current = yield* waitUntilExists(getByName(name), name);
245 }
246
247 if (current === undefined) {
248 return yield* new ResourceNotResolved({ name });
249 }
250
251 current = yield* waitUntilReady(
252 getByName(current.name ?? name),
253 current.name ?? name,
254 (item: managedidentities.Backup) => item.state,
255 (item: managedidentities.Backup) => item.statusMessage,
256 );
257
258 const observedLabels = tagRecord(current.labels);
259 const { upsert, removed } = diffLabels(observedLabels, desiredLabels);
260 const mask = fieldMask([
261 (upsert.length > 0 || removed.length > 0) && "labels",
262 ]);
263
264 if (mask.length > 0) {
265 const operation =
266 yield* managedidentities.patchProjectsLocationsGlobalDomainsBackups({
267 name: current.name ?? name,
268 updateMask: mask,
269 body: {
270 name: current.name ?? name,
271 labels: desiredLabels,
272 },
273 });
274 yield* waitForOperation(operation);
275 current = yield* waitUntilReady(
276 getByName(current.name ?? name),
277 current.name ?? name,
278 (item: managedidentities.Backup) => item.state,
279 (item: managedidentities.Backup) => item.statusMessage,
280 );
281 }
282
283 return toAttrs(current, env.project);
284 }),
285
286 delete: Effect.fn(function* ({ output }) {
287 if (!output.name || output.name.includes("//")) return;
288 const operation = yield* managedidentities
289 .deleteProjectsLocationsGlobalDomainsBackups({ name: output.name })
290 .pipe(
291 Effect.retry({
292 while: (error) => error._tag === "Conflict",
293 times: 8,
294 schedule: Schedule.spaced("2 seconds"),
295 }),
296 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
297 );
298 if (operation !== undefined) {
299 yield* waitForOperation(operation, { notFoundOk: true });
300 }
301 yield* waitUntilGone(getByName(output.name), output.name);
302 }),
303 });