1import * as AWS from "@/AWS";
2import * as Duration from "effect/Duration";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import { HttpServerRequest } from "effect/http/HttpServerRequest";
6import * as HttpServerResponse from "effect/http/HttpServerResponse";
7import path from "pathe";
8import { getDefaultVpc } from "../DefaultVpc.ts";
9
10const main = path.resolve(import.meta.dirname, "handler.ts");
11
12export class CloudMapTestFunction extends AWS.Lambda.Function<AWS.Lambda.Function>()(
13 "CloudMapTestFunction",
14) {}
15
16/**
17 * Resolve the default VPC for the fixture's private DNS namespace.
18 *
19 * Runtime-guarded: the Lambda runtime re-executes this props effect on cold
20 * start, but the namespace's VPC association is deploy-time-only
21 * configuration and the execution role has no ec2:Describe* permissions —
22 * return a placeholder there without touching the EC2 API.
23 */
24const resolveFixtureVpc = Effect.gen(function* () {
25 if (globalThis.__ALCHEMY_RUNTIME__) {
26 return "vpc-unused-at-runtime";
27 }
28 // Deploy-time only: a missing default VPC is an unrecoverable fixture
29 // setup failure, so orDie rather than leak EC2's error channel into the
30 // Function program (whose error channel must stay ConfigError).
31 return (yield* getDefaultVpc.pipe(Effect.orDie)).vpcId as string;
32});
33
34export default CloudMapTestFunction.make(
35 {
36 main,
37 functionUrl: true,
38 // several routes fan out Cloud Map API calls; the 3s default is too tight
39 timeout: Duration.seconds(30),
40 },
41 Effect.gen(function* () {
42 const vpc = yield* resolveFixtureVpc;
43 const namespace = yield* AWS.CloudMap.PrivateDnsNamespace(
44 "FixtureNamespace",
45 {
46 name: "alchemy-cloudmap-fixture.local",
47 vpc,
48 },
49 );
50 const service = yield* AWS.CloudMap.Service("FixtureService", {
51 namespaceId: namespace.namespaceId,
52 name: "backend",
53 dnsRecords: [{ type: "A", ttl: "10 seconds" }],
54 routingPolicy: "MULTIVALUE",
55 attributes: { tier: "backend", version: "1" },
56 });
57 // push-based health: instances report their own status via
58 // UpdateInstanceCustomHealthStatus
59 const customService = yield* AWS.CloudMap.Service("FixtureCustomService", {
60 namespaceId: namespace.namespaceId,
61 name: "custom-health",
62 dnsRecords: [{ type: "A", ttl: "10 seconds" }],
63 routingPolicy: "MULTIVALUE",
64 healthCheckCustomConfig: {},
65 });
66
67 const discoverInstances = yield* AWS.CloudMap.DiscoverInstances(service);
68 const discoverInstancesRevision =
69 yield* AWS.CloudMap.DiscoverInstancesRevision(service);
70 const registerInstance = yield* AWS.CloudMap.RegisterInstance(service);
71 const deregisterInstance = yield* AWS.CloudMap.DeregisterInstance(service);
72 const getInstance = yield* AWS.CloudMap.GetInstance(service);
73 const listInstances = yield* AWS.CloudMap.ListInstances(service);
74 const getServiceAttributes =
75 yield* AWS.CloudMap.GetServiceAttributes(service);
76 const getOperation = yield* AWS.CloudMap.GetOperation();
77 const registerCustomInstance =
78 yield* AWS.CloudMap.RegisterInstance(customService);
79 const deregisterCustomInstance =
80 yield* AWS.CloudMap.DeregisterInstance(customService);
81 const updateCustomHealth =
82 yield* AWS.CloudMap.UpdateInstanceCustomHealthStatus(customService);
83 const getCustomHealthStatus =
84 yield* AWS.CloudMap.GetInstancesHealthStatus(customService);
85 const ServiceId = yield* service.serviceId;
86 const CustomServiceId = yield* customService.serviceId;
87 const NamespaceName = yield* service.namespaceName;
88 const ServiceName = yield* service.serviceName;
89
90 return {
91 fetch: Effect.gen(function* () {
92 const request = yield* HttpServerRequest;
93 const url = new URL(request.originalUrl);
94 const pathname = url.pathname;
95
96 if (request.method === "GET" && pathname === "/info") {
97 const serviceId = yield* ServiceId;
98 const customServiceId = yield* CustomServiceId;
99 const namespaceName = yield* NamespaceName;
100 const serviceName = yield* ServiceName;
101 return yield* HttpServerResponse.json({
102 serviceId,
103 customServiceId,
104 namespaceName,
105 serviceName,
106 });
107 }
108
109 if (request.method === "GET" && pathname === "/discover") {
110 const health = url.searchParams.get("health") ?? "ALL";
111 const result = yield* discoverInstances({
112 HealthStatus: health as "HEALTHY" | "UNHEALTHY" | "ALL",
113 });
114 return yield* HttpServerResponse.json({
115 instances: (result.Instances ?? []).map((instance) => ({
116 instanceId: instance.InstanceId,
117 healthStatus: instance.HealthStatus,
118 attributes: instance.Attributes ?? {},
119 })),
120 });
121 }
122
123 if (request.method === "GET" && pathname === "/revision") {
124 const result = yield* discoverInstancesRevision();
125 return yield* HttpServerResponse.json({
126 revision: result.InstancesRevision,
127 });
128 }
129
130 if (request.method === "GET" && pathname === "/instance") {
131 const instanceId = url.searchParams.get("id") ?? "";
132 const result = yield* getInstance({ InstanceId: instanceId });
133 return yield* HttpServerResponse.json({
134 instanceId: result.Instance?.Id,
135 attributes: result.Instance?.Attributes ?? {},
136 });
137 }
138
139 if (request.method === "GET" && pathname === "/instances") {
140 const result = yield* listInstances();
141 return yield* HttpServerResponse.json({
142 instances: (result.Instances ?? []).map((instance) => ({
143 instanceId: instance.Id,
144 attributes: instance.Attributes ?? {},
145 })),
146 });
147 }
148
149 if (request.method === "GET" && pathname === "/service-attributes") {
150 const result = yield* getServiceAttributes();
151 return yield* HttpServerResponse.json({
152 attributes: result.ServiceAttributes?.Attributes ?? {},
153 });
154 }
155
156 if (request.method === "GET" && pathname === "/operation") {
157 const operationId = url.searchParams.get("id") ?? "";
158 const result = yield* getOperation({ OperationId: operationId });
159 return yield* HttpServerResponse.json({
160 status: result.Operation?.Status,
161 errorMessage: result.Operation?.ErrorMessage,
162 });
163 }
164
165 if (request.method === "POST" && pathname === "/register") {
166 const body = (yield* request.json) as unknown as {
167 instanceId: string;
168 attributes: Record<string, string>;
169 };
170 const result = yield* registerInstance({
171 InstanceId: body.instanceId,
172 Attributes: body.attributes,
173 });
174 return yield* HttpServerResponse.json({
175 operationId: result.OperationId,
176 });
177 }
178
179 if (request.method === "POST" && pathname === "/deregister") {
180 const body = (yield* request.json) as unknown as {
181 instanceId: string;
182 };
183 const result = yield* deregisterInstance({
184 InstanceId: body.instanceId,
185 });
186 return yield* HttpServerResponse.json({
187 operationId: result.OperationId,
188 });
189 }
190
191 if (request.method === "POST" && pathname === "/custom/register") {
192 const body = (yield* request.json) as unknown as {
193 instanceId: string;
194 attributes: Record<string, string>;
195 };
196 const result = yield* registerCustomInstance({
197 InstanceId: body.instanceId,
198 Attributes: body.attributes,
199 });
200 return yield* HttpServerResponse.json({
201 operationId: result.OperationId,
202 });
203 }
204
205 if (request.method === "POST" && pathname === "/custom/deregister") {
206 const body = (yield* request.json) as unknown as {
207 instanceId: string;
208 };
209 const result = yield* deregisterCustomInstance({
210 InstanceId: body.instanceId,
211 });
212 return yield* HttpServerResponse.json({
213 operationId: result.OperationId,
214 });
215 }
216
217 if (request.method === "POST" && pathname === "/custom/health") {
218 const body = (yield* request.json) as unknown as {
219 instanceId: string;
220 status: "HEALTHY" | "UNHEALTHY";
221 };
222 yield* updateCustomHealth({
223 InstanceId: body.instanceId,
224 Status: body.status,
225 });
226 return yield* HttpServerResponse.json({ ok: true });
227 }
228
229 if (request.method === "GET" && pathname === "/custom/health-status") {
230 const result = yield* getCustomHealthStatus();
231 return yield* HttpServerResponse.json({
232 status: result.Status ?? {},
233 });
234 }
235
236 return yield* HttpServerResponse.json(
237 { error: "Not found" },
238 { status: 404 },
239 );
240 }).pipe(Effect.orDie),
241 };
242 }).pipe(
243 Effect.provide(
244 Layer.mergeAll(
245 AWS.CloudMap.DiscoverInstancesHttp,
246 AWS.CloudMap.DiscoverInstancesRevisionHttp,
247 AWS.CloudMap.RegisterInstanceHttp,
248 AWS.CloudMap.DeregisterInstanceHttp,
249 AWS.CloudMap.GetInstanceHttp,
250 AWS.CloudMap.ListInstancesHttp,
251 AWS.CloudMap.GetServiceAttributesHttp,
252 AWS.CloudMap.GetOperationHttp,
253 AWS.CloudMap.UpdateInstanceCustomHealthStatusHttp,
254 AWS.CloudMap.GetInstancesHealthStatusHttp,
255 ),
256 ),
257 ),
258);