1import * as AppConfig from "@/AWS/AppConfig";
2import * as Lambda from "@/AWS/Lambda";
3import * as Output from "@/Output";
4import * as Cause from "effect/Cause";
5import * as Duration from "effect/Duration";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8import { HttpServerRequest } from "effect/http/HttpServerRequest";
9import * as HttpServerResponse from "effect/http/HttpServerResponse";
10import path from "pathe";
11
12const main = path.resolve(import.meta.dirname, "handler.ts");
13
14const CONFIG = { featureX: true, limit: 42 } as const;
15
16export class AppConfigTestFunction extends Lambda.Function<Lambda.Function>()(
17 "AppConfigTestFunction",
18) {}
19
20export default AppConfigTestFunction.make(
21 {
22 main,
23 functionUrl: true,
24 // Starting a configuration session + fetching config fans out two SDK
25 // calls; AWS's 3s default intermittently times out on a cold start.
26 timeout: Duration.seconds(30),
27 },
28 Effect.gen(function* () {
29 const app = yield* AppConfig.Application("App", {});
30 const env = yield* AppConfig.Environment("Env", {
31 applicationId: app.applicationId,
32 });
33 const profile = yield* AppConfig.ConfigurationProfile("Profile", {
34 applicationId: app.applicationId,
35 locationUri: "hosted",
36 });
37 const version = yield* AppConfig.HostedConfigurationVersion("V1", {
38 applicationId: app.applicationId,
39 configurationProfileId: profile.configurationProfileId,
40 content: JSON.stringify(CONFIG),
41 contentType: "application/json",
42 });
43 const strategy = yield* AppConfig.DeploymentStrategy("AllAtOnce", {
44 deploymentDuration: 0,
45 growthFactor: 100,
46 finalBakeTime: 0,
47 replicateTo: "NONE",
48 });
49 // A deliberately slow strategy so a rollout can be stopped mid-flight.
50 const slowStrategy = yield* AppConfig.DeploymentStrategy("Slow", {
51 deploymentDuration: "20 minutes",
52 growthFactor: 5,
53 growthType: "LINEAR",
54 finalBakeTime: 0,
55 replicateTo: "NONE",
56 });
57 // Deploy the version to the environment so the data plane can serve it.
58 yield* AppConfig.Deployment("Deploy", {
59 applicationId: app.applicationId,
60 environmentId: env.environmentId,
61 deploymentStrategyId: strategy.deploymentStrategyId,
62 configurationProfileId: profile.configurationProfileId,
63 configurationVersion: Output.interpolate`${version.versionNumber}`,
64 });
65
66 const getConfig = yield* AppConfig.GetConfiguration(app, env, profile);
67 const createVersion = yield* AppConfig.CreateHostedConfigurationVersion(
68 app,
69 profile,
70 );
71 const startDeployment = yield* AppConfig.StartDeployment(
72 app,
73 env,
74 profile,
75 strategy,
76 );
77 const startSlowDeployment = yield* AppConfig.StartDeployment(
78 app,
79 env,
80 profile,
81 slowStrategy,
82 );
83 const getDeployment = yield* AppConfig.GetDeployment(app, env);
84 const stopDeployment = yield* AppConfig.StopDeployment(app, env);
85 const validateConfiguration = yield* AppConfig.ValidateConfiguration(
86 app,
87 profile,
88 );
89
90 return {
91 fetch: Effect.gen(function* () {
92 const request = yield* HttpServerRequest;
93 const pathname = new URL(request.originalUrl).pathname;
94
95 if (request.method === "GET" && pathname === "/config") {
96 const result = yield* getConfig();
97 return yield* HttpServerResponse.json({
98 content: result.content,
99 contentType: result.contentType,
100 });
101 }
102
103 if (request.method === "POST" && pathname === "/version") {
104 const body = (yield* request.json) as unknown as {
105 content: string;
106 };
107 const created = yield* createVersion({
108 Content: body.content,
109 ContentType: "application/json",
110 });
111 return yield* HttpServerResponse.json({
112 versionNumber: created.VersionNumber,
113 });
114 }
115
116 if (request.method === "POST" && pathname === "/deploy") {
117 const body = (yield* request.json) as unknown as {
118 version: string;
119 slow?: boolean;
120 };
121 const started = yield* (
122 body.slow ? startSlowDeployment : startDeployment
123 )({
124 ConfigurationVersion: body.version,
125 });
126 return yield* HttpServerResponse.json({
127 deploymentNumber: started.DeploymentNumber,
128 state: started.State,
129 });
130 }
131
132 if (request.method === "GET" && pathname === "/deployment") {
133 const number = Number(
134 new URL(request.originalUrl).searchParams.get("number"),
135 );
136 const deployment = yield* getDeployment({
137 DeploymentNumber: number,
138 });
139 return yield* HttpServerResponse.json({
140 state: deployment.State,
141 percentageComplete: deployment.PercentageComplete,
142 configurationVersion: deployment.ConfigurationVersion,
143 });
144 }
145
146 if (request.method === "POST" && pathname === "/stop") {
147 const body = (yield* request.json) as unknown as {
148 number: number;
149 };
150 const stopped = yield* stopDeployment({
151 DeploymentNumber: body.number,
152 });
153 return yield* HttpServerResponse.json({ state: stopped.State });
154 }
155
156 if (request.method === "POST" && pathname === "/validate") {
157 const body = (yield* request.json) as unknown as {
158 version: string;
159 };
160 yield* validateConfiguration({
161 ConfigurationVersion: body.version,
162 });
163 return yield* HttpServerResponse.json({ valid: true });
164 }
165
166 if (request.method === "GET" && pathname === "/ping") {
167 return yield* HttpServerResponse.json({ ok: true });
168 }
169
170 return yield* HttpServerResponse.json(
171 { error: "Not found", method: request.method, pathname },
172 { status: 404 },
173 );
174 }).pipe(
175 // Surface failures as a 500 whose body carries the cause so the
176 // test's transient-retry error logs show WHAT failed in the Lambda.
177 Effect.catchCause((cause) =>
178 Effect.succeed(
179 HttpServerResponse.text(
180 `${Cause.pretty(cause)}\n${JSON.stringify(Cause.squash(cause))}`,
181 { status: 500 },
182 ),
183 ),
184 ),
185 ),
186 };
187 }).pipe(
188 Effect.provide(
189 Layer.mergeAll(
190 AppConfig.GetConfigurationHttp,
191 AppConfig.CreateHostedConfigurationVersionHttp,
192 AppConfig.StartDeploymentHttp,
193 AppConfig.GetDeploymentHttp,
194 AppConfig.StopDeploymentHttp,
195 AppConfig.ValidateConfigurationHttp,
196 ),
197 ),
198 ),
199);