1import * as AWS from "@/AWS";
2import { Accelerator, EndpointGroup, Listener } from "@/AWS/GlobalAccelerator";
3import { Bucket } from "@/AWS/S3";
4import * as Test from "@/Test/Alchemy";
5import * as ga from "@distilled.cloud/aws/global-accelerator";
6import { describe, expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Schedule from "effect/Schedule";
9
10const { test } = Test.make({ providers: AWS.providers() });
11
12// Deterministic (per test case) bucket for accelerator flow logs. Flow-log
13// delivery requires a bucket policy granting the log-delivery service
14// principal write access.
15const FLOW_LOGS_BUCKET = "alchemy-test-ga-flow-logs-4f81c2";
16
17// The testing profile runs in us-west-2, which is exactly where the Global
18// Accelerator control plane lives, so out-of-band verification calls the
19// distilled ops directly.
20
21const assertAcceleratorGone = (acceleratorArn: string) =>
22 ga.describeAccelerator({ AcceleratorArn: acceleratorArn }).pipe(
23 Effect.flatMap(() =>
24 Effect.fail(new Error(`accelerator ${acceleratorArn} still exists`)),
25 ),
26 Effect.catchTag("AcceleratorNotFoundException", () => Effect.void),
27 Effect.retry({
28 while: (e) => e instanceof Error,
29 schedule: Schedule.max([
30 Schedule.fixed("3 seconds"),
31 Schedule.recurs(10),
32 ]),
33 }),
34 );
35
36describe.sequential(
37 "AWS.GlobalAccelerator",
38 {
39 tags: [
40 "provider:aws",
41 "provider:aws:globalaccelerator",
42 "provider:aws:s3",
43 "live",
44 ],
45 },
46 () => {
47 test.provider(
48 "creates accelerator + listener + endpoint group, updates in place, destroys in order",
49 (stack) =>
50 Effect.gen(function* () {
51 yield* stack.destroy();
52
53 const program = (opts: {
54 listenerPorts: { fromPort: number; toPort: number }[];
55 clientAffinity?: "NONE" | "SOURCE_IP";
56 trafficDial?: number;
57 flowLogs?: boolean;
58 tags: Record<string, string>;
59 }) =>
60 Effect.gen(function* () {
61 const bucket = yield* Bucket("FlowLogsBucket", {
62 bucketName: FLOW_LOGS_BUCKET,
63 policy: [
64 {
65 Sid: "AWSLogDeliveryWrite",
66 Effect: "Allow",
67 Principal: { Service: "delivery.logs.amazonaws.com" },
68 Action: ["s3:PutObject"],
69 Resource: [`arn:aws:s3:::${FLOW_LOGS_BUCKET}/*`],
70 },
71 {
72 Sid: "AWSLogDeliveryAclCheck",
73 Effect: "Allow",
74 Principal: { Service: "delivery.logs.amazonaws.com" },
75 Action: ["s3:GetBucketAcl"],
76 Resource: [`arn:aws:s3:::${FLOW_LOGS_BUCKET}`],
77 },
78 ],
79 forceDestroy: true,
80 });
81 const accelerator = yield* Accelerator("TestAccelerator", {
82 flowLogs: opts.flowLogs
83 ? { bucket: bucket.bucketName, prefix: "ga-flow-logs" }
84 : undefined,
85 tags: opts.tags,
86 });
87 const listener = yield* Listener("WebListener", {
88 acceleratorArn: accelerator.acceleratorArn,
89 portRanges: opts.listenerPorts,
90 protocol: "TCP",
91 clientAffinity: opts.clientAffinity,
92 });
93 const group = yield* EndpointGroup("UsWest2Group", {
94 listenerArn: listener.listenerArn,
95 endpointGroupRegion: "us-west-2",
96 trafficDialPercentage: opts.trafficDial,
97 healthCheckProtocol: "TCP",
98 healthCheckPort: 80,
99 });
100 return {
101 acceleratorArn: accelerator.acceleratorArn,
102 acceleratorName: accelerator.name,
103 dnsName: accelerator.dnsName,
104 flowLogsEnabled: accelerator.flowLogsEnabled,
105 listenerArn: listener.listenerArn,
106 endpointGroupArn: group.endpointGroupArn,
107 trafficDialPercentage: group.trafficDialPercentage,
108 };
109 });
110
111 // --- create ---
112 const created = yield* stack.deploy(
113 program({
114 listenerPorts: [{ fromPort: 80, toPort: 80 }],
115 tags: { purpose: "alchemy-test" },
116 }),
117 );
118 expect(created.acceleratorArn).toContain(":accelerator/");
119 expect(created.dnsName).toBeTruthy();
120 expect(created.flowLogsEnabled).toBe(false);
121 expect(created.listenerArn).toContain("/listener/");
122 expect(created.endpointGroupArn).toContain("/endpoint-group/");
123
124 // Out-of-band verification via distilled.
125 const acc = yield* ga.describeAccelerator({
126 AcceleratorArn: created.acceleratorArn,
127 });
128 expect(acc.Accelerator?.Name).toEqual(created.acceleratorName);
129 expect(acc.Accelerator?.Enabled).toBe(true);
130 expect(acc.Accelerator?.DnsName).toEqual(created.dnsName);
131
132 const lst = yield* ga.describeListener({
133 ListenerArn: created.listenerArn,
134 });
135 expect(lst.Listener?.Protocol).toEqual("TCP");
136 expect(lst.Listener?.PortRanges).toEqual([
137 { FromPort: 80, ToPort: 80 },
138 ]);
139 expect(lst.Listener?.ClientAffinity).toEqual("NONE");
140
141 const grp = yield* ga.describeEndpointGroup({
142 EndpointGroupArn: created.endpointGroupArn,
143 });
144 expect(grp.EndpointGroup?.EndpointGroupRegion).toEqual("us-west-2");
145 expect(grp.EndpointGroup?.TrafficDialPercentage).toEqual(100);
146 expect(grp.EndpointGroup?.HealthCheckPort).toEqual(80);
147
148 const tags = yield* ga.listTagsForResource({
149 ResourceArn: created.acceleratorArn,
150 });
151 const tagMap = Object.fromEntries(
152 (tags.Tags ?? []).map((t) => [t.Key, t.Value]),
153 );
154 expect(tagMap.purpose).toEqual("alchemy-test");
155 expect(tagMap["alchemy::id"]).toEqual("TestAccelerator");
156
157 // --- update 1: listener ports + affinity, accelerator tags + flow logs ---
158 const updated = yield* stack.deploy(
159 program({
160 listenerPorts: [
161 { fromPort: 80, toPort: 80 },
162 { fromPort: 443, toPort: 443 },
163 ],
164 clientAffinity: "SOURCE_IP",
165 flowLogs: true,
166 tags: { purpose: "alchemy-test", team: "platform" },
167 }),
168 );
169 // in-place updates — stable ARNs
170 expect(updated.acceleratorArn).toEqual(created.acceleratorArn);
171 expect(updated.listenerArn).toEqual(created.listenerArn);
172 expect(updated.endpointGroupArn).toEqual(created.endpointGroupArn);
173 expect(updated.flowLogsEnabled).toBe(true);
174
175 // Out-of-band: flow logs were enabled via updateAcceleratorAttributes.
176 const attrs = yield* ga.describeAcceleratorAttributes({
177 AcceleratorArn: created.acceleratorArn,
178 });
179 expect(attrs.AcceleratorAttributes?.FlowLogsEnabled).toBe(true);
180 expect(attrs.AcceleratorAttributes?.FlowLogsS3Bucket).toEqual(
181 FLOW_LOGS_BUCKET,
182 );
183 expect(attrs.AcceleratorAttributes?.FlowLogsS3Prefix).toEqual(
184 "ga-flow-logs",
185 );
186
187 const lst2 = yield* ga.describeListener({
188 ListenerArn: created.listenerArn,
189 });
190 const ports = (lst2.Listener?.PortRanges ?? [])
191 .map((r) => r.FromPort)
192 .sort((a, b) => (a ?? 0) - (b ?? 0));
193 expect(ports).toEqual([80, 443]);
194 expect(lst2.Listener?.ClientAffinity).toEqual("SOURCE_IP");
195
196 const tags2 = yield* ga.listTagsForResource({
197 ResourceArn: created.acceleratorArn,
198 });
199 const tagMap2 = Object.fromEntries(
200 (tags2.Tags ?? []).map((t) => [t.Key, t.Value]),
201 );
202 expect(tagMap2.team).toEqual("platform");
203
204 // --- update 2: endpoint group traffic dial + disable flow logs ---
205 const dialed = yield* stack.deploy(
206 program({
207 listenerPorts: [
208 { fromPort: 80, toPort: 80 },
209 { fromPort: 443, toPort: 443 },
210 ],
211 clientAffinity: "SOURCE_IP",
212 trafficDial: 50,
213 tags: { purpose: "alchemy-test", team: "platform" },
214 }),
215 );
216 expect(dialed.endpointGroupArn).toEqual(created.endpointGroupArn);
217 expect(dialed.trafficDialPercentage).toEqual(50);
218 expect(dialed.flowLogsEnabled).toBe(false);
219
220 const grp2 = yield* ga.describeEndpointGroup({
221 EndpointGroupArn: created.endpointGroupArn,
222 });
223 expect(grp2.EndpointGroup?.TrafficDialPercentage).toEqual(50);
224
225 const attrs2 = yield* ga.describeAcceleratorAttributes({
226 AcceleratorArn: created.acceleratorArn,
227 });
228 expect(attrs2.AcceleratorAttributes?.FlowLogsEnabled).toBe(false);
229
230 // --- destroy: endpoint group -> listener -> disable -> delete ---
231 yield* stack.destroy();
232 yield* assertAcceleratorGone(created.acceleratorArn);
233 }),
234 { timeout: 220_000 },
235 );
236 },
237);