1import * as assuredworkloads from "@distilled.cloud/gcp/assuredworkloads_v1";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Schedule from "effect/Schedule";
5import { Unowned } from "../../AdoptPolicy.ts";
6import { isResolved } from "../../Diff.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { tagRecord } from "../../Tags.ts";
10import { GcpEnvironment } from "../Environment.ts";
11import {
12 createInternalLabels,
13 diffLabels,
14 hasAlchemyLabels,
15 toLabels,
16} from "../Labels.ts";
17import type { Providers } from "../Providers.ts";
18import {
19 type ApiWorkload,
20 deleteChildResources,
21 fieldMask,
22 fingerprint,
23 lastSegment,
24 listOwnedWorkloads,
25 listWorkloads,
26 locationParent,
27 normalizeLocation,
28 organizationIdOf,
29 organizationParent,
30 parseName,
31 replaceOnIdentity,
32 resolveOrganization,
33 resourceNameFromOperation,
34 sameBool,
35 sameText,
36 toDisplayName,
37 tryResolveOrganization,
38 userLabels,
39 waitForOperation,
40 waitUntilExists,
41 waitUntilGone,
42} from "./internal.ts";
43
44export type ComplianceRegime =
45 | assuredworkloads.GoogleCloudAssuredworkloadsV1WorkloadComplianceRegimeEnum
46 | (string & {});
47
48export type Partner =
49 | assuredworkloads.GoogleCloudAssuredworkloadsV1WorkloadPartnerEnum
50 | (string & {});
51
52export type ResourceType =
53 | assuredworkloads.GoogleCloudAssuredworkloadsV1WorkloadResourceSettingsResourceTypeEnum
54 | (string & {});
55
56export type ResourceSettings = {
57 /**
58 * Resource identifier. For a project this is `project_id`. For a
59 * KeyRing this is the keyring id. Folders ignore this — Google assigns
60 * the folder id.
61 */
62 resourceId?: string;
63 /**
64 * Resource kind this setting applies to (`CONSUMER_FOLDER`,
65 * `CONSUMER_PROJECT`, `ENCRYPTION_KEYS_PROJECT`, `KEYRING`).
66 */
67 resourceType?: ResourceType;
68 /**
69 * User-assigned display name used when creating the resource.
70 */
71 displayName?: string;
72};
73
74export type ResourceInfo = {
75 /**
76 * Resource identifier. For a project this is the project number.
77 */
78 resourceId?: string;
79 /**
80 * Resource kind (`CONSUMER_FOLDER`, `CONSUMER_PROJECT`,
81 * `ENCRYPTION_KEYS_PROJECT`, `KEYRING`).
82 */
83 resourceType?: ResourceType;
84};
85
86export type KmsSettings = {
87 /**
88 * Time at which KMS creates a new crypto-key version and marks it
89 * primary. Input-only and immutable.
90 */
91 nextRotationTime?: string;
92 /**
93 * Period by which `nextRotationTime` advances on each rotation. At
94 * least 24 hours and at most 876,000 hours. Input-only and immutable.
95 */
96 rotationPeriod?: string;
97};
98
99export type WorkloadOptions = {
100 /**
101 * Key Access Justifications enrollment type.
102 */
103 kajEnrollmentType?: string;
104};
105
106export type PartnerPermissions = {
107 /**
108 * Allow the partner to view inspectability logs and monitoring
109 * violations.
110 */
111 dataLogsViewer?: boolean;
112 /**
113 * Allow the partner to view violation alerts.
114 */
115 assuredWorkloadsMonitoring?: boolean;
116 /**
117 * Allow the partner to view support-case details for an AXT log.
118 */
119 accessTransparencyLogsSupportCaseViewer?: boolean;
120 /**
121 * Allow the partner to view Access Approval logs.
122 */
123 serviceAccessApprover?: boolean;
124};
125
126export type WorkloadProps = {
127 /**
128 * Parent organization (`organizations/{organization}` or the numeric
129 * id). Defaults to `GOOGLE_ORGANIZATION_ID` or the project's Resource
130 * Manager ancestor. Immutable — changing it replaces the workload.
131 */
132 organization?: string;
133 /**
134 * Location (`us-central1`, `europe-west1`, …). Immutable — changing it
135 * replaces the workload. `US-CENTRAL1` is accepted and normalized to
136 * `us-central1`.
137 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
138 */
139 location?: string;
140 /**
141 * User-assigned display name (4-30 characters: letters, numbers,
142 * hyphen, spaces). If omitted, a unique name is generated from the
143 * stack, stage, and logical id.
144 */
145 displayName?: string;
146 /**
147 * Compliance regime. Immutable — changing it replaces the workload.
148 */
149 complianceRegime: ComplianceRegime;
150 /**
151 * Billing account for resources created as children of this workload.
152 * Format `billingAccounts/{billing_account_id}`. Immutable — changing
153 * it replaces the workload.
154 */
155 billingAccount?: string;
156 /**
157 * User labels. Alchemy ownership labels are merged in automatically.
158 */
159 labels?: Record<string, string>;
160 /**
161 * When true, email notifications for violations are enabled. Honored
162 * only on update — create always enables notifications.
163 * @default true
164 */
165 violationNotificationsEnabled?: boolean;
166 /**
167 * Partner regime associated with this workload. Immutable — changing
168 * it replaces the workload.
169 */
170 partner?: Partner;
171 /**
172 * Billing account used to purchase services from sovereign partners.
173 * Format `billingAccounts/{billing_account_id}`. Immutable — changing
174 * it replaces the workload.
175 */
176 partnerServicesBillingAccount?: string;
177 /**
178 * Permissions granted to the Assured Workloads partner service
179 * account. Updates use `mutatePartnerPermissions`.
180 */
181 partnerPermissions?: PartnerPermissions;
182 /**
183 * Enable sovereignty controls (Europe/Canada customers). Immutable —
184 * changing it replaces the workload.
185 */
186 enableSovereignControls?: boolean;
187 /**
188 * Parent folder for provisioned resources, as `folders/{folder_id}`.
189 * When omitted, resources are created under the organization.
190 * Immutable — changing it replaces the workload.
191 */
192 provisionedResourcesParent?: string;
193 /**
194 * Custom settings for resources created with the workload. Input-only
195 * and immutable — changing them replaces the workload.
196 */
197 resourceSettings?: ResourceSettings[];
198 /**
199 * CMEK crypto-key settings. Deprecated — prefer `ENCRYPTION_KEYS_PROJECT`
200 * or `KEYRING` in `resourceSettings`. Immutable — changing them
201 * replaces the workload.
202 */
203 kmsSettings?: KmsSettings;
204 /**
205 * Options applied at create time. Immutable — changing them replaces
206 * the workload.
207 */
208 workloadOptions?: WorkloadOptions;
209 /**
210 * External id attached as a label on the workload and its projects so
211 * billing can be broken down. Create-only.
212 */
213 externalId?: string;
214};
215
216export type Workload = Resource<
217 "GCP.AssuredWorkloads.Workload",
218 WorkloadProps,
219 {
220 /** Full resource name `organizations/{organization}/locations/{location}/workloads/{workload}`. */
221 name: string;
222 /** Server-assigned workload id (last path segment). */
223 workloadId: string;
224 /** Organization resource name. */
225 organization: string;
226 /** Organization id. */
227 organizationId: string;
228 /** Location id. */
229 location: string;
230 /** Project id of the deploying stack. */
231 project: string;
232 /** User-assigned display name. */
233 displayName: string | undefined;
234 /** Compliance regime. */
235 complianceRegime: string | undefined;
236 /** Billing account resource name. */
237 billingAccount: string | undefined;
238 /** User labels (Alchemy ownership labels stripped). */
239 labels: Record<string, string>;
240 /** Whether violation email notifications are enabled. */
241 violationNotificationsEnabled: boolean;
242 /** Partner regime, if set. */
243 partner: string | undefined;
244 /** Partner services billing account, if set. */
245 partnerServicesBillingAccount: string | undefined;
246 /** Partner permissions. */
247 partnerPermissions: PartnerPermissions | undefined;
248 /** Whether sovereignty controls are enabled. */
249 enableSovereignControls: boolean | undefined;
250 /** Child resources created with the workload. */
251 resources: ResourceInfo[];
252 /** RFC3339 creation timestamp. */
253 createTime: string | undefined;
254 /** Server etag for optimistic concurrency. */
255 etag: string | undefined;
256 /** KAJ enrollment state. */
257 kajEnrollmentState: string | undefined;
258 /** Whether resource monitoring is enabled. */
259 resourceMonitoringEnabled: boolean | undefined;
260 },
261 never,
262 Providers
263>;
264
265/**
266 * An organization-scoped Assured Workloads folder that applies a
267 * compliance regime (FedRAMP, IL4, regional data boundary, …) to the
268 * projects created under it.
269 *
270 * Workload ids are assigned by Google. Alchemy stamps ownership into
271 * `labels` so `list` / `pnpm nuke:gcp` can find them. `organization`,
272 * `location`, `complianceRegime`, billing accounts, partner settings,
273 * and resource settings are identity — changing them replaces the
274 * workload. `displayName`, `labels`, `violationNotificationsEnabled`,
275 * and `partnerPermissions` update in place.
276 *
277 * Creating a workload provisions a folder (and optionally projects and
278 * key rings) under the organization. Delete first marks those children
279 * `DELETE_REQUESTED`, then deletes the workload.
280 *
281 * ### Creating a Workload
282 * **Example:** US regional access with generated display name
283 * ```typescript
284 * const workload = yield* GCP.AssuredWorkloads.Workload("Regulated", {
285 * complianceRegime: "US_REGIONAL_ACCESS",
286 * billingAccount: "billingAccounts/000000-000000-000000",
287 * labels: { env: "test" },
288 * });
289 * ```
290 *
291 * **Example:** Named FedRAMP Moderate workload
292 * ```typescript
293 * const workload = yield* GCP.AssuredWorkloads.Workload("Fedramp", {
294 * organization: "organizations/123456789",
295 * location: "us-central1",
296 * displayName: "fedramp moderate",
297 * complianceRegime: "FEDRAMP_MODERATE",
298 * billingAccount: "billingAccounts/000000-000000-000000",
299 * });
300 * ```
301 *
302 * ### Updating a Workload
303 * **Example:** Change display name and labels
304 * ```typescript
305 * const workload = yield* GCP.AssuredWorkloads.Workload("Regulated", {
306 * complianceRegime: "US_REGIONAL_ACCESS",
307 * displayName: "regulated prod",
308 * labels: { env: "prod" },
309 * violationNotificationsEnabled: false,
310 * });
311 * ```
312 *
313 * @resource
314 * @category AssuredWorkloads
315 */
316export const Workload = Resource<Workload>("GCP.AssuredWorkloads.Workload");
317
318export class WorkloadNotResolved extends Data.TaggedError(
319 "GCP.AssuredWorkloads.WorkloadNotResolved",
320)<{
321 name: string;
322}> {}
323
324const toAttrs = (
325 workload: ApiWorkload,
326 project: string,
327 region: string,
328): Workload["Attributes"] => {
329 const name = workload.name ?? "";
330 const parsed = parseName(name, region);
331 const organization = parsed.organization
332 ? organizationParent(parsed.organization)
333 : "";
334 return {
335 name,
336 workloadId: parsed.id || lastSegment(name),
337 organization,
338 organizationId: organizationIdOf(organization),
339 location: parsed.location || region,
340 project,
341 displayName: workload.displayName,
342 complianceRegime: workload.complianceRegime,
343 billingAccount: workload.billingAccount,
344 labels: userLabels(workload.labels),
345 violationNotificationsEnabled:
346 workload.violationNotificationsEnabled ?? true,
347 partner: workload.partner,
348 partnerServicesBillingAccount: workload.partnerServicesBillingAccount,
349 partnerPermissions: workload.partnerPermissions,
350 enableSovereignControls: workload.enableSovereignControls,
351 resources: (workload.resources ?? []).map((resource) => ({
352 resourceId: resource.resourceId,
353 resourceType: resource.resourceType,
354 })),
355 createTime: workload.createTime,
356 etag: workload.etag,
357 kajEnrollmentState: workload.kajEnrollmentState,
358 resourceMonitoringEnabled: workload.resourceMonitoringEnabled,
359 };
360};
361
362const getByName = (name: string) =>
363 name.length === 0
364 ? Effect.succeed(undefined)
365 : assuredworkloads
366 .getOrganizationsLocationsWorkloads({ name })
367 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
368
369const findOwned = (id: string, organization: string, location: string) =>
370 Effect.gen(function* () {
371 const workloads = yield* listWorkloads(organization, location);
372 for (const workload of workloads) {
373 if (yield* hasAlchemyLabels(id, tagRecord(workload.labels))) {
374 return workload;
375 }
376 }
377 return undefined;
378 });
379
380const observe = (
381 id: string,
382 name: string | undefined,
383 organization: string,
384 location: string,
385) =>
386 Effect.gen(function* () {
387 if (name !== undefined && name.length > 0) {
388 const byName = yield* getByName(name);
389 if (byName !== undefined) return byName;
390 }
391 return yield* findOwned(id, organization, location);
392 });
393
394const desiredBody = (
395 news: WorkloadProps,
396 displayName: string,
397 labels: Record<string, string>,
398): ApiWorkload => ({
399 displayName,
400 complianceRegime: news.complianceRegime,
401 labels,
402 billingAccount: news.billingAccount,
403 partner: news.partner,
404 partnerServicesBillingAccount: news.partnerServicesBillingAccount,
405 partnerPermissions: news.partnerPermissions,
406 enableSovereignControls: news.enableSovereignControls,
407 provisionedResourcesParent: news.provisionedResourcesParent,
408 resourceSettings: news.resourceSettings,
409 kmsSettings: news.kmsSettings,
410 workloadOptions: news.workloadOptions,
411});
412
413export const WorkloadProvider = () =>
414 Provider.succeed(Workload, {
415 stables: [
416 "name",
417 "workloadId",
418 "organization",
419 "organizationId",
420 "location",
421 "project",
422 "createTime",
423 ],
424
425 diff: Effect.fn(function* ({ news, olds, output }) {
426 if (!isResolved(news)) return undefined;
427 const previousOrg = olds?.organization ?? output?.organization;
428 const nextOrg =
429 news.organization !== undefined
430 ? organizationParent(news.organization)
431 : previousOrg;
432 const previousLocation = olds?.location ?? output?.location;
433 const previousRegime = olds?.complianceRegime ?? output?.complianceRegime;
434 const previousBilling = olds?.billingAccount ?? output?.billingAccount;
435 const previousPartner = olds?.partner ?? output?.partner;
436 const previousPartnerBilling =
437 olds?.partnerServicesBillingAccount ??
438 output?.partnerServicesBillingAccount;
439 const previousParent = olds?.provisionedResourcesParent ?? undefined;
440 const previousSovereign =
441 olds?.enableSovereignControls ?? output?.enableSovereignControls;
442 return replaceOnIdentity(
443 (previousOrg !== undefined &&
444 nextOrg !== undefined &&
445 organizationParent(previousOrg) !== organizationParent(nextOrg)) ||
446 (previousLocation !== undefined &&
447 news.location !== undefined &&
448 normalizeLocation(previousLocation) !==
449 normalizeLocation(news.location)) ||
450 (previousRegime !== undefined &&
451 previousRegime !== news.complianceRegime) ||
452 !sameText(previousBilling, news.billingAccount) ||
453 !sameText(previousPartner, news.partner) ||
454 !sameText(
455 previousPartnerBilling,
456 news.partnerServicesBillingAccount,
457 ) ||
458 !sameText(previousParent, news.provisionedResourcesParent) ||
459 (news.enableSovereignControls !== undefined &&
460 previousSovereign !== undefined &&
461 previousSovereign !== news.enableSovereignControls) ||
462 (news.resourceSettings !== undefined &&
463 olds?.resourceSettings !== undefined &&
464 fingerprint(olds.resourceSettings) !==
465 fingerprint(news.resourceSettings)) ||
466 (news.kmsSettings !== undefined &&
467 olds?.kmsSettings !== undefined &&
468 fingerprint(olds.kmsSettings) !== fingerprint(news.kmsSettings)) ||
469 (news.workloadOptions !== undefined &&
470 olds?.workloadOptions !== undefined &&
471 fingerprint(olds.workloadOptions) !==
472 fingerprint(news.workloadOptions)),
473 );
474 }),
475
476 read: Effect.fn(function* ({ id, olds, output }) {
477 const env = yield* GcpEnvironment.current;
478 const organization = yield* resolveOrganization(
479 olds?.organization ?? output?.organization,
480 output?.organization,
481 ).pipe(
482 Effect.catchTag("GCP.AssuredWorkloads.OrganizationNotResolved", () =>
483 Effect.succeed(output?.organization ?? ""),
484 ),
485 );
486 const location = normalizeLocation(
487 olds?.location ?? output?.location ?? env.region,
488 );
489 const existing = yield* observe(id, output?.name, organization, location);
490 if (existing === undefined) return undefined;
491 const attrs = toAttrs(existing, env.project, env.region);
492 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
493 ? attrs
494 : Unowned(attrs);
495 }),
496
497 list: () =>
498 Effect.gen(function* () {
499 const env = yield* GcpEnvironment.current;
500 const organization = yield* tryResolveOrganization();
501 if (organization === undefined) return [];
502 const workloads = yield* listOwnedWorkloads(organization, env.region);
503 return workloads.map((workload) =>
504 toAttrs(workload, env.project, env.region),
505 );
506 }),
507
508 reconcile: Effect.fn(function* ({ id, news, output }) {
509 const env = yield* GcpEnvironment.current;
510 const organization = yield* resolveOrganization(
511 news.organization ?? output?.organization,
512 output?.organization,
513 );
514 const location = normalizeLocation(
515 news.location ?? output?.location ?? env.region,
516 );
517 const parent = locationParent(organization, location);
518 const desiredLabels = {
519 ...toLabels(news.labels),
520 ...(yield* createInternalLabels(id)),
521 };
522 const displayName = yield* toDisplayName(
523 id,
524 news.displayName,
525 output?.displayName,
526 );
527
528 let current = yield* observe(id, output?.name, organization, location);
529
530 if (current === undefined) {
531 const created = yield* assuredworkloads
532 .createOrganizationsLocationsWorkloads({
533 parent,
534 externalId: news.externalId,
535 body: desiredBody(news, displayName, desiredLabels),
536 })
537 .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
538 if (created !== undefined) {
539 // ALREADY_EXISTS (code 6) is a create race: find the winner below.
540 const settled = yield* waitForOperation(created).pipe(
541 Effect.catchIf(
542 (error) =>
543 error._tag === "GCP.OperationFailed" && error.code === 6,
544 () => Effect.succeed(created),
545 ),
546 );
547 const createdName =
548 resourceNameFromOperation(settled) ??
549 resourceNameFromOperation(created);
550 current =
551 createdName !== undefined
552 ? yield* waitUntilExists(getByName(createdName), createdName)
553 : yield* waitUntilExists(
554 findOwned(id, organization, location),
555 parent,
556 );
557 } else {
558 current = yield* waitUntilExists(
559 findOwned(id, organization, location),
560 parent,
561 );
562 }
563 }
564
565 if (current === undefined || current.name === undefined) {
566 return yield* new WorkloadNotResolved({
567 name: output?.name ?? parent,
568 });
569 }
570
571 const name = current.name;
572 const observedLabels = tagRecord(current.labels);
573 const { upsert, removed } = diffLabels(observedLabels, desiredLabels);
574 const labelsChanged = upsert.length > 0 || removed.length > 0;
575 const displayChanged = !sameText(current.displayName, displayName);
576 const notifyChanged =
577 news.violationNotificationsEnabled !== undefined &&
578 !sameBool(
579 current.violationNotificationsEnabled,
580 news.violationNotificationsEnabled,
581 );
582 const mask = fieldMask([
583 displayChanged && "display_name",
584 labelsChanged && "labels",
585 notifyChanged && "violation_notifications_enabled",
586 ]);
587
588 if (mask.length > 0) {
589 current = yield* assuredworkloads.patchOrganizationsLocationsWorkloads({
590 name,
591 updateMask: mask,
592 body: {
593 name,
594 displayName,
595 labels: desiredLabels,
596 violationNotificationsEnabled:
597 news.violationNotificationsEnabled ??
598 current.violationNotificationsEnabled,
599 etag: current.etag,
600 },
601 });
602 }
603
604 const partnerChanged =
605 news.partnerPermissions !== undefined &&
606 fingerprint(current.partnerPermissions) !==
607 fingerprint(news.partnerPermissions);
608 if (partnerChanged) {
609 current =
610 yield* assuredworkloads.mutatePartnerPermissionsOrganizationsLocationsWorkloads(
611 {
612 name,
613 body: {
614 etag: current.etag,
615 partnerPermissions: news.partnerPermissions,
616 updateMask: "partner_permissions",
617 },
618 },
619 );
620 }
621
622 if (current === undefined) {
623 return yield* new WorkloadNotResolved({ name });
624 }
625
626 return toAttrs(current, env.project, env.region);
627 }),
628
629 delete: Effect.fn(function* ({ output }) {
630 if (!output.name) return;
631 yield* Effect.gen(function* () {
632 const existing = yield* getByName(output.name);
633 if (existing !== undefined) {
634 yield* deleteChildResources(existing);
635 }
636 yield* assuredworkloads.deleteOrganizationsLocationsWorkloads({
637 name: output.name,
638 etag: output.etag,
639 });
640 }).pipe(
641 Effect.retry({
642 while: (error) =>
643 error._tag === "BadRequest" || error._tag === "Conflict",
644 times: 8,
645 schedule: Schedule.spaced("3 seconds"),
646 }),
647 Effect.catchTag("NotFound", () => Effect.void),
648 );
649 yield* waitUntilGone(getByName(output.name), output.name);
650 }),
651 });