alchemy-effect 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/README.md +13 -7
  2. package/bin/alchemy-effect.js +16 -17
  3. package/bin/alchemy-effect.js.map +1 -1
  4. package/lib/apply.d.ts.map +1 -1
  5. package/lib/apply.js +19 -17
  6. package/lib/apply.js.map +1 -1
  7. package/lib/aws/client.d.ts.map +1 -1
  8. package/lib/aws/client.js +15 -2
  9. package/lib/aws/client.js.map +1 -1
  10. package/lib/aws/dynamodb/secondary-index.d.ts.map +1 -1
  11. package/lib/aws/dynamodb/table.d.ts.map +1 -1
  12. package/lib/aws/dynamodb/table.js.map +1 -1
  13. package/lib/aws/ec2/egress-only-igw.d.ts +55 -0
  14. package/lib/aws/ec2/egress-only-igw.d.ts.map +1 -0
  15. package/lib/aws/ec2/egress-only-igw.js +4 -0
  16. package/lib/aws/ec2/egress-only-igw.js.map +1 -0
  17. package/lib/aws/ec2/egress-only-igw.provider.d.ts +6 -0
  18. package/lib/aws/ec2/egress-only-igw.provider.d.ts.map +1 -0
  19. package/lib/aws/ec2/egress-only-igw.provider.js +114 -0
  20. package/lib/aws/ec2/egress-only-igw.provider.js.map +1 -0
  21. package/lib/aws/ec2/eip.d.ts +85 -0
  22. package/lib/aws/ec2/eip.d.ts.map +1 -0
  23. package/lib/aws/ec2/eip.js +4 -0
  24. package/lib/aws/ec2/eip.js.map +1 -0
  25. package/lib/aws/ec2/eip.provider.d.ts +6 -0
  26. package/lib/aws/ec2/eip.provider.d.ts.map +1 -0
  27. package/lib/aws/ec2/eip.provider.js +135 -0
  28. package/lib/aws/ec2/eip.provider.js.map +1 -0
  29. package/lib/aws/ec2/index.d.ts +18 -0
  30. package/lib/aws/ec2/index.d.ts.map +1 -1
  31. package/lib/aws/ec2/index.js +18 -0
  32. package/lib/aws/ec2/index.js.map +1 -1
  33. package/lib/aws/ec2/internet-gateway.d.ts.map +1 -1
  34. package/lib/aws/ec2/internet-gateway.provider.d.ts.map +1 -1
  35. package/lib/aws/ec2/internet-gateway.provider.js +9 -5
  36. package/lib/aws/ec2/internet-gateway.provider.js.map +1 -1
  37. package/lib/aws/ec2/nat-gateway.d.ts +127 -0
  38. package/lib/aws/ec2/nat-gateway.d.ts.map +1 -0
  39. package/lib/aws/ec2/nat-gateway.js +4 -0
  40. package/lib/aws/ec2/nat-gateway.js.map +1 -0
  41. package/lib/aws/ec2/nat-gateway.provider.d.ts +6 -0
  42. package/lib/aws/ec2/nat-gateway.provider.d.ts.map +1 -0
  43. package/lib/aws/ec2/nat-gateway.provider.js +222 -0
  44. package/lib/aws/ec2/nat-gateway.provider.js.map +1 -0
  45. package/lib/aws/ec2/network-acl-association.d.ts +44 -0
  46. package/lib/aws/ec2/network-acl-association.d.ts.map +1 -0
  47. package/lib/aws/ec2/network-acl-association.js +4 -0
  48. package/lib/aws/ec2/network-acl-association.js.map +1 -0
  49. package/lib/aws/ec2/network-acl-association.provider.d.ts +4 -0
  50. package/lib/aws/ec2/network-acl-association.provider.d.ts.map +1 -0
  51. package/lib/aws/ec2/network-acl-association.provider.js +115 -0
  52. package/lib/aws/ec2/network-acl-association.provider.js.map +1 -0
  53. package/lib/aws/ec2/network-acl-entry.d.ts +118 -0
  54. package/lib/aws/ec2/network-acl-entry.d.ts.map +1 -0
  55. package/lib/aws/ec2/network-acl-entry.js +3 -0
  56. package/lib/aws/ec2/network-acl-entry.js.map +1 -0
  57. package/lib/aws/ec2/network-acl-entry.provider.d.ts +4 -0
  58. package/lib/aws/ec2/network-acl-entry.provider.d.ts.map +1 -0
  59. package/lib/aws/ec2/network-acl-entry.provider.js +129 -0
  60. package/lib/aws/ec2/network-acl-entry.provider.js.map +1 -0
  61. package/lib/aws/ec2/network-acl.d.ts +82 -0
  62. package/lib/aws/ec2/network-acl.d.ts.map +1 -0
  63. package/lib/aws/ec2/network-acl.js +4 -0
  64. package/lib/aws/ec2/network-acl.js.map +1 -0
  65. package/lib/aws/ec2/network-acl.provider.d.ts +6 -0
  66. package/lib/aws/ec2/network-acl.provider.d.ts.map +1 -0
  67. package/lib/aws/ec2/network-acl.provider.js +136 -0
  68. package/lib/aws/ec2/network-acl.provider.js.map +1 -0
  69. package/lib/aws/ec2/route-table-association.d.ts.map +1 -1
  70. package/lib/aws/ec2/route-table-association.provider.d.ts.map +1 -1
  71. package/lib/aws/ec2/route-table-association.provider.js.map +1 -1
  72. package/lib/aws/ec2/route-table.d.ts.map +1 -1
  73. package/lib/aws/ec2/route-table.provider.d.ts.map +1 -1
  74. package/lib/aws/ec2/route-table.provider.js.map +1 -1
  75. package/lib/aws/ec2/route.d.ts.map +1 -1
  76. package/lib/aws/ec2/route.provider.d.ts.map +1 -1
  77. package/lib/aws/ec2/route.provider.js.map +1 -1
  78. package/lib/aws/ec2/security-group-rule.d.ts +118 -0
  79. package/lib/aws/ec2/security-group-rule.d.ts.map +1 -0
  80. package/lib/aws/ec2/security-group-rule.js +4 -0
  81. package/lib/aws/ec2/security-group-rule.js.map +1 -0
  82. package/lib/aws/ec2/security-group-rule.provider.d.ts +4 -0
  83. package/lib/aws/ec2/security-group-rule.provider.d.ts.map +1 -0
  84. package/lib/aws/ec2/security-group-rule.provider.js +193 -0
  85. package/lib/aws/ec2/security-group-rule.provider.js.map +1 -0
  86. package/lib/aws/ec2/security-group.d.ts +147 -0
  87. package/lib/aws/ec2/security-group.d.ts.map +1 -0
  88. package/lib/aws/ec2/security-group.js +4 -0
  89. package/lib/aws/ec2/security-group.js.map +1 -0
  90. package/lib/aws/ec2/security-group.provider.d.ts +6 -0
  91. package/lib/aws/ec2/security-group.provider.d.ts.map +1 -0
  92. package/lib/aws/ec2/security-group.provider.js +291 -0
  93. package/lib/aws/ec2/security-group.provider.js.map +1 -0
  94. package/lib/aws/ec2/subnet.d.ts.map +1 -1
  95. package/lib/aws/ec2/subnet.provider.d.ts.map +1 -1
  96. package/lib/aws/ec2/subnet.provider.js +33 -30
  97. package/lib/aws/ec2/subnet.provider.js.map +1 -1
  98. package/lib/aws/ec2/vpc-endpoint.d.ts +176 -0
  99. package/lib/aws/ec2/vpc-endpoint.d.ts.map +1 -0
  100. package/lib/aws/ec2/vpc-endpoint.js +4 -0
  101. package/lib/aws/ec2/vpc-endpoint.js.map +1 -0
  102. package/lib/aws/ec2/vpc-endpoint.provider.d.ts +6 -0
  103. package/lib/aws/ec2/vpc-endpoint.provider.d.ts.map +1 -0
  104. package/lib/aws/ec2/vpc-endpoint.provider.js +315 -0
  105. package/lib/aws/ec2/vpc-endpoint.provider.js.map +1 -0
  106. package/lib/aws/ec2/vpc.d.ts.map +1 -1
  107. package/lib/aws/ec2/vpc.provider.d.ts +2 -2
  108. package/lib/aws/ec2/vpc.provider.d.ts.map +1 -1
  109. package/lib/aws/ec2/vpc.provider.js +38 -31
  110. package/lib/aws/ec2/vpc.provider.js.map +1 -1
  111. package/lib/aws/index.d.ts +2 -2
  112. package/lib/aws/index.d.ts.map +1 -1
  113. package/lib/aws/index.js +1 -1
  114. package/lib/aws/index.js.map +1 -1
  115. package/lib/aws/lambda/function.d.ts.map +1 -1
  116. package/lib/aws/lambda/function.invoke.d.ts.map +1 -1
  117. package/lib/aws/lambda/function.invoke.js.map +1 -1
  118. package/lib/aws/lambda/function.js.map +1 -1
  119. package/lib/aws/sqs/queue.d.ts +1 -2
  120. package/lib/aws/sqs/queue.d.ts.map +1 -1
  121. package/lib/aws/sqs/queue.event-source.d.ts.map +1 -1
  122. package/lib/aws/sqs/queue.event-source.js +2 -2
  123. package/lib/aws/sqs/queue.event-source.js.map +1 -1
  124. package/lib/aws/sqs/queue.js.map +1 -1
  125. package/lib/aws/sqs/queue.provider.d.ts.map +1 -1
  126. package/lib/aws/sqs/queue.provider.js +22 -14
  127. package/lib/aws/sqs/queue.provider.js.map +1 -1
  128. package/lib/aws/sqs/queue.send-message.d.ts.map +1 -1
  129. package/lib/aws/sqs/queue.send-message.js.map +1 -1
  130. package/lib/binding.d.ts.map +1 -1
  131. package/lib/cli/components/PlanProgress.d.ts.map +1 -1
  132. package/lib/cli/components/PlanProgress.js.map +1 -1
  133. package/lib/cli/index.d.ts +1 -1
  134. package/lib/cli/index.d.ts.map +1 -1
  135. package/lib/cli/index.js.map +1 -1
  136. package/lib/cloudflare/kv/namespace.binding.d.ts.map +1 -1
  137. package/lib/cloudflare/kv/namespace.binding.js.map +1 -1
  138. package/lib/cloudflare/kv/namespace.d.ts.map +1 -1
  139. package/lib/cloudflare/kv/namespace.provider.d.ts +1 -2
  140. package/lib/cloudflare/kv/namespace.provider.d.ts.map +1 -1
  141. package/lib/cloudflare/kv/namespace.provider.js +0 -2
  142. package/lib/cloudflare/kv/namespace.provider.js.map +1 -1
  143. package/lib/cloudflare/r2/bucket.binding.d.ts.map +1 -1
  144. package/lib/cloudflare/r2/bucket.binding.js.map +1 -1
  145. package/lib/cloudflare/r2/bucket.d.ts.map +1 -1
  146. package/lib/output.d.ts.map +1 -1
  147. package/lib/output.js.map +1 -1
  148. package/lib/plan.d.ts +2 -2
  149. package/lib/plan.d.ts.map +1 -1
  150. package/lib/plan.js +14 -13
  151. package/lib/plan.js.map +1 -1
  152. package/lib/provider.d.ts +1 -1
  153. package/lib/provider.d.ts.map +1 -1
  154. package/lib/provider.js.map +1 -1
  155. package/lib/runtime.d.ts.map +1 -1
  156. package/lib/tags.d.ts +12 -0
  157. package/lib/tags.d.ts.map +1 -1
  158. package/lib/tags.js +24 -0
  159. package/lib/tags.js.map +1 -1
  160. package/lib/tsconfig.test.tsbuildinfo +1 -1
  161. package/package.json +51 -51
  162. package/src/apply.ts +21 -12
  163. package/src/aws/client.ts +22 -1
  164. package/src/aws/dynamodb/secondary-index.ts +5 -5
  165. package/src/aws/dynamodb/table.ts +8 -11
  166. package/src/aws/ec2/egress-only-igw.provider.ts +181 -0
  167. package/src/aws/ec2/egress-only-igw.ts +77 -0
  168. package/src/aws/ec2/eip.provider.ts +191 -0
  169. package/src/aws/ec2/eip.ts +106 -0
  170. package/src/aws/ec2/index.ts +18 -0
  171. package/src/aws/ec2/internet-gateway.provider.ts +15 -6
  172. package/src/aws/ec2/internet-gateway.ts +6 -6
  173. package/src/aws/ec2/nat-gateway.provider.ts +341 -0
  174. package/src/aws/ec2/nat-gateway.ts +155 -0
  175. package/src/aws/ec2/network-acl-association.provider.ts +181 -0
  176. package/src/aws/ec2/network-acl-association.ts +60 -0
  177. package/src/aws/ec2/network-acl-entry.provider.ts +218 -0
  178. package/src/aws/ec2/network-acl-entry.ts +140 -0
  179. package/src/aws/ec2/network-acl.provider.ts +195 -0
  180. package/src/aws/ec2/network-acl.ts +102 -0
  181. package/src/aws/ec2/route-table-association.provider.ts +1 -2
  182. package/src/aws/ec2/route-table-association.ts +6 -6
  183. package/src/aws/ec2/route-table.provider.ts +1 -2
  184. package/src/aws/ec2/route-table.ts +6 -6
  185. package/src/aws/ec2/route.provider.ts +1 -2
  186. package/src/aws/ec2/route.ts +6 -6
  187. package/src/aws/ec2/security-group-rule.provider.ts +264 -0
  188. package/src/aws/ec2/security-group-rule.ts +151 -0
  189. package/src/aws/ec2/security-group.provider.ts +392 -0
  190. package/src/aws/ec2/security-group.ts +182 -0
  191. package/src/aws/ec2/subnet.provider.ts +57 -56
  192. package/src/aws/ec2/subnet.ts +6 -6
  193. package/src/aws/ec2/vpc-endpoint.provider.ts +466 -0
  194. package/src/aws/ec2/vpc-endpoint.ts +213 -0
  195. package/src/aws/ec2/vpc.provider.ts +58 -51
  196. package/src/aws/ec2/vpc.ts +6 -6
  197. package/src/aws/index.ts +9 -0
  198. package/src/aws/lambda/function.invoke.ts +4 -2
  199. package/src/aws/lambda/function.ts +4 -2
  200. package/src/aws/sqs/queue.event-source.ts +12 -14
  201. package/src/aws/sqs/queue.provider.ts +25 -15
  202. package/src/aws/sqs/queue.send-message.ts +4 -2
  203. package/src/aws/sqs/queue.ts +1 -8
  204. package/src/binding.ts +7 -7
  205. package/src/cli/components/PlanProgress.tsx +3 -2
  206. package/src/cloudflare/kv/namespace.binding.ts +4 -2
  207. package/src/cloudflare/kv/namespace.provider.ts +0 -2
  208. package/src/cloudflare/kv/namespace.ts +6 -6
  209. package/src/cloudflare/r2/bucket.binding.ts +4 -2
  210. package/src/cloudflare/r2/bucket.ts +6 -6
  211. package/src/output.ts +5 -3
  212. package/src/plan.ts +26 -20
  213. package/src/provider.ts +12 -11
  214. package/src/runtime.ts +2 -2
  215. package/src/tags.ts +29 -0
