alchemy-effect 0.3.0 → 0.4.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 (158) hide show
  1. package/bin/alchemy-effect.js +539 -223
  2. package/bin/alchemy-effect.js.map +1 -1
  3. package/lib/apply.d.ts +4 -4
  4. package/lib/apply.d.ts.map +1 -1
  5. package/lib/apply.js +411 -131
  6. package/lib/apply.js.map +1 -1
  7. package/lib/aws/dynamodb/table.provider.d.ts.map +1 -1
  8. package/lib/aws/dynamodb/table.provider.js +1 -0
  9. package/lib/aws/dynamodb/table.provider.js.map +1 -1
  10. package/lib/aws/ec2/index.d.ts +8 -0
  11. package/lib/aws/ec2/index.d.ts.map +1 -1
  12. package/lib/aws/ec2/index.js +8 -0
  13. package/lib/aws/ec2/index.js.map +1 -1
  14. package/lib/aws/ec2/internet-gateway.d.ts +65 -0
  15. package/lib/aws/ec2/internet-gateway.d.ts.map +1 -0
  16. package/lib/aws/ec2/internet-gateway.js +4 -0
  17. package/lib/aws/ec2/internet-gateway.js.map +1 -0
  18. package/lib/aws/ec2/internet-gateway.provider.d.ts +6 -0
  19. package/lib/aws/ec2/internet-gateway.provider.d.ts.map +1 -0
  20. package/lib/aws/ec2/internet-gateway.provider.js +193 -0
  21. package/lib/aws/ec2/internet-gateway.provider.js.map +1 -0
  22. package/lib/aws/ec2/route-table-association.d.ts +63 -0
  23. package/lib/aws/ec2/route-table-association.d.ts.map +1 -0
  24. package/lib/aws/ec2/route-table-association.js +4 -0
  25. package/lib/aws/ec2/route-table-association.js.map +1 -0
  26. package/lib/aws/ec2/route-table-association.provider.d.ts +4 -0
  27. package/lib/aws/ec2/route-table-association.provider.d.ts.map +1 -0
  28. package/lib/aws/ec2/route-table-association.provider.js +121 -0
  29. package/lib/aws/ec2/route-table-association.provider.js.map +1 -0
  30. package/lib/aws/ec2/route-table.d.ts +159 -0
  31. package/lib/aws/ec2/route-table.d.ts.map +1 -0
  32. package/lib/aws/ec2/route-table.js +4 -0
  33. package/lib/aws/ec2/route-table.js.map +1 -0
  34. package/lib/aws/ec2/route-table.provider.d.ts +6 -0
  35. package/lib/aws/ec2/route-table.provider.d.ts.map +1 -0
  36. package/lib/aws/ec2/route-table.provider.js +213 -0
  37. package/lib/aws/ec2/route-table.provider.js.map +1 -0
  38. package/lib/aws/ec2/route.d.ts +155 -0
  39. package/lib/aws/ec2/route.d.ts.map +1 -0
  40. package/lib/aws/ec2/route.js +3 -0
  41. package/lib/aws/ec2/route.js.map +1 -0
  42. package/lib/aws/ec2/route.provider.d.ts +4 -0
  43. package/lib/aws/ec2/route.provider.d.ts.map +1 -0
  44. package/lib/aws/ec2/route.provider.js +166 -0
  45. package/lib/aws/ec2/route.provider.js.map +1 -0
  46. package/lib/aws/ec2/subnet.provider.d.ts.map +1 -1
  47. package/lib/aws/ec2/subnet.provider.js +1 -1
  48. package/lib/aws/ec2/subnet.provider.js.map +1 -1
  49. package/lib/aws/ec2/vpc.d.ts +1 -0
  50. package/lib/aws/ec2/vpc.d.ts.map +1 -1
  51. package/lib/aws/ec2/vpc.provider.d.ts +2 -2
  52. package/lib/aws/ec2/vpc.provider.d.ts.map +1 -1
  53. package/lib/aws/ec2/vpc.provider.js +38 -15
  54. package/lib/aws/ec2/vpc.provider.js.map +1 -1
  55. package/lib/aws/index.d.ts +2 -3
  56. package/lib/aws/index.d.ts.map +1 -1
  57. package/lib/aws/index.js +2 -1
  58. package/lib/aws/index.js.map +1 -1
  59. package/lib/aws/lambda/function.provider.d.ts +2 -2
  60. package/lib/aws/lambda/function.provider.d.ts.map +1 -1
  61. package/lib/aws/lambda/function.provider.js +21 -20
  62. package/lib/aws/lambda/function.provider.js.map +1 -1
  63. package/lib/aws/sqs/queue.provider.d.ts +2 -2
  64. package/lib/aws/sqs/queue.provider.d.ts.map +1 -1
  65. package/lib/aws/sqs/queue.provider.js +3 -2
  66. package/lib/aws/sqs/queue.provider.js.map +1 -1
  67. package/lib/cli/index.d.ts +178 -99
  68. package/lib/cli/index.d.ts.map +1 -1
  69. package/lib/cloudflare/kv/namespace.client.d.ts +1 -1
  70. package/lib/cloudflare/kv/namespace.provider.d.ts.map +1 -1
  71. package/lib/cloudflare/kv/namespace.provider.js +1 -0
  72. package/lib/cloudflare/kv/namespace.provider.js.map +1 -1
  73. package/lib/cloudflare/r2/bucket.provider.d.ts.map +1 -1
  74. package/lib/cloudflare/r2/bucket.provider.js +6 -1
  75. package/lib/cloudflare/r2/bucket.provider.js.map +1 -1
  76. package/lib/cloudflare/worker/worker.provider.d.ts +1 -1
  77. package/lib/cloudflare/worker/worker.provider.d.ts.map +1 -1
  78. package/lib/cloudflare/worker/worker.provider.js +6 -2
  79. package/lib/cloudflare/worker/worker.provider.js.map +1 -1
  80. package/lib/diff.d.ts +8 -6
  81. package/lib/diff.d.ts.map +1 -1
  82. package/lib/diff.js +13 -0
  83. package/lib/diff.js.map +1 -1
  84. package/lib/event.d.ts +1 -1
  85. package/lib/event.d.ts.map +1 -1
  86. package/lib/instance-id.d.ts +8 -0
  87. package/lib/instance-id.d.ts.map +1 -0
  88. package/lib/instance-id.js +12 -0
  89. package/lib/instance-id.js.map +1 -0
  90. package/lib/output.d.ts +4 -2
  91. package/lib/output.d.ts.map +1 -1
  92. package/lib/output.js +18 -4
  93. package/lib/output.js.map +1 -1
  94. package/lib/physical-name.d.ts +14 -1
  95. package/lib/physical-name.d.ts.map +1 -1
  96. package/lib/physical-name.js +41 -2
  97. package/lib/physical-name.js.map +1 -1
  98. package/lib/plan.d.ts +49 -42
  99. package/lib/plan.d.ts.map +1 -1
  100. package/lib/plan.js +359 -127
  101. package/lib/plan.js.map +1 -1
  102. package/lib/provider.d.ts +26 -9
  103. package/lib/provider.d.ts.map +1 -1
  104. package/lib/provider.js +9 -0
  105. package/lib/provider.js.map +1 -1
  106. package/lib/resource.d.ts +2 -2
  107. package/lib/resource.d.ts.map +1 -1
  108. package/lib/resource.js.map +1 -1
  109. package/lib/state.d.ts +86 -9
  110. package/lib/state.d.ts.map +1 -1
  111. package/lib/state.js +21 -18
  112. package/lib/state.js.map +1 -1
  113. package/lib/tags.d.ts +15 -0
  114. package/lib/tags.d.ts.map +1 -1
  115. package/lib/tags.js +27 -0
  116. package/lib/tags.js.map +1 -1
  117. package/lib/test.d.ts +2 -2
  118. package/lib/test.d.ts.map +1 -1
  119. package/lib/test.js +4 -4
  120. package/lib/test.js.map +1 -1
  121. package/lib/todo.d.ts +3 -0
  122. package/lib/todo.d.ts.map +1 -0
  123. package/lib/todo.js +3 -0
  124. package/lib/todo.js.map +1 -0
  125. package/lib/tsconfig.test.tsbuildinfo +1 -1
  126. package/package.json +2 -2
  127. package/src/apply.ts +758 -374
  128. package/src/aws/dynamodb/table.provider.ts +1 -0
  129. package/src/aws/ec2/index.ts +8 -0
  130. package/src/aws/ec2/internet-gateway.provider.ts +316 -0
  131. package/src/aws/ec2/internet-gateway.ts +79 -0
  132. package/src/aws/ec2/route-table-association.provider.ts +214 -0
  133. package/src/aws/ec2/route-table-association.ts +82 -0
  134. package/src/aws/ec2/route-table.provider.ts +306 -0
  135. package/src/aws/ec2/route-table.ts +175 -0
  136. package/src/aws/ec2/route.provider.ts +213 -0
  137. package/src/aws/ec2/route.ts +192 -0
  138. package/src/aws/ec2/subnet.provider.ts +2 -2
  139. package/src/aws/ec2/vpc.provider.ts +43 -19
  140. package/src/aws/ec2/vpc.ts +2 -0
  141. package/src/aws/index.ts +4 -1
  142. package/src/aws/lambda/function.provider.ts +25 -23
  143. package/src/aws/sqs/queue.provider.ts +3 -2
  144. package/src/cloudflare/kv/namespace.provider.ts +1 -0
  145. package/src/cloudflare/r2/bucket.provider.ts +7 -1
  146. package/src/cloudflare/worker/worker.provider.ts +6 -2
  147. package/src/diff.ts +35 -17
  148. package/src/event.ts +6 -0
  149. package/src/instance-id.ts +16 -0
  150. package/src/output.ts +29 -5
  151. package/src/physical-name.ts +57 -2
  152. package/src/plan.ts +488 -197
  153. package/src/provider.ts +46 -9
  154. package/src/resource.ts +50 -4
  155. package/src/state.ts +150 -35
  156. package/src/tags.ts +31 -0
  157. package/src/test.ts +5 -5
  158. package/src/todo.ts +4 -0
