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