1import * as account from "@distilled.cloud/aws/account";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import * as Stream from "effect/Stream";
5import { isResolved } from "../../Diff.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import type { Providers } from "../Providers.ts";
9
10/**
11 * Opt-in status of a Region as reported by `account:GetRegionOptStatus`:
12 * `ENABLED`, `ENABLING`, `DISABLING`, `DISABLED`, or `ENABLED_BY_DEFAULT`.
13 */
14export type RegionOptStatus = string;
15
16const isEnabledStatus = (status: string | undefined): boolean =>
17 status === "ENABLED" || status === "ENABLED_BY_DEFAULT";
18
19const isTerminalStatus = (status: string | undefined): boolean =>
20 status !== "ENABLING" && status !== "DISABLING";
21
22export interface RegionProps {
23 /**
24 * Name of the Region to manage, e.g. `ap-east-1`. Changing the Region
25 * replaces the resource.
26 */
27 regionName: string;
28 /**
29 * Whether the Region should be opted in (enabled) for the account. Regions
30 * that are `ENABLED_BY_DEFAULT` cannot be disabled — attempting to disable
31 * one fails with a `ValidationException`.
32 */
33 enabled: boolean;
34 /**
35 * Account ID to operate on. Only usable from an Organizations management or
36 * delegated-admin account with trusted access enabled; omit to target the
37 * calling account.
38 */
39 accountId?: string;
40}
41
42export interface Region extends Resource<
43 "AWS.Account.Region",
44 RegionProps,
45 {
46 /** Name of the Region, e.g. `ap-east-1`. */
47 regionName: string;
48 /** Whether the Region is currently enabled for the account. */
49 enabled: boolean;
50 /**
51 * Observed opt-in status: `ENABLED`, `ENABLING`, `DISABLING`, `DISABLED`,
52 * or `ENABLED_BY_DEFAULT`.
53 */
54 regionOptStatus: RegionOptStatus;
55 },
56 never,
57 Providers
58> {}
59
60/**
61 * The opt-in status of an AWS Region for an account. Opt-in Regions (e.g.
62 * `ap-east-1`, `me-south-1`) are disabled by default and must be enabled
63 * before use; this resource drives `account:EnableRegion` /
64 * `account:DisableRegion` to converge the Region to the desired status.
65 *
66 * Enabling or disabling a Region is asynchronous and can take several
67 * minutes; reconcile waits (bounded) for the transition to settle and records
68 * the observed status either way. Disabling a Region removes all IAM access
69 * to resources in it, so destroying this resource intentionally does NOT
70 * disable the Region — it only stops managing it (set `enabled: false`
71 * explicitly to opt out).
72 *
73 * ### Managing Region Opt-In
74 * **Example:** Enable an Opt-In Region
75 * ```typescript
76 * const region = yield* Account.Region("HongKong", {
77 * regionName: "ap-east-1",
78 * enabled: true,
79 * });
80 * ```
81 *
82 * **Example:** Track a Default Region
83 * ```typescript
84 * const region = yield* Account.Region("UsEast1", {
85 * regionName: "us-east-1",
86 * enabled: true, // ENABLED_BY_DEFAULT — reconcile makes no API mutation
87 * });
88 * ```
89 *
90 * **Example:** Opt Out of a Region
91 * ```typescript
92 * const region = yield* Account.Region("HongKong", {
93 * regionName: "ap-east-1",
94 * enabled: false,
95 * });
96 * ```
97 *
98 * @resource
99 */
100export const Region = Resource<Region>("AWS.Account.Region");
101
102export const RegionProvider = () =>
103 Provider.effect(
104 Region,
105 Effect.gen(function* () {
106 const observe = (regionName: string, accountId: string | undefined) =>
107 account
108 .getRegionOptStatus({ RegionName: regionName, AccountId: accountId })
109 .pipe(Effect.map((r) => r.RegionOptStatus));
110
111 // Region opt-in/out is asynchronous (typically a few minutes). Wait,
112 // bounded, for an in-flight transition to settle; if it is still
113 // transitioning after the budget, return the transitional status —
114 // re-running reconcile converges.
115 const settle = (regionName: string, accountId: string | undefined) =>
116 observe(regionName, accountId).pipe(
117 Effect.repeat({
118 schedule: Schedule.spaced("10 seconds"),
119 until: (status): boolean => isTerminalStatus(status),
120 times: 24,
121 }),
122 );
123
124 const toAttributes = (
125 regionName: string,
126 status: string | undefined,
127 ) => ({
128 regionName,
129 enabled: isEnabledStatus(status),
130 regionOptStatus: status ?? "DISABLED",
131 });
132
133 return {
134 // Disabling a Region removes IAM access to everything in it — far too
135 // destructive for an automatic nuke of an account-global setting.
136 nuke: { skip: true },
137 stables: [],
138 // The Region name is the identity of this account-global slot, so a
139 // different Region (or target account) replaces the resource.
140 diff: Effect.fn(function* ({ olds, news }) {
141 if (!isResolved(news)) return;
142 if (
143 (olds?.regionName ?? news.regionName) !== news.regionName ||
144 (olds?.accountId ?? news.accountId) !== news.accountId
145 ) {
146 return { action: "replace" } as const;
147 }
148 }),
149 read: Effect.fn(function* ({ olds, output }) {
150 const regionName = output?.regionName ?? olds?.regionName;
151 if (!regionName) return undefined;
152 const status = yield* observe(regionName, olds?.accountId);
153 return toAttributes(regionName, status);
154 }),
155 reconcile: Effect.fn(function* ({ news, session }) {
156 let status = yield* settle(news.regionName, news.accountId);
157 if (news.enabled && status === "DISABLED") {
158 yield* account
159 .enableRegion({
160 RegionName: news.regionName,
161 AccountId: news.accountId,
162 })
163 .pipe(
164 // A concurrent transition already in flight — settle below
165 // observes whatever it converges to.
166 Effect.catchTag("ConflictException", () => Effect.void),
167 );
168 status = yield* settle(news.regionName, news.accountId);
169 } else if (!news.enabled && isEnabledStatus(status)) {
170 // Disabling an ENABLED_BY_DEFAULT Region is rejected by AWS with
171 // a typed ValidationException — let it propagate.
172 yield* account
173 .disableRegion({
174 RegionName: news.regionName,
175 AccountId: news.accountId,
176 })
177 .pipe(Effect.catchTag("ConflictException", () => Effect.void));
178 status = yield* settle(news.regionName, news.accountId);
179 }
180 yield* session.note(`${news.regionName}:${status ?? "UNKNOWN"}`);
181 return toAttributes(news.regionName, status);
182 }),
183 // Enumerate the opt-in Regions (every status except
184 // ENABLED_BY_DEFAULT — default Regions are not manageable slots).
185 list: () =>
186 account.listRegions
187 .items({
188 RegionOptStatusContains: [
189 "ENABLED",
190 "ENABLING",
191 "DISABLING",
192 "DISABLED",
193 ],
194 })
195 .pipe(
196 Stream.runCollect,
197 Effect.map((regions) =>
198 Array.from(regions)
199 .filter((region) => region.RegionName != null)
200 .map((region) =>
201 toAttributes(region.RegionName!, region.RegionOptStatus),
202 ),
203 ),
204 ),
205 // Destroy intentionally leaves the Region's opt-in status untouched:
206 // disabling a Region removes IAM access to all resources in it. Users
207 // opt out explicitly with `enabled: false` before removing the
208 // resource if that is what they want.
209 delete: Effect.fn(function* () {}),
210 };
211 }),
212 );