@@ -120,6 +120,7 @@ export const tableProvider = (): Layer.Layer<
120
120
  );
121
121
 
122
122
  return {
123
+ stables: ["tableName", "tableId", "tableArn"],
123
124
  diff: Effect.fn(function* ({ news, olds }) {
124
125
  if (
125
126
  // TODO(sam): if the name is hard-coded, REPLACE is impossible - we need a suffix
@@ -1,4 +1,12 @@
1
1
  export * from "./client.ts";
2
+ export * from "./internet-gateway.provider.ts";
3
+ export * from "./internet-gateway.ts";
4
+ export * from "./route-table-association.provider.ts";
5
+ export * from "./route-table-association.ts";
6
+ export * from "./route-table.provider.ts";
7
+ export * from "./route-table.ts";
8
+ export * from "./route.provider.ts";
9
+ export * from "./route.ts";
2
10
  export * from "./subnet.provider.ts";
3
11
  export * from "./subnet.ts";
4
12
  export * from "./vpc.provider.ts";
@@ -0,0 +1,316 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Schedule from "effect/Schedule";
3
+
4
+ import type { EC2 } from "itty-aws/ec2";
5
+
6
+ import type { ScopedPlanStatusSession } from "../../cli/service.ts";
7
+ import type { ProviderService } from "../../provider.ts";
8
+ import { createTagger, createTagsList } from "../../tags.ts";
9
+ import { Account } from "../account.ts";
10
+ import { Region } from "../region.ts";
11
+ import { EC2Client } from "./client.ts";
12
+ import {
13
+ InternetGateway,
14
+ type InternetGatewayAttrs,
15
+ type InternetGatewayId,
16
+ type InternetGatewayProps,
17
+ } from "./internet-gateway.ts";
18
+
19
+ export const internetGatewayProvider = () =>
20
+ InternetGateway.provider.effect(
21
+ Effect.gen(function* () {
22
+ const ec2 = yield* EC2Client;
23
+ const region = yield* Region;
24
+ const accountId = yield* Account;
25
+ const tagged = yield* createTagger();
26
+
27
+ return {
28
+ stables: ["internetGatewayId", "internetGatewayArn", "ownerId"],
29
+ diff: Effect.fn(function* ({ news, olds }) {
30
+ // VPC attachment change can be handled via attach/detach (update)
31
+ // No properties require replacement
32
+ }),
33
+
34
+ create: Effect.fn(function* ({ id, news, session }) {
35
+ // 1. Prepare tags
36
+ const alchemyTags = tagged(id);
37
+ const userTags = news.tags ?? {};
38
+ const allTags = { ...alchemyTags, ...userTags };
39
+
40
+ // 2. Call CreateInternetGateway
41
+ const createResult = yield* ec2.createInternetGateway({
42
+ TagSpecifications: [
43
+ {
44
+ ResourceType: "internet-gateway",
45
+ Tags: createTagsList(allTags),
46
+ },
47
+ ],
48
+ DryRun: false,
49
+ });
50
+
51
+ const internetGatewayId = createResult.InternetGateway!
52
+ .InternetGatewayId! as InternetGatewayId;
53
+ yield* session.note(`Internet gateway created: ${internetGatewayId}`);
54
+
55
+ // 3. Attach to VPC if specified
56
+ if (news.vpcId) {
57
+ yield* ec2
58
+ .attachInternetGateway({
59
+ InternetGatewayId: internetGatewayId,
60
+ VpcId: news.vpcId,
61
+ })
62
+ .pipe(
63
+ Effect.retry({
64
+ // Retry if VPC is not yet available
65
+ while: (e) => e._tag === "InvalidVpcID.NotFound",
66
+ schedule: Schedule.exponential(100),
67
+ }),
68
+ );
69
+ yield* session.note(`Attached to VPC: ${news.vpcId}`);
70
+ }
71
+
72
+ // 4. Describe to get full details
73
+ const igw = yield* describeInternetGateway(
74
+ ec2,
75
+ internetGatewayId,
76
+ session,
77
+ );
78
+
79
+ // 5. Return attributes
80
+ return {
81
+ internetGatewayId,
82
+ internetGatewayArn:
83
+ `arn:aws:ec2:${region}:${accountId}:internet-gateway/${internetGatewayId}` as InternetGatewayAttrs<InternetGatewayProps>["internetGatewayArn"],
84
+ vpcId: news.vpcId,
85
+ ownerId: igw.OwnerId,
86
+ attachments: igw.Attachments?.map((a) => ({
87
+ state: a.State! as
88
+ | "attaching"
89
+ | "available"
90
+ | "detaching"
91
+ | "detached",
92
+ vpcId: a.VpcId!,
93
+ })),
94
+ } satisfies InternetGatewayAttrs<InternetGatewayProps>;
95
+ }),
96
+
97
+ update: Effect.fn(function* ({ news, olds, output, session }) {
98
+ const internetGatewayId = output.internetGatewayId;
99
+
100
+ // Handle VPC attachment changes
101
+ if (news.vpcId !== olds.vpcId) {
102
+ // Detach from old VPC if was attached
103
+ if (olds.vpcId) {
104
+ yield* ec2
105
+ .detachInternetGateway({
106
+ InternetGatewayId: internetGatewayId,
107
+ VpcId: olds.vpcId,
108
+ })
109
+ .pipe(
110
+ Effect.catchTag("Gateway.NotAttached", () => Effect.void),
111
+ );
112
+ yield* session.note(`Detached from VPC: ${olds.vpcId}`);
113
+ }
114
+
115
+ // Attach to new VPC if specified
116
+ if (news.vpcId) {
117
+ yield* ec2
118
+ .attachInternetGateway({
119
+ InternetGatewayId: internetGatewayId,
120
+ VpcId: news.vpcId,
121
+ })
122
+ .pipe(
123
+ Effect.retry({
124
+ while: (e) => e._tag === "InvalidVpcID.NotFound",
125
+ schedule: Schedule.exponential(100),
126
+ }),
127
+ );
128
+ yield* session.note(`Attached to VPC: ${news.vpcId}`);
129
+ }
130
+ }
131
+
132
+ // Handle tag updates
133
+ if (
134
+ JSON.stringify(news.tags ?? {}) !== JSON.stringify(olds.tags ?? {})
135
+ ) {
136
+ const alchemyTags = tagged(output.internetGatewayId);
137
+ const userTags = news.tags ?? {};
138
+ const allTags = { ...alchemyTags, ...userTags };
139
+
140
+ // Delete old tags that are no longer present
141
+ const oldTagKeys = Object.keys(olds.tags ?? {});
142
+ const newTagKeys = Object.keys(news.tags ?? {});
143
+ const tagsToDelete = oldTagKeys.filter(
144
+ (key) => !newTagKeys.includes(key),
145
+ );
146
+
147
+ if (tagsToDelete.length > 0) {
148
+ yield* ec2.deleteTags({
149
+ Resources: [internetGatewayId],
150
+ Tags: tagsToDelete.map((key) => ({ Key: key })),
151
+ });
152
+ }
153
+
154
+ // Create/update tags
155
+ yield* ec2.createTags({
156
+ Resources: [internetGatewayId],
157
+ Tags: createTagsList(allTags),
158
+ });
159
+
160
+ yield* session.note("Updated tags");
161
+ }
162
+
163
+ // Re-describe to get current state
164
+ const igw = yield* describeInternetGateway(
165
+ ec2,
166
+ internetGatewayId,
167
+ session,
168
+ );
169
+
170
+ return {
171
+ ...output,
172
+ vpcId: news.vpcId,
173
+ attachments: igw.Attachments?.map((a) => ({
174
+ state: a.State! as
175
+ | "attaching"
176
+ | "available"
177
+ | "detaching"
178
+ | "detached",
179
+ vpcId: a.VpcId!,
180
+ })),
181
+ };
182
+ }),
183
+
184
+ delete: Effect.fn(function* ({ output, session }) {
185
+ const internetGatewayId = output.internetGatewayId;
186
+
187
+ yield* session.note(
188
+ `Deleting internet gateway: ${internetGatewayId}`,
189
+ );
190
+
191
+ // 1. Detach from all VPCs first
192
+ if (output.attachments && output.attachments.length > 0) {
193
+ for (const attachment of output.attachments) {
194
+ yield* ec2
195
+ .detachInternetGateway({
196
+ InternetGatewayId: internetGatewayId,
197
+ VpcId: attachment.vpcId,
198
+ })
199
+ .pipe(
200
+ Effect.tapError(Effect.logDebug),
201
+ Effect.catchTag("Gateway.NotAttached", () => Effect.void),
202
+ Effect.catchTag(
203
+ "InvalidInternetGatewayID.NotFound",
204
+ () => Effect.void,
205
+ ),
206
+ );
207
+ yield* session.note(`Detached from VPC: ${attachment.vpcId}`);
208
+ }
209
+ }
210
+
211
+ // 2. Delete the internet gateway
212
+ yield* ec2
213
+ .deleteInternetGateway({
214
+ InternetGatewayId: internetGatewayId,
215
+ DryRun: false,
216
+ })
217
+ .pipe(
218
+ Effect.tapError(Effect.logDebug),
219
+ Effect.catchTag(
220
+ "InvalidInternetGatewayID.NotFound",
221
+ () => Effect.void,
222
+ ),
223
+ // Retry on dependency violations
224
+ Effect.retry({
225
+ while: (e) => {
226
+ return (
227
+ e._tag === "DependencyViolation" ||
228
+ (e._tag === "ValidationError" &&
229
+ e.message?.includes("DependencyViolation"))
230
+ );
231
+ },
232
+ schedule: Schedule.exponential(1000, 1.5).pipe(
233
+ Schedule.intersect(Schedule.recurs(10)),
234
+ Schedule.tapOutput(([, attempt]) =>
235
+ session.note(
236
+ `Waiting for dependencies to clear... (attempt ${attempt + 1})`,
237
+ ),
238
+ ),
239
+ ),
240
+ }),
241
+ );
242
+
243
+ // 3. Wait for internet gateway to be fully deleted
244
+ yield* waitForInternetGatewayDeleted(ec2, internetGatewayId, session);
245
+
246
+ yield* session.note(
247
+ `Internet gateway ${internetGatewayId} deleted successfully`,
248
+ );
249
+ }),
250
+ } satisfies ProviderService<InternetGateway>;
251
+ }),
252
+ );
253
+
254
+ /**
255
+ * Describe an internet gateway by ID
256
+ */
257
+ const describeInternetGateway = (
258
+ ec2: EC2,
259
+ internetGatewayId: string,
260
+ _session?: ScopedPlanStatusSession,
261
+ ) =>
262
+ Effect.gen(function* () {
263
+ const result = yield* ec2
264
+ .describeInternetGateways({ InternetGatewayIds: [internetGatewayId] })
265
+ .pipe(
266
+ Effect.catchTag("InvalidInternetGatewayID.NotFound", () =>
267
+ Effect.succeed({ InternetGateways: [] }),
268
+ ),
269
+ );
270
+
271
+ const igw = result.InternetGateways?.[0];
272
+ if (!igw) {
273
+ return yield* Effect.fail(new Error("Internet gateway not found"));
274
+ }
275
+ return igw;
276
+ });
277
+
278
+ /**
279
+ * Wait for internet gateway to be deleted
280
+ */
281
+ const waitForInternetGatewayDeleted = (
282
+ ec2: EC2,
283
+ internetGatewayId: string,
284
+ session: ScopedPlanStatusSession,
285
+ ) =>
286
+ Effect.gen(function* () {
287
+ yield* Effect.retry(
288
+ Effect.gen(function* () {
289
+ const result = yield* ec2
290
+ .describeInternetGateways({ InternetGatewayIds: [internetGatewayId] })
291
+ .pipe(
292
+ Effect.tapError(Effect.logDebug),
293
+ Effect.catchTag("InvalidInternetGatewayID.NotFound", () =>
294
+ Effect.succeed({ InternetGateways: [] }),
295
+ ),
296
+ );
297
+
298
+ if (!result.InternetGateways || result.InternetGateways.length === 0) {
299
+ return; // Successfully deleted
300
+ }
301
+
302
+ // Still exists, fail to trigger retry
303
+ return yield* Effect.fail(new Error("Internet gateway still exists"));
304
+ }),
305
+ {
306
+ schedule: Schedule.fixed(2000).pipe(
307
+ Schedule.intersect(Schedule.recurs(15)),
308
+ Schedule.tapOutput(([, attempt]) =>
309
+ session.note(
310
+ `Waiting for internet gateway deletion... (${(attempt + 1) * 2}s)`,
311
+ ),
312
+ ),
313
+ ),
314
+ },
315
+ );
316
+ });
@@ -0,0 +1,79 @@
1
+ import type { Input } from "../../input.ts";
2
+ import { Resource } from "../../resource.ts";
3
+ import type { AccountID } from "../account.ts";
4
+ import type { RegionID } from "../region.ts";
5
+ import type { VpcId } from "./vpc.ts";
6
+
7
+ export const InternetGateway = Resource<{
8
+ <const ID extends string, const Props extends InternetGatewayProps>(
9
+ id: ID,
10
+ props: Props,
11
+ ): InternetGateway<ID, Props>;
12
+ }>("AWS.EC2.InternetGateway");
13
+
14
+ export interface InternetGateway<
15
+ ID extends string = string,
16
+ Props extends InternetGatewayProps = InternetGatewayProps,
17
+ > extends Resource<
18
+ "AWS.EC2.InternetGateway",
19
+ ID,
20
+ Props,
21
+ InternetGatewayAttrs<Input.Resolve<Props>>,
22
+ InternetGateway
23
+ > {}
24
+
25
+ export type InternetGatewayId<ID extends string = string> = `igw-${ID}`;
26
+ export const InternetGatewayId = <ID extends string>(
27
+ id: ID,
28
+ ): ID & InternetGatewayId<ID> => `igw-${id}` as ID & InternetGatewayId<ID>;
29
+
30
+ export interface InternetGatewayProps {
31
+ /**
32
+ * The VPC to attach the internet gateway to.
33
+ * If provided, the internet gateway will be automatically attached to the VPC.
34
+ * Optional - you can create an unattached internet gateway and attach it later.
35
+ */
36
+ vpcId?: Input<VpcId>;
37
+
38
+ /**
39
+ * Tags to assign to the internet gateway.
40
+ * These will be merged with alchemy auto-tags (alchemy::app, alchemy::stage, alchemy::id).
41
+ */
42
+ tags?: Record<string, Input<string>>;
43
+ }
44
+
45
+ export interface InternetGatewayAttrs<Props extends InternetGatewayProps> {
46
+ /**
47
+ * The ID of the internet gateway.
48
+ */
49
+ internetGatewayId: InternetGatewayId;
50
+
51
+ /**
52
+ * The Amazon Resource Name (ARN) of the internet gateway.
53
+ */
54
+ internetGatewayArn: `arn:aws:ec2:${RegionID}:${AccountID}:internet-gateway/${this["internetGatewayId"]}`;
55
+
56
+ /**
57
+ * The ID of the VPC the internet gateway is attached to (if any).
58
+ */
59
+ vpcId?: Props["vpcId"];
60
+
61
+ /**
62
+ * The ID of the AWS account that owns the internet gateway.
63
+ */
64
+ ownerId?: string;
65
+
66
+ /**
67
+ * The attachments for the internet gateway.
68
+ */
69
+ attachments?: Array<{
70
+ /**
71
+ * The current state of the attachment.
72
+ */
73
+ state: "attaching" | "available" | "detaching" | "detached";
74
+ /**
75
+ * The ID of the VPC.
76
+ */
77
+ vpcId: string;
78
+ }>;
79
+ }
@@ -0,0 +1,214 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Schedule from "effect/Schedule";
3
+
4
+ import type { ScopedPlanStatusSession } from "../../cli/service.ts";
5
+ import type { ProviderService } from "../../provider.ts";
6
+ import { EC2Client } from "./client.ts";
7
+ import {
8
+ RouteTableAssociation,
9
+ type RouteTableAssociationAttrs,
10
+ type RouteTableAssociationId,
11
+ type RouteTableAssociationProps,
12
+ } from "./route-table-association.ts";
13
+
14
+ export const routeTableAssociationProvider = () =>
15
+ RouteTableAssociation.provider.effect(
16
+ Effect.gen(function* () {
17
+ const ec2 = yield* EC2Client;
18
+
19
+ return {
20
+ stables: ["associationId", "subnetId", "gatewayId"],
21
+ diff: Effect.fn(function* ({ news, olds }) {
22
+ // Subnet/Gateway change requires replacement (use ReplaceRouteTableAssociation internally)
23
+ if (olds.subnetId !== news.subnetId) {
24
+ return { action: "replace" };
25
+ }
26
+ if (olds.gatewayId !== news.gatewayId) {
27
+ return { action: "replace" };
28
+ }
29
+ // Route table change can be done via ReplaceRouteTableAssociation
30
+ }),
31
+
32
+ create: Effect.fn(function* ({ news, session }) {
33
+ // Call AssociateRouteTable
34
+ const result = yield* ec2
35
+ .associateRouteTable({
36
+ RouteTableId: news.routeTableId,
37
+ SubnetId: news.subnetId,
38
+ GatewayId: news.gatewayId,
39
+ DryRun: false,
40
+ })
41
+ .pipe(
42
+ Effect.retry({
43
+ // Retry if route table or subnet/gateway is not yet available
44
+ while: (e) =>
45
+ e._tag === "InvalidRouteTableID.NotFound" ||
46
+ e._tag === "InvalidSubnetID.NotFound",
47
+ schedule: Schedule.exponential(100),
48
+ }),
49
+ );
50
+
51
+ const associationId =
52
+ result.AssociationId! as RouteTableAssociationId;
53
+ yield* session.note(
54
+ `Route table association created: ${associationId}`,
55
+ );
56
+
57
+ // Wait for association to be associated
58
+ yield* waitForAssociationState(
59
+ ec2,
60
+ news.routeTableId,
61
+ associationId,
62
+ "associated",
63
+ session,
64
+ );
65
+
66
+ // Return attributes
67
+ return {
68
+ associationId,
69
+ routeTableId: news.routeTableId,
70
+ subnetId: news.subnetId,
71
+ gatewayId: news.gatewayId,
72
+ associationState: {
73
+ state: result.AssociationState?.State ?? "associated",
74
+ statusMessage: result.AssociationState?.StatusMessage,
75
+ },
76
+ } satisfies RouteTableAssociationAttrs<RouteTableAssociationProps>;
77
+ }),
78
+
79
+ update: Effect.fn(function* ({ news, olds, output, session }) {
80
+ // If route table changed, use ReplaceRouteTableAssociation
81
+ if (news.routeTableId !== olds.routeTableId) {
82
+ const result = yield* ec2.replaceRouteTableAssociation({
83
+ AssociationId: output.associationId,
84
+ RouteTableId: news.routeTableId,
85
+ DryRun: false,
86
+ });
87
+
88
+ const newAssociationId =
89
+ result.NewAssociationId! as RouteTableAssociationId;
90
+ yield* session.note(
91
+ `Route table association replaced: ${newAssociationId}`,
92
+ );
93
+
94
+ // Wait for new association to be associated
95
+ yield* waitForAssociationState(
96
+ ec2,
97
+ news.routeTableId,
98
+ newAssociationId,
99
+ "associated",
100
+ session,
101
+ );
102
+
103
+ return {
104
+ associationId: newAssociationId,
105
+ routeTableId: news.routeTableId,
106
+ subnetId: news.subnetId,
107
+ gatewayId: news.gatewayId,
108
+ associationState: {
109
+ state: result.AssociationState?.State ?? "associated",
110
+ statusMessage: result.AssociationState?.StatusMessage,
111
+ },
112
+ };
113
+ }
114
+
115
+ // No changes needed
116
+ return output;
117
+ }),
118
+
119
+ delete: Effect.fn(function* ({ output, session }) {
120
+ yield* session.note(
121
+ `Deleting route table association: ${output.associationId}`,
122
+ );
123
+
124
+ // Disassociate the route table
125
+ yield* ec2
126
+ .disassociateRouteTable({
127
+ AssociationId: output.associationId,
128
+ DryRun: false,
129
+ })
130
+ .pipe(
131
+ Effect.tapError(Effect.log),
132
+ Effect.catchTag(
133
+ "InvalidAssociationID.NotFound",
134
+ () => Effect.void,
135
+ ),
136
+ );
137
+
138
+ yield* session.note(
139
+ `Route table association ${output.associationId} deleted successfully`,
140
+ );
141
+ }),
142
+ } satisfies ProviderService<RouteTableAssociation>;
143
+ }),
144
+ );
145
+
146
+ /**
147
+ * Wait for association to reach a specific state
148
+ */
149
+ const waitForAssociationState = (
150
+ ec2: import("itty-aws/ec2").EC2,
151
+ routeTableId: string,
152
+ associationId: string,
153
+ targetState:
154
+ | "associating"
155
+ | "associated"
156
+ | "disassociating"
157
+ | "disassociated",
158
+ session?: ScopedPlanStatusSession,
159
+ ) =>
160
+ Effect.retry(
161
+ Effect.gen(function* () {
162
+ const result = yield* ec2
163
+ .describeRouteTables({ RouteTableIds: [routeTableId] })
164
+ .pipe(
165
+ Effect.catchTag("InvalidRouteTableID.NotFound", () =>
166
+ Effect.succeed({ RouteTables: [] }),
167
+ ),
168
+ );
169
+
170
+ const routeTable = result.RouteTables?.[0];
171
+ if (!routeTable) {
172
+ return yield* Effect.fail(new Error("Route table not found"));
173
+ }
174
+
175
+ const association = routeTable.Associations?.find(
176
+ (a) => a.RouteTableAssociationId === associationId,
177
+ );
178
+
179
+ if (!association) {
180
+ // Association might not exist yet, retry
181
+ return yield* Effect.fail(new Error("Association not found"));
182
+ }
183
+
184
+ if (association.AssociationState?.State === targetState) {
185
+ return;
186
+ }
187
+
188
+ if (association.AssociationState?.State === "failed") {
189
+ return yield* Effect.fail(
190
+ new Error(
191
+ `Association failed: ${association.AssociationState.StatusMessage}`,
192
+ ),
193
+ );
194
+ }
195
+
196
+ // Still in progress, fail to trigger retry
197
+ return yield* Effect.fail(
198
+ new Error(`Association state: ${association.AssociationState?.State}`),
199
+ );
200
+ }),
201
+ {
202
+ schedule: Schedule.fixed(1000).pipe(
203
+ // Check every second
204
+ Schedule.intersect(Schedule.recurs(30)), // Max 30 seconds
205
+ Schedule.tapOutput(([, attempt]) =>
206
+ session
207
+ ? session.note(
208
+ `Waiting for association to be ${targetState}... (${attempt + 1}s)`,
209
+ )
210
+ : Effect.void,
211
+ ),
212
+ ),
213
+ },
214
+ );