1import * as AWS from "@/AWS";
2import * as Test from "@/Test/Alchemy";
3import * as datasync from "@distilled.cloud/aws/datasync";
4import * as EC2 from "@distilled.cloud/aws/ec2";
5import { Region } from "@distilled.cloud/aws/Region";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Schedule from "effect/Schedule";
9import { getDefaultVpc } from "../DefaultVpc.ts";
10
11const { test } = Test.make({ providers: AWS.providers() });
12
13const waitUntilLocationGone = (locationArn: string) =>
14 datasync.describeLocationEfs({ LocationArn: locationArn }).pipe(
15 Effect.as(false),
16 Effect.catchTag("LocationNotFound", () => Effect.succeed(true)),
17 Effect.repeat({
18 schedule: Schedule.fixed("1 seconds"),
19 until: (gone) => gone,
20 times: 20,
21 }),
22 );
23
24test.provider(
25 "typed LocationNotFound on a nonexistent EFS location",
26 () =>
27 Effect.gen(function* () {
28 const result = yield* datasync
29 .describeLocationEfs({
30 LocationArn:
31 "arn:aws:datasync:us-west-2:391965393224:location/loc-00000000000000000",
32 })
33 .pipe(Effect.result);
34 expect(result._tag).toBe("Failure");
35 if (result._tag === "Failure") {
36 const err = result.failure as { _tag: string };
37 expect(err._tag).toBe("LocationNotFound");
38 }
39 }),
40 { tags: ["provider:aws", "provider:aws:datasync", "live"], timeout: 60_000 },
41);
42
43// The live lifecycle requires an EFS file system + mount target; the mount
44// target's ENI takes 1–3 minutes to become available and again to release on
45// teardown, so the full cycle is gated behind AWS_TEST_SLOW=1.
46test.provider.skipIf(!process.env.AWS_TEST_SLOW)(
47 "create EFS location from a file system + mount target, delete",
48 (stack) =>
49 Effect.gen(function* () {
50 yield* stack.destroy();
51
52 const region = yield* Region;
53 const vpc = yield* getDefaultVpc;
54 const subnets = yield* EC2.describeSubnets({
55 Filters: [
56 { Name: "vpc-id", Values: [vpc.vpcId] },
57 { Name: "default-for-az", Values: ["true"] },
58 ],
59 });
60 const subnetId = subnets.Subnets![0]!.SubnetId!;
61 const groups = yield* EC2.describeSecurityGroups({
62 Filters: [
63 { Name: "vpc-id", Values: [vpc.vpcId] },
64 { Name: "group-name", Values: ["default"] },
65 ],
66 });
67 const sg = groups.SecurityGroups![0]!;
68 const accountId = sg.OwnerId!;
69 const securityGroupId = sg.GroupId!;
70 const subnetArn = `arn:aws:ec2:${region}:${accountId}:subnet/${subnetId}`;
71 const securityGroupArn = `arn:aws:ec2:${region}:${accountId}:security-group/${securityGroupId}`;
72
73 const created = yield* stack.deploy(
74 Effect.gen(function* () {
75 const files = yield* AWS.EFS.FileSystem("EfsLocFiles");
76 const target = yield* AWS.EFS.MountTarget("EfsLocTarget", {
77 fileSystemId: files.fileSystemId,
78 subnetId,
79 });
80 const location = yield* AWS.DataSync.LocationEfs("EfsLoc", {
81 // depend on the mount target so the location is created only once
82 // the network endpoint is available
83 efsFilesystemArn: files.fileSystemArn,
84 subnetArn,
85 securityGroupArns: [securityGroupArn],
86 tags: { mount: target.mountTargetId },
87 });
88 return { location };
89 }),
90 );
91 expect(created.location.locationArn).toContain(":location/loc-");
92 expect(created.location.locationUri).toContain("efs://");
93 const arn = created.location.locationArn;
94
95 const observed = yield* datasync.describeLocationEfs({
96 LocationArn: arn,
97 });
98 expect(observed.Ec2Config?.SubnetArn).toBe(subnetArn);
99
100 yield* stack.destroy();
101 const gone = yield* waitUntilLocationGone(arn);
102 expect(gone).toBe(true);
103 }),
104 {
105 tags: [
106 "provider:aws",
107 "provider:aws:datasync",
108 "provider:aws:ec2",
109 "provider:aws:efs",
110 "live",
111 ],
112 timeout: 600_000,
113 },
114);