1import * as AWS from "@/AWS";
2import { FlowLog, Vpc } from "@/AWS/EC2";
3import { LogGroup } from "@/AWS/Logs/LogGroup.ts";
4import { Role } from "@/AWS/IAM/Role.ts";
5import * as Provider from "@/Provider";
6import * as Test from "./VpcTest.ts";
7import * as EC2 from "@distilled.cloud/aws/ec2";
8import { expect } from "alchemy-test";
9import * as Data from "effect/Data";
10import * as Effect from "effect/Effect";
11import { MinimumLogLevel } from "effect/References";
12import * as Schedule from "effect/Schedule";
13
14const { test } = Test.make({ providers: AWS.providers() });
15
16const logLevel = Effect.provideService(
17 MinimumLogLevel,
18 process.env.DEBUG ? "Debug" : "Info",
19);
20
21class FlowLogStillExists extends Data.TaggedError("FlowLogStillExists") {}
22
23const assertDeleted = Effect.fn(function* (flowLogId: string) {
24 yield* EC2.describeFlowLogs({ FlowLogIds: [flowLogId] }).pipe(
25 Effect.flatMap((r) =>
26 (r.FlowLogs?.length ?? 0) === 0
27 ? Effect.void
28 : Effect.fail(new FlowLogStillExists()),
29 ),
30 Effect.retry({
31 while: (e) => e instanceof FlowLogStillExists,
32 schedule: Schedule.max([Schedule.exponential(300), Schedule.recurs(8)]),
33 }),
34 Effect.catchTag("InvalidFlowLogId.NotFound", () => Effect.void),
35 );
36});
37
38const flowLogStack = (tags?: Record<string, string>) =>
39 Effect.gen(function* () {
40 const vpc = yield* Vpc("FlowLogVpc", { cidrBlock: "10.50.0.0/16" });
41 const logGroup = yield* LogGroup("FlowLogGroup", {});
42 const role = yield* Role("FlowLogRole", {
43 assumeRolePolicyDocument: {
44 Version: "2012-10-17",
45 Statement: [
46 {
47 Effect: "Allow",
48 Principal: { Service: "vpc-flow-logs.amazonaws.com" },
49 Action: ["sts:AssumeRole"],
50 },
51 ],
52 },
53 inlinePolicies: {
54 deliver: {
55 Version: "2012-10-17",
56 Statement: [
57 {
58 Effect: "Allow",
59 Action: [
60 "logs:CreateLogStream",
61 "logs:PutLogEvents",
62 "logs:DescribeLogStreams",
63 ],
64 Resource: "*",
65 },
66 ],
67 },
68 },
69 });
70 const flowLog = yield* FlowLog("VpcFlowLog", {
71 resourceType: "VPC",
72 resourceId: vpc.vpcId,
73 trafficType: "ALL",
74 logGroupName: logGroup.logGroupName,
75 deliverLogsPermissionArn: role.roleArn,
76 tags,
77 });
78 return { vpc, flowLog };
79 });
80
81test.provider(
82 "create, update tags, delete VPC flow log to CloudWatch Logs",
83 (stack) =>
84 Effect.gen(function* () {
85 yield* stack.destroy();
86
87 const { vpc, flowLog } = yield* stack.deploy(flowLogStack());
88
89 expect(flowLog.flowLogId).toMatch(/^fl-/);
90 expect(flowLog.resourceId).toEqual(vpc.vpcId);
91 expect(flowLog.trafficType).toEqual("ALL");
92 expect(flowLog.logDestinationType).toEqual("cloud-watch-logs");
93
94 // Verify out-of-band.
95 const described = yield* EC2.describeFlowLogs({
96 FlowLogIds: [flowLog.flowLogId],
97 });
98 const fl = described.FlowLogs?.[0];
99 expect(fl?.FlowLogId).toEqual(flowLog.flowLogId);
100 expect(fl?.ResourceId).toEqual(vpc.vpcId);
101 expect(fl?.TrafficType).toEqual("ALL");
102 expect(fl?.LogDestinationType).toEqual("cloud-watch-logs");
103
104 // Update tags in place (flow log id unchanged).
105 const { flowLog: updated } = yield* stack.deploy(
106 flowLogStack({ env: "prod" }),
107 );
108 expect(updated.flowLogId).toEqual(flowLog.flowLogId);
109
110 const tags = yield* EC2.describeTags({
111 Filters: [
112 { Name: "resource-id", Values: [flowLog.flowLogId] },
113 { Name: "resource-type", Values: ["vpc-flow-log"] },
114 ],
115 });
116 expect(
117 tags.Tags?.some((t) => t.Key === "env" && t.Value === "prod"),
118 ).toBe(true);
119
120 yield* stack.destroy();
121 yield* assertDeleted(flowLog.flowLogId);
122 }).pipe(logLevel),
123 {
124 tags: [
125 "provider:aws",
126 "provider:aws:ec2",
127 "provider:aws:iam",
128 "provider:aws:logs",
129 "live",
130 ],
131 timeout: 240_000,
132 },
133);
134
135test.provider(
136 "list enumerates the deployed flow log",
137 (stack) =>
138 Effect.gen(function* () {
139 yield* stack.destroy();
140
141 const { flowLog } = yield* stack.deploy(flowLogStack());
142
143 const provider = yield* Provider.findProvider(FlowLog);
144 const all = yield* provider.list();
145 expect(all.some((x) => x.flowLogId === flowLog.flowLogId)).toBe(true);
146
147 yield* stack.destroy();
148 yield* assertDeleted(flowLog.flowLogId);
149 }).pipe(logLevel),
150 {
151 tags: [
152 "provider:aws",
153 "provider:aws:ec2",
154 "provider:aws:iam",
155 "provider:aws:logs",
156 "live",
157 ],
158 timeout: 240_000,
159 },
160);