1import * as ag from "@distilled.cloud/aws/api-gateway";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import * as Stream from "effect/Stream";
5import { deepEqual, isResolved } from "../../Diff.ts";
6import { createPhysicalName } from "../../PhysicalName.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { createInternalTags, tagRecord } from "../../Tags.ts";
10import type { Providers } from "../Providers.ts";
11
12import { AWSEnvironment } from "../Environment.ts";
13import { syncTags, vpcLinkArn } from "./common.ts";
14
15export interface VpcLinkProps {
16 /**
17 * Name of the VPC link.
18 *
19 * If omitted, Alchemy generates a deterministic physical name.
20 */
21 name?: string;
22 /**
23 * Target ARNs for the integration (e.g. load balancer ARNs).
24 */
25 targetArns: string[];
26 /** Description of the VPC link. */
27 description?: string;
28 /** User-defined tags for the VPC link. */
29 tags?: Record<string, string>;
30}
31
32/** @resource */
33export interface VpcLink extends Resource<
34 "AWS.ApiGateway.VpcLink",
35 VpcLinkProps,
36 {
37 vpcLinkId: string;
38 name: string | undefined;
39 description: string | undefined;
40 targetArns: string[] | undefined;
41 status: ag.VpcLinkStatus | undefined;
42 statusMessage: string | undefined;
43 tags: Record<string, string>;
44 },
45 never,
46 Providers
47> {}
48
49/**
50 * VPC link for private integrations (`connectionType: "VPC_LINK"` on a method integration).
51 *
52 * ### Private integrations
53 * **Example:** Create a VPC link
54 * ```typescript
55 * const link = yield* ApiGateway.VpcLink("NlbLink", {
56 * description: "Link to internal NLB",
57 * targetArns: [nlb.loadBalancerArn],
58 * });
59 *
60 * yield* ApiGateway.Method("PrivateGet", {
61 * restApiId: api.restApiId,
62 * resourceId: resource.resourceId,
63 * httpMethod: "GET",
64 * integration: {
65 * type: "HTTP_PROXY",
66 * integrationHttpMethod: "GET",
67 * uri: "https://api.internal.example.com/hello",
68 * connectionType: "VPC_LINK",
69 * connectionId: link.vpcLinkId,
70 * },
71 * });
72 * ```
73 */
74const VpcLinkResource = Resource<VpcLink>("AWS.ApiGateway.VpcLink");
75
76export { VpcLinkResource as VpcLink };
77
78const generatedName = (id: string, props: VpcLinkProps) =>
79 props.name
80 ? Effect.succeed(props.name)
81 : createPhysicalName({
82 id,
83 maxLength: 128,
84 });
85
86const snapshotFromVpcLink = (
87 v: ag.VpcLink,
88 tags: Record<string, string>,
89): VpcLink["Attributes"] => ({
90 vpcLinkId: v.id!,
91 name: v.name,
92 description: v.description,
93 targetArns: v.targetArns,
94 status: v.status,
95 statusMessage: v.statusMessage,
96 tags,
97});
98
99export const VpcLinkProvider = () =>
100 Provider.effect(
101 VpcLinkResource,
102 Effect.gen(function* () {
103 return {
104 stables: ["vpcLinkId"] as const,
105 diff: Effect.fn(function* ({ news: newsIn, olds }) {
106 if (!isResolved(newsIn)) return;
107 const news = newsIn as VpcLinkProps;
108 if (!deepEqual(news.targetArns, olds.targetArns)) {
109 return { action: "replace" } as const;
110 }
111 }),
112 read: Effect.fn(function* ({ output }) {
113 if (!output?.vpcLinkId) return undefined;
114 const v = yield* ag
115 .getVpcLink({ vpcLinkId: output.vpcLinkId })
116 .pipe(
117 Effect.catchTag("NotFoundException", () =>
118 Effect.succeed(undefined),
119 ),
120 );
121 if (!v?.id) return undefined;
122 return snapshotFromVpcLink(v, tagRecord(v.tags));
123 }),
124 list: () =>
125 ag.getVpcLinks.pages({ limit: 500 }).pipe(
126 Stream.runCollect,
127 Effect.map((chunk) =>
128 Array.from(chunk).flatMap((page) =>
129 (page.items ?? [])
130 .filter((v): v is ag.VpcLink & { id: string } => !!v.id)
131 .map((v) => snapshotFromVpcLink(v, tagRecord(v.tags))),
132 ),
133 ),
134 ),
135 reconcile: Effect.fn(function* ({ id, news: newsIn, output, session }) {
136 if (!isResolved(newsIn)) {
137 return yield* Effect.die("VpcLink props were not resolved");
138 }
139 const news = newsIn as VpcLinkProps;
140 const name = yield* generatedName(id, news);
141 const internalTags = yield* createInternalTags(id);
142 const desiredTags = { ...news.tags, ...internalTags };
143 const { region } = yield* AWSEnvironment.current;
144 // Observe — fetch the live VPC link if we have a cached id.
145 // We never trust `output.description`/etc. for diffing; the
146 // observed cloud state drives every sync below.
147 let observed = output?.vpcLinkId
148 ? yield* ag
149 .getVpcLink({ vpcLinkId: output.vpcLinkId })
150 .pipe(
151 Effect.catchTag("NotFoundException", () =>
152 Effect.succeed(undefined),
153 ),
154 )
155 : undefined;
156
157 // Ensure — create the VPC link if missing.
158 if (!observed?.id) {
159 const created = yield* ag.createVpcLink({
160 name,
161 description: news.description,
162 targetArns: news.targetArns,
163 tags: desiredTags,
164 });
165 if (!created.id) {
166 return yield* Effect.die("createVpcLink missing id");
167 }
168 yield* session.note(`Created VPC link ${created.id}`);
169 observed = yield* ag.getVpcLink({ vpcLinkId: created.id });
170 }
171
172 const vpcLinkId = observed.id!;
173
174 // Sync mutable scalar fields — observed ↔ desired patch list.
175 const patches: ag.PatchOperation[] = [];
176 if (news.description !== observed.description) {
177 patches.push({
178 op: "replace",
179 path: "/description",
180 value: news.description ?? "",
181 });
182 }
183 if (news.name !== undefined && news.name !== observed.name) {
184 patches.push({
185 op: "replace",
186 path: "/name",
187 value: news.name,
188 });
189 }
190 if (patches.length > 0) {
191 yield* ag.updateVpcLink({
192 vpcLinkId,
193 patchOperations: patches,
194 });
195 }
196
197 // Sync tags — observed ↔ desired so adoption converges without
198 // fighting the existing tag set.
199 const observedTags = tagRecord(observed.tags);
200 if (!deepEqual(observedTags, desiredTags)) {
201 yield* syncTags({
202 resourceArn: vpcLinkArn(region, vpcLinkId),
203 oldTags: observedTags,
204 newTags: desiredTags,
205 });
206 }
207
208 yield* session.note(`Reconciled VPC link ${vpcLinkId}`);
209 const final = yield* ag.getVpcLink({ vpcLinkId });
210 return snapshotFromVpcLink(final, tagRecord(final.tags));
211 }),
212 delete: Effect.fn(function* ({ output, session }) {
213 yield* ag.deleteVpcLink({ vpcLinkId: output.vpcLinkId }).pipe(
214 Effect.retry({
215 while: (e) => e._tag === "ConflictException",
216 schedule: Schedule.spaced("1 second"),
217 times: 8,
218 }),
219 Effect.catchTag("NotFoundException", () => Effect.void),
220 );
221 yield* session.note(`Deleted VPC link ${output.vpcLinkId}`);
222 }),
223 };
224 }),
225 );