1import * as EKS from "@/AWS/EKS";
2import * as Lambda from "@/AWS/Lambda";
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";
8
9const main = path.resolve(import.meta.dirname, "cluster-bindings-handler.ts");
10
11// The gated test supplies standing infrastructure via env (same convention as
12// Cluster.test.ts). The fallbacks keep the module importable when the test is
13// skipped — the fixture is only ever deployed when the env vars are set.
14const roleArn =
15 process.env.AWS_TEST_EKS_ROLE_ARN ??
16 "arn:aws:iam::000000000000:role/placeholder";
17const subnetIds = (process.env.AWS_TEST_EKS_SUBNET_IDS ?? "").split(",");
18
19// Well-formed-but-nonexistent identifiers: each /probes call drives a
20// cluster-scoped binding against one so the route proves the IAM grant, the
21// cluster-name injection, and the typed error decode without provisioning the
22// sub-resource.
23const BOGUS = "alchemy-nonexistent-probe";
24const BOGUS_PRINCIPAL_ARN =
25 "arn:aws:iam::000000000000:role/alchemy-nonexistent-probe";
26
27export class EKSClusterTestFunction extends Lambda.Function<Lambda.Function>()(
28 "EKSClusterTestFunction",
29) {}
30
31/**
32 * Cluster-scoped binding fixture: deploys a real EKS control plane (~10 min —
33 * gated behind AWS_TEST_EKS_ROLE_ARN + AWS_TEST_EKS_SUBNET_IDS) and a Lambda
34 * bound to it with every cluster-scoped EKS binding.
35 */
36export default EKSClusterTestFunction.make(
37 {
38 main,
39 functionUrl: true,
40 },
41 Effect.gen(function* () {
42 const cluster = yield* EKS.Cluster("BindingsCluster", {
43 clusterName: "alchemy-test-eks-bindings",
44 roleArn,
45 resourcesVpcConfig: {
46 subnetIds,
47 endpointPublicAccess: true,
48 endpointPrivateAccess: true,
49 },
50 });
51
52 const describeCluster = yield* EKS.DescribeCluster(cluster);
53 const listNodegroups = yield* EKS.ListNodegroups(cluster);
54 const listAddons = yield* EKS.ListAddons(cluster);
55 const listFargateProfiles = yield* EKS.ListFargateProfiles(cluster);
56 const listPodIdentityAssociations =
57 yield* EKS.ListPodIdentityAssociations(cluster);
58 const listAccessEntries = yield* EKS.ListAccessEntries(cluster);
59 const listInsights = yield* EKS.ListInsights(cluster);
60 const listUpdates = yield* EKS.ListUpdates(cluster);
61 const listCapabilities = yield* EKS.ListCapabilities(cluster);
62 const listIdentityProviderConfigs =
63 yield* EKS.ListIdentityProviderConfigs(cluster);
64 const listAssociatedAccessPolicies =
65 yield* EKS.ListAssociatedAccessPolicies(cluster);
66 const describeNodegroup = yield* EKS.DescribeNodegroup(cluster);
67 const describeAddon = yield* EKS.DescribeAddon(cluster);
68 const describeFargateProfile = yield* EKS.DescribeFargateProfile(cluster);
69 const describePodIdentityAssociation =
70 yield* EKS.DescribePodIdentityAssociation(cluster);
71 const describeAccessEntry = yield* EKS.DescribeAccessEntry(cluster);
72 const describeUpdate = yield* EKS.DescribeUpdate(cluster);
73 const describeInsight = yield* EKS.DescribeInsight(cluster);
74 const describeCapability = yield* EKS.DescribeCapability(cluster);
75 const describeIdentityProviderConfig =
76 yield* EKS.DescribeIdentityProviderConfig(cluster);
77 const describeInsightsRefresh = yield* EKS.DescribeInsightsRefresh(cluster);
78 const startInsightsRefresh = yield* EKS.StartInsightsRefresh(cluster);
79
80 const bound = {
81 describeCluster,
82 listNodegroups,
83 listAddons,
84 listFargateProfiles,
85 listPodIdentityAssociations,
86 listAccessEntries,
87 listInsights,
88 listUpdates,
89 listAssociatedAccessPolicies,
90 describeNodegroup,
91 describeAddon,
92 describeFargateProfile,
93 describePodIdentityAssociation,
94 describeAccessEntry,
95 describeUpdate,
96 describeInsight,
97 listCapabilities,
98 listIdentityProviderConfigs,
99 describeCapability,
100 describeIdentityProviderConfig,
101 describeInsightsRefresh,
102 startInsightsRefresh,
103 };
104
105 return {
106 fetch: Effect.gen(function* () {
107 const request = yield* HttpServerRequest;
108 const url = new URL(request.originalUrl);
109 const pathname = url.pathname;
110
111 if (request.method === "GET" && pathname === "/bindings") {
112 return yield* HttpServerResponse.json({
113 bound: Object.keys(bound),
114 });
115 }
116
117 if (request.method === "GET" && pathname === "/cluster") {
118 const result = yield* describeCluster();
119 return yield* HttpServerResponse.json({
120 status: result.cluster?.status,
121 endpointPresent: typeof result.cluster?.endpoint === "string",
122 caPresent:
123 typeof result.cluster?.certificateAuthority?.data === "string",
124 });
125 }
126
127 if (request.method === "GET" && pathname === "/lists") {
128 // A fresh control plane has none of these — an empty page from each
129 // proves the grant + clusterName injection + response decode.
130 const nodegroups = yield* listNodegroups();
131 const addons = yield* listAddons();
132 const fargateProfiles = yield* listFargateProfiles();
133 const podIdentityAssociations = yield* listPodIdentityAssociations();
134 const accessEntries = yield* listAccessEntries();
135 const insights = yield* listInsights();
136 const updates = yield* listUpdates();
137 const capabilities = yield* listCapabilities();
138 const identityProviderConfigs = yield* listIdentityProviderConfigs();
139 return yield* HttpServerResponse.json({
140 nodegroups: (nodegroups.nodegroups ?? []).length,
141 addons: (addons.addons ?? []).length,
142 fargateProfiles: (fargateProfiles.fargateProfileNames ?? []).length,
143 podIdentityAssociations: (
144 podIdentityAssociations.associations ?? []
145 ).length,
146 accessEntries: (accessEntries.accessEntries ?? []).length,
147 insights: (insights.insights ?? []).length,
148 updates: (updates.updateIds ?? []).length,
149 capabilities: (capabilities.capabilities ?? []).length,
150 identityProviderConfigs: (
151 identityProviderConfigs.identityProviderConfigs ?? []
152 ).length,
153 });
154 }
155
156 if (request.method === "GET" && pathname === "/probes") {
157 // Nonexistent sub-resources must surface the service's typed
158 // not-found tags — an IAM gap or a broken cluster-name injection
159 // would surface AccessDeniedException / a 500 instead.
160 const nodegroupTag = yield* describeNodegroup({
161 nodegroupName: BOGUS,
162 }).pipe(
163 Effect.map(() => "Found"),
164 Effect.catchTag("ResourceNotFoundException", (e) =>
165 Effect.succeed(e._tag),
166 ),
167 );
168 const addonTag = yield* describeAddon({ addonName: BOGUS }).pipe(
169 Effect.map(() => "Found"),
170 Effect.catchTag(
171 ["ResourceNotFoundException", "InvalidParameterException"],
172 (e) => Effect.succeed(e._tag),
173 ),
174 );
175 const fargateProfileTag = yield* describeFargateProfile({
176 fargateProfileName: BOGUS,
177 }).pipe(
178 Effect.map(() => "Found"),
179 Effect.catchTag("ResourceNotFoundException", (e) =>
180 Effect.succeed(e._tag),
181 ),
182 );
183 const podIdentityTag = yield* describePodIdentityAssociation({
184 associationId: `a-${BOGUS}`,
185 }).pipe(
186 Effect.map(() => "Found"),
187 Effect.catchTag(
188 ["ResourceNotFoundException", "InvalidParameterException"],
189 (e) => Effect.succeed(e._tag),
190 ),
191 );
192 const accessEntryTag = yield* describeAccessEntry({
193 principalArn: BOGUS_PRINCIPAL_ARN,
194 }).pipe(
195 Effect.map(() => "Found"),
196 Effect.catchTag(
197 ["ResourceNotFoundException", "InvalidRequestException"],
198 (e) => Effect.succeed(e._tag),
199 ),
200 );
201 const updateTag = yield* describeUpdate({
202 updateId: "00000000-0000-0000-0000-000000000000",
203 }).pipe(
204 Effect.map(() => "Found"),
205 Effect.catchTag(
206 ["ResourceNotFoundException", "InvalidParameterException"],
207 (e) => Effect.succeed(e._tag),
208 ),
209 );
210 const insightTag = yield* describeInsight({
211 id: "00000000-0000-0000-0000-000000000000",
212 }).pipe(
213 Effect.map(() => "Found"),
214 Effect.catchTag(
215 ["ResourceNotFoundException", "InvalidParameterException"],
216 (e) => Effect.succeed(e._tag),
217 ),
218 );
219 const associatedPoliciesTag = yield* listAssociatedAccessPolicies({
220 principalArn: BOGUS_PRINCIPAL_ARN,
221 }).pipe(
222 Effect.map(() => "Found"),
223 Effect.catchTag(
224 ["ResourceNotFoundException", "InvalidRequestException"],
225 (e) => Effect.succeed(e._tag),
226 ),
227 );
228 const capabilityTag = yield* describeCapability({
229 capabilityName: BOGUS,
230 }).pipe(
231 Effect.map(() => "Found"),
232 Effect.catchTag(
233 ["ResourceNotFoundException", "InvalidParameterException"],
234 (e) => Effect.succeed(e._tag),
235 ),
236 );
237 const identityProviderConfigTag =
238 yield* describeIdentityProviderConfig({
239 identityProviderConfig: { type: "oidc", name: BOGUS },
240 }).pipe(
241 Effect.map(() => "Found"),
242 Effect.catchTag(
243 ["ResourceNotFoundException", "InvalidParameterException"],
244 (e) => Effect.succeed(e._tag),
245 ),
246 );
247 return yield* HttpServerResponse.json({
248 nodegroupTag,
249 addonTag,
250 fargateProfileTag,
251 podIdentityTag,
252 accessEntryTag,
253 updateTag,
254 insightTag,
255 associatedPoliciesTag,
256 capabilityTag,
257 identityProviderConfigTag,
258 });
259 }
260
261 if (request.method === "GET" && pathname === "/insights-refresh") {
262 // Kick an on-demand refresh, then read its status back — proves both
263 // grants. A refresh already in flight surfaces the service's typed
264 // InvalidRequestException, which proves the same wiring.
265 const startTag = yield* startInsightsRefresh().pipe(
266 Effect.map((r) => r.status ?? "STARTED"),
267 Effect.catchTag(
268 ["InvalidRequestException", "ResourceNotFoundException"],
269 (e) => Effect.succeed(e._tag),
270 ),
271 );
272 const describeTag = yield* describeInsightsRefresh().pipe(
273 Effect.map((r) => r.status ?? "UNKNOWN"),
274 Effect.catchTag(
275 ["InvalidRequestException", "ResourceNotFoundException"],
276 (e) => Effect.succeed(e._tag),
277 ),
278 );
279 return yield* HttpServerResponse.json({ startTag, describeTag });
280 }
281
282 return yield* HttpServerResponse.json(
283 { error: "Not found", method: request.method, pathname },
284 { status: 404 },
285 );
286 }).pipe(Effect.orDie),
287 };
288 }).pipe(
289 Effect.provide(
290 Layer.mergeAll(
291 EKS.DescribeClusterHttp,
292 EKS.ListNodegroupsHttp,
293 EKS.ListAddonsHttp,
294 EKS.ListFargateProfilesHttp,
295 EKS.ListPodIdentityAssociationsHttp,
296 EKS.ListAccessEntriesHttp,
297 EKS.ListInsightsHttp,
298 EKS.ListUpdatesHttp,
299 EKS.ListAssociatedAccessPoliciesHttp,
300 EKS.DescribeNodegroupHttp,
301 EKS.DescribeAddonHttp,
302 EKS.DescribeFargateProfileHttp,
303 EKS.DescribePodIdentityAssociationHttp,
304 EKS.DescribeAccessEntryHttp,
305 EKS.DescribeUpdateHttp,
306 EKS.DescribeInsightHttp,
307 EKS.ListCapabilitiesHttp,
308 EKS.ListIdentityProviderConfigsHttp,
309 EKS.DescribeCapabilityHttp,
310 EKS.DescribeIdentityProviderConfigHttp,
311 EKS.DescribeInsightsRefreshHttp,
312 EKS.StartInsightsRefreshHttp,
313 ),
314 ),
315 ),
316);