| 1 | import * as account from "@distilled.cloud/aws/account"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Schedule from "effect/Schedule"; |
| 4 | import { isResolved } from "../../Diff.ts"; |
| 5 | import * as Provider from "../../Provider.ts"; |
| 6 | import { Resource } from "../../Resource.ts"; |
| 7 | import type { Providers } from "../Providers.ts"; |
| 8 | import { unwrapSensitive } from "./internal.ts"; |
| 9 | |
| 10 | export interface AccountNameProps { |
| 11 | /** |
| 12 | * The new name for the account. Between 1 and 50 characters. |
| 13 | */ |
| 14 | accountName: string; |
| 15 | /** |
| 16 | * Account ID to operate on. Only usable from an Organizations management or |
| 17 | * delegated-admin account with trusted access enabled; omit to target the |
| 18 | * calling account. |
| 19 | */ |
| 20 | accountId?: string; |
| 21 | } |
| 22 | |
| 23 | export interface AccountName extends Resource< |
| 24 | "AWS.Account.AccountName", |
| 25 | AccountNameProps, |
| 26 | { |
| 27 | /** The account's current name. */ |
| 28 | accountName: string; |
| 29 | /** The 12-digit AWS account ID the name belongs to. */ |
| 30 | accountId: string; |
| 31 | /** The state of the account, e.g. `ACTIVE`. */ |
| 32 | accountState?: string; |
| 33 | /** ISO-8601 timestamp of when the account was created. */ |
| 34 | accountCreatedDate?: string; |
| 35 | }, |
| 36 | never, |
| 37 | Providers |
| 38 | > {} |
| 39 | |
| 40 | /** |
| 41 | * The display name of an AWS account. Every account has exactly one name; this |
| 42 | * account-global singleton sets it via `account:PutAccountName`. Deleting the |
| 43 | * resource stops managing the name and leaves the last value in place (an |
| 44 | * account always has a name). |
| 45 | * |
| 46 | * ### Naming the Account |
| 47 | * **Example:** Set the Calling Account's Name |
| 48 | * ```typescript |
| 49 | * const name = yield* AccountName("Name", { |
| 50 | * accountName: "acme-prod", |
| 51 | * }); |
| 52 | * ``` |
| 53 | * |
| 54 | * **Example:** Rename an Organizations Member Account |
| 55 | * ```typescript |
| 56 | * const name = yield* AccountName("MemberName", { |
| 57 | * accountName: "acme-sandbox", |
| 58 | * accountId: "123456789012", |
| 59 | * }); |
| 60 | * ``` |
| 61 | * |
| 62 | * @resource |
| 63 | */ |
| 64 | export const AccountName = Resource<AccountName>("AWS.Account.AccountName"); |
| 65 | |
| 66 | export const AccountNameProvider = () => |
| 67 | Provider.effect( |
| 68 | AccountName, |
| 69 | Effect.gen(function* () { |
| 70 | const observe = (accountId: string | undefined) => |
| 71 | account.getAccountInformation({ AccountId: accountId }).pipe( |
| 72 | Effect.map((info) => ({ |
| 73 | accountName: unwrapSensitive(info.AccountName) ?? "", |
| 74 | accountId: info.AccountId ?? accountId ?? "", |
| 75 | accountState: info.AccountState, |
| 76 | accountCreatedDate: info.AccountCreatedDate?.toISOString(), |
| 77 | })), |
| 78 | ); |
| 79 | |
| 80 | return { |
| 81 | // Account-global singleton setting: nuke must not rename accounts. |
| 82 | nuke: { skip: true }, |
| 83 | stables: [], |
| 84 | // Targeting a different account manages a different singleton, so an |
| 85 | // accountId change replaces rather than renaming the old target. |
| 86 | diff: Effect.fn(function* ({ olds, news }) { |
| 87 | if (!isResolved(news)) return; |
| 88 | if ((olds?.accountId ?? news.accountId) !== news.accountId) { |
| 89 | return { action: "replace" } as const; |
| 90 | } |
| 91 | }), |
| 92 | read: Effect.fn(function* ({ olds }) { |
| 93 | return yield* observe(olds?.accountId); |
| 94 | }), |
| 95 | reconcile: Effect.fn(function* ({ news, session }) { |
| 96 | yield* account.putAccountName({ |
| 97 | AccountName: news.accountName, |
| 98 | AccountId: news.accountId, |
| 99 | }); |
| 100 | // putAccountName is eventually consistent: getAccountInformation |
| 101 | // can keep returning the previous name for ~20 seconds. Poll |
| 102 | // (bounded) until the rename is observable; if it is still stale |
| 103 | // after the budget, report the desired name — the put succeeded, |
| 104 | // so it is authoritative. |
| 105 | const observed = yield* observe(news.accountId).pipe( |
| 106 | Effect.repeat({ |
| 107 | schedule: Schedule.spaced("3 seconds"), |
| 108 | until: (o): boolean => o.accountName === news.accountName, |
| 109 | times: 10, |
| 110 | }), |
| 111 | ); |
| 112 | yield* session.note(news.accountName); |
| 113 | return observed.accountName === news.accountName |
| 114 | ? observed |
| 115 | : { ...observed, accountName: news.accountName }; |
| 116 | }), |
| 117 | // Account-global singleton: the one name always exists. |
| 118 | list: () => |
| 119 | observe(undefined).pipe(Effect.map((observed) => [observed])), |
| 120 | // An AWS account always has a name — there is nothing to delete. |
| 121 | // Destroy just stops managing it, mirroring the console behavior. |
| 122 | delete: Effect.fn(function* () {}), |
| 123 | }; |
| 124 | }), |
| 125 | ); |