| 1 | import * as AWS from "@/AWS"; |
| 2 | import { AccessEntry } from "@/AWS/EKS"; |
| 3 | import * as Provider from "@/Provider"; |
| 4 | import * as Test from "@/Test/Alchemy"; |
| 5 | import * as iam from "@distilled.cloud/aws/iam"; |
| 6 | import { expect } from "alchemy-test"; |
| 7 | import * as Effect from "effect/Effect"; |
| 8 | |
| 9 | const { 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). |
| 20 | test.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. |
| 46 | const clusterName = process.env.AWS_TEST_EKS_CLUSTER; |
| 47 | |
| 48 | const principalRoleName = "alchemy-test-eks-accessentry-list-principal"; |
| 49 | |
| 50 | test.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 | ); |