1import * as AWS from "@/AWS";
2import { Bucket } from "@/AWS/S3";
3import { AccessPoint, ObjectLambdaAccessPoint } from "@/AWS/S3Control";
4import * as Test from "@/Test/Alchemy";
5import * as s3control from "@distilled.cloud/aws/s3-control";
6import { expect } from "alchemy-test";
7import * as Data from "effect/Data";
8import * as Effect from "effect/Effect";
9import * as Schedule from "effect/Schedule";
10import {
11 ObjectTransformFunction,
12 ObjectTransformFunctionLive,
13} from "./fixtures/transform-handler.ts";
14
15const { test } = Test.make({ providers: AWS.providers() });
16
17const ACCOUNT_ID = "391965393224";
18
19const findObjectLambdaAccessPoint = (name: string) =>
20 s3control
21 .getAccessPointForObjectLambda({ AccountId: ACCOUNT_ID, Name: name })
22 .pipe(
23 Effect.catchTag("NoSuchAccessPoint", () => Effect.succeed(undefined)),
24 );
25
26class ObjectLambdaAccessPointStillExists extends Data.TaggedError(
27 "ObjectLambdaAccessPointStillExists",
28)<{ readonly name: string }> {}
29
30const assertObjectLambdaAccessPointDeleted = (name: string) =>
31 findObjectLambdaAccessPoint(name).pipe(
32 Effect.flatMap((olap) =>
33 olap === undefined
34 ? Effect.void
35 : Effect.fail(new ObjectLambdaAccessPointStillExists({ name })),
36 ),
37 Effect.retry({
38 while: (e) => e._tag === "ObjectLambdaAccessPointStillExists",
39 schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(8)]),
40 }),
41 );
42
43test.provider(
44 "typed NoSuchAccessPoint tag on a nonexistent object lambda access point",
45 () =>
46 Effect.gen(function* () {
47 const result = yield* s3control
48 .getAccessPointForObjectLambda({
49 AccountId: ACCOUNT_ID,
50 Name: "alchemy-does-not-exist-xyz",
51 })
52 .pipe(
53 Effect.map(() => "found" as const),
54 Effect.catchTag("NoSuchAccessPoint", () =>
55 Effect.succeed("missing" as const),
56 ),
57 );
58 expect(result).toBe("missing");
59 }),
60 { tags: ["provider:aws", "provider:aws:s3control", "live"], timeout: 60_000 },
61);
62
63// S3 Object Lambda is entitlement-gated: "Amazon S3 Object Lambda is
64// available only to existing customers that are currently using the service
65// as well as to select AWS Partner Network (APN) partners." (AccessDenied on
66// CreateAccessPointForObjectLambda, surfaced as the typed
67// ObjectLambdaNotAvailable tag). This probe pins the typed tag; an entitled
68// account instead fails with NoSuchAccessPoint for the bogus supporting AP.
69test.provider(
70 "typed ObjectLambdaNotAvailable tag on a non-entitled account",
71 () =>
72 Effect.gen(function* () {
73 const result = yield* s3control
74 .createAccessPointForObjectLambda({
75 AccountId: ACCOUNT_ID,
76 Name: "alchemy-olap-entitlement-probe",
77 Configuration: {
78 SupportingAccessPoint: `arn:aws:s3:us-west-2:${ACCOUNT_ID}:accesspoint/alchemy-does-not-exist-xyz`,
79 TransformationConfigurations: [
80 {
81 Actions: ["GetObject"],
82 ContentTransformation: {
83 AwsLambda: {
84 FunctionArn: `arn:aws:lambda:us-west-2:${ACCOUNT_ID}:function:does-not-exist`,
85 },
86 },
87 },
88 ],
89 },
90 })
91 .pipe(
92 Effect.map(() => "created" as const),
93 Effect.catchTag("ObjectLambdaNotAvailable", () =>
94 Effect.succeed("gated" as const),
95 ),
96 Effect.catchTag("NoSuchAccessPoint", () =>
97 Effect.succeed("missing-supporting-ap" as const),
98 ),
99 );
100 // this account is not entitled; an entitled account would reach the
101 // supporting-access-point validation instead
102 expect(["gated", "missing-supporting-ap"]).toContain(result);
103 // never actually created
104 expect(result).not.toBe("created");
105 }),
106 { tags: ["provider:aws", "provider:aws:s3control", "live"], timeout: 60_000 },
107);
108
109// Full lifecycle requires an account entitled to S3 Object Lambda (the
110// service is closed to new customers) — gate it so entitled accounts can
111// still run it unchanged: AWS_TEST_OBJECT_LAMBDA=1.
112test.provider.skipIf(!process.env.AWS_TEST_OBJECT_LAMBDA)(
113 "create, update configuration, delete object lambda access point",
114 (stack) =>
115 Effect.gen(function* () {
116 yield* stack.destroy();
117
118 const makeStack = (cloudWatchMetricsEnabled: boolean) =>
119 Effect.gen(function* () {
120 const bucket = yield* Bucket("OlapBucket", { forceDestroy: true });
121 const accessPoint = yield* AccessPoint("OlapSupportingAp", {
122 bucket: bucket.bucketName,
123 });
124 const transformer = yield* ObjectTransformFunction.pipe(
125 Effect.provide(ObjectTransformFunctionLive),
126 );
127 const olap = yield* ObjectLambdaAccessPoint("TestOlap", {
128 supportingAccessPoint: accessPoint.accessPointArn,
129 cloudWatchMetricsEnabled,
130 transformationConfigurations: [
131 {
132 Actions: ["GetObject"],
133 ContentTransformation: {
134 AwsLambda: { FunctionArn: transformer.functionArn },
135 },
136 },
137 ],
138 });
139 return { accessPoint, olap };
140 });
141
142 const deployed = yield* stack.deploy(makeStack(false));
143
144 expect(deployed.olap.objectLambdaAccessPointName).toBeDefined();
145 expect(deployed.olap.objectLambdaAccessPointArn).toContain(
146 ":s3-object-lambda:",
147 );
148
149 // out-of-band verification via distilled
150 const live = yield* findObjectLambdaAccessPoint(
151 deployed.olap.objectLambdaAccessPointName,
152 );
153 expect(live).toBeDefined();
154
155 const config =
156 yield* s3control.getAccessPointConfigurationForObjectLambda({
157 AccountId: ACCOUNT_ID,
158 Name: deployed.olap.objectLambdaAccessPointName,
159 });
160 expect(config.Configuration?.SupportingAccessPoint).toBe(
161 deployed.accessPoint.accessPointArn,
162 );
163 expect(config.Configuration?.CloudWatchMetricsEnabled ?? false).toBe(
164 false,
165 );
166
167 // update the mutable configuration in place — same access point
168 const updated = yield* stack.deploy(makeStack(true));
169 expect(updated.olap.objectLambdaAccessPointName).toBe(
170 deployed.olap.objectLambdaAccessPointName,
171 );
172
173 const updatedConfig =
174 yield* s3control.getAccessPointConfigurationForObjectLambda({
175 AccountId: ACCOUNT_ID,
176 Name: deployed.olap.objectLambdaAccessPointName,
177 });
178 expect(updatedConfig.Configuration?.CloudWatchMetricsEnabled).toBe(true);
179
180 yield* stack.destroy();
181 yield* assertObjectLambdaAccessPointDeleted(
182 deployed.olap.objectLambdaAccessPointName,
183 );
184 }),
185 {
186 tags: [
187 "provider:aws",
188 "provider:aws:lambda",
189 "provider:aws:s3",
190 "provider:aws:s3control",
191 "live",
192 ],
193 timeout: 240_000,
194 },
195);