1import * as AWS from "@/AWS";
2import { AccessEntry } from "@/AWS/EKS";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as iam from "@distilled.cloud/aws/iam";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8
9const { test } = Test.make({ providers: AWS.providers() });
10
11// Ungated live probe: `list()` enumerates every EKS access entry across every
12// cluster in the account/region. It walks `listClusters` (paginated), then
13// `listAccessEntries` per cluster (paginated), and hydrates each via
14// `describeAccessEntry` + `listAssociatedAccessPolicies` into the same
15// Attributes shape `read` returns. We don't deploy here because a fresh EKS
16// cluster takes 10+ minutes to provision (see the gated test below) — but this
17// still verifies the cluster-fan-out + pagination + mapping against the live
18// API and asserts every returned row is well-formed (the array is empty when
19// the account has no clusters).
20test.provider(
21 "list returns the account/region access entries",
22 () =>
23 Effect.gen(function* () {
24 const provider = yield* Provider.findProvider(AccessEntry);
25 const all = yield* provider.list();
26
27 expect(Array.isArray(all)).toBe(true);
28 for (const entry of all) {
29 expect(typeof entry.accessEntryArn).toBe("string");
30 expect(typeof entry.clusterName).toBe("string");
31 expect(typeof entry.principalArn).toBe("string");
32 expect(Array.isArray(entry.kubernetesGroups)).toBe(true);
33 expect(Array.isArray(entry.accessPolicies)).toBe(true);
34 expect(entry.tags).toBeDefined();
35 }
36 }),
37 { tags: ["provider:aws", "provider:aws:eks", "live"] },
38);
39
40// Full deploy-then-list assertion. SKIPPED by default because an EKS cluster
41// takes 10+ minutes to create (and requires a VPC + subnets + cluster IAM
42// role), which exceeds the CI budget. Set AWS_TEST_EKS_CLUSTER to the name of a
43// pre-provisioned cluster to run it on an entitled account unchanged. A
44// throwaway IAM role is created out-of-band to serve as the access entry's
45// principal.
46const clusterName = process.env.AWS_TEST_EKS_CLUSTER;
47
48const principalRoleName = "alchemy-test-eks-accessentry-list-principal";
49
50test.provider.skipIf(!clusterName)(
51 "list enumerates the deployed access entry",
52 (stack) =>
53 Effect.gen(function* () {
54 // Idempotent delete: safe when the role never existed (pre-clean) or was
55 // already removed (post-clean after a partial prior run).
56 const deletePrincipalRole = iam
57 .deleteRole({ RoleName: principalRoleName })
58 .pipe(Effect.catchTag("NoSuchEntityException", () => Effect.void));
59
60 yield* stack.destroy();
61
62 // acquireRelease guarantees the out-of-band role is deleted on success,
63 // failure, AND interruption; the deterministic name + pre-clean above
64 // means a re-run reclaims any orphan from a previously-killed run.
65 yield* Effect.gen(function* () {
66 const role = yield* Effect.acquireRelease(
67 // Pre-clean a leftover from a killed run, then create.
68 deletePrincipalRole.pipe(
69 Effect.andThen(
70 iam.createRole({
71 RoleName: principalRoleName,
72 AssumeRolePolicyDocument: JSON.stringify({
73 Version: "2012-10-17",
74 Statement: [
75 {
76 Effect: "Allow",
77 Principal: { Service: "ec2.amazonaws.com" },
78 Action: "sts:AssumeRole",
79 },
80 ],
81 }),
82 }),
83 ),
84 ),
85 () => deletePrincipalRole.pipe(Effect.catch(() => Effect.void)),
86 );
87
88 const deployed = yield* stack.deploy(
89 Effect.gen(function* () {
90 return yield* AccessEntry("ListAccessEntry", {
91 clusterName: clusterName!,
92 principalArn: role.Role.Arn,
93 });
94 }),
95 );
96
97 const provider = yield* Provider.findProvider(AccessEntry);
98 const all = yield* provider.list();
99
100 expect(
101 all.some((entry) => entry.accessEntryArn === deployed.accessEntryArn),
102 ).toBe(true);
103
104 yield* stack.destroy();
105 }).pipe(Effect.scoped);
106 }),
107 {
108 tags: ["provider:aws", "provider:aws:eks", "provider:aws:iam", "live"],
109 timeout: 240_000,
110 },
111);