1import * as agw2 from "@distilled.cloud/aws/apigatewayv2";
2import * as Effect from "effect/Effect";
3import { deepEqual, isResolved } from "../../Diff.ts";
4import { createPhysicalName } from "../../PhysicalName.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import { createInternalTags } from "../../Tags.ts";
8import { AWSEnvironment } from "../Environment.ts";
9import type { Providers } from "../Providers.ts";
10import {
11 collectAllPages,
12 retryOnTooManyRequests,
13 syncTags,
14 tagRecord,
15 vpcLinkArn,
16} from "./common.ts";
17
18export interface VpcLinkProps {
19 /**
20 * Name of the VPC link. If omitted, Alchemy generates a deterministic
21 * physical name.
22 */
23 name?: string;
24 /**
25 * Subnet IDs the VPC link attaches to. Immutable — changing them
26 * triggers a replacement.
27 */
28 subnetIds: string[];
29 /**
30 * Security group IDs for the VPC link. Immutable — changing them
31 * triggers a replacement.
32 */
33 securityGroupIds?: string[];
34 /**
35 * User-defined tags (Alchemy internal tags are merged automatically).
36 */
37 tags?: Record<string, string>;
38}
39
40export interface VpcLink extends Resource<
41 "AWS.ApiGatewayV2.VpcLink",
42 VpcLinkProps,
43 {
44 /** The VPC link identifier. */
45 vpcLinkId: string;
46 /** The VPC link name. */
47 name: string;
48 /** The subnet IDs. */
49 subnetIds: string[];
50 /** The security group IDs. */
51 securityGroupIds: string[];
52 /**
53 * The provisioning status (`PENDING`, `AVAILABLE`, `DELETING`,
54 * `FAILED`). Provisioning typically takes 1–2 minutes; integrations
55 * that reference the link only work once it is `AVAILABLE`.
56 */
57 status: string | undefined;
58 tags: Record<string, string>;
59 },
60 never,
61 Providers
62> {}
63
64/**
65 * An API Gateway v2 VPC link — lets an HTTP API reach private resources
66 * (ALB/NLB listeners, Cloud Map services) inside a VPC.
67 *
68 * Unlike the v1 VPC link (NLB-only, ~10 min provisioning), the v2 link is
69 * subnet/security-group based and provisions in ~1–2 minutes.
70 * ### Private integrations
71 * **Example:** VPC link + private integration
72 * ```typescript
73 * const link = yield* ApiGatewayV2.VpcLink("Link", {
74 * subnetIds: [subnetA.subnetId, subnetB.subnetId],
75 * securityGroupIds: [securityGroup.securityGroupId],
76 * });
77 *
78 * yield* ApiGatewayV2.Integration("Private", {
79 * api,
80 * integrationType: "HTTP_PROXY",
81 * integrationUri: listener.listenerArn,
82 * integrationMethod: "ANY",
83 * connectionType: "VPC_LINK",
84 * connectionId: link.vpcLinkId,
85 * payloadFormatVersion: "1.0",
86 * });
87 * ```
88 *
89 * @resource
90 */
91export const VpcLink = Resource<VpcLink>("AWS.ApiGatewayV2.VpcLink");
92
93const snapshotFromVpcLink = (
94 link: agw2.GetVpcLinkResponse,
95): VpcLink["Attributes"] => ({
96 vpcLinkId: link.VpcLinkId!,
97 name: link.Name ?? "",
98 subnetIds: [...(link.SubnetIds ?? [])],
99 securityGroupIds: [...(link.SecurityGroupIds ?? [])],
100 status: link.VpcLinkStatus,
101 tags: tagRecord(link.Tags),
102});
103
104export const VpcLinkProvider = () =>
105 Provider.effect(
106 VpcLink,
107 Effect.gen(function* () {
108 const createName = Effect.fn(function* (
109 id: string,
110 props: Pick<VpcLinkProps, "name">,
111 ) {
112 return (
113 props.name ?? (yield* createPhysicalName({ id, maxLength: 128 }))
114 );
115 });
116
117 const getVpcLinkSafe = (vpcLinkId: string) =>
118 agw2
119 .getVpcLink({ VpcLinkId: vpcLinkId })
120 .pipe(
121 Effect.catchTag("NotFoundException", () =>
122 Effect.succeed(undefined),
123 ),
124 );
125
126 return VpcLink.Provider.of({
127 stables: ["vpcLinkId"],
128
129 list: () =>
130 Effect.gen(function* () {
131 const items = yield* collectAllPages((NextToken) =>
132 agw2.getVpcLinks({ NextToken }),
133 );
134 return items
135 .filter((link) => link.VpcLinkId != null)
136 .map((link) => snapshotFromVpcLink(link));
137 }),
138
139 read: Effect.fn(function* ({ output }) {
140 if (!output?.vpcLinkId) return undefined;
141 const link = yield* getVpcLinkSafe(output.vpcLinkId);
142 if (!link?.VpcLinkId) return undefined;
143 return snapshotFromVpcLink(link);
144 }),
145
146 diff: Effect.fn(function* ({ news, olds }) {
147 if (!isResolved(news)) return undefined;
148 // Subnets and security groups are immutable on a v2 VPC link.
149 if (
150 !deepEqual(
151 [...news.subnetIds].sort(),
152 [...olds.subnetIds].sort(),
153 ) ||
154 !deepEqual(
155 [...(news.securityGroupIds ?? [])].sort(),
156 [...(olds.securityGroupIds ?? [])].sort(),
157 )
158 ) {
159 return { action: "replace" } as const;
160 }
161 }),
162
163 reconcile: Effect.fn(function* ({ id, news, output, session }) {
164 const { region } = yield* AWSEnvironment.current;
165 const name = yield* createName(id, news);
166 const internalTags = yield* createInternalTags(id);
167 const desiredTags = { ...news.tags, ...internalTags };
168
169 // 1. OBSERVE
170 let observed = output?.vpcLinkId
171 ? yield* getVpcLinkSafe(output.vpcLinkId)
172 : undefined;
173
174 // 2. ENSURE
175 if (!observed?.VpcLinkId) {
176 observed = yield* retryOnTooManyRequests(
177 agw2.createVpcLink({
178 Name: name,
179 SubnetIds: news.subnetIds,
180 SecurityGroupIds: news.securityGroupIds,
181 Tags: desiredTags,
182 }),
183 );
184 yield* session.note(`Created VPC link ${observed.VpcLinkId}`);
185 }
186 const snapshot = snapshotFromVpcLink(observed);
187
188 // 3. SYNC — only the name is mutable.
189 if (snapshot.name !== name) {
190 yield* retryOnTooManyRequests(
191 agw2.updateVpcLink({
192 VpcLinkId: snapshot.vpcLinkId,
193 Name: name,
194 }),
195 );
196 }
197
198 // 3b. SYNC TAGS — diff against OBSERVED cloud tags.
199 if (!deepEqual(snapshot.tags, desiredTags)) {
200 yield* syncTags({
201 resourceArn: vpcLinkArn(region, snapshot.vpcLinkId),
202 oldTags: snapshot.tags,
203 newTags: desiredTags,
204 });
205 }
206
207 // 4. RETURN fresh state. Provisioning is asynchronous (~1–2 min);
208 // the `status` attribute surfaces it rather than blocking the
209 // deploy on AVAILABLE.
210 const final = yield* agw2.getVpcLink({
211 VpcLinkId: snapshot.vpcLinkId,
212 });
213 yield* session.note(`Reconciled VPC link ${snapshot.vpcLinkId}`);
214 return snapshotFromVpcLink(final);
215 }),
216
217 delete: Effect.fn(function* ({ output, session }) {
218 yield* retryOnTooManyRequests(
219 agw2
220 .deleteVpcLink({ VpcLinkId: output.vpcLinkId })
221 .pipe(Effect.catchTag("NotFoundException", () => Effect.void)),
222 );
223 yield* session.note(`Deleted VPC link ${output.vpcLinkId}`);
224 }),
225 });
226 }),
227 );