1import * as organizations from "@distilled.cloud/aws/organizations";
2import * as Effect from "effect/Effect";
3import * as Provider from "../../Provider.ts";
4import { Resource } from "../../Resource.ts";
5import type { Providers } from "../Providers.ts";
6import { retryOrganizations, unredact } from "./common.ts";
7
8export type OrganizationId = string;
9export type OrganizationArn = string;
10
11export interface OrganizationProps {
12 /**
13 * Organization feature set.
14 * `ALL` unlocks the full AWS Organizations feature set.
15 * @default "ALL"
16 */
17 featureSet?: organizations.OrganizationFeatureSet;
18}
19
20export interface Organization extends Resource<
21 "AWS.Organizations.Organization",
22 OrganizationProps,
23 {
24 /**
25 * ID of the organization (e.g. `o-exampleorgid`).
26 */
27 organizationId: OrganizationId;
28 /**
29 * ARN of the organization.
30 */
31 organizationArn: OrganizationArn;
32 /**
33 * Feature set enabled on the organization (`ALL` or
34 * `CONSOLIDATED_BILLING`).
35 */
36 featureSet: organizations.OrganizationFeatureSet | undefined;
37 /**
38 * ARN of the management account.
39 */
40 managementAccountArn: string | undefined;
41 /**
42 * 12-digit ID of the management account.
43 */
44 managementAccountId: string | undefined;
45 /**
46 * Email address of the management account.
47 */
48 managementAccountEmail: string | undefined;
49 /**
50 * Policy types available to the organization.
51 */
52 availablePolicyTypes: organizations.PolicyTypeSummary[];
53 },
54 never,
55 Providers
56> {}
57
58/**
59 * The AWS Organization for the current management account.
60 *
61 * This is a singleton-style resource. If an organization already exists,
62 * Alchemy adopts and reconciles it instead of creating a second one.
63 * ### Creating An Organization
64 * **Example:** Full Features Organization
65 * ```typescript
66 * const organization = yield* Organization("Org", {
67 * featureSet: "ALL",
68 * });
69 * ```
70 *
71 * @resource
72 */
73export const Organization = Resource<Organization>(
74 "AWS.Organizations.Organization",
75);
76
77export const OrganizationProvider = () =>
78 Provider.effect(
79 Organization,
80 Effect.gen(function* () {
81 return {
82 stables: ["organizationId", "organizationArn", "managementAccountId"],
83 diff: Effect.fn(function* () {}),
84 read: Effect.fn(function* () {
85 const org = yield* readOrganization();
86 return org?.Id && org.Arn ? toAttrs(org) : undefined;
87 }),
88 // Account singleton: there is at most one organization per management
89 // account and no list API. `describeOrganization` (via `readOrganization`)
90 // returns the single org, or the typed `AWSOrganizationsNotInUseException`
91 // is caught to `undefined` when the account isn't a management account.
92 list: () =>
93 Effect.gen(function* () {
94 const org = yield* readOrganization();
95 return org?.Id && org.Arn ? [toAttrs(org)] : [];
96 }),
97 reconcile: Effect.fn(function* ({ news, session }) {
98 const desiredFeatureSet = news.featureSet ?? "ALL";
99
100 // Observe — fetch the live singleton organization. There's at most
101 // one organization per management account, and `read` may have
102 // surfaced an existing one for adoption.
103 let org = yield* readOrganization();
104
105 // Ensure — create the organization if it's missing. Tolerate
106 // `AlreadyInOrganizationException` as a race with a concurrent
107 // creator (e.g. another tool, or a peer reconciler).
108 if (!org) {
109 org = yield* retryOrganizations(
110 organizations.createOrganization({
111 FeatureSet: desiredFeatureSet,
112 }),
113 ).pipe(
114 Effect.map((response) => response.Organization),
115 Effect.catchTag("AlreadyInOrganizationException", () =>
116 readOrganization(),
117 ),
118 );
119 }
120
121 if (!org?.Id || !org.Arn) {
122 return yield* Effect.fail(
123 new Error("failed to resolve organization after reconcile"),
124 );
125 }
126
127 const orgArn = org.Arn;
128
129 // Sync feature set — observed ↔ desired. `ensureFeatureSet`
130 // handles the only legal transition (CONSOLIDATED_BILLING -> ALL)
131 // and rejects anything else.
132 org = yield* ensureFeatureSet({
133 desired: desiredFeatureSet,
134 current: org,
135 });
136
137 yield* session.note(orgArn);
138 return toAttrs(org);
139 }),
140 delete: Effect.fn(function* () {
141 yield* retryOrganizations(
142 organizations
143 .deleteOrganization({})
144 .pipe(
145 Effect.catchTag(
146 "AWSOrganizationsNotInUseException",
147 () => Effect.void,
148 ),
149 ),
150 );
151 }),
152 };
153 }),
154 );
155
156const toAttrs = (
157 org: organizations.Organization,
158): Organization["Attributes"] => ({
159 organizationId: org.Id ?? "",
160 organizationArn: org.Arn ?? "",
161 featureSet: org.FeatureSet,
162 managementAccountArn: org.MasterAccountArn,
163 managementAccountId: org.MasterAccountId,
164 managementAccountEmail: unredact(org.MasterAccountEmail),
165 availablePolicyTypes: org.AvailablePolicyTypes ?? [],
166});
167
168const readOrganization = () =>
169 retryOrganizations(
170 organizations.describeOrganization({}).pipe(
171 Effect.map((response) => response.Organization),
172 Effect.catchTag("AWSOrganizationsNotInUseException", () =>
173 Effect.succeed(undefined),
174 ),
175 ),
176 );
177
178const ensureFeatureSet = Effect.fn(function* ({
179 desired,
180 current,
181}: {
182 desired: organizations.OrganizationFeatureSet | undefined;
183 current: organizations.Organization;
184}) {
185 const desiredFeatureSet = desired ?? "ALL";
186 if (current.FeatureSet === desiredFeatureSet) {
187 return current;
188 }
189
190 if (
191 desiredFeatureSet === "ALL" &&
192 current.FeatureSet === "CONSOLIDATED_BILLING"
193 ) {
194 yield* retryOrganizations(organizations.enableAllFeatures({}));
195
196 const updated = yield* readOrganization();
197 if (updated?.FeatureSet === "ALL") {
198 return updated;
199 }
200
201 return yield* Effect.fail(
202 new Error(
203 "Organization upgrade to ALL features requires handshake completion before deployment can converge",
204 ),
205 );
206 }
207
208 return yield* Effect.fail(
209 new Error(
210 `Organization feature set cannot be changed from '${current.FeatureSet}' to '${desiredFeatureSet}'`,
211 ),
212 );
213});