1import * as appconfig from "@distilled.cloud/aws/appconfig";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import { createPhysicalName } from "../../PhysicalName.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { createInternalTags, hasAlchemyTags } from "../../Tags.ts";
10import { AWSEnvironment } from "../Environment.ts";
11import type { Providers } from "../Providers.ts";
12import {
13 environmentArn,
14 readAppConfigTags,
15 syncAppConfigTags,
16} from "./internal.ts";
17
18/**
19 * A CloudWatch alarm AppConfig monitors during a deployment. If the alarm
20 * fires while the configuration is rolling out, the deployment rolls back.
21 */
22export interface EnvironmentMonitor {
23 /** ARN of the CloudWatch alarm to monitor. */
24 alarmArn: string;
25 /** ARN of an IAM role AppConfig assumes to read the alarm's state. */
26 alarmRoleArn?: string;
27}
28
29export interface EnvironmentProps {
30 /**
31 * ID of the application this environment belongs to. Changing it replaces
32 * the environment.
33 */
34 applicationId: string;
35 /**
36 * Name of the environment. Must be 1-64 characters. If omitted, a
37 * deterministic physical name is generated. Changing the name replaces the
38 * environment.
39 */
40 environmentName?: string;
41 /**
42 * Description of the environment.
43 */
44 description?: string;
45 /**
46 * CloudWatch alarms AppConfig monitors during deployments to this
47 * environment.
48 */
49 monitors?: EnvironmentMonitor[];
50 /**
51 * User-defined tags for the environment.
52 */
53 tags?: Record<string, string>;
54}
55
56export interface Environment extends Resource<
57 "AWS.AppConfig.Environment",
58 EnvironmentProps,
59 {
60 environmentId: string;
61 environmentName: string;
62 applicationId: string;
63 environmentArn: string;
64 state: string;
65 },
66 never,
67 Providers
68> {}
69
70/**
71 * An AWS AppConfig environment — a deployment group of AppConfig targets
72 * (e.g. `Beta`, `Production`) within an application. CloudWatch alarms
73 * attached via `monitors` trigger an automatic rollback if they fire during a
74 * deployment.
75 *
76 * ### Creating an Environment
77 * **Example:** Basic Environment
78 * ```typescript
79 * const app = yield* AppConfig.Application("MyApp", {});
80 * const env = yield* AppConfig.Environment("Prod", {
81 * applicationId: app.applicationId,
82 * });
83 * ```
84 *
85 * **Example:** Environment with Rollback Alarm
86 * ```typescript
87 * const env = yield* AppConfig.Environment("Prod", {
88 * applicationId: app.applicationId,
89 * monitors: [{ alarmArn: alarm.alarmArn, alarmRoleArn: role.roleArn }],
90 * });
91 * ```
92 *
93 * @resource
94 */
95export const Environment = Resource<Environment>("AWS.AppConfig.Environment");
96
97const toWireMonitors = (
98 monitors: EnvironmentMonitor[] | undefined,
99): appconfig.Monitor[] | undefined =>
100 monitors?.map((m) => ({
101 AlarmArn: m.alarmArn,
102 AlarmRoleArn: m.alarmRoleArn,
103 }));
104
105export const EnvironmentProvider = () =>
106 Provider.effect(
107 Environment,
108 Effect.gen(function* () {
109 const toName = (id: string, props: Partial<EnvironmentProps>) =>
110 props.environmentName
111 ? Effect.succeed(props.environmentName)
112 : createPhysicalName({ id, maxLength: 64 });
113
114 const readEnvironment = Effect.fn(function* (
115 applicationId: string,
116 environmentId: string,
117 ) {
118 return yield* appconfig
119 .getEnvironment({
120 ApplicationId: applicationId,
121 EnvironmentId: environmentId,
122 })
123 .pipe(
124 Effect.catchTag("ResourceNotFoundException", () =>
125 Effect.succeed(undefined),
126 ),
127 );
128 });
129
130 const findByName = Effect.fn(function* (
131 applicationId: string,
132 name: string,
133 ) {
134 const envs = yield* appconfig.listEnvironments
135 .pages({ ApplicationId: applicationId })
136 .pipe(
137 Stream.runCollect,
138 Effect.map((chunk) =>
139 Array.from(chunk).flatMap((page) => page.Items ?? []),
140 ),
141 Effect.catchTag("ResourceNotFoundException", () =>
142 Effect.succeed([] as appconfig.Environment[]),
143 ),
144 );
145 return envs.find((e) => e.Name === name);
146 });
147
148 return {
149 stables: [
150 "environmentId",
151 "environmentName",
152 "applicationId",
153 "environmentArn",
154 ],
155
156 diff: Effect.fn(function* ({ id, olds, news }) {
157 if (!isResolved(news)) return undefined;
158 if (
159 (yield* toName(id, olds ?? {})) !== (yield* toName(id, news ?? {}))
160 ) {
161 return { action: "replace" } as const;
162 }
163 if ((olds?.applicationId ?? undefined) !== news?.applicationId) {
164 return { action: "replace" } as const;
165 }
166 }),
167
168 read: Effect.fn(function* ({ id, olds, output }) {
169 const { accountId, region } = yield* AWSEnvironment.current;
170 const applicationId = output?.applicationId ?? olds?.applicationId;
171 if (applicationId === undefined) return undefined;
172 const env = output?.environmentId
173 ? yield* readEnvironment(applicationId, output.environmentId)
174 : yield* findByName(applicationId, yield* toName(id, olds ?? {}));
175 if (env?.Id === undefined) return undefined;
176 const arn = environmentArn(region, accountId, applicationId, env.Id);
177 const attrs = {
178 environmentId: env.Id,
179 environmentName: env.Name!,
180 applicationId,
181 environmentArn: arn,
182 state: env.State ?? "",
183 };
184 const tags = yield* readAppConfigTags(arn);
185 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
186 }),
187
188 reconcile: Effect.fn(function* ({ id, news, output, session }) {
189 const { accountId, region } = yield* AWSEnvironment.current;
190 const applicationId = news.applicationId;
191 const name = output?.environmentName ?? (yield* toName(id, news));
192 const internalTags = yield* createInternalTags(id);
193 const desiredTags = { ...internalTags, ...news.tags };
194
195 // 1. Observe.
196 let observed = output?.environmentId
197 ? yield* readEnvironment(applicationId, output.environmentId)
198 : undefined;
199 if (observed === undefined) {
200 observed = yield* findByName(applicationId, name);
201 }
202
203 // 2. Ensure.
204 if (observed?.Id === undefined) {
205 observed = yield* appconfig.createEnvironment({
206 ApplicationId: applicationId,
207 Name: name,
208 Description: news.description,
209 Monitors: toWireMonitors(news.monitors),
210 Tags: desiredTags,
211 });
212 } else {
213 // 3. Sync — description and monitors are mutable in place.
214 observed = yield* appconfig.updateEnvironment({
215 ApplicationId: applicationId,
216 EnvironmentId: observed.Id,
217 Description: news.description,
218 Monitors: toWireMonitors(news.monitors),
219 });
220 }
221
222 const arn = environmentArn(
223 region,
224 accountId,
225 applicationId,
226 observed.Id!,
227 );
228
229 // 3b. Sync tags.
230 yield* syncAppConfigTags(arn, desiredTags);
231
232 yield* session.note(name);
233 return {
234 environmentId: observed.Id!,
235 environmentName: observed.Name ?? name,
236 applicationId,
237 environmentArn: arn,
238 state: observed.State ?? "",
239 };
240 }),
241
242 delete: Effect.fn(function* ({ output }) {
243 yield* appconfig
244 .deleteEnvironment({
245 ApplicationId: output.applicationId,
246 EnvironmentId: output.environmentId,
247 })
248 .pipe(
249 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
250 );
251 }),
252
253 // Environments are keyed under their parent application, so
254 // enumeration walks every application and lists its environments.
255 // An application cannot be deleted while environments exist under
256 // it, so nuke needs these enumerated as first-class resources.
257 list: () =>
258 Effect.gen(function* () {
259 const { accountId, region } = yield* AWSEnvironment.current;
260 const apps = yield* appconfig.listApplications.pages({}).pipe(
261 Stream.runCollect,
262 Effect.map((chunk) =>
263 Array.from(chunk).flatMap((page) => page.Items ?? []),
264 ),
265 );
266 const results: {
267 environmentId: string;
268 environmentName: string;
269 applicationId: string;
270 environmentArn: string;
271 state: string;
272 }[] = [];
273 for (const app of apps) {
274 if (app.Id === undefined) continue;
275 const envs = yield* appconfig.listEnvironments
276 .pages({ ApplicationId: app.Id })
277 .pipe(
278 Stream.runCollect,
279 Effect.map((chunk) =>
280 Array.from(chunk).flatMap((page) => page.Items ?? []),
281 ),
282 // The application may be deleted between the two calls.
283 Effect.catchTag("ResourceNotFoundException", () =>
284 Effect.succeed([] as appconfig.Environment[]),
285 ),
286 );
287 for (const env of envs) {
288 if (env.Id === undefined || env.Name === undefined) continue;
289 results.push({
290 environmentId: env.Id,
291 environmentName: env.Name,
292 applicationId: app.Id,
293 environmentArn: environmentArn(
294 region,
295 accountId,
296 app.Id,
297 env.Id,
298 ),
299 state: env.State ?? "",
300 });
301 }
302 }
303 return results;
304 }),
305 };
306 }),
307 );