1import * as datasync from "@distilled.cloud/aws/datasync";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { isResolved } from "../../Diff.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import { createInternalTags } from "../../Tags.ts";
8import type { Providers } from "../Providers.ts";
9import {
10 readObservedTags,
11 retryWhileRoleNotAssumable,
12 syncTags,
13} from "./internal.ts";
14
15export interface LocationEfsProps {
16 /**
17 * ARN of the Amazon EFS file system this location points at. Cannot be
18 * changed after creation (replacement).
19 */
20 efsFilesystemArn: string;
21 /**
22 * ARN of the subnet where DataSync creates the network interface used to
23 * mount the file system. The subnet must have an EFS mount target for the
24 * file system. Cannot be changed after creation (replacement).
25 */
26 subnetArn: string;
27 /**
28 * ARNs of the security groups applied to the DataSync network interface.
29 * Must allow NFS (2049) to the file system's mount target. Cannot be
30 * changed after creation (replacement).
31 */
32 securityGroupArns: string[];
33 /**
34 * Subdirectory within the file system to sync, e.g. `/data`. Cannot be
35 * changed after creation (replacement).
36 * @default the file-system root
37 */
38 subdirectory?: string;
39 /**
40 * ARN of an EFS access point DataSync mounts through. Cannot be changed
41 * after creation (replacement).
42 */
43 accessPointArn?: string;
44 /**
45 * ARN of the IAM role DataSync assumes to mount the file system (required
46 * when using an access point or in-transit encryption). Cannot be changed
47 * after creation (replacement).
48 */
49 fileSystemAccessRoleArn?: string;
50 /**
51 * Whether DataSync encrypts data in transit with TLS 1.2. Required when
52 * mounting through an access point. Cannot be changed after creation
53 * (replacement).
54 */
55 inTransitEncryption?: datasync.EfsInTransitEncryption;
56 /**
57 * Tags to apply to the location. Merged with internal Alchemy tags.
58 */
59 tags?: Record<string, string>;
60}
61
62export interface LocationEfs extends Resource<
63 "AWS.DataSync.LocationEfs",
64 LocationEfsProps,
65 {
66 /** ARN of the DataSync location. */
67 locationArn: string;
68 /** URI of the location (`efs://…`). */
69 locationUri: string;
70 },
71 {},
72 Providers
73> {}
74
75/**
76 * A DataSync location backed by an Amazon EFS file system. Used as a source
77 * or destination endpoint of a {@link Task}.
78 *
79 * EFS locations are immutable apart from their tags: any change to the file
80 * system, subnet, security groups, subdirectory, access point, or encryption
81 * replaces the location.
82 *
83 * ### Creating EFS Locations
84 * **Example:** File system + subnet + security group
85 * ```typescript
86 * import * as AWS from "alchemy/AWS";
87 *
88 * const location = yield* AWS.DataSync.LocationEfs("Files", {
89 * efsFilesystemArn: files.fileSystemArn,
90 * subnetArn: `arn:aws:ec2:${region}:${accountId}:subnet/${subnetId}`,
91 * securityGroupArns: [
92 * `arn:aws:ec2:${region}:${accountId}:security-group/${sgId}`,
93 * ],
94 * });
95 * ```
96 *
97 * ### Transferring Data
98 * **Example:** Back up EFS to S3
99 * ```typescript
100 * const dest = yield* AWS.DataSync.LocationS3("Backups", {
101 * s3BucketArn: bucket.bucketArn,
102 * bucketAccessRoleArn: role.roleArn,
103 * });
104 *
105 * const task = yield* AWS.DataSync.Task("EfsBackup", {
106 * sourceLocationArn: location.locationArn,
107 * destinationLocationArn: dest.locationArn,
108 * schedule: { ScheduleExpression: "cron(0 2 * * ? *)" },
109 * });
110 * ```
111 *
112 * @resource
113 */
114export const LocationEfs = Resource<LocationEfs>("AWS.DataSync.LocationEfs");
115
116export const LocationEfsProvider = () =>
117 Provider.effect(
118 LocationEfs,
119 Effect.gen(function* () {
120 const describe = Effect.fn(function* (locationArn: string) {
121 return yield* datasync
122 .describeLocationEfs({ LocationArn: locationArn })
123 .pipe(
124 Effect.catchTag("LocationNotFound", () =>
125 Effect.succeed(undefined),
126 ),
127 );
128 });
129
130 return LocationEfs.Provider.of({
131 stables: ["locationArn", "locationUri"],
132
133 list: () =>
134 datasync.listLocations.pages({}).pipe(
135 Stream.runCollect,
136 Effect.map((chunk) =>
137 Array.from(chunk)
138 .flatMap((p) => p.Locations ?? [])
139 .filter((l) => l.LocationUri?.startsWith("efs://"))
140 .map((l) => ({
141 locationArn: l.LocationArn!,
142 locationUri: l.LocationUri!,
143 })),
144 ),
145 ),
146
147 read: Effect.fn(function* ({ output }) {
148 if (output?.locationArn === undefined) return undefined;
149 const loc = yield* describe(output.locationArn);
150 if (loc === undefined) return undefined;
151 return {
152 locationArn: loc.LocationArn!,
153 locationUri: loc.LocationUri!,
154 };
155 }),
156
157 diff: Effect.fn(function* ({ news, olds }) {
158 if (!isResolved(news)) return undefined;
159 const replaced =
160 news.efsFilesystemArn !== olds.efsFilesystemArn ||
161 news.subnetArn !== olds.subnetArn ||
162 JSON.stringify(news.securityGroupArns) !==
163 JSON.stringify(olds.securityGroupArns) ||
164 (news.subdirectory ?? "/") !== (olds.subdirectory ?? "/") ||
165 news.accessPointArn !== olds.accessPointArn ||
166 news.fileSystemAccessRoleArn !== olds.fileSystemAccessRoleArn ||
167 news.inTransitEncryption !== olds.inTransitEncryption;
168 if (replaced) return { action: "replace" } as const;
169 }),
170
171 reconcile: Effect.fn(function* ({ id, news, output, session }) {
172 const internalTags = yield* createInternalTags(id);
173 const desiredTags = { ...news.tags, ...internalTags };
174
175 // 1. OBSERVE — output cache is the identity (EFS location URIs are
176 // not deterministically reconstructable).
177 let arn = output?.locationArn;
178 let loc = arn ? yield* describe(arn) : undefined;
179
180 // 2. ENSURE — create if missing.
181 if (loc === undefined) {
182 const created = yield* retryWhileRoleNotAssumable(
183 datasync.createLocationEfs({
184 EfsFilesystemArn: news.efsFilesystemArn,
185 Ec2Config: {
186 SubnetArn: news.subnetArn,
187 SecurityGroupArns: news.securityGroupArns,
188 },
189 Subdirectory: news.subdirectory,
190 AccessPointArn: news.accessPointArn,
191 FileSystemAccessRoleArn: news.fileSystemAccessRoleArn,
192 InTransitEncryption: news.inTransitEncryption,
193 Tags: Object.entries(desiredTags).map(([Key, Value]) => ({
194 Key,
195 Value,
196 })),
197 }),
198 );
199 arn = created.LocationArn!;
200 loc = yield* describe(arn);
201 }
202
203 // 3. SYNC tags against observed cloud state.
204 const observed = yield* readObservedTags(arn!);
205 yield* syncTags(arn!, observed, desiredTags);
206
207 yield* session.note(arn!);
208 return {
209 locationArn: loc!.LocationArn!,
210 locationUri: loc!.LocationUri!,
211 };
212 }),
213
214 delete: Effect.fn(function* ({ output }) {
215 yield* datasync
216 .deleteLocation({ LocationArn: output.locationArn })
217 .pipe(Effect.catchTag("LocationNotFound", () => Effect.void));
218 }),
219 });
220 }),
221 );