@@ -0,0 +1,341 @@
1
+ import * as Data from "effect/Data";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Schedule from "effect/Schedule";
4
+ import type * as EC2 from "itty-aws/ec2";
5
+
6
+ import type { ScopedPlanStatusSession } from "../../cli/service.ts";
7
+ import {
8
+ createAlchemyTagFilters,
9
+ createTagger,
10
+ createTagsList,
11
+ diffTags,
12
+ } from "../../tags.ts";
13
+ import { Account } from "../account.ts";
14
+ import { Region } from "../region.ts";
15
+ import { EC2Client } from "./client.ts";
16
+ import {
17
+ NatGateway,
18
+ type NatGatewayAttrs,
19
+ type NatGatewayId,
20
+ type NatGatewayProps,
21
+ } from "./nat-gateway.ts";
22
+
23
+ export const natGatewayProvider = () =>
24
+ NatGateway.provider.effect(
25
+ // @ts-expect-error
26
+ Effect.gen(function* () {
27
+ const ec2 = yield* EC2Client;
28
+ const region = yield* Region;
29
+ const accountId = yield* Account;
30
+ const tagged = yield* createTagger();
31
+
32
+ const createTags = (
33
+ id: string,
34
+ tags?: Record<string, string>,
35
+ ): Record<string, string> => ({
36
+ Name: id,
37
+ ...tagged(id),
38
+ ...tags,
39
+ });
40
+
41
+ const describeNatGateway = (natGatewayId: string) =>
42
+ ec2.describeNatGateways({ NatGatewayIds: [natGatewayId] }).pipe(
43
+ Effect.map((r) => r.NatGateways?.[0]),
44
+ Effect.flatMap((gw) =>
45
+ gw
46
+ ? Effect.succeed(gw)
47
+ : Effect.fail(new Error(`NAT Gateway ${natGatewayId} not found`)),
48
+ ),
49
+ );
50
+
51
+ const toAttrs = (
52
+ gw: EC2.NatGateway,
53
+ ): NatGatewayAttrs<NatGatewayProps> => {
54
+ const primaryAddress =
55
+ gw.NatGatewayAddresses?.find((a) => a.IsPrimary) ??
56
+ gw.NatGatewayAddresses?.[0];
57
+ return {
58
+ natGatewayId: gw.NatGatewayId as NatGatewayId,
59
+ natGatewayArn:
60
+ `arn:aws:ec2:${region}:${accountId}:natgateway/${gw.NatGatewayId}` as NatGatewayAttrs<NatGatewayProps>["natGatewayArn"],
61
+ subnetId: gw.SubnetId as NatGatewayAttrs<NatGatewayProps>["subnetId"],
62
+ vpcId: gw.VpcId!,
63
+ state: gw.State!,
64
+ connectivityType: gw.ConnectivityType!,
65
+ publicIp: primaryAddress?.PublicIp,
66
+ privateIp: primaryAddress?.PrivateIp,
67
+ natGatewayAddresses: gw.NatGatewayAddresses?.map((a) => ({
68
+ allocationId: a.AllocationId,
69
+ networkInterfaceId: a.NetworkInterfaceId,
70
+ privateIp: a.PrivateIp,
71
+ publicIp: a.PublicIp,
72
+ associationId: a.AssociationId,
73
+ isPrimary: a.IsPrimary,
74
+ failureMessage: a.FailureMessage,
75
+ status: a.Status,
76
+ })),
77
+ failureCode: gw.FailureCode,
78
+ failureMessage: gw.FailureMessage,
79
+ createTime:
80
+ gw.CreateTime instanceof Date
81
+ ? gw.CreateTime.toISOString()
82
+ : (gw.CreateTime as string | undefined),
83
+ deleteTime:
84
+ gw.DeleteTime instanceof Date
85
+ ? gw.DeleteTime.toISOString()
86
+ : (gw.DeleteTime as string | undefined),
87
+ };
88
+ };
89
+
90
+ // Find NAT Gateway by alchemy tags when we don't have the ID
91
+ const findNatGatewayByTags = Effect.fn(function* (id: string) {
92
+ const filters = yield* createAlchemyTagFilters(id);
93
+ const result = yield* ec2.describeNatGateways({ Filter: filters });
94
+
95
+ // Find a NAT Gateway that's not deleted and has matching tags
96
+ for (const gw of result.NatGateways ?? []) {
97
+ return gw;
98
+ }
99
+ return undefined;
100
+ });
101
+
102
+ return {
103
+ stables: ["natGatewayId", "natGatewayArn", "vpcId"],
104
+
105
+ read: Effect.fn(function* ({ id, output }) {
106
+ if (output) {
107
+ // We have the NAT Gateway ID, use it directly
108
+ return toAttrs(yield* describeNatGateway(output.natGatewayId));
109
+ }
110
+
111
+ // No output - try to find by tags (recovery from incomplete create)
112
+ const gw = yield* findNatGatewayByTags(id);
113
+ if (gw) {
114
+ return toAttrs(gw);
115
+ }
116
+
117
+ // Not found
118
+ return undefined;
119
+ }),
120
+
121
+ diff: Effect.fn(function* ({ news, olds }) {
122
+ // NAT Gateway is mostly immutable - any change to core properties requires replacement
123
+ if (
124
+ news.subnetId !== olds.subnetId ||
125
+ news.connectivityType !== olds.connectivityType ||
126
+ news.allocationId !== olds.allocationId
127
+ ) {
128
+ return { action: "replace" };
129
+ }
130
+ // Tags can be updated in-place
131
+ }),
132
+
133
+ create: Effect.fn(function* ({ id, news, session }) {
134
+ yield* session.note("Creating NAT Gateway...");
135
+
136
+ const result = yield* ec2.createNatGateway({
137
+ SubnetId: news.subnetId as string,
138
+ AllocationId: news.allocationId as string | undefined,
139
+ ConnectivityType: news.connectivityType ?? "public",
140
+ PrivateIpAddress: news.privateIpAddress,
141
+ SecondaryAllocationIds: news.secondaryAllocationIds as
142
+ | string[]
143
+ | undefined,
144
+ SecondaryPrivateIpAddresses: news.secondaryPrivateIpAddresses,
145
+ SecondaryPrivateIpAddressCount: news.secondaryPrivateIpAddressCount,
146
+ TagSpecifications: [
147
+ {
148
+ ResourceType: "natgateway",
149
+ Tags: createTagsList(createTags(id, news.tags)),
150
+ },
151
+ ],
152
+ DryRun: false,
153
+ });
154
+
155
+ const natGatewayId = result.NatGateway!.NatGatewayId!;
156
+ yield* session.note(`NAT Gateway created: ${natGatewayId}`);
157
+
158
+ // Wait for NAT Gateway to be available
159
+ const gw = yield* waitForNatGatewayAvailable(natGatewayId, session);
160
+
161
+ return toAttrs(gw);
162
+ }),
163
+
164
+ update: Effect.fn(function* ({ id, news, output, session }) {
165
+ const natGatewayId = output.natGatewayId;
166
+
167
+ // Handle tag updates
168
+ const newTags = createTags(id, news.tags);
169
+ const oldTags =
170
+ (yield* ec2
171
+ .describeTags({
172
+ Filters: [
173
+ { Name: "resource-id", Values: [natGatewayId] },
174
+ { Name: "resource-type", Values: ["natgateway"] },
175
+ ],
176
+ })
177
+ .pipe(
178
+ Effect.map(
179
+ (r) =>
180
+ Object.fromEntries(
181
+ r.Tags?.map((t) => [t.Key!, t.Value!]) ?? [],
182
+ ) as Record<string, string>,
183
+ ),
184
+ )) ?? {};
185
+
186
+ const { removed, upsert } = diffTags(oldTags, newTags);
187
+
188
+ if (removed.length > 0) {
189
+ yield* ec2.deleteTags({
190
+ Resources: [natGatewayId],
191
+ Tags: removed.map((key) => ({ Key: key })),
192
+ DryRun: false,
193
+ });
194
+ }
195
+ if (upsert.length > 0) {
196
+ yield* ec2.createTags({
197
+ Resources: [natGatewayId],
198
+ Tags: upsert,
199
+ DryRun: false,
200
+ });
201
+ yield* session.note("Updated tags");
202
+ }
203
+
204
+ // Refresh state
205
+ const gw = yield* describeNatGateway(natGatewayId);
206
+ return toAttrs(gw);
207
+ }),
208
+
209
+ delete: Effect.fn(function* ({ output, session }) {
210
+ const natGatewayId = output.natGatewayId;
211
+
212
+ yield* session.note(`Deleting NAT Gateway: ${natGatewayId}`);
213
+
214
+ yield* ec2
215
+ .deleteNatGateway({
216
+ NatGatewayId: natGatewayId,
217
+ DryRun: false,
218
+ })
219
+ .pipe(Effect.catchTag("NatGatewayNotFound", () => Effect.void));
220
+
221
+ // Wait for NAT Gateway to be deleted
222
+ yield* waitForNatGatewayDeleted(natGatewayId, session);
223
+
224
+ yield* session.note(`NAT Gateway ${natGatewayId} deleted`);
225
+ }),
226
+ };
227
+ }),
228
+ );
229
+
230
+ // Retryable error: NAT Gateway is still pending
231
+ class NatGatewayPending extends Data.TaggedError("NatGatewayPending")<{
232
+ natGatewayId: string;
233
+ state: string;
234
+ }> {}
235
+
236
+ // Terminal error: NAT Gateway creation failed
237
+ class NatGatewayFailed extends Data.TaggedError("NatGatewayFailed")<{
238
+ natGatewayId: string;
239
+ failureCode?: string;
240
+ failureMessage?: string;
241
+ }> {}
242
+
243
+ // Terminal error: NAT Gateway not found
244
+ class NatGatewayNotFound extends Data.TaggedError("NatGatewayNotFound")<{
245
+ natGatewayId: string;
246
+ }> {}
247
+
248
+ /**
249
+ * Wait for NAT Gateway to be in available state
250
+ */
251
+ const waitForNatGatewayAvailable = (
252
+ natGatewayId: string,
253
+ session: ScopedPlanStatusSession,
254
+ ) =>
255
+ Effect.gen(function* () {
256
+ const ec2 = yield* EC2Client;
257
+ const result = yield* ec2.describeNatGateways({
258
+ NatGatewayIds: [natGatewayId],
259
+ });
260
+ const gw = result.NatGateways?.[0];
261
+
262
+ if (!gw) {
263
+ return yield* new NatGatewayNotFound({ natGatewayId });
264
+ }
265
+
266
+ if (gw.State === "available") {
267
+ return gw;
268
+ }
269
+
270
+ if (gw.State === "failed") {
271
+ return yield* new NatGatewayFailed({
272
+ natGatewayId,
273
+ failureCode: gw.FailureCode,
274
+ failureMessage: gw.FailureMessage,
275
+ });
276
+ }
277
+
278
+ // Still pending - this is the only retryable case
279
+ return yield* new NatGatewayPending({ natGatewayId, state: gw.State! });
280
+ }).pipe(
281
+ Effect.tapError(Effect.logDebug),
282
+ Effect.retry({
283
+ while: (e) => e._tag === "NatGatewayPending",
284
+ schedule: Schedule.fixed(5000).pipe(
285
+ Schedule.intersect(Schedule.recurs(60)), // Max 5 minutes
286
+ Schedule.tapOutput(([, attempt]) =>
287
+ session.note(
288
+ `Waiting for NAT Gateway to be available... (${(attempt + 1) * 5}s)`,
289
+ ),
290
+ ),
291
+ ),
292
+ }),
293
+ );
294
+
295
+ // Retryable error: NAT Gateway is still deleting
296
+ class NatGatewayDeleting extends Data.TaggedError("NatGatewayDeleting")<{
297
+ natGatewayId: string;
298
+ state: string;
299
+ }> {}
300
+
301
+ /**
302
+ * Wait for NAT Gateway to be deleted
303
+ */
304
+ const waitForNatGatewayDeleted = (
305
+ natGatewayId: string,
306
+ session: ScopedPlanStatusSession,
307
+ ) =>
308
+ Effect.gen(function* () {
309
+ const ec2 = yield* EC2Client;
310
+ const result = yield* ec2
311
+ .describeNatGateways({ NatGatewayIds: [natGatewayId] })
312
+ .pipe(
313
+ Effect.catchTag("NatGatewayNotFound", () =>
314
+ Effect.succeed({ NatGateways: [] }),
315
+ ),
316
+ );
317
+
318
+ const gw = result.NatGateways?.[0];
319
+
320
+ if (!gw || gw.State === "deleted") {
321
+ return; // Successfully deleted
322
+ }
323
+
324
+ yield* Effect.logDebug(gw);
325
+
326
+ // Still deleting - this is the only retryable case
327
+ return yield* new NatGatewayDeleting({ natGatewayId, state: gw.State! });
328
+ }).pipe(
329
+ Effect.tapError(Effect.logDebug),
330
+ Effect.retry({
331
+ while: (e) => e._tag === "NatGatewayDeleting",
332
+ schedule: Schedule.fixed(5000).pipe(
333
+ Schedule.intersect(Schedule.recurs(60)), // Max 5 minutes
334
+ Schedule.tapOutput(([, attempt]) =>
335
+ session.note(
336
+ `Waiting for NAT Gateway deletion... (${(attempt + 1) * 5}s)`,
337
+ ),
338
+ ),
339
+ ),
340
+ }),
341
+ );
@@ -0,0 +1,155 @@
1
+ import type * as EC2 from "itty-aws/ec2";
2
+ import type { Input } from "../../input.ts";
3
+ import { Resource } from "../../resource.ts";
4
+ import type { AccountID } from "../account.ts";
5
+ import type { RegionID } from "../region.ts";
6
+ import type { AllocationId } from "./eip.ts";
7
+ import type { SubnetId } from "./subnet.ts";
8
+
9
+ export const NatGateway = Resource<{
10
+ <const ID extends string, const Props extends NatGatewayProps>(
11
+ id: ID,
12
+ props: Props,
13
+ ): NatGateway<ID, Props>;
14
+ }>("AWS.EC2.NatGateway");
15
+
16
+ export interface NatGateway<
17
+ ID extends string = string,
18
+ Props extends NatGatewayProps = NatGatewayProps,
19
+ > extends Resource<
20
+ "AWS.EC2.NatGateway",
21
+ ID,
22
+ Props,
23
+ NatGatewayAttrs<Input.Resolve<Props>>,
24
+ NatGateway
25
+ > {}
26
+
27
+ export type NatGatewayId<ID extends string = string> = `nat-${ID}`;
28
+ export const NatGatewayId = <ID extends string>(
29
+ id: ID,
30
+ ): ID & NatGatewayId<ID> => `nat-${id}` as ID & NatGatewayId<ID>;
31
+
32
+ export interface NatGatewayProps {
33
+ /**
34
+ * The subnet in which to create the NAT gateway.
35
+ * For public NAT gateways, this must be a public subnet.
36
+ */
37
+ subnetId: Input<SubnetId>;
38
+
39
+ /**
40
+ * The allocation ID of the Elastic IP address for the gateway.
41
+ * Required for public NAT gateways.
42
+ */
43
+ allocationId?: Input<AllocationId>;
44
+
45
+ /**
46
+ * Indicates whether the NAT gateway supports public or private connectivity.
47
+ * @default "public"
48
+ */
49
+ connectivityType?: EC2.ConnectivityType;
50
+
51
+ /**
52
+ * The private IPv4 address to assign to the NAT gateway.
53
+ * If you don't provide an address, a private IPv4 address will be automatically assigned.
54
+ */
55
+ privateIpAddress?: string;
56
+
57
+ /**
58
+ * Secondary allocation IDs for additional private IP addresses.
59
+ * Only valid for private NAT gateways.
60
+ */
61
+ secondaryAllocationIds?: Input<AllocationId>[];
62
+
63
+ /**
64
+ * Secondary private IPv4 addresses.
65
+ * Only valid for private NAT gateways.
66
+ */
67
+ secondaryPrivateIpAddresses?: string[];
68
+
69
+ /**
70
+ * The number of secondary private IPv4 addresses to assign.
71
+ * Only valid for private NAT gateways.
72
+ */
73
+ secondaryPrivateIpAddressCount?: number;
74
+
75
+ /**
76
+ * Tags to assign to the NAT gateway.
77
+ */
78
+ tags?: Record<string, Input<string>>;
79
+ }
80
+
81
+ export interface NatGatewayAttrs<Props extends NatGatewayProps> {
82
+ /**
83
+ * The ID of the NAT gateway.
84
+ */
85
+ natGatewayId: NatGatewayId;
86
+
87
+ /**
88
+ * The Amazon Resource Name (ARN) of the NAT gateway.
89
+ */
90
+ natGatewayArn: `arn:aws:ec2:${RegionID}:${AccountID}:natgateway/${this["natGatewayId"]}`;
91
+
92
+ /**
93
+ * The ID of the subnet in which the NAT gateway is located.
94
+ */
95
+ subnetId: Props["subnetId"];
96
+
97
+ /**
98
+ * The ID of the VPC in which the NAT gateway is located.
99
+ */
100
+ vpcId: string;
101
+
102
+ /**
103
+ * The current state of the NAT gateway.
104
+ */
105
+ state: EC2.NatGatewayState;
106
+
107
+ /**
108
+ * The connectivity type of the NAT gateway.
109
+ */
110
+ connectivityType: EC2.ConnectivityType;
111
+
112
+ /**
113
+ * The Elastic IP address associated with the NAT gateway (for public NAT gateways).
114
+ */
115
+ publicIp?: string;
116
+
117
+ /**
118
+ * The private IP address associated with the NAT gateway.
119
+ */
120
+ privateIp?: string;
121
+
122
+ /**
123
+ * Information about the IP addresses and network interface associated with the NAT gateway.
124
+ */
125
+ natGatewayAddresses?: Array<{
126
+ allocationId?: string;
127
+ networkInterfaceId?: string;
128
+ privateIp?: string;
129
+ publicIp?: string;
130
+ associationId?: string;
131
+ isPrimary?: boolean;
132
+ failureMessage?: string;
133
+ status?: EC2.NatGatewayAddressStatus;
134
+ }>;
135
+
136
+ /**
137
+ * If the NAT gateway could not be created, specifies the error code for the failure.
138
+ */
139
+ failureCode?: string;
140
+
141
+ /**
142
+ * If the NAT gateway could not be created, specifies the error message for the failure.
143
+ */
144
+ failureMessage?: string;
145
+
146
+ /**
147
+ * The date and time the NAT gateway was created.
148
+ */
149
+ createTime?: string;
150
+
151
+ /**
152
+ * The date and time the NAT gateway was deleted, if applicable.
153
+ */
154
+ deleteTime?: string;
155
+ }
@@ -0,0 +1,181 @@
1
+ import * as Effect from "effect/Effect";
2
+
3
+ import type { ProviderService } from "../../provider.ts";
4
+ import { EC2Client } from "./client.ts";
5
+ import {
6
+ NetworkAclAssociation,
7
+ type NetworkAclAssociationId,
8
+ } from "./network-acl-association.ts";
9
+ import type { NetworkAclId } from "./network-acl.ts";
10
+ import type { SubnetId } from "./subnet.ts";
11
+
12
+ export const networkAclAssociationProvider = () =>
13
+ NetworkAclAssociation.provider.effect(
14
+ Effect.gen(function* () {
15
+ const ec2 = yield* EC2Client;
16
+
17
+ const findAssociation = (subnetId: string) =>
18
+ ec2
19
+ .describeNetworkAcls({
20
+ Filters: [{ Name: "association.subnet-id", Values: [subnetId] }],
21
+ })
22
+ .pipe(
23
+ Effect.map((r) => {
24
+ const acl = r.NetworkAcls?.[0];
25
+ const assoc = acl?.Associations?.find(
26
+ (a) => a.SubnetId === subnetId,
27
+ );
28
+ return assoc
29
+ ? {
30
+ associationId: assoc.NetworkAclAssociationId!,
31
+ networkAclId: assoc.NetworkAclId!,
32
+ subnetId: assoc.SubnetId!,
33
+ }
34
+ : undefined;
35
+ }),
36
+ );
37
+
38
+ return {
39
+ stables: ["subnetId"],
40
+
41
+ read: Effect.fn(function* ({ olds }) {
42
+ if (!olds) return undefined;
43
+ const assoc = yield* findAssociation(olds.subnetId as string);
44
+ if (!assoc) {
45
+ return yield* Effect.fail(
46
+ new Error(
47
+ `Network ACL Association not found for subnet ${olds.subnetId}`,
48
+ ),
49
+ );
50
+ }
51
+ return {
52
+ associationId: assoc.associationId as NetworkAclAssociationId,
53
+ networkAclId: assoc.networkAclId as NetworkAclId,
54
+ subnetId: assoc.subnetId as SubnetId,
55
+ };
56
+ }),
57
+
58
+ diff: Effect.fn(function* ({ news, olds }) {
59
+ // Subnet change requires replacement
60
+ if (news.subnetId !== olds.subnetId) {
61
+ return { action: "replace" };
62
+ }
63
+ // Network ACL change can be done via replaceNetworkAclAssociation
64
+ }),
65
+
66
+ create: Effect.fn(function* ({ news, session }) {
67
+ yield* session.note(
68
+ `Creating Network ACL Association for subnet ${news.subnetId}...`,
69
+ );
70
+
71
+ // First, find the current association for this subnet (every subnet has one)
72
+ const currentAssoc = yield* findAssociation(news.subnetId as string);
73
+ if (!currentAssoc) {
74
+ return yield* Effect.fail(
75
+ new Error(
76
+ `No existing Network ACL Association found for subnet ${news.subnetId}`,
77
+ ),
78
+ );
79
+ }
80
+
81
+ // Replace the association with the new network ACL
82
+ const result = yield* ec2.replaceNetworkAclAssociation({
83
+ AssociationId: currentAssoc.associationId,
84
+ NetworkAclId: news.networkAclId as string,
85
+ DryRun: false,
86
+ });
87
+
88
+ const newAssociationId = result.NewAssociationId!;
89
+ yield* session.note(
90
+ `Network ACL Association created: ${newAssociationId}`,
91
+ );
92
+
93
+ return {
94
+ associationId: newAssociationId as NetworkAclAssociationId,
95
+ networkAclId: news.networkAclId as NetworkAclId,
96
+ subnetId: news.subnetId as SubnetId,
97
+ };
98
+ }),
99
+
100
+ update: Effect.fn(function* ({ news, output, session }) {
101
+ yield* session.note(`Updating Network ACL Association...`);
102
+
103
+ // Replace the association with the new network ACL
104
+ const result = yield* ec2.replaceNetworkAclAssociation({
105
+ AssociationId: output.associationId,
106
+ NetworkAclId: news.networkAclId as string,
107
+ DryRun: false,
108
+ });
109
+
110
+ const newAssociationId = result.NewAssociationId!;
111
+ yield* session.note(
112
+ `Network ACL Association updated: ${newAssociationId}`,
113
+ );
114
+
115
+ return {
116
+ associationId: newAssociationId as NetworkAclAssociationId,
117
+ networkAclId: news.networkAclId as NetworkAclId,
118
+ subnetId: news.subnetId as SubnetId,
119
+ };
120
+ }),
121
+
122
+ delete: Effect.fn(function* ({ olds, output, session }) {
123
+ yield* session.note(`Deleting Network ACL Association...`);
124
+
125
+ // When deleting, we need to associate the subnet back to the default NACL
126
+ // Find the default NACL for the VPC
127
+ const subnetResult = yield* ec2.describeSubnets({
128
+ SubnetIds: [olds.subnetId as string],
129
+ });
130
+ const vpcId = subnetResult.Subnets?.[0]?.VpcId;
131
+
132
+ if (vpcId) {
133
+ const defaultAclResult = yield* ec2.describeNetworkAcls({
134
+ Filters: [
135
+ { Name: "vpc-id", Values: [vpcId] },
136
+ { Name: "default", Values: ["true"] },
137
+ ],
138
+ });
139
+
140
+ const defaultAclId =
141
+ defaultAclResult.NetworkAcls?.[0]?.NetworkAclId;
142
+
143
+ if (
144
+ defaultAclId &&
145
+ defaultAclId !== (olds.networkAclId as string)
146
+ ) {
147
+ // Replace with default NACL
148
+ yield* ec2
149
+ .replaceNetworkAclAssociation({
150
+ AssociationId: output.associationId,
151
+ NetworkAclId: defaultAclId,
152
+ DryRun: false,
153
+ })
154
+ .pipe(
155
+ Effect.catchTag(
156
+ "InvalidAssociationID.NotFound",
157
+ () => Effect.void,
158
+ ),
159
+ );
160
+
161
+ yield* session.note(
162
+ `Network ACL Association reverted to default`,
163
+ );
164
+ } else {
165
+ yield* session.note(
166
+ `Already using default Network ACL, nothing to do`,
167
+ );
168
+ }
169
+ }
170
+ }),
171
+ } satisfies ProviderService<
172
+ NetworkAclAssociation,
173
+ any,
174
+ any,
175
+ any,
176
+ any,
177
+ any,
178
+ any
179
+ >;
180
+ }),
181
+ );