1import * as appconfig from "@distilled.cloud/aws/appconfig";
2import type * as Duration from "effect/Duration";
3import * as Effect from "effect/Effect";
4import * as Stream from "effect/Stream";
5import { Unowned } from "../../AdoptPolicy.ts";
6import { isResolved } from "../../Diff.ts";
7import { createPhysicalName } from "../../PhysicalName.ts";
8import * as Provider from "../../Provider.ts";
9import { Resource } from "../../Resource.ts";
10import { createInternalTags, hasAlchemyTags } from "../../Tags.ts";
11import { toWireMinutes } from "../../Util/Duration.ts";
12import { AWSEnvironment } from "../Environment.ts";
13import type { Providers } from "../Providers.ts";
14import {
15 deploymentStrategyArn,
16 readAppConfigTags,
17 syncAppConfigTags,
18} from "./internal.ts";
19
20export interface DeploymentStrategyProps {
21 /**
22 * Name of the deployment strategy. Must be 1-64 characters. If omitted, a
23 * deterministic physical name is generated. Changing the name replaces the
24 * strategy.
25 */
26 deploymentStrategyName?: string;
27 /**
28 * Total amount of time over which the deployment rolls the configuration
29 * out to targets (e.g. `"10 minutes"` or `Duration.minutes(10)`; a bare
30 * number is milliseconds). Sent to AppConfig as whole minutes.
31 */
32 deploymentDuration: Duration.Input;
33 /**
34 * Percentage of targets to receive a deployed configuration during each
35 * interval.
36 */
37 growthFactor: number;
38 /**
39 * How growth is applied over the deployment. `LINEAR` grows by
40 * `growthFactor` each interval; `EXPONENTIAL` follows `2^(N*growthFactor)`.
41 * @default "LINEAR"
42 */
43 growthType?: "LINEAR" | "EXPONENTIAL";
44 /**
45 * Amount of time AppConfig monitors for alarms before considering the
46 * deployment complete (e.g. `"5 minutes"`). Sent to AppConfig as whole
47 * minutes.
48 * @default 0
49 */
50 finalBakeTime?: Duration.Input;
51 /**
52 * Where to save a copy of the applied configuration. Immutable — changing
53 * it replaces the strategy.
54 * @default "NONE"
55 */
56 replicateTo?: "NONE" | "SSM_DOCUMENT";
57 /**
58 * Description of the deployment strategy.
59 */
60 description?: string;
61 /**
62 * User-defined tags for the deployment strategy.
63 */
64 tags?: Record<string, string>;
65}
66
67export interface DeploymentStrategy extends Resource<
68 "AWS.AppConfig.DeploymentStrategy",
69 DeploymentStrategyProps,
70 {
71 deploymentStrategyId: string;
72 deploymentStrategyName: string;
73 deploymentStrategyArn: string;
74 },
75 never,
76 Providers
77> {}
78
79/**
80 * An AWS AppConfig deployment strategy — defines how a configuration version
81 * rolls out to an environment: the total duration, the per-interval growth,
82 * and the final bake time during which alarms can trigger a rollback.
83 *
84 * ### Creating a Deployment Strategy
85 * **Example:** All-At-Once (instant, no bake)
86 * ```typescript
87 * const strategy = yield* AppConfig.DeploymentStrategy("Fast", {
88 * deploymentDuration: 0,
89 * growthFactor: 100,
90 * finalBakeTime: 0,
91 * replicateTo: "NONE",
92 * });
93 * ```
94 *
95 * **Example:** Linear rollout over 10 minutes
96 * ```typescript
97 * const strategy = yield* AppConfig.DeploymentStrategy("Linear", {
98 * deploymentDuration: "10 minutes",
99 * growthFactor: 25,
100 * growthType: "LINEAR",
101 * finalBakeTime: "5 minutes",
102 * });
103 * ```
104 *
105 * @resource
106 */
107export const DeploymentStrategy = Resource<DeploymentStrategy>(
108 "AWS.AppConfig.DeploymentStrategy",
109);
110
111export const DeploymentStrategyProvider = () =>
112 Provider.effect(
113 DeploymentStrategy,
114 Effect.gen(function* () {
115 const toName = (id: string, props: Partial<DeploymentStrategyProps>) =>
116 props.deploymentStrategyName
117 ? Effect.succeed(props.deploymentStrategyName)
118 : createPhysicalName({ id, maxLength: 64 });
119
120 const readStrategy = Effect.fn(function* (deploymentStrategyId: string) {
121 return yield* appconfig
122 .getDeploymentStrategy({ DeploymentStrategyId: deploymentStrategyId })
123 .pipe(
124 Effect.catchTag("ResourceNotFoundException", () =>
125 Effect.succeed(undefined),
126 ),
127 );
128 });
129
130 const findByName = Effect.fn(function* (name: string) {
131 const strategies = yield* appconfig.listDeploymentStrategies
132 .pages({})
133 .pipe(
134 Stream.runCollect,
135 Effect.map((chunk) =>
136 Array.from(chunk).flatMap((page) => page.Items ?? []),
137 ),
138 );
139 return strategies.find((s) => s.Name === name);
140 });
141
142 return {
143 stables: [
144 "deploymentStrategyId",
145 "deploymentStrategyName",
146 "deploymentStrategyArn",
147 ],
148
149 diff: Effect.fn(function* ({ id, olds, news }) {
150 if (!isResolved(news)) return undefined;
151 if (
152 (yield* toName(id, olds ?? {})) !== (yield* toName(id, news ?? {}))
153 ) {
154 return { action: "replace" } as const;
155 }
156 // ReplicateTo is create-only.
157 if ((olds?.replicateTo ?? "NONE") !== (news?.replicateTo ?? "NONE")) {
158 return { action: "replace" } as const;
159 }
160 }),
161
162 read: Effect.fn(function* ({ id, olds, output }) {
163 const { accountId, region } = yield* AWSEnvironment.current;
164 const strategy = output?.deploymentStrategyId
165 ? yield* readStrategy(output.deploymentStrategyId)
166 : yield* findByName(yield* toName(id, olds ?? {}));
167 if (strategy?.Id === undefined) return undefined;
168 const arn = deploymentStrategyArn(region, accountId, strategy.Id);
169 const attrs = {
170 deploymentStrategyId: strategy.Id,
171 deploymentStrategyName: strategy.Name!,
172 deploymentStrategyArn: arn,
173 };
174 const tags = yield* readAppConfigTags(arn);
175 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
176 }),
177
178 reconcile: Effect.fn(function* ({ id, news, output, session }) {
179 const { accountId, region } = yield* AWSEnvironment.current;
180 const name =
181 output?.deploymentStrategyName ?? (yield* toName(id, news));
182 const internalTags = yield* createInternalTags(id);
183 const desiredTags = { ...internalTags, ...news.tags };
184
185 // 1. Observe.
186 let observed = output?.deploymentStrategyId
187 ? yield* readStrategy(output.deploymentStrategyId)
188 : undefined;
189 if (observed === undefined) {
190 observed = yield* findByName(name);
191 }
192
193 // 2. Ensure.
194 if (observed?.Id === undefined) {
195 observed = yield* appconfig.createDeploymentStrategy({
196 Name: name,
197 Description: news.description,
198 DeploymentDurationInMinutes: toWireMinutes(
199 news.deploymentDuration,
200 )!,
201 FinalBakeTimeInMinutes: toWireMinutes(news.finalBakeTime),
202 GrowthFactor: news.growthFactor,
203 GrowthType: news.growthType,
204 ReplicateTo: news.replicateTo ?? "NONE",
205 Tags: desiredTags,
206 });
207 } else {
208 // 3. Sync — everything but Name and ReplicateTo is mutable.
209 observed = yield* appconfig.updateDeploymentStrategy({
210 DeploymentStrategyId: observed.Id,
211 Description: news.description,
212 DeploymentDurationInMinutes: toWireMinutes(
213 news.deploymentDuration,
214 ),
215 FinalBakeTimeInMinutes: toWireMinutes(news.finalBakeTime),
216 GrowthFactor: news.growthFactor,
217 GrowthType: news.growthType,
218 });
219 }
220
221 const arn = deploymentStrategyArn(region, accountId, observed.Id!);
222
223 // 3b. Sync tags.
224 yield* syncAppConfigTags(arn, desiredTags);
225
226 yield* session.note(name);
227 return {
228 deploymentStrategyId: observed.Id!,
229 deploymentStrategyName: observed.Name ?? name,
230 deploymentStrategyArn: arn,
231 };
232 }),
233
234 delete: Effect.fn(function* ({ output }) {
235 yield* appconfig
236 .deleteDeploymentStrategy({
237 DeploymentStrategyId: output.deploymentStrategyId,
238 })
239 .pipe(
240 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
241 );
242 }),
243
244 list: () =>
245 Effect.gen(function* () {
246 const { accountId, region } = yield* AWSEnvironment.current;
247 const strategies = yield* appconfig.listDeploymentStrategies
248 .pages({})
249 .pipe(
250 Stream.runCollect,
251 Effect.map((chunk) =>
252 Array.from(chunk).flatMap((page) => page.Items ?? []),
253 ),
254 );
255 return strategies.flatMap((s) =>
256 s.Id !== undefined &&
257 s.Name !== undefined &&
258 // AWS-predefined strategies (AppConfig.AllAtOnce,
259 // AppConfig.Linear50PercentEvery30Seconds, ...) are managed by
260 // AWS and cannot be deleted — deleteDeploymentStrategy rejects
261 // them with "Cannot delete predefined Deployment Strategy".
262 !s.Id.startsWith("AppConfig.")
263 ? [
264 {
265 deploymentStrategyId: s.Id,
266 deploymentStrategyName: s.Name,
267 deploymentStrategyArn: deploymentStrategyArn(
268 region,
269 accountId,
270 s.Id,
271 ),
272 },
273 ]
274 : [],
275 );
276 }),
277 };
278 }),
279 );