1import * as AWS from "@/AWS";
2import { SecurityGroup, Subnet, Vpc } from "@/AWS/EC2";
3import { Role } from "@/AWS/IAM";
4import * as Output from "@/Output";
5import * as Provider from "@/Provider";
6import * as Test from "@/Test/Alchemy";
7import * as EC2 from "@distilled.cloud/aws/ec2";
8import * as lambdacore from "@distilled.cloud/aws/lambda-core";
9import { expect } from "alchemy-test";
10import * as Effect from "effect/Effect";
11
12const { test } = Test.make({ providers: AWS.providers() });
13
14// Lambda MicroVM network connectors are a preview feature. Creation provisions
15// ENIs in the VPC and can take several minutes to reach ACTIVE (we've observed
16// the full lifecycle blowing a 25-minute budget and leaking its VPC — which
17// then starves the account's 5-VPC quota and fails unrelated EC2 suites), and
18// the API requires an account that is onboarded to the preview. Gate the live
19// lifecycle behind LAMBDA_TEST_NETWORK_CONNECTOR=1 so an entitled account runs
20// it unchanged.
21test.provider.skipIf(!process.env.LAMBDA_TEST_NETWORK_CONNECTOR)(
22 "create, update, list, delete network connector",
23 (stack) =>
24 Effect.gen(function* () {
25 yield* stack.destroy();
26
27 const azResult = yield* EC2.describeAvailabilityZones({});
28 const az = azResult.AvailabilityZones?.find(
29 (z) => z.State === "available",
30 )?.ZoneName!;
31
32 const infra = (networkProtocol: lambdacore.NetworkProtocol) =>
33 Effect.gen(function* () {
34 // The DualStack update step requires the subnet to carry an IPv6
35 // CIDR, so provision the VPC with an Amazon-provided /56 and carve
36 // the subnet the first /64 out of it.
37 const vpc = yield* Vpc("ConnectorVpc", {
38 cidrBlock: "10.0.0.0/16",
39 amazonProvidedIpv6CidrBlock: true,
40 });
41 const subnet = yield* Subnet("ConnectorSubnet", {
42 vpcId: vpc.vpcId,
43 cidrBlock: "10.0.1.0/24",
44 availabilityZone: az,
45 ipv6CidrBlock: vpc.ipv6CidrBlockAssociationSet.pipe(
46 Output.map((set) => set![0]!.ipv6CidrBlock.replace("/56", "/64")),
47 ),
48 });
49 const sg = yield* SecurityGroup("ConnectorSg", {
50 vpcId: vpc.vpcId,
51 description: "MicroVM egress",
52 });
53 const role = yield* Role("ConnectorOperator", {
54 assumeRolePolicyDocument: {
55 Version: "2012-10-17",
56 Statement: [
57 {
58 Effect: "Allow",
59 Principal: { Service: "lambda.amazonaws.com" },
60 Action: ["sts:AssumeRole"],
61 },
62 ],
63 },
64 // The connector service assumes this role to manage ENIs in the VPC.
65 inlinePolicies: {
66 "eni-management": {
67 Version: "2012-10-17",
68 Statement: [
69 {
70 Effect: "Allow",
71 Action: [
72 "ec2:CreateNetworkInterface",
73 "ec2:DescribeNetworkInterfaces",
74 "ec2:DeleteNetworkInterface",
75 "ec2:DescribeSubnets",
76 "ec2:DescribeSecurityGroups",
77 "ec2:DescribeVpcs",
78 "ec2:AssignPrivateIpAddresses",
79 "ec2:UnassignPrivateIpAddresses",
80 "ec2:CreateTags",
81 ],
82 Resource: "*",
83 },
84 ],
85 },
86 },
87 });
88 const connector = yield* AWS.Lambda.NetworkConnector("Connector", {
89 subnetIds: [subnet.subnetId],
90 securityGroupIds: [sg.groupId],
91 networkProtocol,
92 operatorRole: role.roleArn,
93 });
94 return { connector };
95 });
96
97 // --- create ---
98 const created = yield* stack.deploy(infra("IPv4"));
99 expect(created.connector.state).toBe("ACTIVE");
100 expect(created.connector.networkConnectorArn).toContain(
101 "network-connector",
102 );
103
104 const fetched = yield* lambdacore.getNetworkConnector({
105 Identifier: created.connector.networkConnectorId,
106 });
107 expect(fetched.State).toBe("ACTIVE");
108
109 // --- update (network protocol) ---
110 const updated = yield* stack.deploy(infra("DualStack"));
111 expect(updated.connector.networkConnectorArn).toBe(
112 created.connector.networkConnectorArn,
113 );
114 expect(updated.connector.networkProtocol).toBe("DualStack");
115
116 // --- list ---
117 const provider = yield* Provider.findProvider(
118 AWS.Lambda.NetworkConnector,
119 );
120 const all = yield* provider.list();
121 expect(
122 all.some(
123 (c) => c.networkConnectorId === created.connector.networkConnectorId,
124 ),
125 ).toBe(true);
126
127 // --- delete ---
128 yield* stack.destroy();
129 const afterDestroy = yield* lambdacore
130 .getNetworkConnector({
131 Identifier: created.connector.networkConnectorId,
132 })
133 .pipe(
134 Effect.catchTag("ResourceNotFoundException", () =>
135 Effect.succeed(undefined),
136 ),
137 );
138 expect(afterDestroy).toBeUndefined();
139 }).pipe(
140 Effect.tap(() => stack.destroy()),
141 Effect.onError(() => stack.destroy().pipe(Effect.ignore)),
142 ),
143 {
144 tags: [
145 "provider:aws",
146 "provider:aws:ec2",
147 "provider:aws:iam",
148 "provider:aws:lambda",
149 "live",
150 ],
151 timeout: 1_500_000,
152 },
153);