alchemy 2.0.0-beta.1 → 2.0.0-beta.2
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 +31 -16
- package/bin/alchemy-effect.js.map +1 -1
- package/bin/alchemy-effect.sh +2 -3
- package/bin/alchemy-effect.ts +3 -3
- package/lib/cli/index.d.ts +39 -36
- package/lib/cli/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/AWS/ACM/Certificate.ts +49 -46
- package/src/AWS/AutoScaling/AutoScalingGroup.ts +4 -1
- package/src/AWS/AutoScaling/LaunchTemplate.ts +3 -1
- package/src/AWS/AutoScaling/ScalingPolicy.ts +4 -1
- package/src/AWS/CloudFront/Distribution.ts +4 -1
- package/src/AWS/CloudFront/Function.ts +4 -1
- package/src/AWS/CloudFront/Invalidation.ts +4 -1
- package/src/AWS/CloudFront/KeyValueStore.ts +4 -1
- package/src/AWS/CloudFront/KvEntries.ts +4 -1
- package/src/AWS/CloudFront/KvRoutesUpdate.ts +4 -1
- package/src/AWS/CloudFront/OriginAccessControl.ts +4 -1
- package/src/AWS/CloudWatch/Alarm.ts +4 -1
- package/src/AWS/CloudWatch/AlarmMuteRule.ts +4 -1
- package/src/AWS/CloudWatch/AnomalyDetector.ts +4 -1
- package/src/AWS/CloudWatch/CompositeAlarm.ts +4 -1
- package/src/AWS/CloudWatch/Dashboard.ts +4 -1
- package/src/AWS/CloudWatch/InsightRule.ts +4 -1
- package/src/AWS/CloudWatch/MetricStream.ts +4 -1
- package/src/AWS/DynamoDB/Table.ts +3 -1
- package/src/AWS/EC2/EIP.ts +4 -1
- package/src/AWS/EC2/EgressOnlyInternetGateway.ts +4 -1
- package/src/AWS/EC2/Instance.ts +3 -1
- package/src/AWS/EC2/InternetGateway.ts +4 -1
- package/src/AWS/EC2/NatGateway.ts +4 -1
- package/src/AWS/EC2/NetworkAcl.ts +4 -1
- package/src/AWS/EC2/NetworkAclAssociation.ts +4 -1
- package/src/AWS/EC2/NetworkAclEntry.ts +4 -1
- package/src/AWS/EC2/Route.ts +4 -1
- package/src/AWS/EC2/RouteTable.ts +4 -1
- package/src/AWS/EC2/RouteTableAssociation.ts +4 -1
- package/src/AWS/EC2/SecurityGroup.ts +4 -1
- package/src/AWS/EC2/SecurityGroupRule.ts +4 -1
- package/src/AWS/EC2/Subnet.ts +4 -1
- package/src/AWS/EC2/Vpc.ts +4 -1
- package/src/AWS/EC2/VpcEndpoint.ts +4 -1
- package/src/AWS/ECR/Repository.ts +4 -1
- package/src/AWS/ECS/Cluster.ts +4 -1
- package/src/AWS/ECS/Service.ts +4 -1
- package/src/AWS/ECS/Task.ts +3 -1
- package/src/AWS/EKS/AccessEntry.ts +4 -1
- package/src/AWS/EKS/Addon.ts +4 -1
- package/src/AWS/EKS/Cluster.ts +3 -1
- package/src/AWS/EKS/PodIdentityAssociation.ts +4 -1
- package/src/AWS/ELBv2/Listener.ts +4 -1
- package/src/AWS/ELBv2/LoadBalancer.ts +4 -1
- package/src/AWS/ELBv2/TargetGroup.ts +4 -1
- package/src/AWS/EventBridge/EventBus.ts +4 -1
- package/src/AWS/EventBridge/Permission.ts +4 -1
- package/src/AWS/EventBridge/Rule.ts +4 -1
- package/src/AWS/IAM/AccessKey.ts +4 -1
- package/src/AWS/IAM/AccountAlias.ts +4 -1
- package/src/AWS/IAM/AccountPasswordPolicy.ts +4 -1
- package/src/AWS/IAM/Group.ts +4 -1
- package/src/AWS/IAM/GroupMembership.ts +4 -1
- package/src/AWS/IAM/InstanceProfile.ts +4 -1
- package/src/AWS/IAM/LoginProfile.ts +4 -1
- package/src/AWS/IAM/OpenIDConnectProvider.ts +4 -1
- package/src/AWS/IAM/Policy.ts +4 -1
- package/src/AWS/IAM/Role.ts +4 -1
- package/src/AWS/IAM/SAMLProvider.ts +4 -1
- package/src/AWS/IAM/SSHPublicKey.ts +4 -1
- package/src/AWS/IAM/ServerCertificate.ts +4 -1
- package/src/AWS/IAM/ServiceSpecificCredential.ts +4 -1
- package/src/AWS/IAM/SigningCertificate.ts +4 -1
- package/src/AWS/IAM/User.ts +4 -1
- package/src/AWS/IAM/VirtualMFADevice.ts +4 -1
- package/src/AWS/IdentityCenter/AccountAssignment.ts +4 -1
- package/src/AWS/IdentityCenter/Group.ts +4 -1
- package/src/AWS/IdentityCenter/Instance.ts +4 -1
- package/src/AWS/IdentityCenter/PermissionSet.ts +4 -1
- package/src/AWS/Kinesis/Stream.ts +4 -1
- package/src/AWS/Kinesis/StreamConsumer.ts +4 -1
- package/src/AWS/Lambda/EventSourceMapping.ts +4 -1
- package/src/AWS/Lambda/Function.ts +3 -1
- package/src/AWS/Lambda/Permission.ts +4 -1
- package/src/AWS/Logs/LogGroup.ts +4 -1
- package/src/AWS/Organizations/Account.ts +4 -1
- package/src/AWS/Organizations/DelegatedAdministrator.ts +4 -1
- package/src/AWS/Organizations/Organization.ts +4 -1
- package/src/AWS/Organizations/OrganizationResourcePolicy.ts +4 -1
- package/src/AWS/Organizations/OrganizationalUnit.ts +4 -1
- package/src/AWS/Organizations/Policy.ts +4 -1
- package/src/AWS/Organizations/PolicyAttachment.ts +4 -1
- package/src/AWS/Organizations/Root.ts +4 -1
- package/src/AWS/Organizations/RootPolicyType.ts +4 -1
- package/src/AWS/Organizations/TrustedServiceAccess.ts +4 -1
- package/src/AWS/Providers.ts +484 -438
- package/src/AWS/RDS/DBCluster.ts +4 -1
- package/src/AWS/RDS/DBClusterEndpoint.ts +4 -1
- package/src/AWS/RDS/DBClusterParameterGroup.ts +4 -1
- package/src/AWS/RDS/DBInstance.ts +4 -1
- package/src/AWS/RDS/DBParameterGroup.ts +4 -1
- package/src/AWS/RDS/DBProxy.ts +4 -1
- package/src/AWS/RDS/DBProxyEndpoint.ts +4 -1
- package/src/AWS/RDS/DBProxyTargetGroup.ts +4 -1
- package/src/AWS/RDS/DBSubnetGroup.ts +4 -1
- package/src/AWS/Route53/Record.ts +4 -1
- package/src/AWS/S3/Bucket.ts +3 -1
- package/src/AWS/SNS/Subscription.ts +4 -1
- package/src/AWS/SNS/Topic.ts +4 -1
- package/src/AWS/SQS/Queue.ts +3 -1
- package/src/AWS/Scheduler/Schedule.ts +4 -1
- package/src/AWS/Scheduler/ScheduleGroup.ts +4 -1
- package/src/AWS/SecretsManager/Secret.ts +4 -1
- package/src/AWS/Website/AssetDeployment.ts +4 -1
- package/src/Apply.ts +3 -3
- package/src/Binding.ts +18 -17
- package/src/Cloudflare/Container/ContainerApplication.ts +3 -1
- package/src/Cloudflare/D1/D1Database.ts +4 -1
- package/src/Cloudflare/KV/KVNamespace.ts +7 -4
- package/src/Cloudflare/Providers.ts +62 -60
- package/src/Cloudflare/R2/R2Bucket.ts +4 -1
- package/src/Cloudflare/R2/R2BucketBinding.ts +26 -31
- package/src/Cloudflare/Workers/Worker.ts +3 -1
- package/src/Cloudflare/Workers/Workflow.ts +3 -1
- package/src/Plan.ts +595 -596
- package/src/Platform.ts +9 -8
- package/src/Provider.ts +145 -23
- package/src/Resource.ts +10 -3
- package/src/Stack.ts +1 -1
- package/src/Util/service.ts +1 -1
- package/src/index.ts +10 -7
package/src/Plan.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as Data from "effect/Data";
|
|
2
2
|
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Option from "effect/Option";
|
|
3
4
|
import { asEffect } from ".//Util/types.ts";
|
|
4
5
|
import {
|
|
5
6
|
Artifacts,
|
|
@@ -8,7 +9,6 @@ import {
|
|
|
8
9
|
ensureArtifactStore,
|
|
9
10
|
makeScopedArtifacts,
|
|
10
11
|
} from "./Artifacts.ts";
|
|
11
|
-
import * as Option from "effect/Option";
|
|
12
12
|
import {
|
|
13
13
|
diffBindings,
|
|
14
14
|
havePropsChanged,
|
|
@@ -19,7 +19,7 @@ import { parseFqn } from "./FQN.ts";
|
|
|
19
19
|
import { InstanceId } from "./InstanceId.ts";
|
|
20
20
|
import * as Output from "./Output.ts";
|
|
21
21
|
import {
|
|
22
|
-
|
|
22
|
+
findProviderByType,
|
|
23
23
|
Provider,
|
|
24
24
|
type ProviderService,
|
|
25
25
|
} from "./Provider.ts";
|
|
@@ -157,653 +157,652 @@ export const make = <A>(
|
|
|
157
157
|
options: MakePlanOptions = {},
|
|
158
158
|
): Effect.Effect<Plan<A>, never, State> =>
|
|
159
159
|
// @ts-expect-error
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const props = yield* resolveInput(resource.Props);
|
|
195
|
-
const oldState = yield* state.get({
|
|
196
|
-
stack: stackName,
|
|
197
|
-
stage: stage,
|
|
198
|
-
fqn: resource.FQN,
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
if (!oldState || oldState.status === "creating") {
|
|
202
|
-
return resourceExpr;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const oldProps =
|
|
206
|
-
oldState.status === "updating"
|
|
207
|
-
? oldState.old.props
|
|
208
|
-
: oldState.props;
|
|
209
|
-
|
|
210
|
-
const oldBindings = oldState.bindings ?? [];
|
|
211
|
-
const newBindings = stack.bindings[resource.FQN] ?? [];
|
|
212
|
-
|
|
213
|
-
const diff = yield* provider.diff
|
|
214
|
-
? provider
|
|
215
|
-
.diff({
|
|
216
|
-
id: resource.LogicalId,
|
|
217
|
-
olds: oldProps,
|
|
218
|
-
instanceId: oldState.instanceId,
|
|
219
|
-
news: props,
|
|
220
|
-
output: oldState.attr,
|
|
221
|
-
oldBindings,
|
|
222
|
-
newBindings,
|
|
223
|
-
})
|
|
224
|
-
.pipe(providePlanScope(resource.FQN, oldState.instanceId))
|
|
225
|
-
: Effect.succeed(undefined);
|
|
226
|
-
|
|
227
|
-
const stables: string[] = [
|
|
228
|
-
...(provider.stables ?? []),
|
|
229
|
-
...(diff?.stables ?? []),
|
|
230
|
-
];
|
|
231
|
-
|
|
232
|
-
const withStables = (output: any) =>
|
|
233
|
-
stables.length > 0
|
|
234
|
-
? new Output.ResourceExpr(
|
|
235
|
-
resourceExpr.src,
|
|
236
|
-
Object.fromEntries(
|
|
237
|
-
stables.map((stable) => [stable, output?.[stable]]),
|
|
238
|
-
),
|
|
239
|
-
)
|
|
240
|
-
: // if there are no stable properties, treat every property as changed
|
|
241
|
-
resourceExpr;
|
|
242
|
-
|
|
243
|
-
if (diff == null) {
|
|
244
|
-
if (havePropsChanged(oldProps, props)) {
|
|
245
|
-
// the props have changed but the provider did not provide any hints as to what is stable
|
|
246
|
-
// so we must assume everything has changed
|
|
247
|
-
return withStables(oldState?.attr);
|
|
248
|
-
}
|
|
249
|
-
} else if (diff.action === "update") {
|
|
250
|
-
return withStables(oldState?.attr);
|
|
251
|
-
} else if (diff.action === "replace") {
|
|
252
|
-
return resourceExpr;
|
|
253
|
-
}
|
|
254
|
-
if (
|
|
255
|
-
oldState.status === "created" ||
|
|
256
|
-
oldState.status === "updated" ||
|
|
257
|
-
oldState.status === "replaced"
|
|
258
|
-
) {
|
|
259
|
-
// we can safely return the attributes if we know they have stabilized
|
|
260
|
-
return oldState?.attr;
|
|
261
|
-
} else {
|
|
262
|
-
// we must assume the resource doesn't exist if it hasn't stabilized
|
|
263
|
-
return resourceExpr;
|
|
264
|
-
}
|
|
265
|
-
}),
|
|
266
|
-
));
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
const resolveInput = (input: any): Effect.Effect<any> =>
|
|
270
|
-
Effect.gen(function* () {
|
|
271
|
-
if (!input) {
|
|
272
|
-
return input;
|
|
273
|
-
} else if (Output.isExpr(input)) {
|
|
274
|
-
return yield* resolveOutput(input);
|
|
275
|
-
} else if (Array.isArray(input)) {
|
|
276
|
-
return yield* Effect.all(input.map(resolveInput), {
|
|
277
|
-
concurrency: "unbounded",
|
|
278
|
-
});
|
|
279
|
-
} else if (isResource(input)) {
|
|
280
|
-
// Resource objects have dynamic properties (path, hash, etc.) that are
|
|
281
|
-
// created on-demand by a Proxy getter and aren't enumerable via Object.entries.
|
|
282
|
-
// Resolve the ResourceExpr to get the actual resource output, then continue
|
|
283
|
-
// resolving any nested outputs in the result.
|
|
284
|
-
const resourceExpr = Output.of(input);
|
|
285
|
-
const resolved = yield* resolveOutput(resourceExpr);
|
|
286
|
-
return yield* resolveInput(resolved);
|
|
287
|
-
} else if (typeof input === "object") {
|
|
288
|
-
return Object.fromEntries(
|
|
289
|
-
yield* Effect.all(
|
|
290
|
-
Object.entries(input).map(([key, value]) =>
|
|
291
|
-
resolveInput(value).pipe(Effect.map((value) => [key, value])),
|
|
292
|
-
),
|
|
293
|
-
{ concurrency: "unbounded" },
|
|
294
|
-
),
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
return input;
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
const resolveOutput = (expr: Output.Expr<any>): Effect.Effect<any> =>
|
|
301
|
-
Effect.gen(function* () {
|
|
302
|
-
if (Output.isResourceExpr(expr)) {
|
|
303
|
-
return yield* resolveResource(expr);
|
|
304
|
-
} else if (Output.isPropExpr(expr)) {
|
|
305
|
-
const upstream = yield* resolveOutput(expr.expr);
|
|
306
|
-
return upstream?.[expr.identifier];
|
|
307
|
-
} else if (Output.isApplyExpr(expr)) {
|
|
308
|
-
const upstream = yield* resolveOutput(expr.expr);
|
|
309
|
-
return Output.hasOutputs(upstream) ? expr : expr.f(upstream);
|
|
310
|
-
} else if (Output.isEffectExpr(expr)) {
|
|
311
|
-
const upstream = yield* resolveOutput(expr.expr);
|
|
312
|
-
return Output.hasOutputs(upstream) ? expr : yield* expr.f(upstream);
|
|
313
|
-
} else if (Output.isAllExpr(expr)) {
|
|
314
|
-
return yield* Effect.all(expr.outs.map(resolveOutput), {
|
|
315
|
-
concurrency: "unbounded",
|
|
316
|
-
});
|
|
317
|
-
} else if (Output.isLiteralExpr(expr)) {
|
|
318
|
-
return expr.value;
|
|
319
|
-
}
|
|
320
|
-
return yield* Effect.die(new Error("Not implemented yet"));
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
// map of resource FQN -> its downstream dependencies (resources that depend on it)
|
|
324
|
-
const oldDownstreamDependencies: {
|
|
325
|
-
[fqn: string]: string[];
|
|
326
|
-
} = Object.fromEntries(
|
|
327
|
-
oldResources
|
|
328
|
-
.filter((resource) => !!resource)
|
|
329
|
-
.map((resource) => [resource.fqn, resource.downstream]),
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
// Build a set of FQNs for the new resources to detect orphans
|
|
333
|
-
const newResourceFqns = new Set(resources.map((r) => r.FQN));
|
|
334
|
-
|
|
335
|
-
// Map FQN -> list of upstream FQNs (resources this one depends on via props)
|
|
336
|
-
const newUpstreamDependencies: {
|
|
337
|
-
[fqn: string]: string[];
|
|
338
|
-
} = Object.fromEntries(
|
|
339
|
-
resources.map((resource) => [
|
|
340
|
-
resource.FQN,
|
|
341
|
-
Object.values(Output.upstreamAny(resource.Props)).map((r) => r.FQN),
|
|
342
|
-
]),
|
|
343
|
-
);
|
|
344
|
-
|
|
345
|
-
// Map FQN -> list of upstream FQNs from bindings
|
|
346
|
-
const bindingUpstreamDependencies: {
|
|
347
|
-
[fqn: string]: string[];
|
|
348
|
-
} = Object.fromEntries(
|
|
349
|
-
resources.map((resource) => [
|
|
350
|
-
resource.FQN,
|
|
351
|
-
Object.values(
|
|
352
|
-
Output.upstreamAny(stack.bindings[resource.FQN] ?? []),
|
|
353
|
-
).map((r) => r.FQN),
|
|
354
|
-
]),
|
|
355
|
-
);
|
|
356
|
-
|
|
357
|
-
// Combined prop + binding upstream for the desired graph, including
|
|
358
|
-
// references to resources outside the current graph so delete validation can
|
|
359
|
-
// tell whether any surviving resource still points at an orphan.
|
|
360
|
-
const rawUpstreamDependencies: {
|
|
361
|
-
[fqn: string]: string[];
|
|
362
|
-
} = Object.fromEntries(
|
|
363
|
-
resources.map((resource) => {
|
|
364
|
-
const fqn = resource.FQN;
|
|
365
|
-
const propDeps = newUpstreamDependencies[fqn] ?? [];
|
|
366
|
-
const bindDeps = bindingUpstreamDependencies[fqn] ?? [];
|
|
367
|
-
return [fqn, [...new Set([...propDeps, ...bindDeps])]];
|
|
368
|
-
}),
|
|
369
|
-
);
|
|
370
|
-
|
|
371
|
-
// Combined prop + binding upstream, filtered to resources in this graph for
|
|
372
|
-
// scheduling and cycle detection.
|
|
373
|
-
const allUpstreamDependencies: {
|
|
374
|
-
[fqn: string]: string[];
|
|
375
|
-
} = Object.fromEntries(
|
|
376
|
-
resources.map((resource) => {
|
|
377
|
-
const fqn = resource.FQN;
|
|
378
|
-
const deps = rawUpstreamDependencies[fqn] ?? [];
|
|
379
|
-
return [fqn, deps.filter((dep) => newResourceFqns.has(dep))];
|
|
380
|
-
}),
|
|
381
|
-
);
|
|
382
|
-
|
|
383
|
-
// Map FQN -> list of downstream FQNs (resources that depend on this one)
|
|
384
|
-
const newDownstreamDependencies: {
|
|
385
|
-
[fqn: string]: string[];
|
|
386
|
-
} = Object.fromEntries(
|
|
387
|
-
resources.map((resource) => [
|
|
388
|
-
resource.FQN,
|
|
389
|
-
Object.entries(newUpstreamDependencies)
|
|
390
|
-
.filter(([_, upstream]) => upstream.includes(resource.FQN))
|
|
391
|
-
.map(([depFqn]) => depFqn),
|
|
392
|
-
]),
|
|
393
|
-
);
|
|
394
|
-
|
|
395
|
-
const resourceGraph = Object.fromEntries(
|
|
396
|
-
(yield* Effect.all(
|
|
397
|
-
resources.map(
|
|
398
|
-
Effect.fn(function* (resource) {
|
|
399
|
-
const provider = yield* Provider(resource.Type);
|
|
400
|
-
const id = resource.LogicalId;
|
|
401
|
-
const fqn = resource.FQN;
|
|
402
|
-
const news = yield* resolveInput(resource.Props);
|
|
403
|
-
const downstream = newDownstreamDependencies[fqn] ?? [];
|
|
404
|
-
|
|
405
|
-
const newBindings: ResourceBinding[] = yield* resolveInput(
|
|
406
|
-
stack.bindings[fqn] ?? [],
|
|
407
|
-
);
|
|
160
|
+
Effect.gen(function* () {
|
|
161
|
+
const state = yield* State;
|
|
162
|
+
|
|
163
|
+
const resources = Object.values(stack.resources);
|
|
164
|
+
|
|
165
|
+
// TODO(sam): rename terminology to Stack
|
|
166
|
+
const stackName = stack.name;
|
|
167
|
+
const stage = stack.stage;
|
|
168
|
+
|
|
169
|
+
const resourceFqns = yield* state.list({
|
|
170
|
+
stack: stackName,
|
|
171
|
+
stage: stage,
|
|
172
|
+
});
|
|
173
|
+
const oldResources = yield* Effect.all(
|
|
174
|
+
resourceFqns.map((fqn) =>
|
|
175
|
+
state.get({ stack: stackName, stage: stage, fqn }),
|
|
176
|
+
),
|
|
177
|
+
{ concurrency: "unbounded" },
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const resolvedResources: Record<string, Effect.Effect<any>> = {};
|
|
181
|
+
|
|
182
|
+
const resolveResource = (
|
|
183
|
+
resourceExpr: Output.ResourceExpr<any, any>,
|
|
184
|
+
): Effect.Effect<any> =>
|
|
185
|
+
Effect.gen(function* () {
|
|
186
|
+
// @ts-expect-error
|
|
187
|
+
return yield* (resolvedResources[resourceExpr.src.FQN] ??=
|
|
188
|
+
yield* Effect.cached(
|
|
189
|
+
Effect.gen(function* () {
|
|
190
|
+
const resource = resourceExpr.src;
|
|
191
|
+
|
|
192
|
+
const provider = yield* findProviderByType(resource.Type);
|
|
193
|
+
const props = yield* resolveInput(resource.Props);
|
|
408
194
|
const oldState = yield* state.get({
|
|
409
195
|
stack: stackName,
|
|
410
196
|
stage: stage,
|
|
411
|
-
fqn,
|
|
197
|
+
fqn: resource.FQN,
|
|
412
198
|
});
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
// hoped to reach last time. Recovery is expressed by mapping each
|
|
432
|
-
// intermediate state back onto a fresh CRUD action.
|
|
433
|
-
if (oldState === undefined) {
|
|
434
|
-
return Node<Create>({
|
|
435
|
-
action: "create",
|
|
436
|
-
props: news,
|
|
437
|
-
state: oldState,
|
|
438
|
-
});
|
|
439
|
-
} else if (
|
|
440
|
-
oldState.status === "creating" &&
|
|
441
|
-
oldState.attr === undefined
|
|
442
|
-
) {
|
|
443
|
-
// A create may have succeeded before state persistence failed. If the
|
|
444
|
-
// provider can recover an attribute snapshot, keep driving the same
|
|
445
|
-
// create instead of starting over blindly.
|
|
446
|
-
if (provider.read) {
|
|
447
|
-
const attr = yield* provider
|
|
448
|
-
.read({
|
|
449
|
-
id,
|
|
199
|
+
|
|
200
|
+
if (!oldState || oldState.status === "creating") {
|
|
201
|
+
return resourceExpr;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const oldProps =
|
|
205
|
+
oldState.status === "updating"
|
|
206
|
+
? oldState.old.props
|
|
207
|
+
: oldState.props;
|
|
208
|
+
|
|
209
|
+
const oldBindings = oldState.bindings ?? [];
|
|
210
|
+
const newBindings = stack.bindings[resource.FQN] ?? [];
|
|
211
|
+
|
|
212
|
+
const diff = yield* provider.diff
|
|
213
|
+
? provider
|
|
214
|
+
.diff({
|
|
215
|
+
id: resource.LogicalId,
|
|
216
|
+
olds: oldProps,
|
|
450
217
|
instanceId: oldState.instanceId,
|
|
451
|
-
|
|
218
|
+
news: props,
|
|
452
219
|
output: oldState.attr,
|
|
220
|
+
oldBindings,
|
|
221
|
+
newBindings,
|
|
453
222
|
})
|
|
454
|
-
.pipe(providePlanScope(
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
223
|
+
.pipe(providePlanScope(resource.FQN, oldState.instanceId))
|
|
224
|
+
: Effect.succeed(undefined);
|
|
225
|
+
|
|
226
|
+
const stables: string[] = [
|
|
227
|
+
...(provider.stables ?? []),
|
|
228
|
+
...(diff?.stables ?? []),
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
const withStables = (output: any) =>
|
|
232
|
+
stables.length > 0
|
|
233
|
+
? new Output.ResourceExpr(
|
|
234
|
+
resourceExpr.src,
|
|
235
|
+
Object.fromEntries(
|
|
236
|
+
stables.map((stable) => [stable, output?.[stable]]),
|
|
237
|
+
),
|
|
238
|
+
)
|
|
239
|
+
: // if there are no stable properties, treat every property as changed
|
|
240
|
+
resourceExpr;
|
|
241
|
+
|
|
242
|
+
if (diff == null) {
|
|
243
|
+
if (havePropsChanged(oldProps, props)) {
|
|
244
|
+
// the props have changed but the provider did not provide any hints as to what is stable
|
|
245
|
+
// so we must assume everything has changed
|
|
246
|
+
return withStables(oldState?.attr);
|
|
462
247
|
}
|
|
248
|
+
} else if (diff.action === "update") {
|
|
249
|
+
return withStables(oldState?.attr);
|
|
250
|
+
} else if (diff.action === "replace") {
|
|
251
|
+
return resourceExpr;
|
|
252
|
+
}
|
|
253
|
+
if (
|
|
254
|
+
oldState.status === "created" ||
|
|
255
|
+
oldState.status === "updated" ||
|
|
256
|
+
oldState.status === "replaced"
|
|
257
|
+
) {
|
|
258
|
+
// we can safely return the attributes if we know they have stabilized
|
|
259
|
+
return oldState?.attr;
|
|
260
|
+
} else {
|
|
261
|
+
// we must assume the resource doesn't exist if it hasn't stabilized
|
|
262
|
+
return resourceExpr;
|
|
463
263
|
}
|
|
264
|
+
}),
|
|
265
|
+
));
|
|
266
|
+
});
|
|
464
267
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
268
|
+
const resolveInput = (input: any): Effect.Effect<any> =>
|
|
269
|
+
Effect.gen(function* () {
|
|
270
|
+
if (!input) {
|
|
271
|
+
return input;
|
|
272
|
+
} else if (Output.isExpr(input)) {
|
|
273
|
+
return yield* resolveOutput(input);
|
|
274
|
+
} else if (Array.isArray(input)) {
|
|
275
|
+
return yield* Effect.all(input.map(resolveInput), {
|
|
276
|
+
concurrency: "unbounded",
|
|
277
|
+
});
|
|
278
|
+
} else if (isResource(input)) {
|
|
279
|
+
// Resource objects have dynamic properties (path, hash, etc.) that are
|
|
280
|
+
// created on-demand by a Proxy getter and aren't enumerable via Object.entries.
|
|
281
|
+
// Resolve the ResourceExpr to get the actual resource output, then continue
|
|
282
|
+
// resolving any nested outputs in the result.
|
|
283
|
+
const resourceExpr = Output.of(input);
|
|
284
|
+
const resolved = yield* resolveOutput(resourceExpr);
|
|
285
|
+
return yield* resolveInput(resolved);
|
|
286
|
+
} else if (typeof input === "object") {
|
|
287
|
+
return Object.fromEntries(
|
|
288
|
+
yield* Effect.all(
|
|
289
|
+
Object.entries(input).map(([key, value]) =>
|
|
290
|
+
resolveInput(value).pipe(Effect.map((value) => [key, value])),
|
|
291
|
+
),
|
|
292
|
+
{ concurrency: "unbounded" },
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
return input;
|
|
297
|
+
});
|
|
469
298
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
299
|
+
const resolveOutput = (expr: Output.Expr<any>): Effect.Effect<any> =>
|
|
300
|
+
Effect.gen(function* () {
|
|
301
|
+
if (Output.isResourceExpr(expr)) {
|
|
302
|
+
return yield* resolveResource(expr);
|
|
303
|
+
} else if (Output.isPropExpr(expr)) {
|
|
304
|
+
const upstream = yield* resolveOutput(expr.expr);
|
|
305
|
+
return upstream?.[expr.identifier];
|
|
306
|
+
} else if (Output.isApplyExpr(expr)) {
|
|
307
|
+
const upstream = yield* resolveOutput(expr.expr);
|
|
308
|
+
return Output.hasOutputs(upstream) ? expr : expr.f(upstream);
|
|
309
|
+
} else if (Output.isEffectExpr(expr)) {
|
|
310
|
+
const upstream = yield* resolveOutput(expr.expr);
|
|
311
|
+
return Output.hasOutputs(upstream) ? expr : yield* expr.f(upstream);
|
|
312
|
+
} else if (Output.isAllExpr(expr)) {
|
|
313
|
+
return yield* Effect.all(expr.outs.map(resolveOutput), {
|
|
314
|
+
concurrency: "unbounded",
|
|
315
|
+
});
|
|
316
|
+
} else if (Output.isLiteralExpr(expr)) {
|
|
317
|
+
return expr.value;
|
|
318
|
+
}
|
|
319
|
+
return yield* Effect.die(new Error("Not implemented yet"));
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// map of resource FQN -> its downstream dependencies (resources that depend on it)
|
|
323
|
+
const oldDownstreamDependencies: {
|
|
324
|
+
[fqn: string]: string[];
|
|
325
|
+
} = Object.fromEntries(
|
|
326
|
+
oldResources
|
|
327
|
+
.filter((resource) => !!resource)
|
|
328
|
+
.map((resource) => [resource.fqn, resource.downstream]),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// Build a set of FQNs for the new resources to detect orphans
|
|
332
|
+
const newResourceFqns = new Set(resources.map((r) => r.FQN));
|
|
333
|
+
|
|
334
|
+
// Map FQN -> list of upstream FQNs (resources this one depends on via props)
|
|
335
|
+
const newUpstreamDependencies: {
|
|
336
|
+
[fqn: string]: string[];
|
|
337
|
+
} = Object.fromEntries(
|
|
338
|
+
resources.map((resource) => [
|
|
339
|
+
resource.FQN,
|
|
340
|
+
Object.values(Output.upstreamAny(resource.Props)).map((r) => r.FQN),
|
|
341
|
+
]),
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
// Map FQN -> list of upstream FQNs from bindings
|
|
345
|
+
const bindingUpstreamDependencies: {
|
|
346
|
+
[fqn: string]: string[];
|
|
347
|
+
} = Object.fromEntries(
|
|
348
|
+
resources.map((resource) => [
|
|
349
|
+
resource.FQN,
|
|
350
|
+
Object.values(
|
|
351
|
+
Output.upstreamAny(stack.bindings[resource.FQN] ?? []),
|
|
352
|
+
).map((r) => r.FQN),
|
|
353
|
+
]),
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// Combined prop + binding upstream for the desired graph, including
|
|
357
|
+
// references to resources outside the current graph so delete validation can
|
|
358
|
+
// tell whether any surviving resource still points at an orphan.
|
|
359
|
+
const rawUpstreamDependencies: {
|
|
360
|
+
[fqn: string]: string[];
|
|
361
|
+
} = Object.fromEntries(
|
|
362
|
+
resources.map((resource) => {
|
|
363
|
+
const fqn = resource.FQN;
|
|
364
|
+
const propDeps = newUpstreamDependencies[fqn] ?? [];
|
|
365
|
+
const bindDeps = bindingUpstreamDependencies[fqn] ?? [];
|
|
366
|
+
return [fqn, [...new Set([...propDeps, ...bindDeps])]];
|
|
367
|
+
}),
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
// Combined prop + binding upstream, filtered to resources in this graph for
|
|
371
|
+
// scheduling and cycle detection.
|
|
372
|
+
const allUpstreamDependencies: {
|
|
373
|
+
[fqn: string]: string[];
|
|
374
|
+
} = Object.fromEntries(
|
|
375
|
+
resources.map((resource) => {
|
|
376
|
+
const fqn = resource.FQN;
|
|
377
|
+
const deps = rawUpstreamDependencies[fqn] ?? [];
|
|
378
|
+
return [fqn, deps.filter((dep) => newResourceFqns.has(dep))];
|
|
379
|
+
}),
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
// Map FQN -> list of downstream FQNs (resources that depend on this one)
|
|
383
|
+
const newDownstreamDependencies: {
|
|
384
|
+
[fqn: string]: string[];
|
|
385
|
+
} = Object.fromEntries(
|
|
386
|
+
resources.map((resource) => [
|
|
387
|
+
resource.FQN,
|
|
388
|
+
Object.entries(newUpstreamDependencies)
|
|
389
|
+
.filter(([_, upstream]) => upstream.includes(resource.FQN))
|
|
390
|
+
.map(([depFqn]) => depFqn),
|
|
391
|
+
]),
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
const resourceGraph = Object.fromEntries(
|
|
395
|
+
(yield* Effect.all(
|
|
396
|
+
resources.map(
|
|
397
|
+
Effect.fn(function* (resource) {
|
|
398
|
+
const provider = yield* findProviderByType(resource.Type);
|
|
399
|
+
const id = resource.LogicalId;
|
|
400
|
+
const fqn = resource.FQN;
|
|
401
|
+
const news = yield* resolveInput(resource.Props);
|
|
402
|
+
const downstream = newDownstreamDependencies[fqn] ?? [];
|
|
403
|
+
|
|
404
|
+
const newBindings: ResourceBinding[] = yield* resolveInput(
|
|
405
|
+
stack.bindings[fqn] ?? [],
|
|
406
|
+
);
|
|
407
|
+
const oldState = yield* state.get({
|
|
408
|
+
stack: stackName,
|
|
409
|
+
stage: stage,
|
|
410
|
+
fqn,
|
|
411
|
+
});
|
|
412
|
+
const oldBindings = oldState?.bindings ?? [];
|
|
413
|
+
const bindingDiffs = diffBindings(oldBindings, newBindings);
|
|
414
|
+
|
|
415
|
+
const Node = <T extends Apply>(
|
|
416
|
+
node: Omit<
|
|
417
|
+
T,
|
|
418
|
+
"provider" | "resource" | "bindings" | "downstream"
|
|
419
|
+
>,
|
|
420
|
+
) =>
|
|
421
|
+
({
|
|
422
|
+
...node,
|
|
423
|
+
provider,
|
|
424
|
+
resource,
|
|
425
|
+
bindings: bindingDiffs,
|
|
426
|
+
downstream,
|
|
427
|
+
}) as any as T;
|
|
428
|
+
|
|
429
|
+
// Plan against the persisted state we have, not the ideal final state we
|
|
430
|
+
// hoped to reach last time. Recovery is expressed by mapping each
|
|
431
|
+
// intermediate state back onto a fresh CRUD action.
|
|
432
|
+
if (oldState === undefined) {
|
|
433
|
+
return Node<Create>({
|
|
434
|
+
action: "create",
|
|
435
|
+
props: news,
|
|
436
|
+
state: oldState,
|
|
437
|
+
});
|
|
438
|
+
} else if (
|
|
439
|
+
oldState.status === "creating" &&
|
|
440
|
+
oldState.attr === undefined
|
|
441
|
+
) {
|
|
442
|
+
// A create may have succeeded before state persistence failed. If the
|
|
443
|
+
// provider can recover an attribute snapshot, keep driving the same
|
|
444
|
+
// create instead of starting over blindly.
|
|
445
|
+
if (provider.read) {
|
|
446
|
+
const attr = yield* provider
|
|
447
|
+
.read({
|
|
473
448
|
id,
|
|
474
|
-
olds: oldProps,
|
|
475
449
|
instanceId: oldState.instanceId,
|
|
450
|
+
olds: oldState.props,
|
|
476
451
|
output: oldState.attr,
|
|
477
|
-
news,
|
|
478
|
-
oldBindings,
|
|
479
|
-
newBindings,
|
|
480
452
|
})
|
|
481
|
-
.pipe(providePlanScope(fqn, oldState.instanceId))
|
|
482
|
-
|
|
483
|
-
Effect.map(
|
|
484
|
-
(diff) =>
|
|
485
|
-
diff ??
|
|
486
|
-
({
|
|
487
|
-
action:
|
|
488
|
-
havePropsChanged(oldProps, news) ||
|
|
489
|
-
bindingDiffs.some((b) => b.action !== "noop")
|
|
490
|
-
? "update"
|
|
491
|
-
: "noop",
|
|
492
|
-
} as UpdateDiff | NoopDiff),
|
|
493
|
-
),
|
|
494
|
-
Effect.map((diff) =>
|
|
495
|
-
options.force && diff.action === "noop"
|
|
496
|
-
? ({
|
|
497
|
-
action: "update",
|
|
498
|
-
} satisfies UpdateDiff)
|
|
499
|
-
: diff,
|
|
500
|
-
),
|
|
501
|
-
);
|
|
502
|
-
|
|
503
|
-
if (oldState.status === "creating") {
|
|
504
|
-
if (diff.action === "noop") {
|
|
505
|
-
// we're in the creating state and props are un-changed
|
|
506
|
-
// let's just continue where we left off
|
|
507
|
-
return Node<Create>({
|
|
508
|
-
action: "create",
|
|
509
|
-
props: news,
|
|
510
|
-
state: oldState,
|
|
511
|
-
});
|
|
512
|
-
} else if (diff.action === "update") {
|
|
513
|
-
// props have changed in a way that is updatable
|
|
514
|
-
// again, just continue with the create
|
|
515
|
-
// TODO(sam): should we maybe try an update instead?
|
|
453
|
+
.pipe(providePlanScope(fqn, oldState.instanceId));
|
|
454
|
+
if (attr) {
|
|
516
455
|
return Node<Create>({
|
|
517
456
|
action: "create",
|
|
518
457
|
props: news,
|
|
519
|
-
state: oldState,
|
|
520
|
-
});
|
|
521
|
-
} else {
|
|
522
|
-
// props have changed in an incompatible way
|
|
523
|
-
// because it's possible that an un-updatable resource has already been created
|
|
524
|
-
// we must use a replace step to create a new one and delete the potential old one
|
|
525
|
-
return Node<Replace>({
|
|
526
|
-
action: "replace",
|
|
527
|
-
props: news,
|
|
528
|
-
deleteFirst: diff.deleteFirst ?? false,
|
|
529
|
-
state: oldState,
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
} else if (oldState.status === "updating") {
|
|
533
|
-
// Updating already targets the live resource, so noop/update both mean
|
|
534
|
-
// "finish the interrupted update". Only a replace diff escalates it
|
|
535
|
-
// into a fresh replacement.
|
|
536
|
-
if (diff.action === "update" || diff.action === "noop") {
|
|
537
|
-
// we can continue where we left off
|
|
538
|
-
return Node<Update>({
|
|
539
|
-
action: "update",
|
|
540
|
-
props: news,
|
|
541
|
-
state: oldState,
|
|
542
|
-
});
|
|
543
|
-
} else {
|
|
544
|
-
// we started to update a resource but now believe we should replace it
|
|
545
|
-
return Node<Replace>({
|
|
546
|
-
action: "replace",
|
|
547
|
-
deleteFirst: diff.deleteFirst ?? false,
|
|
548
|
-
props: news,
|
|
549
|
-
// TODO(sam): can Apply handle replacements when the oldState is UpdatingResourceState?
|
|
550
|
-
// -> or should we do a provider.read to try and reconcile back to UpdatedResourceState?
|
|
551
|
-
state: oldState,
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
} else if (oldState.status === "replacing") {
|
|
555
|
-
// The replacement candidate is still being created. Noop/update keep
|
|
556
|
-
// driving the same generation; replace means that candidate itself is
|
|
557
|
-
// now obsolete and must be wrapped in a new outer generation.
|
|
558
|
-
if (diff.action === "noop") {
|
|
559
|
-
// this is the stable case - noop means just continue with the replacement
|
|
560
|
-
return Node<Replace>({
|
|
561
|
-
action: "replace",
|
|
562
|
-
deleteFirst: oldState.deleteFirst,
|
|
563
|
-
props: news,
|
|
564
|
-
state: oldState,
|
|
565
|
-
});
|
|
566
|
-
} else if (diff.action === "update") {
|
|
567
|
-
// potential problem here - the props have changed since we tried to replace,
|
|
568
|
-
// but not enough to trigger another replacement. the resource provider should
|
|
569
|
-
// be designed as idempotent to converge to the right state when creating the new resource
|
|
570
|
-
// the newly generated instanceId is intended to assist with this
|
|
571
|
-
return Node<Replace>({
|
|
572
|
-
action: "replace",
|
|
573
|
-
deleteFirst: oldState.deleteFirst,
|
|
574
|
-
props: news,
|
|
575
|
-
state: oldState,
|
|
576
|
-
});
|
|
577
|
-
} else {
|
|
578
|
-
// The in-flight replacement candidate itself now needs replacement.
|
|
579
|
-
// Mark this as a restart so Apply creates a fresh generation instead
|
|
580
|
-
// of resuming the old replacement instance.
|
|
581
|
-
return Node<Replace>({
|
|
582
|
-
restart: true,
|
|
583
|
-
action: "replace",
|
|
584
|
-
deleteFirst: diff.deleteFirst ?? oldState.deleteFirst,
|
|
585
|
-
props: news,
|
|
586
|
-
state: oldState,
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
} else if (oldState.status === "replaced") {
|
|
590
|
-
// The new resource already exists. Noop means "just let GC finish",
|
|
591
|
-
// update means "mutate the current replacement before GC finishes",
|
|
592
|
-
// and replace means "the current replacement also became obsolete".
|
|
593
|
-
if (diff.action === "noop") {
|
|
594
|
-
// this is the stable case - noop means just continue cleaning up the replacement
|
|
595
|
-
return Node<Replace>({
|
|
596
|
-
action: "replace",
|
|
597
|
-
deleteFirst: oldState.deleteFirst,
|
|
598
|
-
props: news,
|
|
599
|
-
state: oldState,
|
|
600
|
-
});
|
|
601
|
-
} else if (diff.action === "update") {
|
|
602
|
-
// the replacement has been created but now also needs to be updated
|
|
603
|
-
// the resource provider should:
|
|
604
|
-
// 1. Update the newly created replacement resource
|
|
605
|
-
// 2. Then proceed as normal to delete the replaced resources (after all downstream references are updated)
|
|
606
|
-
return Node<Update>({
|
|
607
|
-
action: "update",
|
|
608
|
-
props: news,
|
|
609
|
-
state: oldState,
|
|
610
|
-
});
|
|
611
|
-
} else {
|
|
612
|
-
// Cleanup is still pending, but the current "new" resource has already
|
|
613
|
-
// become obsolete. Start another replacement generation and preserve
|
|
614
|
-
// the existing replaced node as part of the recursive old chain.
|
|
615
|
-
return Node<Replace>({
|
|
616
|
-
restart: true,
|
|
617
|
-
action: "replace",
|
|
618
|
-
deleteFirst: diff.deleteFirst ?? oldState.deleteFirst,
|
|
619
|
-
props: news,
|
|
620
|
-
state: oldState,
|
|
458
|
+
state: { ...oldState, attr },
|
|
621
459
|
});
|
|
622
460
|
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Diff against whatever props represent the best-known current attempt.
|
|
465
|
+
// For replacement recovery that means the top-level replacement props,
|
|
466
|
+
// not the older generations stored under `old`.
|
|
467
|
+
const oldProps = oldState.props;
|
|
468
|
+
|
|
469
|
+
const diff = yield* asEffect(
|
|
470
|
+
provider
|
|
471
|
+
?.diff?.({
|
|
472
|
+
id,
|
|
473
|
+
olds: oldProps,
|
|
474
|
+
instanceId: oldState.instanceId,
|
|
475
|
+
output: oldState.attr,
|
|
476
|
+
news,
|
|
477
|
+
oldBindings,
|
|
478
|
+
newBindings,
|
|
479
|
+
})
|
|
480
|
+
.pipe(providePlanScope(fqn, oldState.instanceId)),
|
|
481
|
+
).pipe(
|
|
482
|
+
Effect.map(
|
|
483
|
+
(diff) =>
|
|
484
|
+
diff ??
|
|
485
|
+
({
|
|
486
|
+
action:
|
|
487
|
+
havePropsChanged(oldProps, news) ||
|
|
488
|
+
bindingDiffs.some((b) => b.action !== "noop")
|
|
489
|
+
? "update"
|
|
490
|
+
: "noop",
|
|
491
|
+
} as UpdateDiff | NoopDiff),
|
|
492
|
+
),
|
|
493
|
+
Effect.map((diff) =>
|
|
494
|
+
options.force && diff.action === "noop"
|
|
495
|
+
? ({
|
|
496
|
+
action: "update",
|
|
497
|
+
} satisfies UpdateDiff)
|
|
498
|
+
: diff,
|
|
499
|
+
),
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
if (oldState.status === "creating") {
|
|
503
|
+
if (diff.action === "noop") {
|
|
504
|
+
// we're in the creating state and props are un-changed
|
|
505
|
+
// let's just continue where we left off
|
|
626
506
|
return Node<Create>({
|
|
627
507
|
action: "create",
|
|
628
508
|
props: news,
|
|
629
|
-
state:
|
|
630
|
-
...oldState,
|
|
631
|
-
status: "creating",
|
|
632
|
-
props: news,
|
|
633
|
-
},
|
|
509
|
+
state: oldState,
|
|
634
510
|
});
|
|
635
511
|
} else if (diff.action === "update") {
|
|
636
|
-
//
|
|
512
|
+
// props have changed in a way that is updatable
|
|
513
|
+
// again, just continue with the create
|
|
514
|
+
// TODO(sam): should we maybe try an update instead?
|
|
515
|
+
return Node<Create>({
|
|
516
|
+
action: "create",
|
|
517
|
+
props: news,
|
|
518
|
+
state: oldState,
|
|
519
|
+
});
|
|
520
|
+
} else {
|
|
521
|
+
// props have changed in an incompatible way
|
|
522
|
+
// because it's possible that an un-updatable resource has already been created
|
|
523
|
+
// we must use a replace step to create a new one and delete the potential old one
|
|
524
|
+
return Node<Replace>({
|
|
525
|
+
action: "replace",
|
|
526
|
+
props: news,
|
|
527
|
+
deleteFirst: diff.deleteFirst ?? false,
|
|
528
|
+
state: oldState,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
} else if (oldState.status === "updating") {
|
|
532
|
+
// Updating already targets the live resource, so noop/update both mean
|
|
533
|
+
// "finish the interrupted update". Only a replace diff escalates it
|
|
534
|
+
// into a fresh replacement.
|
|
535
|
+
if (diff.action === "update" || diff.action === "noop") {
|
|
536
|
+
// we can continue where we left off
|
|
637
537
|
return Node<Update>({
|
|
638
538
|
action: "update",
|
|
639
539
|
props: news,
|
|
640
540
|
state: oldState,
|
|
641
541
|
});
|
|
642
|
-
} else
|
|
542
|
+
} else {
|
|
543
|
+
// we started to update a resource but now believe we should replace it
|
|
544
|
+
return Node<Replace>({
|
|
545
|
+
action: "replace",
|
|
546
|
+
deleteFirst: diff.deleteFirst ?? false,
|
|
547
|
+
props: news,
|
|
548
|
+
// TODO(sam): can Apply handle replacements when the oldState is UpdatingResourceState?
|
|
549
|
+
// -> or should we do a provider.read to try and reconcile back to UpdatedResourceState?
|
|
550
|
+
state: oldState,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
} else if (oldState.status === "replacing") {
|
|
554
|
+
// The replacement candidate is still being created. Noop/update keep
|
|
555
|
+
// driving the same generation; replace means that candidate itself is
|
|
556
|
+
// now obsolete and must be wrapped in a new outer generation.
|
|
557
|
+
if (diff.action === "noop") {
|
|
558
|
+
// this is the stable case - noop means just continue with the replacement
|
|
559
|
+
return Node<Replace>({
|
|
560
|
+
action: "replace",
|
|
561
|
+
deleteFirst: oldState.deleteFirst,
|
|
562
|
+
props: news,
|
|
563
|
+
state: oldState,
|
|
564
|
+
});
|
|
565
|
+
} else if (diff.action === "update") {
|
|
566
|
+
// potential problem here - the props have changed since we tried to replace,
|
|
567
|
+
// but not enough to trigger another replacement. the resource provider should
|
|
568
|
+
// be designed as idempotent to converge to the right state when creating the new resource
|
|
569
|
+
// the newly generated instanceId is intended to assist with this
|
|
643
570
|
return Node<Replace>({
|
|
644
571
|
action: "replace",
|
|
572
|
+
deleteFirst: oldState.deleteFirst,
|
|
645
573
|
props: news,
|
|
646
574
|
state: oldState,
|
|
647
|
-
deleteFirst: diff?.deleteFirst ?? false,
|
|
648
575
|
});
|
|
649
576
|
} else {
|
|
650
|
-
|
|
651
|
-
|
|
577
|
+
// The in-flight replacement candidate itself now needs replacement.
|
|
578
|
+
// Mark this as a restart so Apply creates a fresh generation instead
|
|
579
|
+
// of resuming the old replacement instance.
|
|
580
|
+
return Node<Replace>({
|
|
581
|
+
restart: true,
|
|
582
|
+
action: "replace",
|
|
583
|
+
deleteFirst: diff.deleteFirst ?? oldState.deleteFirst,
|
|
584
|
+
props: news,
|
|
652
585
|
state: oldState,
|
|
653
586
|
});
|
|
654
587
|
}
|
|
655
|
-
})
|
|
588
|
+
} else if (oldState.status === "replaced") {
|
|
589
|
+
// The new resource already exists. Noop means "just let GC finish",
|
|
590
|
+
// update means "mutate the current replacement before GC finishes",
|
|
591
|
+
// and replace means "the current replacement also became obsolete".
|
|
592
|
+
if (diff.action === "noop") {
|
|
593
|
+
// this is the stable case - noop means just continue cleaning up the replacement
|
|
594
|
+
return Node<Replace>({
|
|
595
|
+
action: "replace",
|
|
596
|
+
deleteFirst: oldState.deleteFirst,
|
|
597
|
+
props: news,
|
|
598
|
+
state: oldState,
|
|
599
|
+
});
|
|
600
|
+
} else if (diff.action === "update") {
|
|
601
|
+
// the replacement has been created but now also needs to be updated
|
|
602
|
+
// the resource provider should:
|
|
603
|
+
// 1. Update the newly created replacement resource
|
|
604
|
+
// 2. Then proceed as normal to delete the replaced resources (after all downstream references are updated)
|
|
605
|
+
return Node<Update>({
|
|
606
|
+
action: "update",
|
|
607
|
+
props: news,
|
|
608
|
+
state: oldState,
|
|
609
|
+
});
|
|
610
|
+
} else {
|
|
611
|
+
// Cleanup is still pending, but the current "new" resource has already
|
|
612
|
+
// become obsolete. Start another replacement generation and preserve
|
|
613
|
+
// the existing replaced node as part of the recursive old chain.
|
|
614
|
+
return Node<Replace>({
|
|
615
|
+
restart: true,
|
|
616
|
+
action: "replace",
|
|
617
|
+
deleteFirst: diff.deleteFirst ?? oldState.deleteFirst,
|
|
618
|
+
props: news,
|
|
619
|
+
state: oldState,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
} else if (oldState.status === "deleting") {
|
|
623
|
+
// we're in a partially deleted state, it is unclear whether it was or was not deleted
|
|
624
|
+
// so continue by re-creating it with the same instanceId and desired props
|
|
625
|
+
return Node<Create>({
|
|
626
|
+
action: "create",
|
|
627
|
+
props: news,
|
|
628
|
+
state: {
|
|
629
|
+
...oldState,
|
|
630
|
+
status: "creating",
|
|
631
|
+
props: news,
|
|
632
|
+
},
|
|
633
|
+
});
|
|
634
|
+
} else if (diff.action === "update") {
|
|
635
|
+
// Stable created/updated resources follow the normal CRUD mapping.
|
|
636
|
+
return Node<Update>({
|
|
637
|
+
action: "update",
|
|
638
|
+
props: news,
|
|
639
|
+
state: oldState,
|
|
640
|
+
});
|
|
641
|
+
} else if (diff.action === "replace") {
|
|
642
|
+
return Node<Replace>({
|
|
643
|
+
action: "replace",
|
|
644
|
+
props: news,
|
|
645
|
+
state: oldState,
|
|
646
|
+
deleteFirst: diff?.deleteFirst ?? false,
|
|
647
|
+
});
|
|
648
|
+
} else {
|
|
649
|
+
return Node<NoopUpdate>({
|
|
650
|
+
action: "noop",
|
|
651
|
+
state: oldState,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
}),
|
|
655
|
+
),
|
|
656
|
+
{ concurrency: "unbounded" },
|
|
657
|
+
)).map((update) => [update.resource.FQN, update]),
|
|
658
|
+
) as Plan["resources"];
|
|
659
|
+
|
|
660
|
+
// Detect unsatisfiable dependency cycles among create/replace nodes.
|
|
661
|
+
// Update/noop nodes signal their Deferred before waitForDeps so they
|
|
662
|
+
// cannot deadlock. Create/replace nodes only signal early when they
|
|
663
|
+
// have a precreate handler. Simulate the concurrent execution:
|
|
664
|
+
// precreate nodes are immediately "resolved", then iteratively resolve
|
|
665
|
+
// any node whose deps are all resolved. Remaining nodes would deadlock.
|
|
666
|
+
{
|
|
667
|
+
const createReplaceNodes = new Set(
|
|
668
|
+
Object.entries(resourceGraph)
|
|
669
|
+
.filter(
|
|
670
|
+
([_, node]) =>
|
|
671
|
+
node.action === "create" || node.action === "replace",
|
|
672
|
+
)
|
|
673
|
+
.map(([fqn]) => fqn),
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
if (createReplaceNodes.size > 0) {
|
|
677
|
+
const hasPrecreate = new Set(
|
|
678
|
+
[...createReplaceNodes].filter(
|
|
679
|
+
(fqn) => !!resourceGraph[fqn]?.provider?.precreate,
|
|
656
680
|
),
|
|
657
|
-
{ concurrency: "unbounded" },
|
|
658
|
-
)).map((update) => [update.resource.FQN, update]),
|
|
659
|
-
) as Plan["resources"];
|
|
660
|
-
|
|
661
|
-
// Detect unsatisfiable dependency cycles among create/replace nodes.
|
|
662
|
-
// Update/noop nodes signal their Deferred before waitForDeps so they
|
|
663
|
-
// cannot deadlock. Create/replace nodes only signal early when they
|
|
664
|
-
// have a precreate handler. Simulate the concurrent execution:
|
|
665
|
-
// precreate nodes are immediately "resolved", then iteratively resolve
|
|
666
|
-
// any node whose deps are all resolved. Remaining nodes would deadlock.
|
|
667
|
-
{
|
|
668
|
-
const createReplaceNodes = new Set(
|
|
669
|
-
Object.entries(resourceGraph)
|
|
670
|
-
.filter(
|
|
671
|
-
([_, node]) =>
|
|
672
|
-
node.action === "create" || node.action === "replace",
|
|
673
|
-
)
|
|
674
|
-
.map(([fqn]) => fqn),
|
|
675
681
|
);
|
|
676
682
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (resolved.has(fqn)) continue;
|
|
690
|
-
const deps = (allUpstreamDependencies[fqn] ?? []).filter((dep) =>
|
|
691
|
-
createReplaceNodes.has(dep),
|
|
692
|
-
);
|
|
693
|
-
if (deps.every((dep) => resolved.has(dep))) {
|
|
694
|
-
resolved.add(fqn);
|
|
695
|
-
changed = true;
|
|
696
|
-
}
|
|
683
|
+
const resolved = new Set(hasPrecreate);
|
|
684
|
+
let changed = true;
|
|
685
|
+
while (changed) {
|
|
686
|
+
changed = false;
|
|
687
|
+
for (const fqn of createReplaceNodes) {
|
|
688
|
+
if (resolved.has(fqn)) continue;
|
|
689
|
+
const deps = (allUpstreamDependencies[fqn] ?? []).filter((dep) =>
|
|
690
|
+
createReplaceNodes.has(dep),
|
|
691
|
+
);
|
|
692
|
+
if (deps.every((dep) => resolved.has(dep))) {
|
|
693
|
+
resolved.add(fqn);
|
|
694
|
+
changed = true;
|
|
697
695
|
}
|
|
698
696
|
}
|
|
697
|
+
}
|
|
699
698
|
|
|
700
|
-
|
|
701
|
-
|
|
699
|
+
const deadlocked = [...createReplaceNodes].filter(
|
|
700
|
+
(fqn) => !resolved.has(fqn),
|
|
701
|
+
);
|
|
702
|
+
if (deadlocked.length > 0) {
|
|
703
|
+
const missingPrecreate = deadlocked.filter(
|
|
704
|
+
(fqn) => !hasPrecreate.has(fqn),
|
|
705
|
+
);
|
|
706
|
+
return yield* Effect.die(
|
|
707
|
+
new UnsatisfiedResourceCycle({
|
|
708
|
+
message:
|
|
709
|
+
`Circular dependency detected that cannot be resolved: [${deadlocked.join(", ")}]. ` +
|
|
710
|
+
`Resources lacking a precreate handler: [${missingPrecreate.join(", ")}]. ` +
|
|
711
|
+
`All resources in a dependency cycle must implement precreate to allow early signaling.`,
|
|
712
|
+
cycle: deadlocked,
|
|
713
|
+
missingPrecreate,
|
|
714
|
+
}),
|
|
702
715
|
);
|
|
703
|
-
if (deadlocked.length > 0) {
|
|
704
|
-
const missingPrecreate = deadlocked.filter(
|
|
705
|
-
(fqn) => !hasPrecreate.has(fqn),
|
|
706
|
-
);
|
|
707
|
-
return yield* Effect.die(
|
|
708
|
-
new UnsatisfiedResourceCycle({
|
|
709
|
-
message:
|
|
710
|
-
`Circular dependency detected that cannot be resolved: [${deadlocked.join(", ")}]. ` +
|
|
711
|
-
`Resources lacking a precreate handler: [${missingPrecreate.join(", ")}]. ` +
|
|
712
|
-
`All resources in a dependency cycle must implement precreate to allow early signaling.`,
|
|
713
|
-
cycle: deadlocked,
|
|
714
|
-
missingPrecreate,
|
|
715
|
-
}),
|
|
716
|
-
);
|
|
717
|
-
}
|
|
718
716
|
}
|
|
719
717
|
}
|
|
718
|
+
}
|
|
720
719
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
}
|
|
720
|
+
const deletions = Object.fromEntries(
|
|
721
|
+
(yield* Effect.all(
|
|
722
|
+
(yield* state.list({ stack: stackName, stage: stage })).map(
|
|
723
|
+
Effect.fn(function* (fqn) {
|
|
724
|
+
// Check if this FQN is in the new resources
|
|
725
|
+
if (newResourceFqns.has(fqn)) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const oldState = yield* state.get({
|
|
729
|
+
stack: stackName,
|
|
730
|
+
stage: stage,
|
|
731
|
+
fqn,
|
|
732
|
+
});
|
|
733
|
+
let attr: any = oldState?.attr;
|
|
734
|
+
if (oldState) {
|
|
735
|
+
const { logicalId } = parseFqn(fqn);
|
|
736
|
+
const resourceType = oldState.resourceType;
|
|
737
|
+
const provider = yield* findProviderByType(resourceType);
|
|
738
|
+
if (oldState.attr === undefined) {
|
|
739
|
+
if (provider.read) {
|
|
740
|
+
attr = yield* provider
|
|
741
|
+
.read({
|
|
742
|
+
id: logicalId,
|
|
743
|
+
instanceId: oldState.instanceId,
|
|
744
|
+
olds: oldState.props as never,
|
|
745
|
+
output: oldState.attr as never,
|
|
746
|
+
})
|
|
747
|
+
.pipe(providePlanScope(fqn, oldState.instanceId));
|
|
750
748
|
}
|
|
751
|
-
return [
|
|
752
|
-
fqn,
|
|
753
|
-
{
|
|
754
|
-
action: "delete",
|
|
755
|
-
state: { ...oldState, attr },
|
|
756
|
-
provider: provider,
|
|
757
|
-
resource: {
|
|
758
|
-
Namespace: oldState.namespace,
|
|
759
|
-
FQN: fqn,
|
|
760
|
-
LogicalId: logicalId,
|
|
761
|
-
Type: oldState.resourceType,
|
|
762
|
-
Attributes: attr,
|
|
763
|
-
Props: oldState.props,
|
|
764
|
-
Binding: undefined!,
|
|
765
|
-
Provider: Provider(resourceType),
|
|
766
|
-
RemovalPolicy: oldState.removalPolicy,
|
|
767
|
-
ExecutionContext: undefined!,
|
|
768
|
-
} as ResourceLike,
|
|
769
|
-
downstream: oldDownstreamDependencies[fqn] ?? [],
|
|
770
|
-
bindings: oldState.bindings.map((binding) => ({
|
|
771
|
-
sid: binding.sid,
|
|
772
|
-
action: "delete" as const,
|
|
773
|
-
data: binding.data,
|
|
774
|
-
})),
|
|
775
|
-
} satisfies Delete,
|
|
776
|
-
] as const;
|
|
777
749
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
750
|
+
return [
|
|
751
|
+
fqn,
|
|
752
|
+
{
|
|
753
|
+
action: "delete",
|
|
754
|
+
state: { ...oldState, attr },
|
|
755
|
+
provider: provider,
|
|
756
|
+
resource: {
|
|
757
|
+
Namespace: oldState.namespace,
|
|
758
|
+
FQN: fqn,
|
|
759
|
+
LogicalId: logicalId,
|
|
760
|
+
Type: oldState.resourceType,
|
|
761
|
+
Attributes: attr,
|
|
762
|
+
Props: oldState.props,
|
|
763
|
+
Binding: undefined!,
|
|
764
|
+
Provider: Provider(resourceType),
|
|
765
|
+
RemovalPolicy: oldState.removalPolicy,
|
|
766
|
+
ExecutionContext: undefined!,
|
|
767
|
+
Providers: undefined,
|
|
768
|
+
} as ResourceLike,
|
|
769
|
+
downstream: oldDownstreamDependencies[fqn] ?? [],
|
|
770
|
+
bindings: oldState.bindings.map((binding) => ({
|
|
771
|
+
sid: binding.sid,
|
|
772
|
+
action: "delete" as const,
|
|
773
|
+
data: binding.data,
|
|
774
|
+
})),
|
|
775
|
+
} satisfies Delete,
|
|
776
|
+
] as const;
|
|
777
|
+
}
|
|
778
|
+
}),
|
|
779
|
+
),
|
|
780
|
+
{ concurrency: "unbounded" },
|
|
781
|
+
)).filter((v) => !!v),
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
for (const resourceFqn of Object.keys(deletions)) {
|
|
785
|
+
const dependencies = Object.entries(rawUpstreamDependencies)
|
|
786
|
+
.filter(
|
|
787
|
+
([survivorFqn, upstream]) =>
|
|
788
|
+
survivorFqn in resourceGraph && upstream.includes(resourceFqn),
|
|
789
|
+
)
|
|
790
|
+
.map(([survivorFqn]) => survivorFqn);
|
|
791
|
+
if (dependencies.length > 0) {
|
|
792
|
+
return yield* new DeleteResourceHasDownstreamDependencies({
|
|
793
|
+
message: `Resource ${resourceFqn} has downstream dependencies`,
|
|
794
|
+
resourceId: resourceFqn,
|
|
795
|
+
dependencies,
|
|
796
|
+
});
|
|
798
797
|
}
|
|
798
|
+
}
|
|
799
799
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
);
|
|
800
|
+
return {
|
|
801
|
+
resources: resourceGraph,
|
|
802
|
+
deletions,
|
|
803
|
+
output: stack.output,
|
|
804
|
+
} satisfies Plan<A> as Plan<A>;
|
|
805
|
+
}).pipe(ensureArtifactStore);
|
|
807
806
|
|
|
808
807
|
const providePlanScope =
|
|
809
808
|
(fqn: string, instanceId: string) =>
|