1import * as EFS from "@/AWS/EFS";
2import * as Lambda from "@/AWS/Lambda";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Schedule from "effect/Schedule";
6import { HttpServerRequest } from "effect/http/HttpServerRequest";
7import * as HttpServerResponse from "effect/http/HttpServerResponse";
8import path from "pathe";
9
10const main = path.resolve(import.meta.dirname, "bindings-handler.ts");
11
12// Deterministic idempotency token for the runtime create/delete round-trip.
13const ACCESS_POINT_CLIENT_TOKEN = "alchemy-efs-bindings-access-point";
14
15export class EfsBindingsFunction extends Lambda.Function<Lambda.Function>()(
16 "EfsBindingsFunction",
17) {}
18
19export default EfsBindingsFunction.make(
20 {
21 main,
22 functionUrl: true,
23 },
24 Effect.gen(function* () {
25 // Control-plane bindings only — no VPC/mount target needed: EFS's data
26 // plane is NFS (covered by the Lambda `fileSystemConfigs` mount), while
27 // these bindings drive the management API over HTTPS.
28 const files = yield* EFS.FileSystem("BindingsFileSystem");
29
30 const describeFileSystem = yield* EFS.DescribeFileSystem(files);
31 const describeMountTargets = yield* EFS.DescribeMountTargets(files);
32 const describeAccessPoints = yield* EFS.DescribeAccessPoints(files);
33 const describeBackupPolicy = yield* EFS.DescribeBackupPolicy(files);
34 const putBackupPolicy = yield* EFS.PutBackupPolicy(files);
35 const describeLifecycleConfiguration =
36 yield* EFS.DescribeLifecycleConfiguration(files);
37 const putLifecycleConfiguration =
38 yield* EFS.PutLifecycleConfiguration(files);
39 const createAccessPoint = yield* EFS.CreateAccessPoint(files);
40 const deleteAccessPoint = yield* EFS.DeleteAccessPoint();
41 const describeReplicationConfigurations =
42 yield* EFS.DescribeReplicationConfigurations(files);
43
44 const bound = {
45 describeFileSystem,
46 describeMountTargets,
47 describeAccessPoints,
48 describeBackupPolicy,
49 putBackupPolicy,
50 describeLifecycleConfiguration,
51 putLifecycleConfiguration,
52 createAccessPoint,
53 deleteAccessPoint,
54 describeReplicationConfigurations,
55 };
56
57 // A put racing a still-transitioning previous change surfaces the typed
58 // IncorrectFileSystemLifeCycleState — retry it bounded (~20s).
59 const retryWhileUpdating = <A, E extends { _tag: string }, R>(
60 self: Effect.Effect<A, E, R>,
61 ): Effect.Effect<A, E, R> =>
62 Effect.retry(self, {
63 while: (e): boolean => e._tag === "IncorrectFileSystemLifeCycleState",
64 schedule: Schedule.max([
65 Schedule.fixed("2 seconds"),
66 Schedule.recurs(10),
67 ]),
68 });
69
70 return {
71 fetch: Effect.gen(function* () {
72 const request = yield* HttpServerRequest;
73 const url = new URL(request.originalUrl);
74 const pathname = url.pathname;
75
76 if (request.method === "GET" && pathname === "/bindings") {
77 return yield* HttpServerResponse.json({
78 bound: Object.keys(bound),
79 });
80 }
81
82 if (request.method === "GET" && pathname === "/file-system") {
83 // FileSystemId injection scopes the response to the bound file
84 // system.
85 const response = yield* describeFileSystem();
86 const fs = response.FileSystems?.[0];
87 return yield* HttpServerResponse.json({
88 fileSystemId: fs?.FileSystemId,
89 state: fs?.LifeCycleState,
90 encrypted: fs?.Encrypted,
91 });
92 }
93
94 if (request.method === "GET" && pathname === "/mount-targets") {
95 const response = yield* describeMountTargets();
96 return yield* HttpServerResponse.json({
97 count: (response.MountTargets ?? []).length,
98 });
99 }
100
101 if (request.method === "GET" && pathname === "/access-points") {
102 const response = yield* describeAccessPoints();
103 return yield* HttpServerResponse.json({
104 count: (response.AccessPoints ?? []).length,
105 });
106 }
107
108 if (request.method === "GET" && pathname === "/backup-policy") {
109 // A file system that never had a backup policy fails with the
110 // typed PolicyNotFound — normalize it to "NONE".
111 const status = yield* describeBackupPolicy().pipe(
112 Effect.map((r) => r.BackupPolicy?.Status ?? "NONE"),
113 Effect.catchTag("PolicyNotFound", () => Effect.succeed("NONE")),
114 );
115 return yield* HttpServerResponse.json({ status });
116 }
117
118 if (request.method === "POST" && pathname === "/backup-policy/enable") {
119 const response = yield* retryWhileUpdating(
120 putBackupPolicy({ BackupPolicy: { Status: "ENABLED" } }),
121 );
122 return yield* HttpServerResponse.json({
123 status: response.BackupPolicy?.Status,
124 });
125 }
126
127 if (
128 request.method === "POST" &&
129 pathname === "/backup-policy/disable"
130 ) {
131 const response = yield* retryWhileUpdating(
132 putBackupPolicy({ BackupPolicy: { Status: "DISABLED" } }),
133 );
134 return yield* HttpServerResponse.json({
135 status: response.BackupPolicy?.Status,
136 });
137 }
138
139 if (request.method === "GET" && pathname === "/lifecycle") {
140 const response = yield* describeLifecycleConfiguration();
141 return yield* HttpServerResponse.json({
142 count: (response.LifecyclePolicies ?? []).length,
143 });
144 }
145
146 if (request.method === "POST" && pathname === "/lifecycle/set") {
147 const response = yield* retryWhileUpdating(
148 putLifecycleConfiguration({
149 LifecyclePolicies: [{ TransitionToIA: "AFTER_30_DAYS" }],
150 }),
151 );
152 return yield* HttpServerResponse.json({
153 count: (response.LifecyclePolicies ?? []).length,
154 });
155 }
156
157 if (request.method === "POST" && pathname === "/lifecycle/clear") {
158 const response = yield* retryWhileUpdating(
159 putLifecycleConfiguration({ LifecyclePolicies: [] }),
160 );
161 return yield* HttpServerResponse.json({
162 count: (response.LifecyclePolicies ?? []).length,
163 });
164 }
165
166 if (request.method === "POST" && pathname === "/access-point") {
167 // Create-then-delete round-trip: exercises the runtime
168 // multi-tenant pattern end-to-end and leaves no orphan. A crashed
169 // previous invocation surfaces the typed AccessPointAlreadyExists
170 // (same client token) — resolve it by describing the winner.
171 const accessPoint = yield* createAccessPoint({
172 ClientToken: ACCESS_POINT_CLIENT_TOKEN,
173 PosixUser: { Uid: 1000, Gid: 1000 },
174 RootDirectory: {
175 Path: "/bindings-test",
176 CreationInfo: {
177 OwnerUid: 1000,
178 OwnerGid: 1000,
179 Permissions: "750",
180 },
181 },
182 }).pipe(
183 Effect.catchTag("AccessPointAlreadyExists", () =>
184 describeAccessPoints().pipe(
185 Effect.map((r) =>
186 r.AccessPoints?.find(
187 (ap) => ap.ClientToken === ACCESS_POINT_CLIENT_TOKEN,
188 )!,
189 ),
190 ),
191 ),
192 );
193 yield* deleteAccessPoint({
194 AccessPointId: accessPoint.AccessPointId!,
195 }).pipe(Effect.catchTag("AccessPointNotFound", () => Effect.void));
196 return yield* HttpServerResponse.json({
197 accessPointId: accessPoint.AccessPointId,
198 deleted: true,
199 });
200 }
201
202 if (request.method === "GET" && pathname === "/replication") {
203 // The fixture file system has no replication configuration — the
204 // typed ReplicationNotFound proves the grant end-to-end (an IAM
205 // gap would surface AccessDeniedException, a 500).
206 const hasReplication =
207 yield* describeReplicationConfigurations().pipe(
208 Effect.map((r) => (r.Replications ?? []).length > 0),
209 Effect.catchTag("ReplicationNotFound", () =>
210 Effect.succeed(false),
211 ),
212 );
213 return yield* HttpServerResponse.json({ hasReplication });
214 }
215
216 return yield* HttpServerResponse.json(
217 { error: "Not found", method: request.method, pathname },
218 { status: 404 },
219 );
220 }).pipe(Effect.orDie),
221 };
222 }).pipe(
223 Effect.provide(
224 Layer.mergeAll(
225 EFS.DescribeFileSystemHttp,
226 EFS.DescribeMountTargetsHttp,
227 EFS.DescribeAccessPointsHttp,
228 EFS.DescribeBackupPolicyHttp,
229 EFS.PutBackupPolicyHttp,
230 EFS.DescribeLifecycleConfigurationHttp,
231 EFS.PutLifecycleConfigurationHttp,
232 EFS.CreateAccessPointHttp,
233 EFS.DeleteAccessPointHttp,
234 EFS.DescribeReplicationConfigurationsHttp,
235 ),
236 ),
237 ),
238);