1import * as controltower from "@distilled.cloud/aws/controltower";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Schedule from "effect/Schedule";
5import * as Stream from "effect/Stream";
6import { Unowned } from "../../AdoptPolicy.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { createInternalTags, hasAlchemyTags } from "../../Tags.ts";
10import type { Providers } from "../Providers.ts";
11import {
12 canonicalJson,
13 observeControlTowerTags,
14 syncControlTowerTags,
15} from "./internal.ts";
16
17export interface LandingZoneProps {
18 /**
19 * The landing zone version, e.g. `"3.3"`. Updating the version upgrades
20 * the landing zone in place (an asynchronous operation that can take an
21 * hour or more).
22 */
23 version: string;
24 /**
25 * The landing zone manifest JSON document — the full configuration of
26 * the landing zone (governed regions, organization structure, logging
27 * and access-management settings). See the AWS Control Tower User Guide
28 * for the manifest schema.
29 */
30 manifest: Record<string, unknown>;
31 /**
32 * Remediation types enabled for the landing zone, e.g.
33 * `["INHERITANCE_DRIFT"]`.
34 */
35 remediationTypes?: string[];
36 /**
37 * Tags to apply to the landing zone. Merged with internal Alchemy tags.
38 */
39 tags?: Record<string, string>;
40}
41
42export interface LandingZone extends Resource<
43 "AWS.ControlTower.LandingZone",
44 LandingZoneProps,
45 {
46 /**
47 * The unique identifier of the landing zone (its ARN).
48 */
49 landingZoneIdentifier: string;
50 /**
51 * The ARN of the landing zone.
52 */
53 landingZoneArn: string;
54 /**
55 * The deployed landing zone version.
56 */
57 version: string;
58 /**
59 * The landing zone deployment status (`ACTIVE`, `PROCESSING`,
60 * `FAILED`).
61 */
62 status: string | undefined;
63 /**
64 * The most recent landing zone version available for upgrade.
65 */
66 latestAvailableVersion: string | undefined;
67 /**
68 * The landing zone drift status (`IN_SYNC` or `DRIFTED`).
69 */
70 driftStatus: string | undefined;
71 },
72 never,
73 Providers
74> {}
75
76/**
77 * An AWS Control Tower landing zone — the org-wide multi-account
78 * environment (organization structure, governed regions, centralized
79 * logging, and access management) that Control Tower governs.
80 *
81 * A landing zone is a singleton per AWS Organization and can only be
82 * managed from the Organizations management account. Creating, updating,
83 * and decommissioning a landing zone are asynchronous operations that can
84 * take an hour or more.
85 * ### Creating a Landing Zone
86 * **Example:** Landing Zone from a manifest
87 * ```typescript
88 * import * as ControlTower from "alchemy/AWS/ControlTower";
89 *
90 * const landingZone = yield* ControlTower.LandingZone("LandingZone", {
91 * version: "3.3",
92 * manifest: {
93 * governedRegions: ["us-east-1", "us-west-2"],
94 * organizationStructure: {
95 * security: { name: "Security" },
96 * sandbox: { name: "Sandbox" },
97 * },
98 * centralizedLogging: {
99 * accountId: "111122223333",
100 * configurations: {
101 * loggingBucket: { retentionDays: 365 },
102 * accessLoggingBucket: { retentionDays: 365 },
103 * },
104 * enabled: true,
105 * },
106 * securityRoles: { accountId: "444455556666" },
107 * accessManagement: { enabled: true },
108 * },
109 * });
110 * ```
111 *
112 * ### Upgrading
113 * **Example:** Upgrade the landing zone version
114 * ```typescript
115 * const landingZone = yield* ControlTower.LandingZone("LandingZone", {
116 * version: "3.3", // bump to upgrade in place
117 * manifest,
118 * });
119 * ```
120 *
121 * @resource
122 */
123export const LandingZone = Resource<LandingZone>(
124 "AWS.ControlTower.LandingZone",
125);
126
127/**
128 * An asynchronous landing zone operation (CREATE / UPDATE / DELETE /
129 * RESET) converged to the terminal `FAILED` status.
130 */
131export class LandingZoneOperationFailed extends Data.TaggedError(
132 "LandingZoneOperationFailed",
133)<{
134 readonly operationIdentifier: string;
135 readonly status: string;
136 readonly statusMessage: string | undefined;
137}> {}
138
139/**
140 * Internal signal that a landing zone operation is still `IN_PROGRESS`,
141 * consumed by {@link waitForLandingZoneOperation}'s bounded schedule.
142 */
143class LandingZoneOperationPending extends Data.TaggedError(
144 "LandingZoneOperationPending",
145)<{
146 readonly operationIdentifier: string;
147 readonly status: string | undefined;
148}> {}
149
150// Explicitly-typed retry wrapper — an inline `Effect.retry` in provider
151// lifecycle code leaks `Retry.Return`'s conditional type into declaration
152// emit and widens the provider layer to `unknown` for every consumer of
153// `AWS.providers()`.
154const retryWhileLandingZoneOperationPending = <
155 A,
156 E extends { readonly _tag: string },
157 R,
158>(
159 self: Effect.Effect<A, E, R>,
160): Effect.Effect<A, E, R> =>
161 Effect.retry(self, {
162 while: (e) => e._tag === "LandingZoneOperationPending",
163 // Landing zone operations routinely take ~60 minutes; poll every 30s
164 // up to 90 minutes (bounded).
165 schedule: Schedule.max([
166 Schedule.spaced("30 seconds"),
167 Schedule.recurs(180),
168 ]),
169 });
170
171const waitForLandingZoneOperation = (operationIdentifier: string) =>
172 retryWhileLandingZoneOperationPending(
173 Effect.gen(function* () {
174 const { operationDetails } = yield* controltower.getLandingZoneOperation({
175 operationIdentifier,
176 });
177 if (operationDetails.status === "SUCCEEDED") {
178 return;
179 }
180 if (operationDetails.status === "FAILED") {
181 return yield* Effect.fail(
182 new LandingZoneOperationFailed({
183 operationIdentifier,
184 status: operationDetails.status,
185 statusMessage: operationDetails.statusMessage,
186 }),
187 );
188 }
189 return yield* Effect.fail(
190 new LandingZoneOperationPending({
191 operationIdentifier,
192 status: operationDetails.status,
193 }),
194 );
195 }),
196 );
197
198// A landing zone is a singleton per organization — `listLandingZones`
199// returns at most one ARN.
200const findLandingZoneArn = controltower.listLandingZones.pages({}).pipe(
201 Stream.runCollect,
202 Effect.map((chunk) =>
203 Array.from(chunk)
204 .flatMap((page) => page.landingZones)
205 .map((zone) => zone.arn)
206 .find((arn): arn is string => arn !== undefined),
207 ),
208);
209
210const readLandingZone = (landingZoneIdentifier: string) =>
211 controltower.getLandingZone({ landingZoneIdentifier }).pipe(
212 Effect.map((r) => r.landingZone),
213 Effect.catchTag("ResourceNotFoundException", () =>
214 Effect.succeed(undefined),
215 ),
216 );
217
218const toAttributes = (
219 arn: string,
220 detail: controltower.LandingZoneDetail,
221): LandingZone["Attributes"] => ({
222 landingZoneIdentifier: arn,
223 landingZoneArn: arn,
224 version: detail.version,
225 status: detail.status,
226 latestAvailableVersion: detail.latestAvailableVersion,
227 driftStatus: detail.driftStatus?.status,
228});
229
230export const LandingZoneProvider = () =>
231 Provider.effect(
232 LandingZone,
233 Effect.gen(function* () {
234 return LandingZone.Provider.of({
235 stables: ["landingZoneIdentifier", "landingZoneArn"],
236 // Decommissioning a landing zone dismantles org-wide governance
237 // (an irreversible, ~1 hour operation) — never do it from `nuke`.
238 nuke: { skip: true },
239 list: () =>
240 Effect.gen(function* () {
241 const arn = yield* findLandingZoneArn;
242 if (arn === undefined) return [];
243 const detail = yield* readLandingZone(arn);
244 if (detail === undefined) return [];
245 return [toAttributes(arn, detail)];
246 }),
247 read: Effect.fn(function* ({ id, output }) {
248 const arn = output?.landingZoneArn ?? (yield* findLandingZoneArn);
249 if (arn === undefined) return undefined;
250 const detail = yield* readLandingZone(arn);
251 if (detail === undefined) return undefined;
252 const attrs = toAttributes(arn, detail);
253 const tags = yield* observeControlTowerTags(arn);
254 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
255 }),
256 reconcile: Effect.fn(function* ({ id, news, output, session }) {
257 // 1. Observe — the landing zone is an org singleton; enumerate
258 // rather than trusting a cached identifier.
259 let arn = output?.landingZoneArn ?? (yield* findLandingZoneArn);
260 let detail =
261 arn === undefined ? undefined : yield* readLandingZone(arn);
262
263 // 2. Ensure — create if missing and wait for the asynchronous
264 // operation to converge.
265 if (detail === undefined) {
266 const internalTags = yield* createInternalTags(id);
267 const created = yield* controltower.createLandingZone({
268 version: news.version,
269 manifest: news.manifest,
270 remediationTypes: news.remediationTypes,
271 tags: { ...news.tags, ...internalTags },
272 });
273 arn = created.arn;
274 yield* session.note(
275 `landing zone operation ${created.operationIdentifier}`,
276 );
277 yield* waitForLandingZoneOperation(created.operationIdentifier);
278 detail = yield* readLandingZone(arn);
279 } else {
280 // 3. Sync — version / manifest / remediation types are updated
281 // in place; diff observed cloud state against desired and
282 // skip the (very slow) update API on a no-op.
283 const versionChanged = detail.version !== news.version;
284 const manifestChanged =
285 canonicalJson(detail.manifest ?? {}) !==
286 canonicalJson(news.manifest);
287 const remediationChanged =
288 news.remediationTypes !== undefined &&
289 canonicalJson([...(detail.remediationTypes ?? [])].sort()) !==
290 canonicalJson([...news.remediationTypes].sort());
291 if (versionChanged || manifestChanged || remediationChanged) {
292 const updated = yield* controltower.updateLandingZone({
293 landingZoneIdentifier: arn!,
294 version: news.version,
295 manifest: news.manifest,
296 remediationTypes: news.remediationTypes,
297 });
298 yield* session.note(
299 `landing zone operation ${updated.operationIdentifier}`,
300 );
301 yield* waitForLandingZoneOperation(updated.operationIdentifier);
302 detail = yield* readLandingZone(arn!);
303 }
304 }
305
306 // 3b. Sync tags against observed cloud tags.
307 yield* syncControlTowerTags(arn!, id, news.tags);
308
309 // 4. Return fresh attributes.
310 return detail === undefined
311 ? {
312 landingZoneIdentifier: arn!,
313 landingZoneArn: arn!,
314 version: news.version,
315 status: undefined,
316 latestAvailableVersion: undefined,
317 driftStatus: undefined,
318 }
319 : toAttributes(arn!, detail);
320 }),
321 delete: Effect.fn(function* ({ output, session }) {
322 const result = yield* controltower
323 .deleteLandingZone({
324 landingZoneIdentifier: output.landingZoneArn,
325 })
326 .pipe(
327 Effect.catchTag("ResourceNotFoundException", () =>
328 Effect.succeed(undefined),
329 ),
330 );
331 if (result !== undefined) {
332 yield* session.note(
333 `landing zone operation ${result.operationIdentifier}`,
334 );
335 yield* waitForLandingZoneOperation(result.operationIdentifier);
336 }
337 }),
338 });
339 }),
340 );