1import * as osconfig from "@distilled.cloud/gcp/osconfig_v2";
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 DEFAULT_ACTION,
19 DEFAULT_STATE,
20 OsconfigNotResolved,
21 collectOrchestrators,
22 defaultOrchestratedResource,
23 fieldMask,
24 fingerprint,
25 globalParent,
26 hasAlchemyLabelMap,
27 orchestratedPayload,
28 parseName,
29 projectParent,
30 replaceOnIdentity,
31 resourceName,
32 toPhysicalId,
33 userLabels,
34 waitForOperation,
35 waitUntilExists,
36 waitUntilGone,
37 type OrchestratedResource,
38 type OrchestrationScope,
39} from "./internal.ts";
40
41export type ProjectsLocationsGlobalPolicyOrchestratorProps = {
42 /**
43 * Orchestrator id (the `{policyOrchestrator}` segment of
44 * `projects/{project}/locations/global/policyOrchestrators/{policyOrchestrator}`).
45 * If omitted, a unique RFC1035 name is generated. Immutable — changing
46 * it replaces the orchestrator.
47 */
48 policyOrchestratorId?: string;
49 /**
50 * Action applied to matching OS policy assignments.
51 * `UPSERT` creates or updates; `DELETE` removes them.
52 * @default "UPSERT"
53 */
54 action?: string;
55 /**
56 * Orchestrator state. `ACTIVE` applies changes; `STOPPED` is idle.
57 * Defaults to `STOPPED` so create does not roll out assignments.
58 * @default "STOPPED"
59 */
60 state?: string;
61 /**
62 * Free-form description of the orchestrator.
63 */
64 description?: string;
65 /**
66 * User labels. Alchemy ownership labels are merged in automatically.
67 */
68 labels?: Record<string, string>;
69 /**
70 * Resource the orchestrator creates, updates, or deletes. When omitted,
71 * a no-op VALIDATION OS policy assignment is used.
72 */
73 orchestratedResource?: OrchestratedResource;
74 /**
75 * Scope of projects, folders, and zones the orchestrator targets.
76 * When omitted, the parent project is the entire scope.
77 */
78 orchestrationScope?: OrchestrationScope;
79};
80
81export type ProjectsLocationsGlobalPolicyOrchestrator = Resource<
82 "GCP.OSConfig.ProjectsLocationsGlobalPolicyOrchestrator",
83 ProjectsLocationsGlobalPolicyOrchestratorProps,
84 {
85 /** Full resource name. */
86 name: string;
87 /** Orchestrator id (last path segment). */
88 policyOrchestratorId: string;
89 /** Parent `projects/{project}/locations/global`. */
90 parent: string;
91 /** Project id. */
92 project: string;
93 /** Orchestration action (`UPSERT` or `DELETE`). */
94 action: string | undefined;
95 /** Orchestrator state (`ACTIVE` or `STOPPED`). */
96 state: string | undefined;
97 /** User description. */
98 description: string | undefined;
99 /** User labels (Alchemy ownership labels stripped). */
100 labels: Record<string, string>;
101 /** Orchestrated OS policy assignment payload. */
102 orchestratedResource: OrchestratedResource | undefined;
103 /** Orchestration scope selectors. */
104 orchestrationScope: OrchestrationScope | undefined;
105 /** Whether the orchestrator is currently applying changes. */
106 reconciling: boolean | undefined;
107 /** Server checksum used on update and delete. */
108 etag: string | undefined;
109 /** RFC3339 creation timestamp. */
110 createTime: string | undefined;
111 /** RFC3339 last-update timestamp. */
112 updateTime: string | undefined;
113 },
114 never,
115 Providers
116>;
117
118/**
119 * A project-scoped OS Config policy orchestrator.
120 *
121 * Policy orchestrators upsert or delete OS policy assignments across
122 * zones in a project. Id is identity — changing `policyOrchestratorId`
123 * replaces the resource. Action, state, description, labels, the
124 * orchestrated payload, and scope update in place.
125 *
126 * ### Creating a Policy Orchestrator
127 * **Example:** Generated name, stopped
128 * ```typescript
129 * const orch = yield* GCP.OSConfig.ProjectsLocationsGlobalPolicyOrchestrator(
130 * "Debian",
131 * { labels: { env: "test" } },
132 * );
133 * ```
134 *
135 * **Example:** Explicit id and description
136 * ```typescript
137 * const orch = yield* GCP.OSConfig.ProjectsLocationsGlobalPolicyOrchestrator(
138 * "Debian",
139 * {
140 * policyOrchestratorId: "debian-noop",
141 * description: "validation only",
142 * state: "STOPPED",
143 * },
144 * );
145 * ```
146 *
147 * ### Updating a Policy Orchestrator
148 * **Example:** Change labels and description
149 * ```typescript
150 * const orch = yield* GCP.OSConfig.ProjectsLocationsGlobalPolicyOrchestrator(
151 * "Debian",
152 * {
153 * description: "updated",
154 * labels: { env: "prod" },
155 * },
156 * );
157 * ```
158 *
159 * @resource
160 * @category OSConfig
161 */
162export const ProjectsLocationsGlobalPolicyOrchestrator =
163 Resource<ProjectsLocationsGlobalPolicyOrchestrator>(
164 "GCP.OSConfig.ProjectsLocationsGlobalPolicyOrchestrator",
165 );
166
167const toAttrs = (
168 item: osconfig.GoogleCloudOsconfigV2__PolicyOrchestrator,
169 project: string,
170) => {
171 const name = item.name ?? "";
172 const parsed = parseName(name);
173 return {
174 name,
175 policyOrchestratorId: parsed.id,
176 parent: parsed.parent,
177 project: parsed.project || project,
178 action: item.action,
179 state: item.state,
180 description: item.description,
181 labels: userLabels(item.labels),
182 orchestratedResource: item.orchestratedResource,
183 orchestrationScope: item.orchestrationScope,
184 reconciling: item.reconciling,
185 etag: item.etag,
186 createTime: item.createTime,
187 updateTime: item.updateTime,
188 };
189};
190
191const getByName = (name: string) =>
192 name.length === 0
193 ? Effect.succeed(undefined)
194 : osconfig
195 .getProjectsLocationsGlobalPolicyOrchestrators({ name })
196 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
197
198const desiredBody = (
199 news: ProjectsLocationsGlobalPolicyOrchestratorProps,
200 policyOrchestratorId: string,
201 desiredLabels: Record<string, string>,
202): osconfig.GoogleCloudOsconfigV2__PolicyOrchestrator => ({
203 action: news.action ?? DEFAULT_ACTION,
204 state: news.state ?? DEFAULT_STATE,
205 description: news.description,
206 labels: desiredLabels,
207 orchestratedResource: orchestratedPayload(
208 news.orchestratedResource ??
209 defaultOrchestratedResource(rfcResourceId(policyOrchestratorId)),
210 ),
211 orchestrationScope: news.orchestrationScope,
212});
213
214const rfcResourceId = (policyOrchestratorId: string) =>
215 policyOrchestratorId.slice(0, 63);
216
217export const ProjectsLocationsGlobalPolicyOrchestratorProvider = () =>
218 Provider.succeed(ProjectsLocationsGlobalPolicyOrchestrator, {
219 stables: [
220 "name",
221 "policyOrchestratorId",
222 "parent",
223 "project",
224 "createTime",
225 ],
226
227 diff: Effect.fn(function* ({ news, olds, output }) {
228 if (!isResolved(news)) return undefined;
229 const previousParent = output?.parent ?? "";
230 const nextParent = previousParent;
231 return replaceOnIdentity({
232 previousId: olds?.policyOrchestratorId ?? output?.policyOrchestratorId,
233 nextId:
234 news.policyOrchestratorId ??
235 olds?.policyOrchestratorId ??
236 output?.policyOrchestratorId,
237 previousParent,
238 nextParent,
239 });
240 }),
241
242 read: Effect.fn(function* ({ id, olds, output }) {
243 const env = yield* GcpEnvironment.current;
244 const policyOrchestratorId = yield* toPhysicalId(
245 id,
246 olds?.policyOrchestratorId,
247 output?.policyOrchestratorId,
248 );
249 const parent = globalParent(projectParent(env.project));
250 const name = output?.name ?? resourceName(parent, policyOrchestratorId);
251 const existing = yield* getByName(name);
252 if (existing === undefined) return undefined;
253 const attrs = toAttrs(existing, env.project);
254 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
255 ? attrs
256 : Unowned(attrs);
257 }),
258
259 list: () =>
260 Effect.gen(function* () {
261 const env = yield* GcpEnvironment.current;
262 const items = yield* collectOrchestrators(
263 osconfig.listProjectsLocationsGlobalPolicyOrchestrators.pages({
264 parent: globalParent(projectParent(env.project)),
265 pageSize: 1000,
266 }),
267 );
268 return items
269 .filter((item) => hasAlchemyLabelMap(item.labels))
270 .map((item) => toAttrs(item, env.project));
271 }),
272
273 reconcile: Effect.fn(function* ({ id, news, output }) {
274 const env = yield* GcpEnvironment.current;
275 const policyOrchestratorId = yield* toPhysicalId(
276 id,
277 news.policyOrchestratorId,
278 output?.policyOrchestratorId,
279 );
280 const parent = globalParent(projectParent(env.project));
281 const name = resourceName(parent, policyOrchestratorId);
282 const desiredLabels = {
283 ...toLabels(news.labels),
284 ...(yield* createInternalLabels(id)),
285 };
286 const desired = desiredBody(news, policyOrchestratorId, desiredLabels);
287
288 let current = yield* getByName(output?.name ?? name);
289
290 if (current === undefined) {
291 const created = yield* osconfig
292 .createProjectsLocationsGlobalPolicyOrchestrators({
293 parent,
294 policyOrchestratorId,
295 body: desired,
296 })
297 .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
298 if (created !== undefined) {
299 yield* waitForOperation(created);
300 }
301 current = yield* waitUntilExists(getByName(name), name);
302 }
303
304 if (current === undefined) {
305 return yield* new OsconfigNotResolved({ name });
306 }
307
308 const observedLabels = tagRecord(current.labels);
309 const { upsert, removed } = diffLabels(observedLabels, desiredLabels);
310 const labelsChanged = upsert.length > 0 || removed.length > 0;
311 const actionChanged =
312 (current.action ?? DEFAULT_ACTION) !==
313 (desired.action ?? DEFAULT_ACTION);
314 const stateChanged =
315 (current.state ?? DEFAULT_STATE) !== (desired.state ?? DEFAULT_STATE);
316 const descriptionChanged =
317 (current.description ?? "") !== (desired.description ?? "");
318 const resourceChanged =
319 fingerprint(orchestratedPayload(current.orchestratedResource)) !==
320 fingerprint(desired.orchestratedResource);
321 const scopeChanged =
322 desired.orchestrationScope !== undefined &&
323 fingerprint(current.orchestrationScope) !==
324 fingerprint(desired.orchestrationScope);
325 const mask = fieldMask([
326 labelsChanged && "labels",
327 actionChanged && "action",
328 stateChanged && "state",
329 descriptionChanged && "description",
330 resourceChanged && "orchestratedResource",
331 scopeChanged && "orchestrationScope",
332 ]);
333
334 if (mask.length > 0) {
335 const operation =
336 yield* osconfig.patchProjectsLocationsGlobalPolicyOrchestrators({
337 name: current.name ?? name,
338 updateMask: mask,
339 body: {
340 ...desired,
341 name: current.name ?? name,
342 etag: current.etag,
343 },
344 });
345 yield* waitForOperation(operation);
346 current = yield* waitUntilExists(
347 getByName(current.name ?? name),
348 current.name ?? name,
349 );
350 }
351
352 return toAttrs(current, env.project);
353 }),
354
355 delete: Effect.fn(function* ({ output }) {
356 if (!output.name) return;
357 const operation = yield* osconfig
358 .deleteProjectsLocationsGlobalPolicyOrchestrators({
359 name: output.name,
360 etag: output.etag,
361 })
362 .pipe(
363 Effect.retry({
364 while: (error) => error._tag === "Conflict",
365 times: 8,
366 schedule: Schedule.spaced("2 seconds"),
367 }),
368 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
369 );
370 if (operation !== undefined) {
371 yield* waitForOperation(operation, { notFoundOk: true });
372 }
373 yield* waitUntilGone(getByName(output.name), output.name);
374 }),
375 });