@go-to-k/cdkd 0.23.2 → 0.25.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/README.md +91 -7
- package/dist/cli.js +570 -257
- package/dist/cli.js.map +4 -4
- package/dist/go-to-k-cdkd-0.25.0.tgz +0 -0
- package/dist/index.js +384 -210
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/dist/go-to-k-cdkd-0.23.2.tgz +0 -0
package/dist/cli.js
CHANGED
|
@@ -508,6 +508,51 @@ var stateOptions = [
|
|
|
508
508
|
new Option("--state-prefix <prefix>", "S3 key prefix for state files").default("cdkd")
|
|
509
509
|
];
|
|
510
510
|
var stackOptions = [new Option("--stack <name>", "Stack name to operate on")];
|
|
511
|
+
function parseDuration(value) {
|
|
512
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
513
|
+
throw new Error(
|
|
514
|
+
`Invalid duration "${value}": expected <number>s, <number>m, or <number>h (e.g. 30s, 5m, 1h)`
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
const match = /^(\d+(?:\.\d+)?)([smh])$/.exec(value.trim());
|
|
518
|
+
if (!match) {
|
|
519
|
+
throw new Error(
|
|
520
|
+
`Invalid duration "${value}": expected <number>s, <number>m, or <number>h (e.g. 30s, 5m, 1h)`
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
const num = Number(match[1]);
|
|
524
|
+
if (!Number.isFinite(num) || num <= 0) {
|
|
525
|
+
throw new Error(`Invalid duration "${value}": must be greater than zero`);
|
|
526
|
+
}
|
|
527
|
+
const unit = match[2];
|
|
528
|
+
const multiplier = unit === "s" ? 1e3 : unit === "m" ? 6e4 : 36e5;
|
|
529
|
+
return Math.round(num * multiplier);
|
|
530
|
+
}
|
|
531
|
+
var resourceTimeoutOptions = [
|
|
532
|
+
// Default values are stored as parsed milliseconds (NOT the source
|
|
533
|
+
// string) because commander's `argParser` only runs on user-supplied
|
|
534
|
+
// values, never on defaults — without this every command handler
|
|
535
|
+
// would see `'5m'` (string) when the user did not pass the flag and
|
|
536
|
+
// `300_000` (number) when they did. The second `defaultValueDescription`
|
|
537
|
+
// argument keeps `--help` showing the human-readable form.
|
|
538
|
+
new Option(
|
|
539
|
+
"--resource-warn-after <duration>",
|
|
540
|
+
"Warn when a single resource operation exceeds this wall-clock duration (e.g. 5m, 90s, 1h)"
|
|
541
|
+
).default(parseDuration("5m"), "5m").argParser(parseDuration),
|
|
542
|
+
new Option(
|
|
543
|
+
"--resource-timeout <duration>",
|
|
544
|
+
"Abort a single resource operation that exceeds this wall-clock duration. Custom-Resource-heavy stacks may need to raise this above the default 30m (the Custom Resource provider's polling cap is 1h)."
|
|
545
|
+
).default(parseDuration("30m"), "30m").argParser(parseDuration)
|
|
546
|
+
];
|
|
547
|
+
function validateResourceTimeouts(opts) {
|
|
548
|
+
const warn = opts.resourceWarnAfter;
|
|
549
|
+
const timeout = opts.resourceTimeout;
|
|
550
|
+
if (typeof warn === "number" && typeof timeout === "number" && warn >= timeout) {
|
|
551
|
+
throw new Error(
|
|
552
|
+
`--resource-warn-after (${warn}ms) must be less than --resource-timeout (${timeout}ms)`
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
511
556
|
var deployOptions = [
|
|
512
557
|
new Option("--concurrency <number>", "Maximum concurrent resource operations").default(10).argParser((value) => parseInt(value, 10)),
|
|
513
558
|
new Option("--stack-concurrency <number>", "Maximum concurrent stack deployments").default(4).argParser((value) => parseInt(value, 10)),
|
|
@@ -523,7 +568,8 @@ var deployOptions = [
|
|
|
523
568
|
new Option(
|
|
524
569
|
"-e, --exclusively",
|
|
525
570
|
"Only deploy requested stacks, do not include dependencies"
|
|
526
|
-
).default(false)
|
|
571
|
+
).default(false),
|
|
572
|
+
...resourceTimeoutOptions
|
|
527
573
|
];
|
|
528
574
|
var contextOptions = [
|
|
529
575
|
new Option(
|
|
@@ -718,6 +764,22 @@ var LiveRenderer = class {
|
|
|
718
764
|
if (this.active)
|
|
719
765
|
this.draw();
|
|
720
766
|
}
|
|
767
|
+
/**
|
|
768
|
+
* Replace the label of a previously-added task in place. No-op if the
|
|
769
|
+
* task is not currently tracked (e.g. it already finished). Used by the
|
|
770
|
+
* per-resource deadline wrapper to surface a "[taking longer than
|
|
771
|
+
* expected, Nm+]" suffix without disturbing the elapsed-time counter
|
|
772
|
+
* the renderer tracks via `startedAt`.
|
|
773
|
+
*/
|
|
774
|
+
updateTaskLabel(id, label) {
|
|
775
|
+
const stackName = getCurrentStackName();
|
|
776
|
+
const task = this.tasks.get(scopedKey(id, stackName));
|
|
777
|
+
if (!task)
|
|
778
|
+
return;
|
|
779
|
+
task.label = label;
|
|
780
|
+
if (this.active)
|
|
781
|
+
this.draw();
|
|
782
|
+
}
|
|
721
783
|
/**
|
|
722
784
|
* Print content above the live area. Clears the live area, runs the writer,
|
|
723
785
|
* then redraws the live area. When the renderer is inactive, the writer
|
|
@@ -1011,6 +1073,38 @@ var ProvisioningError = class _ProvisioningError extends CdkdError {
|
|
|
1011
1073
|
Object.setPrototypeOf(this, _ProvisioningError.prototype);
|
|
1012
1074
|
}
|
|
1013
1075
|
};
|
|
1076
|
+
var ResourceTimeoutError = class _ResourceTimeoutError extends CdkdError {
|
|
1077
|
+
constructor(logicalId, resourceType, region, elapsedMs, operation, timeoutMs) {
|
|
1078
|
+
const elapsedLabel = formatDuration(elapsedMs);
|
|
1079
|
+
const timeoutLabel = formatDuration(timeoutMs);
|
|
1080
|
+
super(
|
|
1081
|
+
`Resource ${logicalId} (${resourceType}) in ${region} timed out after ${timeoutLabel} during ${operation} (elapsed ${elapsedLabel}).
|
|
1082
|
+
This may indicate a stuck Cloud Control polling loop, hung Custom Resource, or
|
|
1083
|
+
slow ENI provisioning. Re-run with --resource-timeout 1h if the resource genuinely
|
|
1084
|
+
needs more time, or --verbose to see the underlying provider activity.`,
|
|
1085
|
+
"RESOURCE_TIMEOUT"
|
|
1086
|
+
);
|
|
1087
|
+
this.logicalId = logicalId;
|
|
1088
|
+
this.resourceType = resourceType;
|
|
1089
|
+
this.region = region;
|
|
1090
|
+
this.elapsedMs = elapsedMs;
|
|
1091
|
+
this.operation = operation;
|
|
1092
|
+
this.timeoutMs = timeoutMs;
|
|
1093
|
+
this.name = "ResourceTimeoutError";
|
|
1094
|
+
Object.setPrototypeOf(this, _ResourceTimeoutError.prototype);
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
function formatDuration(ms) {
|
|
1098
|
+
if (ms < 6e4) {
|
|
1099
|
+
return `${Math.round(ms / 1e3)}s`;
|
|
1100
|
+
}
|
|
1101
|
+
const totalMinutes = Math.round(ms / 6e4);
|
|
1102
|
+
if (totalMinutes < 60)
|
|
1103
|
+
return `${totalMinutes}m`;
|
|
1104
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
1105
|
+
const minutes = totalMinutes % 60;
|
|
1106
|
+
return minutes === 0 ? `${hours}h` : `${hours}h${minutes}m`;
|
|
1107
|
+
}
|
|
1014
1108
|
var DependencyError = class _DependencyError extends CdkdError {
|
|
1015
1109
|
constructor(message, cause) {
|
|
1016
1110
|
super(message, "DEPENDENCY_ERROR", cause);
|
|
@@ -30781,7 +30875,85 @@ async function withRetry(operation, logicalId, opts = {}) {
|
|
|
30781
30875
|
throw lastError;
|
|
30782
30876
|
}
|
|
30783
30877
|
|
|
30878
|
+
// src/deployment/resource-deadline.ts
|
|
30879
|
+
var InvalidResourceDeadlineError = class extends Error {
|
|
30880
|
+
constructor(message) {
|
|
30881
|
+
super(message);
|
|
30882
|
+
this.name = "InvalidResourceDeadlineError";
|
|
30883
|
+
}
|
|
30884
|
+
};
|
|
30885
|
+
function validateOptions(opts) {
|
|
30886
|
+
const { warnAfterMs, timeoutMs } = opts;
|
|
30887
|
+
if (!Number.isFinite(warnAfterMs) || !Number.isFinite(timeoutMs) || warnAfterMs <= 0 || timeoutMs <= 0) {
|
|
30888
|
+
throw new InvalidResourceDeadlineError(
|
|
30889
|
+
`withResourceDeadline: warnAfterMs and timeoutMs must be positive finite numbers (got warnAfterMs=${warnAfterMs}, timeoutMs=${timeoutMs})`
|
|
30890
|
+
);
|
|
30891
|
+
}
|
|
30892
|
+
if (warnAfterMs >= timeoutMs) {
|
|
30893
|
+
throw new InvalidResourceDeadlineError(
|
|
30894
|
+
`withResourceDeadline: warnAfterMs (${warnAfterMs}ms) must be less than timeoutMs (${timeoutMs}ms)`
|
|
30895
|
+
);
|
|
30896
|
+
}
|
|
30897
|
+
}
|
|
30898
|
+
async function withResourceDeadline(operation, opts) {
|
|
30899
|
+
validateOptions(opts);
|
|
30900
|
+
const startedAt = Date.now();
|
|
30901
|
+
return new Promise((resolve4, reject) => {
|
|
30902
|
+
let settled = false;
|
|
30903
|
+
let warnTimer;
|
|
30904
|
+
let timeoutTimer;
|
|
30905
|
+
const cleanup = () => {
|
|
30906
|
+
if (warnTimer !== void 0)
|
|
30907
|
+
clearTimeout(warnTimer);
|
|
30908
|
+
if (timeoutTimer !== void 0)
|
|
30909
|
+
clearTimeout(timeoutTimer);
|
|
30910
|
+
warnTimer = void 0;
|
|
30911
|
+
timeoutTimer = void 0;
|
|
30912
|
+
};
|
|
30913
|
+
if (opts.onWarn) {
|
|
30914
|
+
warnTimer = setTimeout(() => {
|
|
30915
|
+
if (settled)
|
|
30916
|
+
return;
|
|
30917
|
+
try {
|
|
30918
|
+
opts.onWarn(Date.now() - startedAt);
|
|
30919
|
+
} catch {
|
|
30920
|
+
}
|
|
30921
|
+
}, opts.warnAfterMs);
|
|
30922
|
+
if (typeof warnTimer.unref === "function")
|
|
30923
|
+
warnTimer.unref();
|
|
30924
|
+
}
|
|
30925
|
+
timeoutTimer = setTimeout(() => {
|
|
30926
|
+
if (settled)
|
|
30927
|
+
return;
|
|
30928
|
+
settled = true;
|
|
30929
|
+
cleanup();
|
|
30930
|
+
const elapsed = Date.now() - startedAt;
|
|
30931
|
+
reject(opts.onTimeout(elapsed));
|
|
30932
|
+
}, opts.timeoutMs);
|
|
30933
|
+
if (typeof timeoutTimer.unref === "function")
|
|
30934
|
+
timeoutTimer.unref();
|
|
30935
|
+
Promise.resolve().then(() => operation()).then(
|
|
30936
|
+
(value) => {
|
|
30937
|
+
if (settled)
|
|
30938
|
+
return;
|
|
30939
|
+
settled = true;
|
|
30940
|
+
cleanup();
|
|
30941
|
+
resolve4(value);
|
|
30942
|
+
},
|
|
30943
|
+
(err) => {
|
|
30944
|
+
if (settled)
|
|
30945
|
+
return;
|
|
30946
|
+
settled = true;
|
|
30947
|
+
cleanup();
|
|
30948
|
+
reject(err);
|
|
30949
|
+
}
|
|
30950
|
+
);
|
|
30951
|
+
});
|
|
30952
|
+
}
|
|
30953
|
+
|
|
30784
30954
|
// src/deployment/deploy-engine.ts
|
|
30955
|
+
var DEFAULT_RESOURCE_WARN_AFTER_MS = 5 * 60 * 1e3;
|
|
30956
|
+
var DEFAULT_RESOURCE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
30785
30957
|
var InterruptedError = class extends Error {
|
|
30786
30958
|
constructor() {
|
|
30787
30959
|
super("Deployment interrupted by user (Ctrl+C)");
|
|
@@ -30802,6 +30974,8 @@ var DeployEngine = class {
|
|
|
30802
30974
|
this.options.dryRun = options.dryRun ?? false;
|
|
30803
30975
|
this.options.lockTimeout = options.lockTimeout ?? 5 * 60 * 1e3;
|
|
30804
30976
|
this.options.noRollback = options.noRollback ?? false;
|
|
30977
|
+
this.options.resourceWarnAfterMs = options.resourceWarnAfterMs ?? DEFAULT_RESOURCE_WARN_AFTER_MS;
|
|
30978
|
+
this.options.resourceTimeoutMs = options.resourceTimeoutMs ?? DEFAULT_RESOURCE_TIMEOUT_MS;
|
|
30805
30979
|
}
|
|
30806
30980
|
logger = getLogger().child("DeployEngine");
|
|
30807
30981
|
resolver;
|
|
@@ -31400,259 +31574,305 @@ var DeployEngine = class {
|
|
|
31400
31574
|
*/
|
|
31401
31575
|
async provisionResource(logicalId, change, stateResources, stackName, template, parameterValues, conditions, counts, progress) {
|
|
31402
31576
|
const resourceType = change.resourceType;
|
|
31403
|
-
const provider = this.providerRegistry.getProvider(resourceType);
|
|
31404
31577
|
const renderer = getLiveRenderer();
|
|
31405
31578
|
const needsReplacement = change.changeType === "UPDATE" && (change.propertyChanges?.some((pc) => pc.requiresReplacement) ?? false);
|
|
31406
31579
|
const verb = change.changeType === "CREATE" ? "Creating" : change.changeType === "DELETE" ? "Deleting" : needsReplacement ? "Replacing" : "Updating";
|
|
31407
|
-
|
|
31408
|
-
|
|
31409
|
-
|
|
31410
|
-
|
|
31411
|
-
|
|
31412
|
-
|
|
31580
|
+
const baseLabel = `${verb} ${logicalId} (${resourceType})`;
|
|
31581
|
+
renderer.addTask(logicalId, baseLabel);
|
|
31582
|
+
const operationKind = change.changeType === "CREATE" ? "CREATE" : change.changeType === "DELETE" ? "DELETE" : "UPDATE";
|
|
31583
|
+
const warnAfterMs = this.options.resourceWarnAfterMs ?? DEFAULT_RESOURCE_WARN_AFTER_MS;
|
|
31584
|
+
const timeoutMs = this.options.resourceTimeoutMs ?? DEFAULT_RESOURCE_TIMEOUT_MS;
|
|
31585
|
+
try {
|
|
31586
|
+
await withResourceDeadline(
|
|
31587
|
+
async () => {
|
|
31588
|
+
await this.provisionResourceBody(
|
|
31589
|
+
logicalId,
|
|
31590
|
+
change,
|
|
31591
|
+
stateResources,
|
|
31592
|
+
stackName,
|
|
31413
31593
|
template,
|
|
31414
|
-
|
|
31415
|
-
|
|
31416
|
-
|
|
31417
|
-
|
|
31418
|
-
|
|
31419
|
-
|
|
31420
|
-
|
|
31421
|
-
|
|
31422
|
-
|
|
31423
|
-
|
|
31594
|
+
parameterValues,
|
|
31595
|
+
conditions,
|
|
31596
|
+
counts,
|
|
31597
|
+
progress
|
|
31598
|
+
);
|
|
31599
|
+
},
|
|
31600
|
+
{
|
|
31601
|
+
warnAfterMs,
|
|
31602
|
+
timeoutMs,
|
|
31603
|
+
onWarn: (elapsedMs) => {
|
|
31604
|
+
const minutes = Math.max(1, Math.round(elapsedMs / 6e4));
|
|
31605
|
+
const warnSuffix = ` [taking longer than expected, ${minutes}m+]`;
|
|
31606
|
+
renderer.updateTaskLabel(logicalId, `${baseLabel}${warnSuffix}`);
|
|
31607
|
+
renderer.printAbove(() => {
|
|
31608
|
+
this.logger.warn(
|
|
31609
|
+
`${logicalId} (${resourceType}) has been ${operationKind === "CREATE" ? "creating" : operationKind === "DELETE" ? "deleting" : "updating"} for ${minutes}m \u2014 still waiting`
|
|
31610
|
+
);
|
|
31611
|
+
});
|
|
31612
|
+
},
|
|
31613
|
+
onTimeout: (elapsedMs) => new ResourceTimeoutError(
|
|
31614
|
+
logicalId,
|
|
31615
|
+
resourceType,
|
|
31616
|
+
this.stackRegion,
|
|
31617
|
+
elapsedMs,
|
|
31618
|
+
operationKind,
|
|
31619
|
+
timeoutMs
|
|
31620
|
+
)
|
|
31621
|
+
}
|
|
31622
|
+
);
|
|
31623
|
+
} catch (error) {
|
|
31624
|
+
renderer.removeTask(logicalId);
|
|
31625
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
31626
|
+
this.logger.error(`Failed to ${change.changeType.toLowerCase()} ${logicalId}: ${message}`);
|
|
31627
|
+
throw new ProvisioningError(
|
|
31628
|
+
`Failed to ${change.changeType.toLowerCase()} resource ${logicalId}`,
|
|
31629
|
+
resourceType,
|
|
31630
|
+
logicalId,
|
|
31631
|
+
stateResources[logicalId]?.physicalId,
|
|
31632
|
+
error instanceof Error ? error : void 0
|
|
31633
|
+
);
|
|
31634
|
+
} finally {
|
|
31635
|
+
renderer.removeTask(logicalId);
|
|
31636
|
+
}
|
|
31637
|
+
}
|
|
31638
|
+
/**
|
|
31639
|
+
* Inner body of provisionResource, extracted so the outer wrapper can
|
|
31640
|
+
* apply the per-resource deadline (`withResourceDeadline`) without
|
|
31641
|
+
* having the timeout / warn timer code dwarf the real provisioning
|
|
31642
|
+
* logic. Behaviour is unchanged from the pre-deadline implementation.
|
|
31643
|
+
*/
|
|
31644
|
+
async provisionResourceBody(logicalId, change, stateResources, stackName, template, parameterValues, conditions, counts, progress) {
|
|
31645
|
+
const resourceType = change.resourceType;
|
|
31646
|
+
const provider = this.providerRegistry.getProvider(resourceType);
|
|
31647
|
+
const renderer = getLiveRenderer();
|
|
31648
|
+
switch (change.changeType) {
|
|
31649
|
+
case "CREATE": {
|
|
31650
|
+
const desiredProps = change.desiredProperties || {};
|
|
31651
|
+
const context = {
|
|
31652
|
+
template,
|
|
31653
|
+
resources: stateResources,
|
|
31654
|
+
...parameterValues && Object.keys(parameterValues).length > 0 && { parameters: parameterValues },
|
|
31655
|
+
...conditions && Object.keys(conditions).length > 0 && { conditions },
|
|
31656
|
+
stateBackend: this.stateBackend,
|
|
31657
|
+
stackName
|
|
31658
|
+
};
|
|
31659
|
+
const resolvedProps = await this.resolver.resolve(desiredProps, context);
|
|
31660
|
+
const { provider: createProvider, properties: createProps } = this.selectProviderWithSafetyNet(provider, resourceType, resolvedProps, logicalId);
|
|
31661
|
+
const result = await this.withRetry(
|
|
31662
|
+
() => createProvider.create(logicalId, resourceType, createProps),
|
|
31663
|
+
logicalId
|
|
31664
|
+
);
|
|
31665
|
+
const dependencies = this.extractAllDependencies(template, logicalId);
|
|
31666
|
+
stateResources[logicalId] = {
|
|
31667
|
+
physicalId: result.physicalId,
|
|
31668
|
+
resourceType,
|
|
31669
|
+
properties: resolvedProps,
|
|
31670
|
+
...result.attributes && { attributes: result.attributes },
|
|
31671
|
+
...dependencies && dependencies.length > 0 && { dependencies }
|
|
31672
|
+
};
|
|
31673
|
+
if (counts)
|
|
31674
|
+
counts.created++;
|
|
31675
|
+
if (progress)
|
|
31676
|
+
progress.current++;
|
|
31677
|
+
const createPrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
|
|
31678
|
+
renderer.removeTask(logicalId);
|
|
31679
|
+
this.logger.info(`${createPrefix}\u2705 ${logicalId} (${resourceType}) created`);
|
|
31680
|
+
break;
|
|
31681
|
+
}
|
|
31682
|
+
case "UPDATE": {
|
|
31683
|
+
const currentResource = stateResources[logicalId];
|
|
31684
|
+
if (!currentResource) {
|
|
31685
|
+
throw new Error(`Cannot update ${logicalId}: resource not found in state`);
|
|
31686
|
+
}
|
|
31687
|
+
const desiredProps = change.desiredProperties || {};
|
|
31688
|
+
const currentProps = change.currentProperties || {};
|
|
31689
|
+
const context = {
|
|
31690
|
+
template,
|
|
31691
|
+
resources: stateResources,
|
|
31692
|
+
...parameterValues && Object.keys(parameterValues).length > 0 && { parameters: parameterValues },
|
|
31693
|
+
...conditions && Object.keys(conditions).length > 0 && { conditions },
|
|
31694
|
+
stateBackend: this.stateBackend,
|
|
31695
|
+
stackName
|
|
31696
|
+
};
|
|
31697
|
+
const resolvedProps = await this.resolver.resolve(desiredProps, context);
|
|
31698
|
+
if (JSON.stringify(resolvedProps) === JSON.stringify(currentProps)) {
|
|
31699
|
+
this.logger.debug(
|
|
31700
|
+
`Skipping ${logicalId}: no actual changes after intrinsic function resolution`
|
|
31701
|
+
);
|
|
31702
|
+
if (counts)
|
|
31703
|
+
counts.skipped++;
|
|
31704
|
+
break;
|
|
31705
|
+
}
|
|
31706
|
+
const needsReplacement = change.propertyChanges?.some((pc) => pc.requiresReplacement);
|
|
31707
|
+
const dependencies = this.extractAllDependencies(template, logicalId);
|
|
31708
|
+
if (needsReplacement) {
|
|
31709
|
+
const replacedProps = change.propertyChanges?.filter((pc) => pc.requiresReplacement).map((pc) => pc.path).join(", ");
|
|
31710
|
+
this.logger.info(
|
|
31711
|
+
`Replacing ${logicalId} (${resourceType}) - immutable properties changed: ${replacedProps}`
|
|
31712
|
+
);
|
|
31713
|
+
this.logger.info(` Creating new ${logicalId}...`);
|
|
31714
|
+
const { provider: replaceProvider, properties: replaceProps } = this.selectProviderWithSafetyNet(provider, resourceType, resolvedProps, logicalId);
|
|
31715
|
+
const createResult = await this.withRetry(
|
|
31716
|
+
() => replaceProvider.create(logicalId, resourceType, replaceProps),
|
|
31424
31717
|
logicalId
|
|
31425
31718
|
);
|
|
31426
|
-
const
|
|
31719
|
+
const updateReplacePolicy = template?.Resources?.[logicalId]?.UpdateReplacePolicy;
|
|
31720
|
+
if (updateReplacePolicy === "Retain") {
|
|
31721
|
+
this.logger.info(
|
|
31722
|
+
` Retaining old ${logicalId} (${currentResource.physicalId}) - UpdateReplacePolicy: Retain`
|
|
31723
|
+
);
|
|
31724
|
+
} else {
|
|
31725
|
+
this.logger.info(` Deleting old ${logicalId} (${currentResource.physicalId})...`);
|
|
31726
|
+
try {
|
|
31727
|
+
await provider.delete(
|
|
31728
|
+
logicalId,
|
|
31729
|
+
currentResource.physicalId,
|
|
31730
|
+
resourceType,
|
|
31731
|
+
currentResource.properties,
|
|
31732
|
+
{ expectedRegion: this.stackRegion }
|
|
31733
|
+
);
|
|
31734
|
+
this.logger.info(` \u2713 Old resource deleted`);
|
|
31735
|
+
} catch (deleteError) {
|
|
31736
|
+
this.logger.warn(
|
|
31737
|
+
` \u26A0 Failed to delete old resource ${logicalId} (${currentResource.physicalId}): ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`
|
|
31738
|
+
);
|
|
31739
|
+
}
|
|
31740
|
+
}
|
|
31427
31741
|
stateResources[logicalId] = {
|
|
31428
|
-
physicalId:
|
|
31742
|
+
physicalId: createResult.physicalId,
|
|
31429
31743
|
resourceType,
|
|
31430
31744
|
properties: resolvedProps,
|
|
31431
|
-
...
|
|
31745
|
+
...createResult.attributes && { attributes: createResult.attributes },
|
|
31432
31746
|
...dependencies && dependencies.length > 0 && { dependencies }
|
|
31433
31747
|
};
|
|
31434
31748
|
if (counts)
|
|
31435
|
-
counts.
|
|
31749
|
+
counts.updated++;
|
|
31436
31750
|
if (progress)
|
|
31437
31751
|
progress.current++;
|
|
31438
|
-
const
|
|
31752
|
+
const replacePrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
|
|
31439
31753
|
renderer.removeTask(logicalId);
|
|
31440
|
-
this.logger.info(`${
|
|
31441
|
-
|
|
31442
|
-
|
|
31443
|
-
|
|
31444
|
-
|
|
31445
|
-
|
|
31446
|
-
|
|
31447
|
-
|
|
31448
|
-
|
|
31449
|
-
|
|
31450
|
-
|
|
31451
|
-
|
|
31452
|
-
|
|
31453
|
-
|
|
31454
|
-
...conditions && Object.keys(conditions).length > 0 && { conditions },
|
|
31455
|
-
stateBackend: this.stateBackend,
|
|
31456
|
-
stackName
|
|
31457
|
-
};
|
|
31458
|
-
const resolvedProps = await this.resolver.resolve(desiredProps, context);
|
|
31459
|
-
if (JSON.stringify(resolvedProps) === JSON.stringify(currentProps)) {
|
|
31460
|
-
this.logger.debug(
|
|
31461
|
-
`Skipping ${logicalId}: no actual changes after intrinsic function resolution`
|
|
31462
|
-
);
|
|
31463
|
-
if (counts)
|
|
31464
|
-
counts.skipped++;
|
|
31465
|
-
break;
|
|
31466
|
-
}
|
|
31467
|
-
const needsReplacement2 = change.propertyChanges?.some((pc) => pc.requiresReplacement);
|
|
31468
|
-
const dependencies = this.extractAllDependencies(template, logicalId);
|
|
31469
|
-
if (needsReplacement2) {
|
|
31470
|
-
const replacedProps = change.propertyChanges?.filter((pc) => pc.requiresReplacement).map((pc) => pc.path).join(", ");
|
|
31471
|
-
this.logger.info(
|
|
31472
|
-
`Replacing ${logicalId} (${resourceType}) - immutable properties changed: ${replacedProps}`
|
|
31473
|
-
);
|
|
31474
|
-
this.logger.info(` Creating new ${logicalId}...`);
|
|
31475
|
-
const { provider: replaceProvider, properties: replaceProps } = this.selectProviderWithSafetyNet(provider, resourceType, resolvedProps, logicalId);
|
|
31476
|
-
const createResult = await this.withRetry(
|
|
31477
|
-
() => replaceProvider.create(logicalId, resourceType, replaceProps),
|
|
31754
|
+
this.logger.info(`${replacePrefix}\u2705 ${logicalId} (${resourceType}) replaced`);
|
|
31755
|
+
} else {
|
|
31756
|
+
this.logger.debug(`Updating ${logicalId} (${resourceType})`);
|
|
31757
|
+
const { provider: updateProvider, properties: updateProps } = this.selectProviderWithSafetyNet(provider, resourceType, resolvedProps, logicalId);
|
|
31758
|
+
let result;
|
|
31759
|
+
try {
|
|
31760
|
+
result = await this.withRetry(
|
|
31761
|
+
() => updateProvider.update(
|
|
31762
|
+
logicalId,
|
|
31763
|
+
currentResource.physicalId,
|
|
31764
|
+
resourceType,
|
|
31765
|
+
updateProps,
|
|
31766
|
+
currentProps
|
|
31767
|
+
),
|
|
31478
31768
|
logicalId
|
|
31479
31769
|
);
|
|
31480
|
-
|
|
31481
|
-
|
|
31770
|
+
} catch (updateError) {
|
|
31771
|
+
const msg = updateError instanceof Error ? updateError.message : String(updateError);
|
|
31772
|
+
if (msg.includes("UnsupportedActionException") || msg.includes("does not support UPDATE")) {
|
|
31482
31773
|
this.logger.info(
|
|
31483
|
-
`
|
|
31774
|
+
`UPDATE not supported for ${logicalId} (${resourceType}), replacing (DELETE \u2192 CREATE)`
|
|
31484
31775
|
);
|
|
31485
|
-
} else {
|
|
31486
|
-
this.logger.info(` Deleting old ${logicalId} (${currentResource.physicalId})...`);
|
|
31487
31776
|
try {
|
|
31488
31777
|
await provider.delete(
|
|
31489
31778
|
logicalId,
|
|
31490
31779
|
currentResource.physicalId,
|
|
31491
31780
|
resourceType,
|
|
31492
|
-
|
|
31781
|
+
currentProps,
|
|
31493
31782
|
{ expectedRegion: this.stackRegion }
|
|
31494
31783
|
);
|
|
31495
|
-
this.logger.info(` \u2713 Old resource deleted`);
|
|
31496
31784
|
} catch (deleteError) {
|
|
31497
|
-
|
|
31498
|
-
|
|
31499
|
-
|
|
31500
|
-
|
|
31501
|
-
}
|
|
31502
|
-
stateResources[logicalId] = {
|
|
31503
|
-
physicalId: createResult.physicalId,
|
|
31504
|
-
resourceType,
|
|
31505
|
-
properties: resolvedProps,
|
|
31506
|
-
...createResult.attributes && { attributes: createResult.attributes },
|
|
31507
|
-
...dependencies && dependencies.length > 0 && { dependencies }
|
|
31508
|
-
};
|
|
31509
|
-
if (counts)
|
|
31510
|
-
counts.updated++;
|
|
31511
|
-
if (progress)
|
|
31512
|
-
progress.current++;
|
|
31513
|
-
const replacePrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
|
|
31514
|
-
renderer.removeTask(logicalId);
|
|
31515
|
-
this.logger.info(`${replacePrefix}\u2705 ${logicalId} (${resourceType}) replaced`);
|
|
31516
|
-
} else {
|
|
31517
|
-
this.logger.debug(`Updating ${logicalId} (${resourceType})`);
|
|
31518
|
-
const { provider: updateProvider, properties: updateProps } = this.selectProviderWithSafetyNet(provider, resourceType, resolvedProps, logicalId);
|
|
31519
|
-
let result;
|
|
31520
|
-
try {
|
|
31521
|
-
result = await this.withRetry(
|
|
31522
|
-
() => updateProvider.update(
|
|
31523
|
-
logicalId,
|
|
31524
|
-
currentResource.physicalId,
|
|
31525
|
-
resourceType,
|
|
31526
|
-
updateProps,
|
|
31527
|
-
currentProps
|
|
31528
|
-
),
|
|
31529
|
-
logicalId
|
|
31530
|
-
);
|
|
31531
|
-
} catch (updateError) {
|
|
31532
|
-
const msg = updateError instanceof Error ? updateError.message : String(updateError);
|
|
31533
|
-
if (msg.includes("UnsupportedActionException") || msg.includes("does not support UPDATE")) {
|
|
31534
|
-
this.logger.info(
|
|
31535
|
-
`UPDATE not supported for ${logicalId} (${resourceType}), replacing (DELETE \u2192 CREATE)`
|
|
31536
|
-
);
|
|
31537
|
-
try {
|
|
31538
|
-
await provider.delete(
|
|
31539
|
-
logicalId,
|
|
31540
|
-
currentResource.physicalId,
|
|
31541
|
-
resourceType,
|
|
31542
|
-
currentProps,
|
|
31543
|
-
{ expectedRegion: this.stackRegion }
|
|
31785
|
+
const deleteMsg = deleteError instanceof Error ? deleteError.message : String(deleteError);
|
|
31786
|
+
if (deleteMsg.includes("does not exist") || deleteMsg.includes("not found") || deleteMsg.includes("NotFound")) {
|
|
31787
|
+
this.logger.debug(
|
|
31788
|
+
`Old resource ${logicalId} already gone, proceeding with CREATE`
|
|
31544
31789
|
);
|
|
31545
|
-
}
|
|
31546
|
-
|
|
31547
|
-
if (deleteMsg.includes("does not exist") || deleteMsg.includes("not found") || deleteMsg.includes("NotFound")) {
|
|
31548
|
-
this.logger.debug(
|
|
31549
|
-
`Old resource ${logicalId} already gone, proceeding with CREATE`
|
|
31550
|
-
);
|
|
31551
|
-
} else {
|
|
31552
|
-
throw deleteError;
|
|
31553
|
-
}
|
|
31790
|
+
} else {
|
|
31791
|
+
throw deleteError;
|
|
31554
31792
|
}
|
|
31555
|
-
const { provider: replProvider, properties: replProps } = this.selectProviderWithSafetyNet(
|
|
31556
|
-
provider,
|
|
31557
|
-
resourceType,
|
|
31558
|
-
resolvedProps,
|
|
31559
|
-
logicalId
|
|
31560
|
-
);
|
|
31561
|
-
const createResult = await this.withRetry(
|
|
31562
|
-
() => replProvider.create(logicalId, resourceType, replProps),
|
|
31563
|
-
logicalId
|
|
31564
|
-
);
|
|
31565
|
-
result = {
|
|
31566
|
-
physicalId: createResult.physicalId,
|
|
31567
|
-
attributes: createResult.attributes,
|
|
31568
|
-
wasReplaced: true
|
|
31569
|
-
};
|
|
31570
|
-
} else {
|
|
31571
|
-
throw updateError;
|
|
31572
31793
|
}
|
|
31573
|
-
|
|
31574
|
-
|
|
31575
|
-
|
|
31576
|
-
|
|
31794
|
+
const { provider: replProvider, properties: replProps } = this.selectProviderWithSafetyNet(provider, resourceType, resolvedProps, logicalId);
|
|
31795
|
+
const createResult = await this.withRetry(
|
|
31796
|
+
() => replProvider.create(logicalId, resourceType, replProps),
|
|
31797
|
+
logicalId
|
|
31577
31798
|
);
|
|
31799
|
+
result = {
|
|
31800
|
+
physicalId: createResult.physicalId,
|
|
31801
|
+
attributes: createResult.attributes,
|
|
31802
|
+
wasReplaced: true
|
|
31803
|
+
};
|
|
31804
|
+
} else {
|
|
31805
|
+
throw updateError;
|
|
31578
31806
|
}
|
|
31579
|
-
stateResources[logicalId] = {
|
|
31580
|
-
physicalId: result.physicalId,
|
|
31581
|
-
resourceType,
|
|
31582
|
-
properties: resolvedProps,
|
|
31583
|
-
...result.attributes && { attributes: result.attributes },
|
|
31584
|
-
...dependencies && dependencies.length > 0 && { dependencies }
|
|
31585
|
-
};
|
|
31586
|
-
if (counts)
|
|
31587
|
-
counts.updated++;
|
|
31588
|
-
if (progress)
|
|
31589
|
-
progress.current++;
|
|
31590
|
-
const updatePrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
|
|
31591
|
-
renderer.removeTask(logicalId);
|
|
31592
|
-
this.logger.info(`${updatePrefix}\u2705 ${logicalId} (${resourceType}) updated`);
|
|
31593
31807
|
}
|
|
31594
|
-
|
|
31595
|
-
|
|
31596
|
-
|
|
31597
|
-
const currentResource = stateResources[logicalId];
|
|
31598
|
-
if (!currentResource) {
|
|
31599
|
-
throw new Error(`Cannot delete ${logicalId}: resource not found in state`);
|
|
31600
|
-
}
|
|
31601
|
-
const deletionPolicy = template?.Resources?.[logicalId]?.DeletionPolicy;
|
|
31602
|
-
if (deletionPolicy === "Retain") {
|
|
31603
|
-
this.logger.info(`Retaining ${logicalId} (${resourceType}) - DeletionPolicy: Retain`);
|
|
31604
|
-
delete stateResources[logicalId];
|
|
31605
|
-
break;
|
|
31606
|
-
}
|
|
31607
|
-
this.logger.debug(`Deleting ${logicalId} (${resourceType})`);
|
|
31608
|
-
try {
|
|
31609
|
-
await this.withRetry(
|
|
31610
|
-
() => provider.delete(
|
|
31611
|
-
logicalId,
|
|
31612
|
-
currentResource.physicalId,
|
|
31613
|
-
resourceType,
|
|
31614
|
-
currentResource.properties,
|
|
31615
|
-
{ expectedRegion: this.stackRegion }
|
|
31616
|
-
),
|
|
31617
|
-
logicalId,
|
|
31618
|
-
3,
|
|
31619
|
-
// fewer retries for DELETE
|
|
31620
|
-
5e3
|
|
31808
|
+
if (result.wasReplaced) {
|
|
31809
|
+
this.logger.info(
|
|
31810
|
+
`Resource ${logicalId} was replaced: ${currentResource.physicalId} -> ${result.physicalId}`
|
|
31621
31811
|
);
|
|
31622
|
-
} catch (deleteError) {
|
|
31623
|
-
const msg = deleteError instanceof Error ? deleteError.message : String(deleteError);
|
|
31624
|
-
if (msg.includes("does not exist") || msg.includes("was not found") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException") || msg.includes("ResourceNotFoundException")) {
|
|
31625
|
-
this.logger.debug(
|
|
31626
|
-
`Resource ${logicalId} already deleted (${msg}), removing from state`
|
|
31627
|
-
);
|
|
31628
|
-
} else {
|
|
31629
|
-
throw deleteError;
|
|
31630
|
-
}
|
|
31631
31812
|
}
|
|
31632
|
-
|
|
31813
|
+
stateResources[logicalId] = {
|
|
31814
|
+
physicalId: result.physicalId,
|
|
31815
|
+
resourceType,
|
|
31816
|
+
properties: resolvedProps,
|
|
31817
|
+
...result.attributes && { attributes: result.attributes },
|
|
31818
|
+
...dependencies && dependencies.length > 0 && { dependencies }
|
|
31819
|
+
};
|
|
31633
31820
|
if (counts)
|
|
31634
|
-
counts.
|
|
31821
|
+
counts.updated++;
|
|
31635
31822
|
if (progress)
|
|
31636
31823
|
progress.current++;
|
|
31637
|
-
const
|
|
31824
|
+
const updatePrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
|
|
31638
31825
|
renderer.removeTask(logicalId);
|
|
31639
|
-
this.logger.info(`${
|
|
31826
|
+
this.logger.info(`${updatePrefix}\u2705 ${logicalId} (${resourceType}) updated`);
|
|
31827
|
+
}
|
|
31828
|
+
break;
|
|
31829
|
+
}
|
|
31830
|
+
case "DELETE": {
|
|
31831
|
+
const currentResource = stateResources[logicalId];
|
|
31832
|
+
if (!currentResource) {
|
|
31833
|
+
throw new Error(`Cannot delete ${logicalId}: resource not found in state`);
|
|
31834
|
+
}
|
|
31835
|
+
const deletionPolicy = template?.Resources?.[logicalId]?.DeletionPolicy;
|
|
31836
|
+
if (deletionPolicy === "Retain") {
|
|
31837
|
+
this.logger.info(`Retaining ${logicalId} (${resourceType}) - DeletionPolicy: Retain`);
|
|
31838
|
+
delete stateResources[logicalId];
|
|
31640
31839
|
break;
|
|
31641
31840
|
}
|
|
31841
|
+
this.logger.debug(`Deleting ${logicalId} (${resourceType})`);
|
|
31842
|
+
try {
|
|
31843
|
+
await this.withRetry(
|
|
31844
|
+
() => provider.delete(
|
|
31845
|
+
logicalId,
|
|
31846
|
+
currentResource.physicalId,
|
|
31847
|
+
resourceType,
|
|
31848
|
+
currentResource.properties,
|
|
31849
|
+
{ expectedRegion: this.stackRegion }
|
|
31850
|
+
),
|
|
31851
|
+
logicalId,
|
|
31852
|
+
3,
|
|
31853
|
+
// fewer retries for DELETE
|
|
31854
|
+
5e3
|
|
31855
|
+
);
|
|
31856
|
+
} catch (deleteError) {
|
|
31857
|
+
const msg = deleteError instanceof Error ? deleteError.message : String(deleteError);
|
|
31858
|
+
if (msg.includes("does not exist") || msg.includes("was not found") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException") || msg.includes("ResourceNotFoundException")) {
|
|
31859
|
+
this.logger.debug(
|
|
31860
|
+
`Resource ${logicalId} already deleted (${msg}), removing from state`
|
|
31861
|
+
);
|
|
31862
|
+
} else {
|
|
31863
|
+
throw deleteError;
|
|
31864
|
+
}
|
|
31865
|
+
}
|
|
31866
|
+
delete stateResources[logicalId];
|
|
31867
|
+
if (counts)
|
|
31868
|
+
counts.deleted++;
|
|
31869
|
+
if (progress)
|
|
31870
|
+
progress.current++;
|
|
31871
|
+
const deletePrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
|
|
31872
|
+
renderer.removeTask(logicalId);
|
|
31873
|
+
this.logger.info(`${deletePrefix}\u2705 ${logicalId} (${resourceType}) deleted`);
|
|
31874
|
+
break;
|
|
31642
31875
|
}
|
|
31643
|
-
} catch (error) {
|
|
31644
|
-
renderer.removeTask(logicalId);
|
|
31645
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
31646
|
-
this.logger.error(`Failed to ${change.changeType.toLowerCase()} ${logicalId}: ${message}`);
|
|
31647
|
-
throw new ProvisioningError(
|
|
31648
|
-
`Failed to ${change.changeType.toLowerCase()} resource ${logicalId}`,
|
|
31649
|
-
resourceType,
|
|
31650
|
-
logicalId,
|
|
31651
|
-
stateResources[logicalId]?.physicalId,
|
|
31652
|
-
error instanceof Error ? error : void 0
|
|
31653
|
-
);
|
|
31654
|
-
} finally {
|
|
31655
|
-
renderer.removeTask(logicalId);
|
|
31656
31876
|
}
|
|
31657
31877
|
}
|
|
31658
31878
|
/**
|
|
@@ -31864,6 +32084,10 @@ async function deployCommand(stacks, options) {
|
|
|
31864
32084
|
process.env["CDKD_NO_LIVE"] = "1";
|
|
31865
32085
|
}
|
|
31866
32086
|
warnIfDeprecatedRegion(options);
|
|
32087
|
+
validateResourceTimeouts({
|
|
32088
|
+
resourceWarnAfter: options.resourceWarnAfter,
|
|
32089
|
+
resourceTimeout: options.resourceTimeout
|
|
32090
|
+
});
|
|
31867
32091
|
if (!options.wait) {
|
|
31868
32092
|
process.env["CDKD_NO_WAIT"] = "true";
|
|
31869
32093
|
}
|
|
@@ -32056,7 +32280,9 @@ Deploying stack: ${stackInfo.stackName}${stackRegion !== baseRegion ? ` (region:
|
|
|
32056
32280
|
{
|
|
32057
32281
|
concurrency: options.concurrency,
|
|
32058
32282
|
dryRun: options.dryRun,
|
|
32059
|
-
noRollback: !options.rollback
|
|
32283
|
+
noRollback: !options.rollback,
|
|
32284
|
+
resourceWarnAfterMs: options.resourceWarnAfter,
|
|
32285
|
+
resourceTimeoutMs: options.resourceTimeout
|
|
32060
32286
|
},
|
|
32061
32287
|
stackRegion
|
|
32062
32288
|
);
|
|
@@ -32466,41 +32692,73 @@ Acquiring lock for stack ${stackName}...`);
|
|
|
32466
32692
|
logger.debug(
|
|
32467
32693
|
`Deletion level ${executionLevels.length - levelIndex}/${executionLevels.length} (${level.length} resources)`
|
|
32468
32694
|
);
|
|
32695
|
+
const warnAfterMs = ctx.resourceWarnAfterMs ?? DEFAULT_RESOURCE_WARN_AFTER_MS;
|
|
32696
|
+
const timeoutMs = ctx.resourceTimeoutMs ?? DEFAULT_RESOURCE_TIMEOUT_MS;
|
|
32697
|
+
const stackRegion2 = state.region ?? ctx.baseRegion;
|
|
32469
32698
|
const deletePromises = level.map(async (logicalId) => {
|
|
32470
32699
|
const resource = state.resources[logicalId];
|
|
32471
32700
|
if (!resource) {
|
|
32472
32701
|
logger.warn(`Resource ${logicalId} not found in state, skipping`);
|
|
32473
32702
|
return;
|
|
32474
32703
|
}
|
|
32475
|
-
|
|
32704
|
+
const baseLabel = `Deleting ${logicalId} (${resource.resourceType})`;
|
|
32705
|
+
renderer.addTask(logicalId, baseLabel);
|
|
32476
32706
|
try {
|
|
32477
32707
|
const provider = destroyProviderRegistry.getProvider(resource.resourceType);
|
|
32478
|
-
|
|
32479
|
-
|
|
32480
|
-
|
|
32481
|
-
|
|
32708
|
+
await withResourceDeadline(
|
|
32709
|
+
async () => {
|
|
32710
|
+
let lastDeleteError;
|
|
32711
|
+
for (let attempt = 0; attempt <= 3; attempt++) {
|
|
32712
|
+
try {
|
|
32713
|
+
await provider.delete(
|
|
32714
|
+
logicalId,
|
|
32715
|
+
resource.physicalId,
|
|
32716
|
+
resource.resourceType,
|
|
32717
|
+
resource.properties
|
|
32718
|
+
);
|
|
32719
|
+
lastDeleteError = null;
|
|
32720
|
+
break;
|
|
32721
|
+
} catch (retryError) {
|
|
32722
|
+
lastDeleteError = retryError;
|
|
32723
|
+
const msg = retryError instanceof Error ? retryError.message : String(retryError);
|
|
32724
|
+
const isRetryable = msg.includes("Too Many Requests") || msg.includes("has dependencies") || msg.includes("can't be deleted since") || msg.includes("DependencyViolation");
|
|
32725
|
+
if (!isRetryable || attempt >= 3)
|
|
32726
|
+
break;
|
|
32727
|
+
const delay = 5e3 * Math.pow(2, attempt);
|
|
32728
|
+
logger.debug(
|
|
32729
|
+
` \u23F3 Retrying delete ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/3)`
|
|
32730
|
+
);
|
|
32731
|
+
await new Promise((resolve4) => setTimeout(resolve4, delay));
|
|
32732
|
+
}
|
|
32733
|
+
}
|
|
32734
|
+
if (lastDeleteError)
|
|
32735
|
+
throw lastDeleteError;
|
|
32736
|
+
},
|
|
32737
|
+
{
|
|
32738
|
+
warnAfterMs,
|
|
32739
|
+
timeoutMs,
|
|
32740
|
+
onWarn: (elapsedMs) => {
|
|
32741
|
+
const minutes = Math.max(1, Math.round(elapsedMs / 6e4));
|
|
32742
|
+
renderer.updateTaskLabel(
|
|
32743
|
+
logicalId,
|
|
32744
|
+
`${baseLabel} [taking longer than expected, ${minutes}m+]`
|
|
32745
|
+
);
|
|
32746
|
+
renderer.printAbove(() => {
|
|
32747
|
+
logger.warn(
|
|
32748
|
+
`${logicalId} (${resource.resourceType}) has been deleting for ${minutes}m \u2014 still waiting`
|
|
32749
|
+
);
|
|
32750
|
+
});
|
|
32751
|
+
},
|
|
32752
|
+
onTimeout: (elapsedMs) => new ResourceTimeoutError(
|
|
32482
32753
|
logicalId,
|
|
32483
|
-
resource.physicalId,
|
|
32484
32754
|
resource.resourceType,
|
|
32485
|
-
|
|
32486
|
-
|
|
32487
|
-
|
|
32488
|
-
|
|
32489
|
-
|
|
32490
|
-
lastDeleteError = retryError;
|
|
32491
|
-
const msg = retryError instanceof Error ? retryError.message : String(retryError);
|
|
32492
|
-
const isRetryable = msg.includes("Too Many Requests") || msg.includes("has dependencies") || msg.includes("can't be deleted since") || msg.includes("DependencyViolation");
|
|
32493
|
-
if (!isRetryable || attempt >= 3)
|
|
32494
|
-
break;
|
|
32495
|
-
const delay = 5e3 * Math.pow(2, attempt);
|
|
32496
|
-
logger.debug(
|
|
32497
|
-
` \u23F3 Retrying delete ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/3)`
|
|
32498
|
-
);
|
|
32499
|
-
await new Promise((resolve4) => setTimeout(resolve4, delay));
|
|
32755
|
+
stackRegion2,
|
|
32756
|
+
elapsedMs,
|
|
32757
|
+
"DELETE",
|
|
32758
|
+
timeoutMs
|
|
32759
|
+
)
|
|
32500
32760
|
}
|
|
32501
|
-
|
|
32502
|
-
if (lastDeleteError)
|
|
32503
|
-
throw lastDeleteError;
|
|
32761
|
+
);
|
|
32504
32762
|
renderer.removeTask(logicalId);
|
|
32505
32763
|
logger.info(` \u2705 ${logicalId} (${resource.resourceType}) deleted`);
|
|
32506
32764
|
result.deletedCount++;
|
|
@@ -32510,6 +32768,16 @@ Acquiring lock for stack ${stackName}...`);
|
|
|
32510
32768
|
if (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException")) {
|
|
32511
32769
|
logger.debug(` ${logicalId} already deleted, removing from state`);
|
|
32512
32770
|
result.deletedCount++;
|
|
32771
|
+
} else if (error instanceof ResourceTimeoutError) {
|
|
32772
|
+
const wrapped = new ProvisioningError(
|
|
32773
|
+
error.message,
|
|
32774
|
+
resource.resourceType,
|
|
32775
|
+
logicalId,
|
|
32776
|
+
resource.physicalId,
|
|
32777
|
+
error
|
|
32778
|
+
);
|
|
32779
|
+
logger.error(` \u2717 Failed to delete ${logicalId}:`, wrapped.message);
|
|
32780
|
+
result.errorCount++;
|
|
32513
32781
|
} else {
|
|
32514
32782
|
logger.error(` \u2717 Failed to delete ${logicalId}:`, String(error));
|
|
32515
32783
|
result.errorCount++;
|
|
@@ -32552,6 +32820,10 @@ async function destroyCommand(stackArgs, options) {
|
|
|
32552
32820
|
process.env["CDKD_NO_LIVE"] = "1";
|
|
32553
32821
|
}
|
|
32554
32822
|
warnIfDeprecatedRegion(options);
|
|
32823
|
+
validateResourceTimeouts({
|
|
32824
|
+
resourceWarnAfter: options.resourceWarnAfter,
|
|
32825
|
+
resourceTimeout: options.resourceTimeout
|
|
32826
|
+
});
|
|
32555
32827
|
const region = options.region || process.env["AWS_REGION"] || "us-east-1";
|
|
32556
32828
|
const stateBucket = await resolveStateBucketWithDefault(options.stateBucket, region);
|
|
32557
32829
|
logger.info("Starting stack destruction...");
|
|
@@ -32683,7 +32955,9 @@ Preparing to destroy stack: ${stackName}`);
|
|
|
32683
32955
|
baseRegion: region,
|
|
32684
32956
|
...options.profile && { profile: options.profile },
|
|
32685
32957
|
stateBucket,
|
|
32686
|
-
skipConfirmation: options.yes || options.force
|
|
32958
|
+
skipConfirmation: options.yes || options.force,
|
|
32959
|
+
resourceWarnAfterMs: options.resourceWarnAfter,
|
|
32960
|
+
resourceTimeoutMs: options.resourceTimeout
|
|
32687
32961
|
});
|
|
32688
32962
|
}
|
|
32689
32963
|
} finally {
|
|
@@ -32701,6 +32975,7 @@ function createDestroyCommand() {
|
|
|
32701
32975
|
...stateOptions,
|
|
32702
32976
|
...stackOptions,
|
|
32703
32977
|
...destroyOptions,
|
|
32978
|
+
...resourceTimeoutOptions,
|
|
32704
32979
|
...contextOptions
|
|
32705
32980
|
].forEach((opt) => cmd.addOption(opt));
|
|
32706
32981
|
cmd.addOption(deprecatedRegionOption);
|
|
@@ -33536,7 +33811,7 @@ function formatAttributeValue(value) {
|
|
|
33536
33811
|
}
|
|
33537
33812
|
return JSON.stringify(value);
|
|
33538
33813
|
}
|
|
33539
|
-
function
|
|
33814
|
+
function formatDuration2(ms) {
|
|
33540
33815
|
const seconds = Math.floor(ms / 1e3);
|
|
33541
33816
|
if (seconds < 60)
|
|
33542
33817
|
return `${seconds}s`;
|
|
@@ -33549,7 +33824,7 @@ function formatLockSummary(lockInfo) {
|
|
|
33549
33824
|
return "unlocked";
|
|
33550
33825
|
const opStr = lockInfo.operation ? ` (operation: ${lockInfo.operation})` : "";
|
|
33551
33826
|
const expiresInMs = lockInfo.expiresAt - Date.now();
|
|
33552
|
-
const expiresStr = expiresInMs > 0 ? `expires in ${
|
|
33827
|
+
const expiresStr = expiresInMs > 0 ? `expires in ${formatDuration2(expiresInMs)}` : `expired ${formatDuration2(-expiresInMs)} ago`;
|
|
33553
33828
|
return `locked by ${lockInfo.owner}${opStr}, ${expiresStr}`;
|
|
33554
33829
|
}
|
|
33555
33830
|
function createStateResourcesCommand() {
|
|
@@ -33737,6 +34012,10 @@ async function stateDestroyCommand(stackArgs, options) {
|
|
|
33737
34012
|
logger.setLevel("debug");
|
|
33738
34013
|
process.env["CDKD_NO_LIVE"] = "1";
|
|
33739
34014
|
}
|
|
34015
|
+
validateResourceTimeouts({
|
|
34016
|
+
resourceWarnAfter: options.resourceWarnAfter,
|
|
34017
|
+
resourceTimeout: options.resourceTimeout
|
|
34018
|
+
});
|
|
33740
34019
|
if (!options.all && stackArgs.length === 0) {
|
|
33741
34020
|
throw new Error(
|
|
33742
34021
|
"Stack name is required. Usage: cdkd state destroy <stack> [<stack>...] | --all"
|
|
@@ -33836,7 +34115,9 @@ Preparing to destroy stack: ${stackName}${ref.region ? ` (${ref.region})` : ""}`
|
|
|
33836
34115
|
// and the per-stack prompt inside the runner. Per-stack prompts are
|
|
33837
34116
|
// skipped when `options.yes` is set OR `--all` was set (the user
|
|
33838
34117
|
// already accepted the batch prompt).
|
|
33839
|
-
skipConfirmation: options.yes || options.all === true
|
|
34118
|
+
skipConfirmation: options.yes || options.all === true,
|
|
34119
|
+
resourceWarnAfterMs: options.resourceWarnAfter,
|
|
34120
|
+
resourceTimeoutMs: options.resourceTimeout
|
|
33840
34121
|
});
|
|
33841
34122
|
totalErrors += result.errorCount;
|
|
33842
34123
|
}
|
|
@@ -33867,7 +34148,9 @@ function createStateDestroyCommand() {
|
|
|
33867
34148
|
"For removing only the state record (keeping AWS resources intact), use 'cdkd state orphan'."
|
|
33868
34149
|
].join("\n")
|
|
33869
34150
|
).action(withErrorHandling(stateDestroyCommand));
|
|
33870
|
-
[...commonOptions, ...stateOptions].forEach(
|
|
34151
|
+
[...commonOptions, ...stateOptions, ...resourceTimeoutOptions].forEach(
|
|
34152
|
+
(opt) => cmd.addOption(opt)
|
|
34153
|
+
);
|
|
33871
34154
|
cmd.addOption(deprecatedRegionOption);
|
|
33872
34155
|
return cmd;
|
|
33873
34156
|
}
|
|
@@ -34070,7 +34353,11 @@ async function importCommand(stackArg, options) {
|
|
|
34070
34353
|
`State already exists for stack '${stackInfo.stackName}' (${targetRegion}). Pass --force to overwrite. (cdkd state import rebuilds the resource map from AWS, so the existing state \u2014 including any drift you've manually edited \u2014 will be lost.)`
|
|
34071
34354
|
);
|
|
34072
34355
|
}
|
|
34073
|
-
const overrides = parseResourceOverrides(
|
|
34356
|
+
const overrides = parseResourceOverrides(
|
|
34357
|
+
options.resource,
|
|
34358
|
+
options.resourceMapping,
|
|
34359
|
+
options.resourceMappingInline
|
|
34360
|
+
);
|
|
34074
34361
|
if (overrides.size > 0) {
|
|
34075
34362
|
logger.debug(`User-supplied physical IDs: ${[...overrides.keys()].join(", ")}`);
|
|
34076
34363
|
}
|
|
@@ -34213,28 +34500,30 @@ async function importOne(task) {
|
|
|
34213
34500
|
};
|
|
34214
34501
|
}
|
|
34215
34502
|
}
|
|
34216
|
-
function parseResourceOverrides(flags, mappingFile) {
|
|
34503
|
+
function parseResourceOverrides(flags, mappingFile, mappingInline) {
|
|
34217
34504
|
const map = /* @__PURE__ */ new Map();
|
|
34505
|
+
if (mappingFile && mappingInline) {
|
|
34506
|
+
throw new Error(
|
|
34507
|
+
"--resource-mapping and --resource-mapping-inline are mutually exclusive; pass only one."
|
|
34508
|
+
);
|
|
34509
|
+
}
|
|
34218
34510
|
if (mappingFile) {
|
|
34219
|
-
let
|
|
34511
|
+
let raw;
|
|
34220
34512
|
try {
|
|
34221
|
-
|
|
34513
|
+
raw = readFileSync5(mappingFile, "utf-8");
|
|
34222
34514
|
} catch (err) {
|
|
34223
34515
|
throw new Error(
|
|
34224
34516
|
`Failed to read --resource-mapping file '${mappingFile}': ` + (err instanceof Error ? err.message : String(err))
|
|
34225
34517
|
);
|
|
34226
34518
|
}
|
|
34227
|
-
|
|
34228
|
-
|
|
34229
|
-
|
|
34230
|
-
);
|
|
34519
|
+
const parsed = parseMappingJson(raw, `--resource-mapping file '${mappingFile}'`);
|
|
34520
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
34521
|
+
map.set(key, value);
|
|
34231
34522
|
}
|
|
34523
|
+
}
|
|
34524
|
+
if (mappingInline) {
|
|
34525
|
+
const parsed = parseMappingJson(mappingInline, "--resource-mapping-inline");
|
|
34232
34526
|
for (const [key, value] of Object.entries(parsed)) {
|
|
34233
|
-
if (typeof value !== "string") {
|
|
34234
|
-
throw new Error(
|
|
34235
|
-
`--resource-mapping: value for '${key}' must be a string, got ${typeof value}`
|
|
34236
|
-
);
|
|
34237
|
-
}
|
|
34238
34527
|
map.set(key, value);
|
|
34239
34528
|
}
|
|
34240
34529
|
}
|
|
@@ -34247,6 +34536,27 @@ function parseResourceOverrides(flags, mappingFile) {
|
|
|
34247
34536
|
}
|
|
34248
34537
|
return map;
|
|
34249
34538
|
}
|
|
34539
|
+
function parseMappingJson(raw, source) {
|
|
34540
|
+
let parsed;
|
|
34541
|
+
try {
|
|
34542
|
+
parsed = JSON.parse(raw);
|
|
34543
|
+
} catch (err) {
|
|
34544
|
+
throw new Error(
|
|
34545
|
+
`Failed to parse ${source} as JSON: ` + (err instanceof Error ? err.message : String(err))
|
|
34546
|
+
);
|
|
34547
|
+
}
|
|
34548
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
34549
|
+
throw new Error(`${source} must be a JSON object {logicalId: physicalId}`);
|
|
34550
|
+
}
|
|
34551
|
+
const out = {};
|
|
34552
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
34553
|
+
if (typeof value !== "string") {
|
|
34554
|
+
throw new Error(`${source}: value for '${key}' must be a string, got ${typeof value}`);
|
|
34555
|
+
}
|
|
34556
|
+
out[key] = value;
|
|
34557
|
+
}
|
|
34558
|
+
return out;
|
|
34559
|
+
}
|
|
34250
34560
|
function readCdkPath(resource) {
|
|
34251
34561
|
const meta = resource.Metadata;
|
|
34252
34562
|
if (!meta)
|
|
@@ -34347,7 +34657,10 @@ function createImportCommand() {
|
|
|
34347
34657
|
[]
|
|
34348
34658
|
).option(
|
|
34349
34659
|
"--resource-mapping <file>",
|
|
34350
|
-
"Path to a JSON file of {logicalId: physicalId} overrides (CDK CLI `cdk import --resource-mapping` compatible). Implies selective mode unless --auto is set."
|
|
34660
|
+
"Path to a JSON file of {logicalId: physicalId} overrides (CDK CLI `cdk import --resource-mapping` compatible). Implies selective mode unless --auto is set. Mutually exclusive with --resource-mapping-inline."
|
|
34661
|
+
).option(
|
|
34662
|
+
"--resource-mapping-inline <json>",
|
|
34663
|
+
"Inline JSON object of {logicalId: physicalId} overrides (CDK CLI `cdk import --resource-mapping-inline` compatible). Same shape as --resource-mapping but supplied as a string \u2014 useful for non-TTY CI scripts that do not want a separate file. Implies selective mode unless --auto is set. Mutually exclusive with --resource-mapping."
|
|
34351
34664
|
).option(
|
|
34352
34665
|
"--auto",
|
|
34353
34666
|
"Hybrid mode: when explicit overrides are supplied, ALSO tag-import every other resource in the template. Without this flag, --resource / --resource-mapping behave as a whitelist (CDK CLI parity).",
|
|
@@ -34393,7 +34706,7 @@ function reorderArgs(argv) {
|
|
|
34393
34706
|
}
|
|
34394
34707
|
async function main() {
|
|
34395
34708
|
const program = new Command13();
|
|
34396
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
34709
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.25.0");
|
|
34397
34710
|
program.addCommand(createBootstrapCommand());
|
|
34398
34711
|
program.addCommand(createSynthCommand());
|
|
34399
34712
|
program.addCommand(createListCommand());
|