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.
- package/bin/alchemy-effect.js +539 -223
- package/bin/alchemy-effect.js.map +1 -1
- package/lib/apply.d.ts +4 -4
- package/lib/apply.d.ts.map +1 -1
- package/lib/apply.js +411 -131
- package/lib/apply.js.map +1 -1
- package/lib/aws/dynamodb/table.provider.d.ts.map +1 -1
- package/lib/aws/dynamodb/table.provider.js +1 -0
- package/lib/aws/dynamodb/table.provider.js.map +1 -1
- package/lib/aws/ec2/index.d.ts +8 -0
- package/lib/aws/ec2/index.d.ts.map +1 -1
- package/lib/aws/ec2/index.js +8 -0
- package/lib/aws/ec2/index.js.map +1 -1
- package/lib/aws/ec2/internet-gateway.d.ts +65 -0
- package/lib/aws/ec2/internet-gateway.d.ts.map +1 -0
- package/lib/aws/ec2/internet-gateway.js +4 -0
- package/lib/aws/ec2/internet-gateway.js.map +1 -0
- package/lib/aws/ec2/internet-gateway.provider.d.ts +6 -0
- package/lib/aws/ec2/internet-gateway.provider.d.ts.map +1 -0
- package/lib/aws/ec2/internet-gateway.provider.js +193 -0
- package/lib/aws/ec2/internet-gateway.provider.js.map +1 -0
- package/lib/aws/ec2/route-table-association.d.ts +63 -0
- package/lib/aws/ec2/route-table-association.d.ts.map +1 -0
- package/lib/aws/ec2/route-table-association.js +4 -0
- package/lib/aws/ec2/route-table-association.js.map +1 -0
- package/lib/aws/ec2/route-table-association.provider.d.ts +4 -0
- package/lib/aws/ec2/route-table-association.provider.d.ts.map +1 -0
- package/lib/aws/ec2/route-table-association.provider.js +121 -0
- package/lib/aws/ec2/route-table-association.provider.js.map +1 -0
- package/lib/aws/ec2/route-table.d.ts +159 -0
- package/lib/aws/ec2/route-table.d.ts.map +1 -0
- package/lib/aws/ec2/route-table.js +4 -0
- package/lib/aws/ec2/route-table.js.map +1 -0
- package/lib/aws/ec2/route-table.provider.d.ts +6 -0
- package/lib/aws/ec2/route-table.provider.d.ts.map +1 -0
- package/lib/aws/ec2/route-table.provider.js +213 -0
- package/lib/aws/ec2/route-table.provider.js.map +1 -0
- package/lib/aws/ec2/route.d.ts +155 -0
- package/lib/aws/ec2/route.d.ts.map +1 -0
- package/lib/aws/ec2/route.js +3 -0
- package/lib/aws/ec2/route.js.map +1 -0
- package/lib/aws/ec2/route.provider.d.ts +4 -0
- package/lib/aws/ec2/route.provider.d.ts.map +1 -0
- package/lib/aws/ec2/route.provider.js +166 -0
- package/lib/aws/ec2/route.provider.js.map +1 -0
- package/lib/aws/ec2/subnet.provider.d.ts.map +1 -1
- package/lib/aws/ec2/subnet.provider.js +1 -1
- package/lib/aws/ec2/subnet.provider.js.map +1 -1
- package/lib/aws/ec2/vpc.d.ts +1 -0
- package/lib/aws/ec2/vpc.d.ts.map +1 -1
- package/lib/aws/ec2/vpc.provider.d.ts +2 -2
- package/lib/aws/ec2/vpc.provider.d.ts.map +1 -1
- package/lib/aws/ec2/vpc.provider.js +38 -15
- package/lib/aws/ec2/vpc.provider.js.map +1 -1
- package/lib/aws/index.d.ts +2 -3
- package/lib/aws/index.d.ts.map +1 -1
- package/lib/aws/index.js +2 -1
- package/lib/aws/index.js.map +1 -1
- package/lib/aws/lambda/function.provider.d.ts +2 -2
- package/lib/aws/lambda/function.provider.d.ts.map +1 -1
- package/lib/aws/lambda/function.provider.js +21 -20
- package/lib/aws/lambda/function.provider.js.map +1 -1
- package/lib/aws/sqs/queue.provider.d.ts +2 -2
- package/lib/aws/sqs/queue.provider.d.ts.map +1 -1
- package/lib/aws/sqs/queue.provider.js +3 -2
- package/lib/aws/sqs/queue.provider.js.map +1 -1
- package/lib/cli/index.d.ts +178 -99
- package/lib/cli/index.d.ts.map +1 -1
- package/lib/cloudflare/kv/namespace.client.d.ts +1 -1
- package/lib/cloudflare/kv/namespace.provider.d.ts.map +1 -1
- package/lib/cloudflare/kv/namespace.provider.js +1 -0
- package/lib/cloudflare/kv/namespace.provider.js.map +1 -1
- package/lib/cloudflare/r2/bucket.provider.d.ts.map +1 -1
- package/lib/cloudflare/r2/bucket.provider.js +6 -1
- package/lib/cloudflare/r2/bucket.provider.js.map +1 -1
- package/lib/cloudflare/worker/worker.provider.d.ts +1 -1
- package/lib/cloudflare/worker/worker.provider.d.ts.map +1 -1
- package/lib/cloudflare/worker/worker.provider.js +6 -2
- package/lib/cloudflare/worker/worker.provider.js.map +1 -1
- package/lib/diff.d.ts +8 -6
- package/lib/diff.d.ts.map +1 -1
- package/lib/diff.js +13 -0
- package/lib/diff.js.map +1 -1
- package/lib/event.d.ts +1 -1
- package/lib/event.d.ts.map +1 -1
- package/lib/instance-id.d.ts +8 -0
- package/lib/instance-id.d.ts.map +1 -0
- package/lib/instance-id.js +12 -0
- package/lib/instance-id.js.map +1 -0
- package/lib/output.d.ts +4 -2
- package/lib/output.d.ts.map +1 -1
- package/lib/output.js +18 -4
- package/lib/output.js.map +1 -1
- package/lib/physical-name.d.ts +14 -1
- package/lib/physical-name.d.ts.map +1 -1
- package/lib/physical-name.js +41 -2
- package/lib/physical-name.js.map +1 -1
- package/lib/plan.d.ts +49 -42
- package/lib/plan.d.ts.map +1 -1
- package/lib/plan.js +359 -127
- package/lib/plan.js.map +1 -1
- package/lib/provider.d.ts +26 -9
- package/lib/provider.d.ts.map +1 -1
- package/lib/provider.js +9 -0
- package/lib/provider.js.map +1 -1
- package/lib/resource.d.ts +2 -2
- package/lib/resource.d.ts.map +1 -1
- package/lib/resource.js.map +1 -1
- package/lib/state.d.ts +86 -9
- package/lib/state.d.ts.map +1 -1
- package/lib/state.js +21 -18
- package/lib/state.js.map +1 -1
- package/lib/tags.d.ts +15 -0
- package/lib/tags.d.ts.map +1 -1
- package/lib/tags.js +27 -0
- package/lib/tags.js.map +1 -1
- package/lib/test.d.ts +2 -2
- package/lib/test.d.ts.map +1 -1
- package/lib/test.js +4 -4
- package/lib/test.js.map +1 -1
- package/lib/todo.d.ts +3 -0
- package/lib/todo.d.ts.map +1 -0
- package/lib/todo.js +3 -0
- package/lib/todo.js.map +1 -0
- package/lib/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/apply.ts +758 -374
- package/src/aws/dynamodb/table.provider.ts +1 -0
- package/src/aws/ec2/index.ts +8 -0
- package/src/aws/ec2/internet-gateway.provider.ts +316 -0
- package/src/aws/ec2/internet-gateway.ts +79 -0
- package/src/aws/ec2/route-table-association.provider.ts +214 -0
- package/src/aws/ec2/route-table-association.ts +82 -0
- package/src/aws/ec2/route-table.provider.ts +306 -0
- package/src/aws/ec2/route-table.ts +175 -0
- package/src/aws/ec2/route.provider.ts +213 -0
- package/src/aws/ec2/route.ts +192 -0
- package/src/aws/ec2/subnet.provider.ts +2 -2
- package/src/aws/ec2/vpc.provider.ts +43 -19
- package/src/aws/ec2/vpc.ts +2 -0
- package/src/aws/index.ts +4 -1
- package/src/aws/lambda/function.provider.ts +25 -23
- package/src/aws/sqs/queue.provider.ts +3 -2
- package/src/cloudflare/kv/namespace.provider.ts +1 -0
- package/src/cloudflare/r2/bucket.provider.ts +7 -1
- package/src/cloudflare/worker/worker.provider.ts +6 -2
- package/src/diff.ts +35 -17
- package/src/event.ts +6 -0
- package/src/instance-id.ts +16 -0
- package/src/output.ts +29 -5
- package/src/physical-name.ts +57 -2
- package/src/plan.ts +488 -197
- package/src/provider.ts +46 -9
- package/src/resource.ts +50 -4
- package/src/state.ts +150 -35
- package/src/tags.ts +31 -0
- package/src/test.ts +5 -5
- 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
|
package/src/aws/ec2/index.ts
CHANGED
|
@@ -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
|
+
);
|