1import * as codedeploy from "@distilled.cloud/aws/codedeploy";
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, diffTags, hasAlchemyTags } from "../../Tags.ts";
10import { AWSEnvironment } from "../Environment.ts";
11import type { Providers } from "../Providers.ts";
12
13export interface ApplicationProps {
14 /**
15 * Name of the application (1-100 chars). If omitted a deterministic physical
16 * name is generated. Changing the name replaces the application.
17 */
18 applicationName?: string;
19 /**
20 * The compute platform the application deploys to. Immutable — changing it
21 * replaces the application.
22 * @default "Server"
23 */
24 computePlatform?: "Server" | "Lambda" | "ECS";
25 /**
26 * User-defined tags.
27 */
28 tags?: Record<string, string>;
29}
30
31export interface Application extends Resource<
32 "AWS.CodeDeploy.Application",
33 ApplicationProps,
34 {
35 /** Physical name of the application. */
36 applicationName: string;
37 /** Unique CodeDeploy-assigned application ID. */
38 applicationId: string;
39 /** ARN of the application. */
40 applicationArn: string;
41 /** The compute platform (`Lambda`, `Server`, or `ECS`). */
42 computePlatform: string;
43 },
44 never,
45 Providers
46> {}
47
48/**
49 * An AWS CodeDeploy application — a logical container that groups the
50 * deployment groups and revisions for a single deployable unit on a given
51 * compute platform (EC2/on-prem `Server`, `Lambda`, or `ECS`).
52 *
53 * ### Creating an Application
54 * **Example:** Lambda Application
55 * ```typescript
56 * const app = yield* CodeDeploy.Application("api", {
57 * computePlatform: "Lambda",
58 * });
59 * ```
60 *
61 * **Example:** EC2/On-Premises Application
62 * ```typescript
63 * const app = yield* CodeDeploy.Application("web", {
64 * applicationName: "web-fleet",
65 * computePlatform: "Server",
66 * tags: { team: "platform" },
67 * });
68 * ```
69 *
70 * @resource
71 */
72export const Application = Resource<Application>("AWS.CodeDeploy.Application");
73
74/** Build the ARN for a CodeDeploy application. */
75const applicationArn = (
76 region: string,
77 account: string,
78 name: string,
79): string => `arn:aws:codedeploy:${region}:${account}:application:${name}`;
80
81/** Convert a CodeDeploy wire tag list into a plain record. */
82const toTagRecord = (
83 tags: ReadonlyArray<{ Key?: string; Value?: string }> | undefined,
84): Record<string, string> =>
85 Object.fromEntries(
86 (tags ?? [])
87 .filter(
88 (tag): tag is { Key: string; Value: string } =>
89 typeof tag.Key === "string" && typeof tag.Value === "string",
90 )
91 .map((tag) => [tag.Key, tag.Value]),
92 );
93
94export const ApplicationProvider = () =>
95 Provider.effect(
96 Application,
97 Effect.gen(function* () {
98 const toName = (id: string, props: Partial<ApplicationProps>) =>
99 props.applicationName
100 ? Effect.succeed(props.applicationName)
101 : createPhysicalName({ id, maxLength: 100 });
102
103 const getApplication = Effect.fn(function* (name: string) {
104 const response = yield* codedeploy
105 .getApplication({ applicationName: name })
106 .pipe(
107 Effect.catchTag("ApplicationDoesNotExistException", () =>
108 Effect.succeed(undefined),
109 ),
110 );
111 return response?.application;
112 });
113
114 const syncTags = Effect.fn(function* (
115 arn: string,
116 desiredTags: Record<string, string>,
117 ) {
118 const observed = yield* codedeploy
119 .listTagsForResource({ ResourceArn: arn })
120 .pipe(Effect.catch(() => Effect.succeed(undefined)));
121 const { removed, upsert } = diffTags(
122 toTagRecord(observed?.Tags),
123 desiredTags,
124 );
125 if (upsert.length > 0) {
126 yield* codedeploy.tagResource({ ResourceArn: arn, Tags: upsert });
127 }
128 if (removed.length > 0) {
129 yield* codedeploy.untagResource({
130 ResourceArn: arn,
131 TagKeys: removed,
132 });
133 }
134 });
135
136 return {
137 stables: ["applicationName", "applicationId", "computePlatform"],
138
139 diff: Effect.fn(function* ({ id, olds, news }) {
140 if (!isResolved(news)) return undefined;
141 if (
142 (yield* toName(id, olds ?? {})) !== (yield* toName(id, news ?? {}))
143 ) {
144 return { action: "replace" } as const;
145 }
146 // Compute platform is immutable — replace on change.
147 if (
148 (news?.computePlatform ?? "Server") !==
149 (olds?.computePlatform ?? "Server")
150 ) {
151 return { action: "replace" } as const;
152 }
153 }),
154
155 read: Effect.fn(function* ({ id, olds, output }) {
156 const { accountId, region } = yield* AWSEnvironment.current;
157 const name =
158 output?.applicationName ?? (yield* toName(id, olds ?? {}));
159 const application = yield* getApplication(name);
160 if (application?.applicationId === undefined) return undefined;
161 const arn = applicationArn(region, accountId, name);
162 const attrs = {
163 applicationName: application.applicationName ?? name,
164 applicationId: application.applicationId,
165 applicationArn: arn,
166 computePlatform: application.computePlatform ?? "Server",
167 };
168 const tags = yield* codedeploy
169 .listTagsForResource({ ResourceArn: arn })
170 .pipe(
171 Effect.map((res) => toTagRecord(res.Tags)),
172 Effect.catch(() => Effect.succeed({})),
173 );
174 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
175 }),
176
177 reconcile: Effect.fn(function* ({ id, news, output, session }) {
178 const { accountId, region } = yield* AWSEnvironment.current;
179 const name = output?.applicationName ?? (yield* toName(id, news));
180 const arn = applicationArn(region, accountId, name);
181 const computePlatform = news.computePlatform ?? "Server";
182 const internalTags = yield* createInternalTags(id);
183 const desiredTags = { ...internalTags, ...news.tags };
184
185 // 1. Observe — cloud state is authoritative.
186 let application = yield* getApplication(name);
187
188 // 2. Ensure — create if missing. Tolerate an already-exists race.
189 if (application?.applicationId === undefined) {
190 yield* codedeploy
191 .createApplication({
192 applicationName: name,
193 computePlatform,
194 tags: Object.entries(desiredTags).map(([Key, Value]) => ({
195 Key,
196 Value,
197 })),
198 })
199 .pipe(
200 Effect.catchTag("ApplicationAlreadyExistsException", () =>
201 Effect.succeed(undefined),
202 ),
203 );
204 application = yield* getApplication(name);
205 }
206
207 // 3. Sync tags — diff against OBSERVED cloud tags.
208 yield* syncTags(arn, desiredTags);
209
210 // 4. Return fresh attributes.
211 yield* session.note(name);
212 return {
213 applicationName: application?.applicationName ?? name,
214 applicationId: application?.applicationId ?? "",
215 applicationArn: arn,
216 computePlatform: application?.computePlatform ?? computePlatform,
217 };
218 }),
219
220 delete: Effect.fn(function* ({ output }) {
221 // DeleteApplication is idempotent — deleting a non-existent
222 // application succeeds (no not-found variant in its error union).
223 yield* codedeploy.deleteApplication({
224 applicationName: output.applicationName,
225 });
226 }),
227
228 list: () =>
229 Effect.gen(function* () {
230 const { accountId, region } = yield* AWSEnvironment.current;
231 const names = yield* codedeploy.listApplications.pages({}).pipe(
232 Stream.runCollect,
233 Effect.map((chunk) =>
234 Array.from(chunk).flatMap((page) => page.applications ?? []),
235 ),
236 );
237 return names.map((name) => ({
238 applicationName: name,
239 applicationId: "",
240 applicationArn: applicationArn(region, accountId, name),
241 computePlatform: "",
242 }));
243 }),
244 };
245 }),
246 );