| 1 | import * as AWS from "@/AWS"; |
| 2 | import * as Test from "@/Test/Alchemy"; |
| 3 | import * as efs from "@distilled.cloud/aws/efs"; |
| 4 | import { expect } from "alchemy-test"; |
| 5 | import * as Effect from "effect/Effect"; |
| 6 | import * as Schedule from "effect/Schedule"; |
| 7 | |
| 8 | const { test } = Test.make({ providers: AWS.providers() }); |
| 9 | |
| 10 | const efsTagsToRecord = ( |
| 11 | tags: readonly efs.Tag[] | undefined, |
| 12 | ): Record<string, string> => |
| 13 | Object.fromEntries((tags ?? []).map((t) => [t.Key, t.Value])); |
| 14 | |
| 15 | // Typed wait-until-gone for an access point. |
| 16 | const waitUntilAccessPointGone = (accessPointId: string) => |
| 17 | efs.describeAccessPoints({ AccessPointId: accessPointId }).pipe( |
| 18 | Effect.map((r) => |
| 19 | (r.AccessPoints ?? []).every((ap) => ap.LifeCycleState === "deleted"), |
| 20 | ), |
| 21 | Effect.catchTag("AccessPointNotFound", () => Effect.succeed(true)), |
| 22 | Effect.repeat({ |
| 23 | schedule: Schedule.fixed("2 seconds"), |
| 24 | until: (gone) => gone, |
| 25 | times: 30, |
| 26 | }), |
| 27 | ); |
| 28 | |
| 29 | test.provider( |
| 30 | "create, update tags, replace, delete access point", |
| 31 | (stack) => |
| 32 | Effect.gen(function* () { |
| 33 | yield* stack.destroy(); |
| 34 | |
| 35 | const infra = (posixUid: number, tag: string) => |
| 36 | Effect.gen(function* () { |
| 37 | const files = yield* AWS.EFS.FileSystem("ApFiles"); |
| 38 | const accessPoint = yield* AWS.EFS.AccessPoint("ApAccess", { |
| 39 | fileSystemId: files.fileSystemId, |
| 40 | posixUser: { uid: posixUid, gid: 1000 }, |
| 41 | rootDirectory: { |
| 42 | path: "/app", |
| 43 | creationInfo: { |
| 44 | ownerUid: posixUid, |
| 45 | ownerGid: 1000, |
| 46 | permissions: "750", |
| 47 | }, |
| 48 | }, |
| 49 | tags: { purpose: tag }, |
| 50 | }); |
| 51 | return { files, accessPoint }; |
| 52 | }); |
| 53 | |
| 54 | // --- create --- |
| 55 | const created = yield* stack.deploy(infra(1000, "alchemy-efs-ap-test")); |
| 56 | expect(created.accessPoint.accessPointId).toMatch(/^fsap-/); |
| 57 | expect(created.accessPoint.accessPointArn).toContain(":access-point/"); |
| 58 | expect(created.accessPoint.fileSystemId).toBe(created.files.fileSystemId); |
| 59 | |
| 60 | const observed = yield* efs |
| 61 | .describeAccessPoints({ |
| 62 | AccessPointId: created.accessPoint.accessPointId, |
| 63 | }) |
| 64 | .pipe(Effect.map((r) => r.AccessPoints![0])); |
| 65 | expect(observed.LifeCycleState).toBe("available"); |
| 66 | expect(observed.PosixUser?.Uid).toBe(1000); |
| 67 | expect(observed.RootDirectory?.Path).toBe("/app"); |
| 68 | const observedTags = efsTagsToRecord(observed.Tags); |
| 69 | expect(observedTags.purpose).toBe("alchemy-efs-ap-test"); |
| 70 | expect(observedTags["alchemy::id"]).toBe("ApAccess"); |
| 71 | |
| 72 | // --- update tags in place --- |
| 73 | const retagged = yield* stack.deploy(infra(1000, "alchemy-efs-ap-retag")); |
| 74 | expect(retagged.accessPoint.accessPointId).toBe( |
| 75 | created.accessPoint.accessPointId, |
| 76 | ); |
| 77 | const retaggedObserved = yield* efs |
| 78 | .describeAccessPoints({ |
| 79 | AccessPointId: created.accessPoint.accessPointId, |
| 80 | }) |
| 81 | .pipe(Effect.map((r) => efsTagsToRecord(r.AccessPoints![0].Tags))); |
| 82 | expect(retaggedObserved.purpose).toBe("alchemy-efs-ap-retag"); |
| 83 | |
| 84 | // --- posixUser change replaces the access point --- |
| 85 | const replaced = yield* stack.deploy(infra(1001, "alchemy-efs-ap-retag")); |
| 86 | expect(replaced.accessPoint.accessPointId).not.toBe( |
| 87 | created.accessPoint.accessPointId, |
| 88 | ); |
| 89 | const replacedObserved = yield* efs |
| 90 | .describeAccessPoints({ |
| 91 | AccessPointId: replaced.accessPoint.accessPointId, |
| 92 | }) |
| 93 | .pipe(Effect.map((r) => r.AccessPoints![0])); |
| 94 | expect(replacedObserved.PosixUser?.Uid).toBe(1001); |
| 95 | const oldGone = yield* waitUntilAccessPointGone( |
| 96 | created.accessPoint.accessPointId, |
| 97 | ); |
| 98 | expect(oldGone).toBe(true); |
| 99 | |
| 100 | // --- destroy: access point and file system both gone --- |
| 101 | yield* stack.destroy(); |
| 102 | const apGone = yield* waitUntilAccessPointGone( |
| 103 | replaced.accessPoint.accessPointId, |
| 104 | ); |
| 105 | expect(apGone).toBe(true); |
| 106 | const fsGone = yield* efs |
| 107 | .describeFileSystems({ FileSystemId: created.files.fileSystemId }) |
| 108 | .pipe( |
| 109 | Effect.map((r) => |
| 110 | (r.FileSystems ?? []).every((f) => f.LifeCycleState === "deleted"), |
| 111 | ), |
| 112 | Effect.catchTag("FileSystemNotFound", () => Effect.succeed(true)), |
| 113 | Effect.repeat({ |
| 114 | schedule: Schedule.fixed("2 seconds"), |
| 115 | until: (gone) => gone, |
| 116 | times: 30, |
| 117 | }), |
| 118 | ); |
| 119 | expect(fsGone).toBe(true); |
| 120 | }).pipe( |
| 121 | Effect.tap(() => stack.destroy()), |
| 122 | Effect.onError(() => stack.destroy().pipe(Effect.ignore)), |
| 123 | ), |
| 124 | { tags: ["provider:aws", "provider:aws:efs", "live"], timeout: 120_000 }, |
| 125 | ); |