@go-to-k/cdkd 0.24.0 → 0.26.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 -14
- package/dist/cli.js +1268 -375
- package/dist/cli.js.map +4 -4
- package/dist/go-to-k-cdkd-0.26.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.24.0.tgz +0 -0
package/dist/cli.js
CHANGED
|
@@ -508,6 +508,104 @@ 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 RESOURCE_TYPE_REGEX = /^[A-Z][A-Za-z0-9]+::[A-Z][A-Za-z0-9]+::[A-Z][A-Za-z0-9]+$/;
|
|
532
|
+
function parseResourceTimeoutToken(flagName) {
|
|
533
|
+
return (raw, previous) => {
|
|
534
|
+
const acc = previous ?? { perTypeMs: {} };
|
|
535
|
+
if (!acc.perTypeMs)
|
|
536
|
+
acc.perTypeMs = {};
|
|
537
|
+
const eqIndex = raw.indexOf("=");
|
|
538
|
+
if (eqIndex === -1) {
|
|
539
|
+
acc.globalMs = parseDuration(raw);
|
|
540
|
+
return acc;
|
|
541
|
+
}
|
|
542
|
+
const typePart = raw.substring(0, eqIndex).trim();
|
|
543
|
+
const durationPart = raw.substring(eqIndex + 1).trim();
|
|
544
|
+
if (!RESOURCE_TYPE_REGEX.test(typePart)) {
|
|
545
|
+
throw new Error(
|
|
546
|
+
`Invalid ${flagName} value "${raw}": left-hand side must be a CloudFormation resource type like AWS::Service::Resource (got "${typePart}")`
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
if (durationPart.length === 0) {
|
|
550
|
+
throw new Error(
|
|
551
|
+
`Invalid ${flagName} value "${raw}": missing duration after '=' (e.g. ${typePart}=1h)`
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
let ms;
|
|
555
|
+
try {
|
|
556
|
+
ms = parseDuration(durationPart);
|
|
557
|
+
} catch (err) {
|
|
558
|
+
const inner = err instanceof Error ? err.message : String(err);
|
|
559
|
+
throw new Error(`Invalid ${flagName} value "${raw}": ${inner}`);
|
|
560
|
+
}
|
|
561
|
+
acc.perTypeMs[typePart] = ms;
|
|
562
|
+
return acc;
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
var resourceTimeoutOptions = [
|
|
566
|
+
// Default is `undefined` (NOT a pre-seeded ResourceTimeoutOption) — the
|
|
567
|
+
// command handler resolves missing globalMs to DEFAULT_RESOURCE_*_MS
|
|
568
|
+
// at the call site. Pre-seeding here would force every accumulator
|
|
569
|
+
// call to carry a snapshot, and would also surprise unit tests that
|
|
570
|
+
// expect `opts.resourceTimeout` to be `undefined` when the flag is not
|
|
571
|
+
// passed.
|
|
572
|
+
new Option(
|
|
573
|
+
"--resource-warn-after <duration_or_type=duration>",
|
|
574
|
+
"Warn when a single resource operation exceeds this wall-clock duration. Repeatable: pass a bare duration (e.g. 5m) to set the global default, or TYPE=DURATION (e.g. AWS::CloudFront::Distribution=10m) for a per-type override."
|
|
575
|
+
).default(void 0, "5m").argParser(parseResourceTimeoutToken("--resource-warn-after")),
|
|
576
|
+
new Option(
|
|
577
|
+
"--resource-timeout <duration_or_type=duration>",
|
|
578
|
+
"Abort a single resource operation that exceeds this wall-clock duration. Repeatable: pass a bare duration (e.g. 30m) to set the global default, or TYPE=DURATION (e.g. AWS::CloudFront::Distribution=1h) for a per-type override. Custom-Resource-heavy stacks may need to raise this above the default 30m (the Custom Resource provider's polling cap is 1h)."
|
|
579
|
+
).default(void 0, "30m").argParser(parseResourceTimeoutToken("--resource-timeout"))
|
|
580
|
+
];
|
|
581
|
+
function validateResourceTimeouts(opts) {
|
|
582
|
+
const warn = opts.resourceWarnAfter;
|
|
583
|
+
const timeout = opts.resourceTimeout;
|
|
584
|
+
const globalWarn = warn?.globalMs;
|
|
585
|
+
const globalTimeout = timeout?.globalMs;
|
|
586
|
+
if (typeof globalWarn === "number" && typeof globalTimeout === "number") {
|
|
587
|
+
if (globalWarn >= globalTimeout) {
|
|
588
|
+
throw new Error(
|
|
589
|
+
`--resource-warn-after (${globalWarn}ms) must be less than --resource-timeout (${globalTimeout}ms)`
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const warnPerType = warn?.perTypeMs ?? {};
|
|
594
|
+
const timeoutPerType = timeout?.perTypeMs ?? {};
|
|
595
|
+
const types = /* @__PURE__ */ new Set([...Object.keys(warnPerType), ...Object.keys(timeoutPerType)]);
|
|
596
|
+
for (const t of types) {
|
|
597
|
+
const effectiveWarn = warnPerType[t] ?? globalWarn;
|
|
598
|
+
const effectiveTimeout = timeoutPerType[t] ?? globalTimeout;
|
|
599
|
+
if (typeof effectiveWarn !== "number" || typeof effectiveTimeout !== "number") {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (effectiveWarn >= effectiveTimeout) {
|
|
603
|
+
throw new Error(
|
|
604
|
+
`--resource-warn-after for ${t} (${effectiveWarn}ms) must be less than --resource-timeout for ${t} (${effectiveTimeout}ms)`
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
511
609
|
var deployOptions = [
|
|
512
610
|
new Option("--concurrency <number>", "Maximum concurrent resource operations").default(10).argParser((value) => parseInt(value, 10)),
|
|
513
611
|
new Option("--stack-concurrency <number>", "Maximum concurrent stack deployments").default(4).argParser((value) => parseInt(value, 10)),
|
|
@@ -523,7 +621,8 @@ var deployOptions = [
|
|
|
523
621
|
new Option(
|
|
524
622
|
"-e, --exclusively",
|
|
525
623
|
"Only deploy requested stacks, do not include dependencies"
|
|
526
|
-
).default(false)
|
|
624
|
+
).default(false),
|
|
625
|
+
...resourceTimeoutOptions
|
|
527
626
|
];
|
|
528
627
|
var contextOptions = [
|
|
529
628
|
new Option(
|
|
@@ -718,6 +817,22 @@ var LiveRenderer = class {
|
|
|
718
817
|
if (this.active)
|
|
719
818
|
this.draw();
|
|
720
819
|
}
|
|
820
|
+
/**
|
|
821
|
+
* Replace the label of a previously-added task in place. No-op if the
|
|
822
|
+
* task is not currently tracked (e.g. it already finished). Used by the
|
|
823
|
+
* per-resource deadline wrapper to surface a "[taking longer than
|
|
824
|
+
* expected, Nm+]" suffix without disturbing the elapsed-time counter
|
|
825
|
+
* the renderer tracks via `startedAt`.
|
|
826
|
+
*/
|
|
827
|
+
updateTaskLabel(id, label) {
|
|
828
|
+
const stackName = getCurrentStackName();
|
|
829
|
+
const task = this.tasks.get(scopedKey(id, stackName));
|
|
830
|
+
if (!task)
|
|
831
|
+
return;
|
|
832
|
+
task.label = label;
|
|
833
|
+
if (this.active)
|
|
834
|
+
this.draw();
|
|
835
|
+
}
|
|
721
836
|
/**
|
|
722
837
|
* Print content above the live area. Clears the live area, runs the writer,
|
|
723
838
|
* then redraws the live area. When the renderer is inactive, the writer
|
|
@@ -1011,6 +1126,38 @@ var ProvisioningError = class _ProvisioningError extends CdkdError {
|
|
|
1011
1126
|
Object.setPrototypeOf(this, _ProvisioningError.prototype);
|
|
1012
1127
|
}
|
|
1013
1128
|
};
|
|
1129
|
+
var ResourceTimeoutError = class _ResourceTimeoutError extends CdkdError {
|
|
1130
|
+
constructor(logicalId, resourceType, region, elapsedMs, operation, timeoutMs) {
|
|
1131
|
+
const elapsedLabel = formatDuration(elapsedMs);
|
|
1132
|
+
const timeoutLabel = formatDuration(timeoutMs);
|
|
1133
|
+
super(
|
|
1134
|
+
`Resource ${logicalId} (${resourceType}) in ${region} timed out after ${timeoutLabel} during ${operation} (elapsed ${elapsedLabel}).
|
|
1135
|
+
This may indicate a stuck Cloud Control polling loop, hung Custom Resource, or
|
|
1136
|
+
slow ENI provisioning. Re-run with --resource-timeout 1h if the resource genuinely
|
|
1137
|
+
needs more time, or --verbose to see the underlying provider activity.`,
|
|
1138
|
+
"RESOURCE_TIMEOUT"
|
|
1139
|
+
);
|
|
1140
|
+
this.logicalId = logicalId;
|
|
1141
|
+
this.resourceType = resourceType;
|
|
1142
|
+
this.region = region;
|
|
1143
|
+
this.elapsedMs = elapsedMs;
|
|
1144
|
+
this.operation = operation;
|
|
1145
|
+
this.timeoutMs = timeoutMs;
|
|
1146
|
+
this.name = "ResourceTimeoutError";
|
|
1147
|
+
Object.setPrototypeOf(this, _ResourceTimeoutError.prototype);
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
function formatDuration(ms) {
|
|
1151
|
+
if (ms < 6e4) {
|
|
1152
|
+
return `${Math.round(ms / 1e3)}s`;
|
|
1153
|
+
}
|
|
1154
|
+
const totalMinutes = Math.round(ms / 6e4);
|
|
1155
|
+
if (totalMinutes < 60)
|
|
1156
|
+
return `${totalMinutes}m`;
|
|
1157
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
1158
|
+
const minutes = totalMinutes % 60;
|
|
1159
|
+
return minutes === 0 ? `${hours}h` : `${hours}h${minutes}m`;
|
|
1160
|
+
}
|
|
1014
1161
|
var DependencyError = class _DependencyError extends CdkdError {
|
|
1015
1162
|
constructor(message, cause) {
|
|
1016
1163
|
super(message, "DEPENDENCY_ERROR", cause);
|
|
@@ -30781,7 +30928,85 @@ async function withRetry(operation, logicalId, opts = {}) {
|
|
|
30781
30928
|
throw lastError;
|
|
30782
30929
|
}
|
|
30783
30930
|
|
|
30931
|
+
// src/deployment/resource-deadline.ts
|
|
30932
|
+
var InvalidResourceDeadlineError = class extends Error {
|
|
30933
|
+
constructor(message) {
|
|
30934
|
+
super(message);
|
|
30935
|
+
this.name = "InvalidResourceDeadlineError";
|
|
30936
|
+
}
|
|
30937
|
+
};
|
|
30938
|
+
function validateOptions(opts) {
|
|
30939
|
+
const { warnAfterMs, timeoutMs } = opts;
|
|
30940
|
+
if (!Number.isFinite(warnAfterMs) || !Number.isFinite(timeoutMs) || warnAfterMs <= 0 || timeoutMs <= 0) {
|
|
30941
|
+
throw new InvalidResourceDeadlineError(
|
|
30942
|
+
`withResourceDeadline: warnAfterMs and timeoutMs must be positive finite numbers (got warnAfterMs=${warnAfterMs}, timeoutMs=${timeoutMs})`
|
|
30943
|
+
);
|
|
30944
|
+
}
|
|
30945
|
+
if (warnAfterMs >= timeoutMs) {
|
|
30946
|
+
throw new InvalidResourceDeadlineError(
|
|
30947
|
+
`withResourceDeadline: warnAfterMs (${warnAfterMs}ms) must be less than timeoutMs (${timeoutMs}ms)`
|
|
30948
|
+
);
|
|
30949
|
+
}
|
|
30950
|
+
}
|
|
30951
|
+
async function withResourceDeadline(operation, opts) {
|
|
30952
|
+
validateOptions(opts);
|
|
30953
|
+
const startedAt = Date.now();
|
|
30954
|
+
return new Promise((resolve4, reject) => {
|
|
30955
|
+
let settled = false;
|
|
30956
|
+
let warnTimer;
|
|
30957
|
+
let timeoutTimer;
|
|
30958
|
+
const cleanup = () => {
|
|
30959
|
+
if (warnTimer !== void 0)
|
|
30960
|
+
clearTimeout(warnTimer);
|
|
30961
|
+
if (timeoutTimer !== void 0)
|
|
30962
|
+
clearTimeout(timeoutTimer);
|
|
30963
|
+
warnTimer = void 0;
|
|
30964
|
+
timeoutTimer = void 0;
|
|
30965
|
+
};
|
|
30966
|
+
if (opts.onWarn) {
|
|
30967
|
+
warnTimer = setTimeout(() => {
|
|
30968
|
+
if (settled)
|
|
30969
|
+
return;
|
|
30970
|
+
try {
|
|
30971
|
+
opts.onWarn(Date.now() - startedAt);
|
|
30972
|
+
} catch {
|
|
30973
|
+
}
|
|
30974
|
+
}, opts.warnAfterMs);
|
|
30975
|
+
if (typeof warnTimer.unref === "function")
|
|
30976
|
+
warnTimer.unref();
|
|
30977
|
+
}
|
|
30978
|
+
timeoutTimer = setTimeout(() => {
|
|
30979
|
+
if (settled)
|
|
30980
|
+
return;
|
|
30981
|
+
settled = true;
|
|
30982
|
+
cleanup();
|
|
30983
|
+
const elapsed = Date.now() - startedAt;
|
|
30984
|
+
reject(opts.onTimeout(elapsed));
|
|
30985
|
+
}, opts.timeoutMs);
|
|
30986
|
+
if (typeof timeoutTimer.unref === "function")
|
|
30987
|
+
timeoutTimer.unref();
|
|
30988
|
+
Promise.resolve().then(() => operation()).then(
|
|
30989
|
+
(value) => {
|
|
30990
|
+
if (settled)
|
|
30991
|
+
return;
|
|
30992
|
+
settled = true;
|
|
30993
|
+
cleanup();
|
|
30994
|
+
resolve4(value);
|
|
30995
|
+
},
|
|
30996
|
+
(err) => {
|
|
30997
|
+
if (settled)
|
|
30998
|
+
return;
|
|
30999
|
+
settled = true;
|
|
31000
|
+
cleanup();
|
|
31001
|
+
reject(err);
|
|
31002
|
+
}
|
|
31003
|
+
);
|
|
31004
|
+
});
|
|
31005
|
+
}
|
|
31006
|
+
|
|
30784
31007
|
// src/deployment/deploy-engine.ts
|
|
31008
|
+
var DEFAULT_RESOURCE_WARN_AFTER_MS = 5 * 60 * 1e3;
|
|
31009
|
+
var DEFAULT_RESOURCE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
30785
31010
|
var InterruptedError = class extends Error {
|
|
30786
31011
|
constructor() {
|
|
30787
31012
|
super("Deployment interrupted by user (Ctrl+C)");
|
|
@@ -30802,6 +31027,8 @@ var DeployEngine = class {
|
|
|
30802
31027
|
this.options.dryRun = options.dryRun ?? false;
|
|
30803
31028
|
this.options.lockTimeout = options.lockTimeout ?? 5 * 60 * 1e3;
|
|
30804
31029
|
this.options.noRollback = options.noRollback ?? false;
|
|
31030
|
+
this.options.resourceWarnAfterMs = options.resourceWarnAfterMs ?? DEFAULT_RESOURCE_WARN_AFTER_MS;
|
|
31031
|
+
this.options.resourceTimeoutMs = options.resourceTimeoutMs ?? DEFAULT_RESOURCE_TIMEOUT_MS;
|
|
30805
31032
|
}
|
|
30806
31033
|
logger = getLogger().child("DeployEngine");
|
|
30807
31034
|
resolver;
|
|
@@ -31400,259 +31627,305 @@ var DeployEngine = class {
|
|
|
31400
31627
|
*/
|
|
31401
31628
|
async provisionResource(logicalId, change, stateResources, stackName, template, parameterValues, conditions, counts, progress) {
|
|
31402
31629
|
const resourceType = change.resourceType;
|
|
31403
|
-
const provider = this.providerRegistry.getProvider(resourceType);
|
|
31404
31630
|
const renderer = getLiveRenderer();
|
|
31405
31631
|
const needsReplacement = change.changeType === "UPDATE" && (change.propertyChanges?.some((pc) => pc.requiresReplacement) ?? false);
|
|
31406
31632
|
const verb = change.changeType === "CREATE" ? "Creating" : change.changeType === "DELETE" ? "Deleting" : needsReplacement ? "Replacing" : "Updating";
|
|
31407
|
-
|
|
31408
|
-
|
|
31409
|
-
|
|
31410
|
-
|
|
31411
|
-
|
|
31412
|
-
|
|
31633
|
+
const baseLabel = `${verb} ${logicalId} (${resourceType})`;
|
|
31634
|
+
renderer.addTask(logicalId, baseLabel);
|
|
31635
|
+
const operationKind = change.changeType === "CREATE" ? "CREATE" : change.changeType === "DELETE" ? "DELETE" : "UPDATE";
|
|
31636
|
+
const warnAfterMs = this.options.resourceWarnAfterByType?.[resourceType] ?? this.options.resourceWarnAfterMs ?? DEFAULT_RESOURCE_WARN_AFTER_MS;
|
|
31637
|
+
const timeoutMs = this.options.resourceTimeoutByType?.[resourceType] ?? this.options.resourceTimeoutMs ?? DEFAULT_RESOURCE_TIMEOUT_MS;
|
|
31638
|
+
try {
|
|
31639
|
+
await withResourceDeadline(
|
|
31640
|
+
async () => {
|
|
31641
|
+
await this.provisionResourceBody(
|
|
31642
|
+
logicalId,
|
|
31643
|
+
change,
|
|
31644
|
+
stateResources,
|
|
31645
|
+
stackName,
|
|
31413
31646
|
template,
|
|
31414
|
-
|
|
31415
|
-
|
|
31416
|
-
|
|
31417
|
-
|
|
31418
|
-
|
|
31419
|
-
|
|
31420
|
-
|
|
31421
|
-
|
|
31422
|
-
|
|
31423
|
-
|
|
31647
|
+
parameterValues,
|
|
31648
|
+
conditions,
|
|
31649
|
+
counts,
|
|
31650
|
+
progress
|
|
31651
|
+
);
|
|
31652
|
+
},
|
|
31653
|
+
{
|
|
31654
|
+
warnAfterMs,
|
|
31655
|
+
timeoutMs,
|
|
31656
|
+
onWarn: (elapsedMs) => {
|
|
31657
|
+
const minutes = Math.max(1, Math.round(elapsedMs / 6e4));
|
|
31658
|
+
const warnSuffix = ` [taking longer than expected, ${minutes}m+]`;
|
|
31659
|
+
renderer.updateTaskLabel(logicalId, `${baseLabel}${warnSuffix}`);
|
|
31660
|
+
renderer.printAbove(() => {
|
|
31661
|
+
this.logger.warn(
|
|
31662
|
+
`${logicalId} (${resourceType}) has been ${operationKind === "CREATE" ? "creating" : operationKind === "DELETE" ? "deleting" : "updating"} for ${minutes}m \u2014 still waiting`
|
|
31663
|
+
);
|
|
31664
|
+
});
|
|
31665
|
+
},
|
|
31666
|
+
onTimeout: (elapsedMs) => new ResourceTimeoutError(
|
|
31667
|
+
logicalId,
|
|
31668
|
+
resourceType,
|
|
31669
|
+
this.stackRegion,
|
|
31670
|
+
elapsedMs,
|
|
31671
|
+
operationKind,
|
|
31672
|
+
timeoutMs
|
|
31673
|
+
)
|
|
31674
|
+
}
|
|
31675
|
+
);
|
|
31676
|
+
} catch (error) {
|
|
31677
|
+
renderer.removeTask(logicalId);
|
|
31678
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
31679
|
+
this.logger.error(`Failed to ${change.changeType.toLowerCase()} ${logicalId}: ${message}`);
|
|
31680
|
+
throw new ProvisioningError(
|
|
31681
|
+
`Failed to ${change.changeType.toLowerCase()} resource ${logicalId}`,
|
|
31682
|
+
resourceType,
|
|
31683
|
+
logicalId,
|
|
31684
|
+
stateResources[logicalId]?.physicalId,
|
|
31685
|
+
error instanceof Error ? error : void 0
|
|
31686
|
+
);
|
|
31687
|
+
} finally {
|
|
31688
|
+
renderer.removeTask(logicalId);
|
|
31689
|
+
}
|
|
31690
|
+
}
|
|
31691
|
+
/**
|
|
31692
|
+
* Inner body of provisionResource, extracted so the outer wrapper can
|
|
31693
|
+
* apply the per-resource deadline (`withResourceDeadline`) without
|
|
31694
|
+
* having the timeout / warn timer code dwarf the real provisioning
|
|
31695
|
+
* logic. Behaviour is unchanged from the pre-deadline implementation.
|
|
31696
|
+
*/
|
|
31697
|
+
async provisionResourceBody(logicalId, change, stateResources, stackName, template, parameterValues, conditions, counts, progress) {
|
|
31698
|
+
const resourceType = change.resourceType;
|
|
31699
|
+
const provider = this.providerRegistry.getProvider(resourceType);
|
|
31700
|
+
const renderer = getLiveRenderer();
|
|
31701
|
+
switch (change.changeType) {
|
|
31702
|
+
case "CREATE": {
|
|
31703
|
+
const desiredProps = change.desiredProperties || {};
|
|
31704
|
+
const context = {
|
|
31705
|
+
template,
|
|
31706
|
+
resources: stateResources,
|
|
31707
|
+
...parameterValues && Object.keys(parameterValues).length > 0 && { parameters: parameterValues },
|
|
31708
|
+
...conditions && Object.keys(conditions).length > 0 && { conditions },
|
|
31709
|
+
stateBackend: this.stateBackend,
|
|
31710
|
+
stackName
|
|
31711
|
+
};
|
|
31712
|
+
const resolvedProps = await this.resolver.resolve(desiredProps, context);
|
|
31713
|
+
const { provider: createProvider, properties: createProps } = this.selectProviderWithSafetyNet(provider, resourceType, resolvedProps, logicalId);
|
|
31714
|
+
const result = await this.withRetry(
|
|
31715
|
+
() => createProvider.create(logicalId, resourceType, createProps),
|
|
31716
|
+
logicalId
|
|
31717
|
+
);
|
|
31718
|
+
const dependencies = this.extractAllDependencies(template, logicalId);
|
|
31719
|
+
stateResources[logicalId] = {
|
|
31720
|
+
physicalId: result.physicalId,
|
|
31721
|
+
resourceType,
|
|
31722
|
+
properties: resolvedProps,
|
|
31723
|
+
...result.attributes && { attributes: result.attributes },
|
|
31724
|
+
...dependencies && dependencies.length > 0 && { dependencies }
|
|
31725
|
+
};
|
|
31726
|
+
if (counts)
|
|
31727
|
+
counts.created++;
|
|
31728
|
+
if (progress)
|
|
31729
|
+
progress.current++;
|
|
31730
|
+
const createPrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
|
|
31731
|
+
renderer.removeTask(logicalId);
|
|
31732
|
+
this.logger.info(`${createPrefix}\u2705 ${logicalId} (${resourceType}) created`);
|
|
31733
|
+
break;
|
|
31734
|
+
}
|
|
31735
|
+
case "UPDATE": {
|
|
31736
|
+
const currentResource = stateResources[logicalId];
|
|
31737
|
+
if (!currentResource) {
|
|
31738
|
+
throw new Error(`Cannot update ${logicalId}: resource not found in state`);
|
|
31739
|
+
}
|
|
31740
|
+
const desiredProps = change.desiredProperties || {};
|
|
31741
|
+
const currentProps = change.currentProperties || {};
|
|
31742
|
+
const context = {
|
|
31743
|
+
template,
|
|
31744
|
+
resources: stateResources,
|
|
31745
|
+
...parameterValues && Object.keys(parameterValues).length > 0 && { parameters: parameterValues },
|
|
31746
|
+
...conditions && Object.keys(conditions).length > 0 && { conditions },
|
|
31747
|
+
stateBackend: this.stateBackend,
|
|
31748
|
+
stackName
|
|
31749
|
+
};
|
|
31750
|
+
const resolvedProps = await this.resolver.resolve(desiredProps, context);
|
|
31751
|
+
if (JSON.stringify(resolvedProps) === JSON.stringify(currentProps)) {
|
|
31752
|
+
this.logger.debug(
|
|
31753
|
+
`Skipping ${logicalId}: no actual changes after intrinsic function resolution`
|
|
31754
|
+
);
|
|
31755
|
+
if (counts)
|
|
31756
|
+
counts.skipped++;
|
|
31757
|
+
break;
|
|
31758
|
+
}
|
|
31759
|
+
const needsReplacement = change.propertyChanges?.some((pc) => pc.requiresReplacement);
|
|
31760
|
+
const dependencies = this.extractAllDependencies(template, logicalId);
|
|
31761
|
+
if (needsReplacement) {
|
|
31762
|
+
const replacedProps = change.propertyChanges?.filter((pc) => pc.requiresReplacement).map((pc) => pc.path).join(", ");
|
|
31763
|
+
this.logger.info(
|
|
31764
|
+
`Replacing ${logicalId} (${resourceType}) - immutable properties changed: ${replacedProps}`
|
|
31765
|
+
);
|
|
31766
|
+
this.logger.info(` Creating new ${logicalId}...`);
|
|
31767
|
+
const { provider: replaceProvider, properties: replaceProps } = this.selectProviderWithSafetyNet(provider, resourceType, resolvedProps, logicalId);
|
|
31768
|
+
const createResult = await this.withRetry(
|
|
31769
|
+
() => replaceProvider.create(logicalId, resourceType, replaceProps),
|
|
31424
31770
|
logicalId
|
|
31425
31771
|
);
|
|
31426
|
-
const
|
|
31772
|
+
const updateReplacePolicy = template?.Resources?.[logicalId]?.UpdateReplacePolicy;
|
|
31773
|
+
if (updateReplacePolicy === "Retain") {
|
|
31774
|
+
this.logger.info(
|
|
31775
|
+
` Retaining old ${logicalId} (${currentResource.physicalId}) - UpdateReplacePolicy: Retain`
|
|
31776
|
+
);
|
|
31777
|
+
} else {
|
|
31778
|
+
this.logger.info(` Deleting old ${logicalId} (${currentResource.physicalId})...`);
|
|
31779
|
+
try {
|
|
31780
|
+
await provider.delete(
|
|
31781
|
+
logicalId,
|
|
31782
|
+
currentResource.physicalId,
|
|
31783
|
+
resourceType,
|
|
31784
|
+
currentResource.properties,
|
|
31785
|
+
{ expectedRegion: this.stackRegion }
|
|
31786
|
+
);
|
|
31787
|
+
this.logger.info(` \u2713 Old resource deleted`);
|
|
31788
|
+
} catch (deleteError) {
|
|
31789
|
+
this.logger.warn(
|
|
31790
|
+
` \u26A0 Failed to delete old resource ${logicalId} (${currentResource.physicalId}): ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`
|
|
31791
|
+
);
|
|
31792
|
+
}
|
|
31793
|
+
}
|
|
31427
31794
|
stateResources[logicalId] = {
|
|
31428
|
-
physicalId:
|
|
31795
|
+
physicalId: createResult.physicalId,
|
|
31429
31796
|
resourceType,
|
|
31430
31797
|
properties: resolvedProps,
|
|
31431
|
-
...
|
|
31798
|
+
...createResult.attributes && { attributes: createResult.attributes },
|
|
31432
31799
|
...dependencies && dependencies.length > 0 && { dependencies }
|
|
31433
31800
|
};
|
|
31434
31801
|
if (counts)
|
|
31435
|
-
counts.
|
|
31802
|
+
counts.updated++;
|
|
31436
31803
|
if (progress)
|
|
31437
31804
|
progress.current++;
|
|
31438
|
-
const
|
|
31805
|
+
const replacePrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
|
|
31439
31806
|
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),
|
|
31807
|
+
this.logger.info(`${replacePrefix}\u2705 ${logicalId} (${resourceType}) replaced`);
|
|
31808
|
+
} else {
|
|
31809
|
+
this.logger.debug(`Updating ${logicalId} (${resourceType})`);
|
|
31810
|
+
const { provider: updateProvider, properties: updateProps } = this.selectProviderWithSafetyNet(provider, resourceType, resolvedProps, logicalId);
|
|
31811
|
+
let result;
|
|
31812
|
+
try {
|
|
31813
|
+
result = await this.withRetry(
|
|
31814
|
+
() => updateProvider.update(
|
|
31815
|
+
logicalId,
|
|
31816
|
+
currentResource.physicalId,
|
|
31817
|
+
resourceType,
|
|
31818
|
+
updateProps,
|
|
31819
|
+
currentProps
|
|
31820
|
+
),
|
|
31478
31821
|
logicalId
|
|
31479
31822
|
);
|
|
31480
|
-
|
|
31481
|
-
|
|
31823
|
+
} catch (updateError) {
|
|
31824
|
+
const msg = updateError instanceof Error ? updateError.message : String(updateError);
|
|
31825
|
+
if (msg.includes("UnsupportedActionException") || msg.includes("does not support UPDATE")) {
|
|
31482
31826
|
this.logger.info(
|
|
31483
|
-
`
|
|
31827
|
+
`UPDATE not supported for ${logicalId} (${resourceType}), replacing (DELETE \u2192 CREATE)`
|
|
31484
31828
|
);
|
|
31485
|
-
} else {
|
|
31486
|
-
this.logger.info(` Deleting old ${logicalId} (${currentResource.physicalId})...`);
|
|
31487
31829
|
try {
|
|
31488
31830
|
await provider.delete(
|
|
31489
31831
|
logicalId,
|
|
31490
31832
|
currentResource.physicalId,
|
|
31491
31833
|
resourceType,
|
|
31492
|
-
|
|
31834
|
+
currentProps,
|
|
31493
31835
|
{ expectedRegion: this.stackRegion }
|
|
31494
31836
|
);
|
|
31495
|
-
this.logger.info(` \u2713 Old resource deleted`);
|
|
31496
31837
|
} 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 }
|
|
31838
|
+
const deleteMsg = deleteError instanceof Error ? deleteError.message : String(deleteError);
|
|
31839
|
+
if (deleteMsg.includes("does not exist") || deleteMsg.includes("not found") || deleteMsg.includes("NotFound")) {
|
|
31840
|
+
this.logger.debug(
|
|
31841
|
+
`Old resource ${logicalId} already gone, proceeding with CREATE`
|
|
31544
31842
|
);
|
|
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
|
-
}
|
|
31843
|
+
} else {
|
|
31844
|
+
throw deleteError;
|
|
31554
31845
|
}
|
|
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
31846
|
}
|
|
31573
|
-
|
|
31574
|
-
|
|
31575
|
-
|
|
31576
|
-
|
|
31847
|
+
const { provider: replProvider, properties: replProps } = this.selectProviderWithSafetyNet(provider, resourceType, resolvedProps, logicalId);
|
|
31848
|
+
const createResult = await this.withRetry(
|
|
31849
|
+
() => replProvider.create(logicalId, resourceType, replProps),
|
|
31850
|
+
logicalId
|
|
31577
31851
|
);
|
|
31852
|
+
result = {
|
|
31853
|
+
physicalId: createResult.physicalId,
|
|
31854
|
+
attributes: createResult.attributes,
|
|
31855
|
+
wasReplaced: true
|
|
31856
|
+
};
|
|
31857
|
+
} else {
|
|
31858
|
+
throw updateError;
|
|
31578
31859
|
}
|
|
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
|
-
}
|
|
31594
|
-
break;
|
|
31595
|
-
}
|
|
31596
|
-
case "DELETE": {
|
|
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
31860
|
}
|
|
31607
|
-
|
|
31608
|
-
|
|
31609
|
-
|
|
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
|
|
31861
|
+
if (result.wasReplaced) {
|
|
31862
|
+
this.logger.info(
|
|
31863
|
+
`Resource ${logicalId} was replaced: ${currentResource.physicalId} -> ${result.physicalId}`
|
|
31621
31864
|
);
|
|
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
31865
|
}
|
|
31632
|
-
|
|
31866
|
+
stateResources[logicalId] = {
|
|
31867
|
+
physicalId: result.physicalId,
|
|
31868
|
+
resourceType,
|
|
31869
|
+
properties: resolvedProps,
|
|
31870
|
+
...result.attributes && { attributes: result.attributes },
|
|
31871
|
+
...dependencies && dependencies.length > 0 && { dependencies }
|
|
31872
|
+
};
|
|
31633
31873
|
if (counts)
|
|
31634
|
-
counts.
|
|
31874
|
+
counts.updated++;
|
|
31635
31875
|
if (progress)
|
|
31636
31876
|
progress.current++;
|
|
31637
|
-
const
|
|
31877
|
+
const updatePrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
|
|
31638
31878
|
renderer.removeTask(logicalId);
|
|
31639
|
-
this.logger.info(`${
|
|
31879
|
+
this.logger.info(`${updatePrefix}\u2705 ${logicalId} (${resourceType}) updated`);
|
|
31880
|
+
}
|
|
31881
|
+
break;
|
|
31882
|
+
}
|
|
31883
|
+
case "DELETE": {
|
|
31884
|
+
const currentResource = stateResources[logicalId];
|
|
31885
|
+
if (!currentResource) {
|
|
31886
|
+
throw new Error(`Cannot delete ${logicalId}: resource not found in state`);
|
|
31887
|
+
}
|
|
31888
|
+
const deletionPolicy = template?.Resources?.[logicalId]?.DeletionPolicy;
|
|
31889
|
+
if (deletionPolicy === "Retain") {
|
|
31890
|
+
this.logger.info(`Retaining ${logicalId} (${resourceType}) - DeletionPolicy: Retain`);
|
|
31891
|
+
delete stateResources[logicalId];
|
|
31640
31892
|
break;
|
|
31641
31893
|
}
|
|
31894
|
+
this.logger.debug(`Deleting ${logicalId} (${resourceType})`);
|
|
31895
|
+
try {
|
|
31896
|
+
await this.withRetry(
|
|
31897
|
+
() => provider.delete(
|
|
31898
|
+
logicalId,
|
|
31899
|
+
currentResource.physicalId,
|
|
31900
|
+
resourceType,
|
|
31901
|
+
currentResource.properties,
|
|
31902
|
+
{ expectedRegion: this.stackRegion }
|
|
31903
|
+
),
|
|
31904
|
+
logicalId,
|
|
31905
|
+
3,
|
|
31906
|
+
// fewer retries for DELETE
|
|
31907
|
+
5e3
|
|
31908
|
+
);
|
|
31909
|
+
} catch (deleteError) {
|
|
31910
|
+
const msg = deleteError instanceof Error ? deleteError.message : String(deleteError);
|
|
31911
|
+
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")) {
|
|
31912
|
+
this.logger.debug(
|
|
31913
|
+
`Resource ${logicalId} already deleted (${msg}), removing from state`
|
|
31914
|
+
);
|
|
31915
|
+
} else {
|
|
31916
|
+
throw deleteError;
|
|
31917
|
+
}
|
|
31918
|
+
}
|
|
31919
|
+
delete stateResources[logicalId];
|
|
31920
|
+
if (counts)
|
|
31921
|
+
counts.deleted++;
|
|
31922
|
+
if (progress)
|
|
31923
|
+
progress.current++;
|
|
31924
|
+
const deletePrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
|
|
31925
|
+
renderer.removeTask(logicalId);
|
|
31926
|
+
this.logger.info(`${deletePrefix}\u2705 ${logicalId} (${resourceType}) deleted`);
|
|
31927
|
+
break;
|
|
31642
31928
|
}
|
|
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
31929
|
}
|
|
31657
31930
|
}
|
|
31658
31931
|
/**
|
|
@@ -31864,6 +32137,10 @@ async function deployCommand(stacks, options) {
|
|
|
31864
32137
|
process.env["CDKD_NO_LIVE"] = "1";
|
|
31865
32138
|
}
|
|
31866
32139
|
warnIfDeprecatedRegion(options);
|
|
32140
|
+
validateResourceTimeouts({
|
|
32141
|
+
...options.resourceWarnAfter && { resourceWarnAfter: options.resourceWarnAfter },
|
|
32142
|
+
...options.resourceTimeout && { resourceTimeout: options.resourceTimeout }
|
|
32143
|
+
});
|
|
31867
32144
|
if (!options.wait) {
|
|
31868
32145
|
process.env["CDKD_NO_WAIT"] = "true";
|
|
31869
32146
|
}
|
|
@@ -32056,7 +32333,19 @@ Deploying stack: ${stackInfo.stackName}${stackRegion !== baseRegion ? ` (region:
|
|
|
32056
32333
|
{
|
|
32057
32334
|
concurrency: options.concurrency,
|
|
32058
32335
|
dryRun: options.dryRun,
|
|
32059
|
-
noRollback: !options.rollback
|
|
32336
|
+
noRollback: !options.rollback,
|
|
32337
|
+
...options.resourceWarnAfter?.globalMs !== void 0 && {
|
|
32338
|
+
resourceWarnAfterMs: options.resourceWarnAfter.globalMs
|
|
32339
|
+
},
|
|
32340
|
+
...options.resourceTimeout?.globalMs !== void 0 && {
|
|
32341
|
+
resourceTimeoutMs: options.resourceTimeout.globalMs
|
|
32342
|
+
},
|
|
32343
|
+
...options.resourceWarnAfter?.perTypeMs && {
|
|
32344
|
+
resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs
|
|
32345
|
+
},
|
|
32346
|
+
...options.resourceTimeout?.perTypeMs && {
|
|
32347
|
+
resourceTimeoutByType: options.resourceTimeout.perTypeMs
|
|
32348
|
+
}
|
|
32060
32349
|
},
|
|
32061
32350
|
stackRegion
|
|
32062
32351
|
);
|
|
@@ -32466,41 +32755,73 @@ Acquiring lock for stack ${stackName}...`);
|
|
|
32466
32755
|
logger.debug(
|
|
32467
32756
|
`Deletion level ${executionLevels.length - levelIndex}/${executionLevels.length} (${level.length} resources)`
|
|
32468
32757
|
);
|
|
32758
|
+
const stackRegion2 = state.region ?? ctx.baseRegion;
|
|
32469
32759
|
const deletePromises = level.map(async (logicalId) => {
|
|
32470
32760
|
const resource = state.resources[logicalId];
|
|
32471
32761
|
if (!resource) {
|
|
32472
32762
|
logger.warn(`Resource ${logicalId} not found in state, skipping`);
|
|
32473
32763
|
return;
|
|
32474
32764
|
}
|
|
32475
|
-
|
|
32765
|
+
const warnAfterMs = ctx.resourceWarnAfterByType?.[resource.resourceType] ?? ctx.resourceWarnAfterMs ?? DEFAULT_RESOURCE_WARN_AFTER_MS;
|
|
32766
|
+
const timeoutMs = ctx.resourceTimeoutByType?.[resource.resourceType] ?? ctx.resourceTimeoutMs ?? DEFAULT_RESOURCE_TIMEOUT_MS;
|
|
32767
|
+
const baseLabel = `Deleting ${logicalId} (${resource.resourceType})`;
|
|
32768
|
+
renderer.addTask(logicalId, baseLabel);
|
|
32476
32769
|
try {
|
|
32477
32770
|
const provider = destroyProviderRegistry.getProvider(resource.resourceType);
|
|
32478
|
-
|
|
32479
|
-
|
|
32480
|
-
|
|
32481
|
-
|
|
32771
|
+
await withResourceDeadline(
|
|
32772
|
+
async () => {
|
|
32773
|
+
let lastDeleteError;
|
|
32774
|
+
for (let attempt = 0; attempt <= 3; attempt++) {
|
|
32775
|
+
try {
|
|
32776
|
+
await provider.delete(
|
|
32777
|
+
logicalId,
|
|
32778
|
+
resource.physicalId,
|
|
32779
|
+
resource.resourceType,
|
|
32780
|
+
resource.properties
|
|
32781
|
+
);
|
|
32782
|
+
lastDeleteError = null;
|
|
32783
|
+
break;
|
|
32784
|
+
} catch (retryError) {
|
|
32785
|
+
lastDeleteError = retryError;
|
|
32786
|
+
const msg = retryError instanceof Error ? retryError.message : String(retryError);
|
|
32787
|
+
const isRetryable = msg.includes("Too Many Requests") || msg.includes("has dependencies") || msg.includes("can't be deleted since") || msg.includes("DependencyViolation");
|
|
32788
|
+
if (!isRetryable || attempt >= 3)
|
|
32789
|
+
break;
|
|
32790
|
+
const delay = 5e3 * Math.pow(2, attempt);
|
|
32791
|
+
logger.debug(
|
|
32792
|
+
` \u23F3 Retrying delete ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/3)`
|
|
32793
|
+
);
|
|
32794
|
+
await new Promise((resolve4) => setTimeout(resolve4, delay));
|
|
32795
|
+
}
|
|
32796
|
+
}
|
|
32797
|
+
if (lastDeleteError)
|
|
32798
|
+
throw lastDeleteError;
|
|
32799
|
+
},
|
|
32800
|
+
{
|
|
32801
|
+
warnAfterMs,
|
|
32802
|
+
timeoutMs,
|
|
32803
|
+
onWarn: (elapsedMs) => {
|
|
32804
|
+
const minutes = Math.max(1, Math.round(elapsedMs / 6e4));
|
|
32805
|
+
renderer.updateTaskLabel(
|
|
32806
|
+
logicalId,
|
|
32807
|
+
`${baseLabel} [taking longer than expected, ${minutes}m+]`
|
|
32808
|
+
);
|
|
32809
|
+
renderer.printAbove(() => {
|
|
32810
|
+
logger.warn(
|
|
32811
|
+
`${logicalId} (${resource.resourceType}) has been deleting for ${minutes}m \u2014 still waiting`
|
|
32812
|
+
);
|
|
32813
|
+
});
|
|
32814
|
+
},
|
|
32815
|
+
onTimeout: (elapsedMs) => new ResourceTimeoutError(
|
|
32482
32816
|
logicalId,
|
|
32483
|
-
resource.physicalId,
|
|
32484
32817
|
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));
|
|
32818
|
+
stackRegion2,
|
|
32819
|
+
elapsedMs,
|
|
32820
|
+
"DELETE",
|
|
32821
|
+
timeoutMs
|
|
32822
|
+
)
|
|
32500
32823
|
}
|
|
32501
|
-
|
|
32502
|
-
if (lastDeleteError)
|
|
32503
|
-
throw lastDeleteError;
|
|
32824
|
+
);
|
|
32504
32825
|
renderer.removeTask(logicalId);
|
|
32505
32826
|
logger.info(` \u2705 ${logicalId} (${resource.resourceType}) deleted`);
|
|
32506
32827
|
result.deletedCount++;
|
|
@@ -32510,6 +32831,16 @@ Acquiring lock for stack ${stackName}...`);
|
|
|
32510
32831
|
if (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException")) {
|
|
32511
32832
|
logger.debug(` ${logicalId} already deleted, removing from state`);
|
|
32512
32833
|
result.deletedCount++;
|
|
32834
|
+
} else if (error instanceof ResourceTimeoutError) {
|
|
32835
|
+
const wrapped = new ProvisioningError(
|
|
32836
|
+
error.message,
|
|
32837
|
+
resource.resourceType,
|
|
32838
|
+
logicalId,
|
|
32839
|
+
resource.physicalId,
|
|
32840
|
+
error
|
|
32841
|
+
);
|
|
32842
|
+
logger.error(` \u2717 Failed to delete ${logicalId}:`, wrapped.message);
|
|
32843
|
+
result.errorCount++;
|
|
32513
32844
|
} else {
|
|
32514
32845
|
logger.error(` \u2717 Failed to delete ${logicalId}:`, String(error));
|
|
32515
32846
|
result.errorCount++;
|
|
@@ -32552,6 +32883,10 @@ async function destroyCommand(stackArgs, options) {
|
|
|
32552
32883
|
process.env["CDKD_NO_LIVE"] = "1";
|
|
32553
32884
|
}
|
|
32554
32885
|
warnIfDeprecatedRegion(options);
|
|
32886
|
+
validateResourceTimeouts({
|
|
32887
|
+
...options.resourceWarnAfter && { resourceWarnAfter: options.resourceWarnAfter },
|
|
32888
|
+
...options.resourceTimeout && { resourceTimeout: options.resourceTimeout }
|
|
32889
|
+
});
|
|
32555
32890
|
const region = options.region || process.env["AWS_REGION"] || "us-east-1";
|
|
32556
32891
|
const stateBucket = await resolveStateBucketWithDefault(options.stateBucket, region);
|
|
32557
32892
|
logger.info("Starting stack destruction...");
|
|
@@ -32683,7 +33018,19 @@ Preparing to destroy stack: ${stackName}`);
|
|
|
32683
33018
|
baseRegion: region,
|
|
32684
33019
|
...options.profile && { profile: options.profile },
|
|
32685
33020
|
stateBucket,
|
|
32686
|
-
skipConfirmation: options.yes || options.force
|
|
33021
|
+
skipConfirmation: options.yes || options.force,
|
|
33022
|
+
...options.resourceWarnAfter?.globalMs !== void 0 && {
|
|
33023
|
+
resourceWarnAfterMs: options.resourceWarnAfter.globalMs
|
|
33024
|
+
},
|
|
33025
|
+
...options.resourceTimeout?.globalMs !== void 0 && {
|
|
33026
|
+
resourceTimeoutMs: options.resourceTimeout.globalMs
|
|
33027
|
+
},
|
|
33028
|
+
...options.resourceWarnAfter?.perTypeMs && {
|
|
33029
|
+
resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs
|
|
33030
|
+
},
|
|
33031
|
+
...options.resourceTimeout?.perTypeMs && {
|
|
33032
|
+
resourceTimeoutByType: options.resourceTimeout.perTypeMs
|
|
33033
|
+
}
|
|
32687
33034
|
});
|
|
32688
33035
|
}
|
|
32689
33036
|
} finally {
|
|
@@ -32701,6 +33048,7 @@ function createDestroyCommand() {
|
|
|
32701
33048
|
...stateOptions,
|
|
32702
33049
|
...stackOptions,
|
|
32703
33050
|
...destroyOptions,
|
|
33051
|
+
...resourceTimeoutOptions,
|
|
32704
33052
|
...contextOptions
|
|
32705
33053
|
].forEach((opt) => cmd.addOption(opt));
|
|
32706
33054
|
cmd.addOption(deprecatedRegionOption);
|
|
@@ -32711,15 +33059,432 @@ function createDestroyCommand() {
|
|
|
32711
33059
|
import * as readline2 from "node:readline/promises";
|
|
32712
33060
|
import { Command as Command7 } from "commander";
|
|
32713
33061
|
init_aws_clients();
|
|
32714
|
-
|
|
33062
|
+
|
|
33063
|
+
// src/cli/cdk-path.ts
|
|
33064
|
+
function readCdkPath(resource) {
|
|
33065
|
+
const meta = resource.Metadata;
|
|
33066
|
+
if (!meta)
|
|
33067
|
+
return "";
|
|
33068
|
+
const v = meta["aws:cdk:path"];
|
|
33069
|
+
return typeof v === "string" ? v : "";
|
|
33070
|
+
}
|
|
33071
|
+
function buildCdkPathIndex(template) {
|
|
33072
|
+
const index = /* @__PURE__ */ new Map();
|
|
33073
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
33074
|
+
const path = readCdkPath(resource);
|
|
33075
|
+
if (path)
|
|
33076
|
+
index.set(path, logicalId);
|
|
33077
|
+
}
|
|
33078
|
+
return index;
|
|
33079
|
+
}
|
|
33080
|
+
|
|
33081
|
+
// src/analyzer/orphan-rewriter.ts
|
|
33082
|
+
var AttributeFetcher = class {
|
|
33083
|
+
constructor(orphans, providerRegistry, options) {
|
|
33084
|
+
this.orphans = orphans;
|
|
33085
|
+
this.providerRegistry = providerRegistry;
|
|
33086
|
+
this.options = options;
|
|
33087
|
+
}
|
|
33088
|
+
cache = /* @__PURE__ */ new Map();
|
|
33089
|
+
logger = getLogger().child("OrphanRewriter");
|
|
33090
|
+
/**
|
|
33091
|
+
* Return the orphan's resolved value for `Ref` (its physicalId) — never
|
|
33092
|
+
* needs an AWS call.
|
|
33093
|
+
*/
|
|
33094
|
+
ref(orphanLogicalId) {
|
|
33095
|
+
const o = this.orphans[orphanLogicalId];
|
|
33096
|
+
if (!o) {
|
|
33097
|
+
throw new Error(
|
|
33098
|
+
`Internal: Ref to '${orphanLogicalId}' has no orphan entry \u2014 should have been filtered out`
|
|
33099
|
+
);
|
|
33100
|
+
}
|
|
33101
|
+
return o.physicalId;
|
|
33102
|
+
}
|
|
33103
|
+
/**
|
|
33104
|
+
* Return the orphan's resolved value for `Fn::GetAtt`. Hits the live
|
|
33105
|
+
* provider on first call; subsequent calls reuse the cached result.
|
|
33106
|
+
*
|
|
33107
|
+
* Returns `{ ok: true, value }` on success; `{ ok: false, reason }`
|
|
33108
|
+
* when the live fetch failed AND the `--force` cache fallback either
|
|
33109
|
+
* was disabled or also lacked the attribute. In the cache-fallback
|
|
33110
|
+
* success path returns `{ ok: true, value, fromCache: true }`.
|
|
33111
|
+
*/
|
|
33112
|
+
async getAtt(orphanLogicalId, attribute) {
|
|
33113
|
+
const cacheKey = `${orphanLogicalId}\0${attribute}`;
|
|
33114
|
+
if (this.cache.has(cacheKey)) {
|
|
33115
|
+
return { ok: true, value: this.cache.get(cacheKey) };
|
|
33116
|
+
}
|
|
33117
|
+
const orphan = this.orphans[orphanLogicalId];
|
|
33118
|
+
if (!orphan) {
|
|
33119
|
+
return {
|
|
33120
|
+
ok: false,
|
|
33121
|
+
reason: `Internal: GetAtt to '${orphanLogicalId}' has no orphan entry`
|
|
33122
|
+
};
|
|
33123
|
+
}
|
|
33124
|
+
let provider;
|
|
33125
|
+
try {
|
|
33126
|
+
provider = this.providerRegistry.getProvider(orphan.resourceType);
|
|
33127
|
+
} catch (err) {
|
|
33128
|
+
return {
|
|
33129
|
+
ok: false,
|
|
33130
|
+
reason: `no provider available for ${orphan.resourceType}: ${err instanceof Error ? err.message : String(err)}`
|
|
33131
|
+
};
|
|
33132
|
+
}
|
|
33133
|
+
if (!provider.getAttribute) {
|
|
33134
|
+
return this.cacheFallback(
|
|
33135
|
+
orphanLogicalId,
|
|
33136
|
+
attribute,
|
|
33137
|
+
`provider for ${orphan.resourceType} does not implement getAttribute`
|
|
33138
|
+
);
|
|
33139
|
+
}
|
|
33140
|
+
try {
|
|
33141
|
+
const value = await provider.getAttribute(orphan.physicalId, orphan.resourceType, attribute);
|
|
33142
|
+
if (value === void 0) {
|
|
33143
|
+
return this.cacheFallback(
|
|
33144
|
+
orphanLogicalId,
|
|
33145
|
+
attribute,
|
|
33146
|
+
`provider returned undefined for ${orphan.resourceType}.${attribute}`
|
|
33147
|
+
);
|
|
33148
|
+
}
|
|
33149
|
+
this.cache.set(cacheKey, value);
|
|
33150
|
+
return { ok: true, value };
|
|
33151
|
+
} catch (err) {
|
|
33152
|
+
return this.cacheFallback(
|
|
33153
|
+
orphanLogicalId,
|
|
33154
|
+
attribute,
|
|
33155
|
+
err instanceof Error ? err.message : String(err)
|
|
33156
|
+
);
|
|
33157
|
+
}
|
|
33158
|
+
}
|
|
33159
|
+
/**
|
|
33160
|
+
* Try the orphan's `state.attributes[attribute]` as a last-resort value
|
|
33161
|
+
* source under `--force`. Without `--force`, returns the original
|
|
33162
|
+
* failure reason unchanged (caller pushes to `unresolvable`).
|
|
33163
|
+
*/
|
|
33164
|
+
cacheFallback(orphanLogicalId, attribute, reason) {
|
|
33165
|
+
if (!this.options.force) {
|
|
33166
|
+
return { ok: false, reason };
|
|
33167
|
+
}
|
|
33168
|
+
const orphan = this.orphans[orphanLogicalId];
|
|
33169
|
+
const cached = orphan.attributes?.[attribute];
|
|
33170
|
+
if (cached === void 0) {
|
|
33171
|
+
this.logger.warn(
|
|
33172
|
+
`--force: state.attributes also lacks '${orphanLogicalId}.${attribute}'; leaving the original intrinsic in place.`
|
|
33173
|
+
);
|
|
33174
|
+
return {
|
|
33175
|
+
ok: false,
|
|
33176
|
+
reason: `${reason}; state.attributes cache also has no value for '${attribute}'`
|
|
33177
|
+
};
|
|
33178
|
+
}
|
|
33179
|
+
this.logger.warn(
|
|
33180
|
+
`--force: live fetch failed for '${orphanLogicalId}.${attribute}' (${reason}); falling back to cached value from state.attributes.`
|
|
33181
|
+
);
|
|
33182
|
+
const cacheKey = `${orphanLogicalId}\0${attribute}`;
|
|
33183
|
+
this.cache.set(cacheKey, cached);
|
|
33184
|
+
return { ok: true, value: cached, fromCache: true };
|
|
33185
|
+
}
|
|
33186
|
+
};
|
|
33187
|
+
async function rewriteResourceReferences(state, orphanLogicalIds, providerRegistry, options = {}) {
|
|
33188
|
+
const orphanSet = new Set(orphanLogicalIds);
|
|
33189
|
+
const orphans = {};
|
|
33190
|
+
for (const id of orphanLogicalIds) {
|
|
33191
|
+
const r = state.resources[id];
|
|
33192
|
+
if (!r) {
|
|
33193
|
+
throw new Error(`rewriteResourceReferences: orphan '${id}' not found in state.resources`);
|
|
33194
|
+
}
|
|
33195
|
+
orphans[id] = r;
|
|
33196
|
+
}
|
|
33197
|
+
const fetcher = new AttributeFetcher(orphans, providerRegistry, options);
|
|
33198
|
+
const rewrites = [];
|
|
33199
|
+
const unresolvable = [];
|
|
33200
|
+
const newResources = {};
|
|
33201
|
+
for (const [logicalId, resource] of Object.entries(state.resources)) {
|
|
33202
|
+
if (orphanSet.has(logicalId))
|
|
33203
|
+
continue;
|
|
33204
|
+
const rewrittenProperties = await rewriteValue(
|
|
33205
|
+
resource.properties,
|
|
33206
|
+
`properties`,
|
|
33207
|
+
logicalId,
|
|
33208
|
+
orphanSet,
|
|
33209
|
+
fetcher,
|
|
33210
|
+
rewrites,
|
|
33211
|
+
unresolvable
|
|
33212
|
+
);
|
|
33213
|
+
const rewrittenAttributes = resource.attributes ? await rewriteValue(
|
|
33214
|
+
resource.attributes,
|
|
33215
|
+
`attributes`,
|
|
33216
|
+
logicalId,
|
|
33217
|
+
orphanSet,
|
|
33218
|
+
fetcher,
|
|
33219
|
+
rewrites,
|
|
33220
|
+
unresolvable
|
|
33221
|
+
) : void 0;
|
|
33222
|
+
const newDeps = (resource.dependencies ?? []).filter((dep) => {
|
|
33223
|
+
if (orphanSet.has(dep)) {
|
|
33224
|
+
rewrites.push({
|
|
33225
|
+
logicalId,
|
|
33226
|
+
path: "dependencies",
|
|
33227
|
+
kind: "dependency",
|
|
33228
|
+
before: dep,
|
|
33229
|
+
after: null,
|
|
33230
|
+
orphanLogicalId: dep
|
|
33231
|
+
});
|
|
33232
|
+
return false;
|
|
33233
|
+
}
|
|
33234
|
+
return true;
|
|
33235
|
+
});
|
|
33236
|
+
newResources[logicalId] = {
|
|
33237
|
+
...resource,
|
|
33238
|
+
properties: rewrittenProperties,
|
|
33239
|
+
...rewrittenAttributes !== void 0 && {
|
|
33240
|
+
attributes: rewrittenAttributes
|
|
33241
|
+
},
|
|
33242
|
+
dependencies: newDeps
|
|
33243
|
+
};
|
|
33244
|
+
}
|
|
33245
|
+
const newOutputs = {};
|
|
33246
|
+
for (const [name, value] of Object.entries(state.outputs ?? {})) {
|
|
33247
|
+
newOutputs[name] = await rewriteValue(
|
|
33248
|
+
value,
|
|
33249
|
+
`outputs.${name}`,
|
|
33250
|
+
`<output:${name}>`,
|
|
33251
|
+
orphanSet,
|
|
33252
|
+
fetcher,
|
|
33253
|
+
rewrites,
|
|
33254
|
+
unresolvable
|
|
33255
|
+
);
|
|
33256
|
+
}
|
|
33257
|
+
return {
|
|
33258
|
+
state: {
|
|
33259
|
+
...state,
|
|
33260
|
+
resources: newResources,
|
|
33261
|
+
outputs: newOutputs,
|
|
33262
|
+
lastModified: Date.now()
|
|
33263
|
+
},
|
|
33264
|
+
rewrites,
|
|
33265
|
+
unresolvable
|
|
33266
|
+
};
|
|
33267
|
+
}
|
|
33268
|
+
async function rewriteValue(value, pathPrefix, ownerLogicalId, orphanSet, fetcher, rewrites, unresolvable) {
|
|
33269
|
+
if (typeof value !== "object" || value === null)
|
|
33270
|
+
return value;
|
|
33271
|
+
if (Array.isArray(value)) {
|
|
33272
|
+
const out2 = [];
|
|
33273
|
+
for (let i = 0; i < value.length; i++) {
|
|
33274
|
+
out2.push(
|
|
33275
|
+
await rewriteValue(
|
|
33276
|
+
value[i],
|
|
33277
|
+
`${pathPrefix}[${i}]`,
|
|
33278
|
+
ownerLogicalId,
|
|
33279
|
+
orphanSet,
|
|
33280
|
+
fetcher,
|
|
33281
|
+
rewrites,
|
|
33282
|
+
unresolvable
|
|
33283
|
+
)
|
|
33284
|
+
);
|
|
33285
|
+
}
|
|
33286
|
+
return out2;
|
|
33287
|
+
}
|
|
33288
|
+
const obj = value;
|
|
33289
|
+
if ("Ref" in obj && Object.keys(obj).length === 1 && typeof obj["Ref"] === "string") {
|
|
33290
|
+
const target = obj["Ref"];
|
|
33291
|
+
if (orphanSet.has(target)) {
|
|
33292
|
+
const replaced = fetcher.ref(target);
|
|
33293
|
+
rewrites.push({
|
|
33294
|
+
logicalId: ownerLogicalId,
|
|
33295
|
+
path: pathPrefix,
|
|
33296
|
+
kind: "ref",
|
|
33297
|
+
before: { Ref: target },
|
|
33298
|
+
after: replaced,
|
|
33299
|
+
orphanLogicalId: target
|
|
33300
|
+
});
|
|
33301
|
+
return replaced;
|
|
33302
|
+
}
|
|
33303
|
+
return value;
|
|
33304
|
+
}
|
|
33305
|
+
if ("Fn::GetAtt" in obj && Object.keys(obj).length === 1) {
|
|
33306
|
+
const arg = obj["Fn::GetAtt"];
|
|
33307
|
+
let target;
|
|
33308
|
+
let attribute;
|
|
33309
|
+
if (Array.isArray(arg) && arg.length === 2 && typeof arg[0] === "string" && typeof arg[1] === "string") {
|
|
33310
|
+
target = arg[0];
|
|
33311
|
+
attribute = arg[1];
|
|
33312
|
+
} else if (typeof arg === "string") {
|
|
33313
|
+
const dot = arg.indexOf(".");
|
|
33314
|
+
if (dot > 0) {
|
|
33315
|
+
target = arg.slice(0, dot);
|
|
33316
|
+
attribute = arg.slice(dot + 1);
|
|
33317
|
+
}
|
|
33318
|
+
}
|
|
33319
|
+
if (target && attribute && orphanSet.has(target)) {
|
|
33320
|
+
const result = await fetcher.getAtt(target, attribute);
|
|
33321
|
+
if (result.ok) {
|
|
33322
|
+
rewrites.push({
|
|
33323
|
+
logicalId: ownerLogicalId,
|
|
33324
|
+
path: pathPrefix,
|
|
33325
|
+
kind: "getAtt",
|
|
33326
|
+
before: { "Fn::GetAtt": [target, attribute] },
|
|
33327
|
+
after: result.value,
|
|
33328
|
+
orphanLogicalId: target
|
|
33329
|
+
});
|
|
33330
|
+
return result.value;
|
|
33331
|
+
}
|
|
33332
|
+
unresolvable.push({
|
|
33333
|
+
logicalId: ownerLogicalId,
|
|
33334
|
+
path: pathPrefix,
|
|
33335
|
+
orphanLogicalId: target,
|
|
33336
|
+
attribute,
|
|
33337
|
+
reason: result.reason
|
|
33338
|
+
});
|
|
33339
|
+
return value;
|
|
33340
|
+
}
|
|
33341
|
+
return value;
|
|
33342
|
+
}
|
|
33343
|
+
if ("Fn::Sub" in obj && Object.keys(obj).length === 1) {
|
|
33344
|
+
const arg = obj["Fn::Sub"];
|
|
33345
|
+
let template;
|
|
33346
|
+
let varMap;
|
|
33347
|
+
if (typeof arg === "string") {
|
|
33348
|
+
template = arg;
|
|
33349
|
+
} else if (Array.isArray(arg) && arg.length === 2 && typeof arg[0] === "string" && typeof arg[1] === "object" && arg[1] !== null) {
|
|
33350
|
+
template = arg[0];
|
|
33351
|
+
varMap = arg[1];
|
|
33352
|
+
}
|
|
33353
|
+
if (template !== void 0) {
|
|
33354
|
+
const { rewritten, didChange, hasUnresolvable } = await rewriteSubTemplate(
|
|
33355
|
+
template,
|
|
33356
|
+
ownerLogicalId,
|
|
33357
|
+
pathPrefix,
|
|
33358
|
+
orphanSet,
|
|
33359
|
+
fetcher,
|
|
33360
|
+
rewrites,
|
|
33361
|
+
unresolvable,
|
|
33362
|
+
varMap
|
|
33363
|
+
);
|
|
33364
|
+
if (didChange) {
|
|
33365
|
+
const stillHasIntrinsics = /\$\{[^}]+\}/.test(rewritten);
|
|
33366
|
+
if (varMap && stillHasIntrinsics) {
|
|
33367
|
+
return { "Fn::Sub": [rewritten, varMap] };
|
|
33368
|
+
}
|
|
33369
|
+
if (stillHasIntrinsics) {
|
|
33370
|
+
return { "Fn::Sub": rewritten };
|
|
33371
|
+
}
|
|
33372
|
+
return rewritten;
|
|
33373
|
+
}
|
|
33374
|
+
return value;
|
|
33375
|
+
}
|
|
33376
|
+
return value;
|
|
33377
|
+
}
|
|
33378
|
+
const out = {};
|
|
33379
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
33380
|
+
out[k] = await rewriteValue(
|
|
33381
|
+
v,
|
|
33382
|
+
pathPrefix === "" ? k : `${pathPrefix}.${k}`,
|
|
33383
|
+
ownerLogicalId,
|
|
33384
|
+
orphanSet,
|
|
33385
|
+
fetcher,
|
|
33386
|
+
rewrites,
|
|
33387
|
+
unresolvable
|
|
33388
|
+
);
|
|
33389
|
+
}
|
|
33390
|
+
return out;
|
|
33391
|
+
}
|
|
33392
|
+
async function rewriteSubTemplate(template, ownerLogicalId, pathPrefix, orphanSet, fetcher, rewrites, unresolvable, varMap) {
|
|
33393
|
+
const placeholderRe = /\$\{([^}]+)\}/g;
|
|
33394
|
+
const matches = [...template.matchAll(placeholderRe)];
|
|
33395
|
+
if (matches.length === 0) {
|
|
33396
|
+
return { rewritten: template, didChange: false, hasUnresolvable: false };
|
|
33397
|
+
}
|
|
33398
|
+
let didChange = false;
|
|
33399
|
+
let hasUnresolvable = false;
|
|
33400
|
+
let cursor = 0;
|
|
33401
|
+
let out = "";
|
|
33402
|
+
for (const m of matches) {
|
|
33403
|
+
const inner = m[1] ?? "";
|
|
33404
|
+
const start = m.index ?? 0;
|
|
33405
|
+
out += template.slice(cursor, start);
|
|
33406
|
+
cursor = start + m[0].length;
|
|
33407
|
+
if (varMap && inner in varMap) {
|
|
33408
|
+
out += m[0];
|
|
33409
|
+
continue;
|
|
33410
|
+
}
|
|
33411
|
+
const dot = inner.indexOf(".");
|
|
33412
|
+
if (dot < 0) {
|
|
33413
|
+
if (orphanSet.has(inner)) {
|
|
33414
|
+
const replaced = fetcher.ref(inner);
|
|
33415
|
+
rewrites.push({
|
|
33416
|
+
logicalId: ownerLogicalId,
|
|
33417
|
+
path: pathPrefix,
|
|
33418
|
+
kind: "sub",
|
|
33419
|
+
before: m[0],
|
|
33420
|
+
after: replaced,
|
|
33421
|
+
orphanLogicalId: inner
|
|
33422
|
+
});
|
|
33423
|
+
out += replaced;
|
|
33424
|
+
didChange = true;
|
|
33425
|
+
} else {
|
|
33426
|
+
out += m[0];
|
|
33427
|
+
}
|
|
33428
|
+
} else {
|
|
33429
|
+
const target = inner.slice(0, dot);
|
|
33430
|
+
const attribute = inner.slice(dot + 1);
|
|
33431
|
+
if (orphanSet.has(target)) {
|
|
33432
|
+
const result = await fetcher.getAtt(target, attribute);
|
|
33433
|
+
if (result.ok) {
|
|
33434
|
+
const stringified = String(result.value);
|
|
33435
|
+
rewrites.push({
|
|
33436
|
+
logicalId: ownerLogicalId,
|
|
33437
|
+
path: pathPrefix,
|
|
33438
|
+
kind: "sub",
|
|
33439
|
+
before: m[0],
|
|
33440
|
+
after: stringified,
|
|
33441
|
+
orphanLogicalId: target
|
|
33442
|
+
});
|
|
33443
|
+
out += stringified;
|
|
33444
|
+
didChange = true;
|
|
33445
|
+
} else {
|
|
33446
|
+
unresolvable.push({
|
|
33447
|
+
logicalId: ownerLogicalId,
|
|
33448
|
+
path: pathPrefix,
|
|
33449
|
+
orphanLogicalId: target,
|
|
33450
|
+
attribute,
|
|
33451
|
+
reason: result.reason
|
|
33452
|
+
});
|
|
33453
|
+
out += m[0];
|
|
33454
|
+
hasUnresolvable = true;
|
|
33455
|
+
}
|
|
33456
|
+
} else {
|
|
33457
|
+
out += m[0];
|
|
33458
|
+
}
|
|
33459
|
+
}
|
|
33460
|
+
}
|
|
33461
|
+
out += template.slice(cursor);
|
|
33462
|
+
return { rewritten: out, didChange, hasUnresolvable };
|
|
33463
|
+
}
|
|
33464
|
+
|
|
33465
|
+
// src/cli/commands/orphan.ts
|
|
33466
|
+
async function orphanCommand(pathArgs, options) {
|
|
32715
33467
|
const logger = getLogger();
|
|
32716
33468
|
if (options.verbose)
|
|
32717
33469
|
logger.setLevel("debug");
|
|
32718
33470
|
warnIfDeprecatedRegion(options);
|
|
33471
|
+
if (pathArgs.length === 0) {
|
|
33472
|
+
throw new Error(
|
|
33473
|
+
"'cdkd orphan' requires at least one construct path, e.g. 'cdkd orphan MyStack/MyTable'.\n To remove a stack's state record (the previous behavior), use:\n cdkd state orphan MyStack"
|
|
33474
|
+
);
|
|
33475
|
+
}
|
|
33476
|
+
for (const p of pathArgs) {
|
|
33477
|
+
if (!p.includes("/")) {
|
|
33478
|
+
throw new Error(
|
|
33479
|
+
`'cdkd orphan' now expects a construct path like 'MyStack/MyTable'.
|
|
33480
|
+
Got: '${p}'
|
|
33481
|
+
To remove a stack's state record (the previous behavior), use:
|
|
33482
|
+
cdkd state orphan ${p}`
|
|
33483
|
+
);
|
|
33484
|
+
}
|
|
33485
|
+
}
|
|
32719
33486
|
const region = options.region || process.env["AWS_REGION"] || "us-east-1";
|
|
32720
33487
|
const stateBucket = await resolveStateBucketWithDefault(options.stateBucket, region);
|
|
32721
|
-
logger.info("Starting stack orphan...");
|
|
32722
|
-
logger.debug("Options:", options);
|
|
32723
33488
|
if (options.region) {
|
|
32724
33489
|
process.env["AWS_REGION"] = options.region;
|
|
32725
33490
|
process.env["AWS_DEFAULT_REGION"] = options.region;
|
|
@@ -32730,10 +33495,7 @@ async function orphanCommand(stackArgs, options) {
|
|
|
32730
33495
|
});
|
|
32731
33496
|
setAwsClients(awsClients);
|
|
32732
33497
|
try {
|
|
32733
|
-
const stateConfig = {
|
|
32734
|
-
bucket: stateBucket,
|
|
32735
|
-
prefix: options.statePrefix
|
|
32736
|
-
};
|
|
33498
|
+
const stateConfig = { bucket: stateBucket, prefix: options.statePrefix };
|
|
32737
33499
|
const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
|
|
32738
33500
|
...options.region && { region: options.region },
|
|
32739
33501
|
...options.profile && { profile: options.profile }
|
|
@@ -32741,150 +33503,245 @@ async function orphanCommand(stackArgs, options) {
|
|
|
32741
33503
|
await stateBackend.verifyBucketExists();
|
|
32742
33504
|
const lockManager = new LockManager(awsClients.s3, stateConfig);
|
|
32743
33505
|
const appCmd = options.app || resolveApp();
|
|
32744
|
-
|
|
32745
|
-
if (appCmd) {
|
|
32746
|
-
try {
|
|
32747
|
-
const synthesizer = new Synthesizer();
|
|
32748
|
-
const context = parseContextOptions(options.context);
|
|
32749
|
-
const result = await synthesizer.synthesize({
|
|
32750
|
-
app: appCmd,
|
|
32751
|
-
output: options.output || "cdk.out",
|
|
32752
|
-
...Object.keys(context).length > 0 && { context }
|
|
32753
|
-
});
|
|
32754
|
-
appStacks = result.stacks.map((s) => ({
|
|
32755
|
-
stackName: s.stackName,
|
|
32756
|
-
displayName: s.displayName,
|
|
32757
|
-
...s.region && { region: s.region }
|
|
32758
|
-
}));
|
|
32759
|
-
} catch {
|
|
32760
|
-
logger.debug("Could not synthesize app, falling back to state-based stack list");
|
|
32761
|
-
}
|
|
32762
|
-
}
|
|
32763
|
-
const allStateRefs = await stateBackend.listStacks();
|
|
32764
|
-
let candidateStacks;
|
|
32765
|
-
if (appStacks.length > 0) {
|
|
32766
|
-
const stateNames = new Set(allStateRefs.map((r) => r.stackName));
|
|
32767
|
-
candidateStacks = appStacks.filter((s) => stateNames.has(s.stackName));
|
|
32768
|
-
} else if (stackArgs.length > 0 || options.stack || options.all) {
|
|
32769
|
-
const seen = /* @__PURE__ */ new Set();
|
|
32770
|
-
candidateStacks = [];
|
|
32771
|
-
for (const ref of allStateRefs) {
|
|
32772
|
-
if (seen.has(ref.stackName))
|
|
32773
|
-
continue;
|
|
32774
|
-
seen.add(ref.stackName);
|
|
32775
|
-
candidateStacks.push({ stackName: ref.stackName });
|
|
32776
|
-
}
|
|
32777
|
-
} else {
|
|
32778
|
-
throw new Error(
|
|
32779
|
-
"Could not determine which stacks belong to this app. Specify stack names explicitly, use --all, or ensure --app / cdk.json is configured."
|
|
32780
|
-
);
|
|
32781
|
-
}
|
|
32782
|
-
const stackPatterns = stackArgs.length > 0 ? stackArgs : options.stack ? [options.stack] : [];
|
|
32783
|
-
let stackNames;
|
|
32784
|
-
if (options.all) {
|
|
32785
|
-
stackNames = candidateStacks.map((s) => s.stackName);
|
|
32786
|
-
} else if (stackPatterns.length > 0) {
|
|
32787
|
-
stackNames = matchStacks(candidateStacks, stackPatterns).map((s) => s.stackName);
|
|
32788
|
-
} else if (candidateStacks.length === 1) {
|
|
32789
|
-
stackNames = candidateStacks.map((s) => s.stackName);
|
|
32790
|
-
} else if (candidateStacks.length === 0) {
|
|
32791
|
-
logger.info("No stacks found in state");
|
|
32792
|
-
return;
|
|
32793
|
-
} else {
|
|
33506
|
+
if (!appCmd) {
|
|
32794
33507
|
throw new Error(
|
|
32795
|
-
|
|
33508
|
+
"'cdkd orphan' requires a CDK app: pass --app or set it in cdk.json. The template is read to resolve construct paths to logical IDs."
|
|
32796
33509
|
);
|
|
32797
33510
|
}
|
|
32798
|
-
|
|
32799
|
-
|
|
32800
|
-
|
|
32801
|
-
|
|
32802
|
-
|
|
32803
|
-
|
|
32804
|
-
|
|
32805
|
-
|
|
32806
|
-
|
|
32807
|
-
|
|
33511
|
+
logger.info("Synthesizing CDK app to read template...");
|
|
33512
|
+
const synthesizer = new Synthesizer();
|
|
33513
|
+
const context = parseContextOptions(options.context);
|
|
33514
|
+
const result = await synthesizer.synthesize({
|
|
33515
|
+
app: appCmd,
|
|
33516
|
+
output: options.output || "cdk.out",
|
|
33517
|
+
...Object.keys(context).length > 0 && { context }
|
|
33518
|
+
});
|
|
33519
|
+
const resolved = resolveConstructPaths(pathArgs, result.stacks);
|
|
33520
|
+
const stackInfo = resolved.stack;
|
|
33521
|
+
const orphanLogicalIds = resolved.logicalIds;
|
|
33522
|
+
const targetRegion = await pickStackRegion(
|
|
33523
|
+
stateBackend,
|
|
33524
|
+
stackInfo.stackName,
|
|
33525
|
+
stackInfo.region,
|
|
33526
|
+
options.stackRegion
|
|
33527
|
+
);
|
|
33528
|
+
logger.info(
|
|
33529
|
+
`Target: ${stackInfo.stackName} (${targetRegion}); orphaning ${orphanLogicalIds.length} resource(s): ${orphanLogicalIds.join(", ")}`
|
|
33530
|
+
);
|
|
33531
|
+
const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
|
|
33532
|
+
if (!options.dryRun) {
|
|
33533
|
+
await lockManager.acquireLock(stackInfo.stackName, targetRegion, owner, "orphan");
|
|
32808
33534
|
}
|
|
32809
|
-
|
|
32810
|
-
|
|
32811
|
-
|
|
32812
|
-
|
|
32813
|
-
|
|
32814
|
-
|
|
33535
|
+
try {
|
|
33536
|
+
const stateData = await stateBackend.getState(stackInfo.stackName, targetRegion);
|
|
33537
|
+
if (!stateData) {
|
|
33538
|
+
throw new Error(
|
|
33539
|
+
`No state found for stack '${stackInfo.stackName}' (${targetRegion}). Nothing to orphan. (Did the stack get deployed?)`
|
|
33540
|
+
);
|
|
32815
33541
|
}
|
|
32816
|
-
const
|
|
32817
|
-
|
|
32818
|
-
|
|
33542
|
+
const { state, etag, migrationPending } = stateData;
|
|
33543
|
+
const missing = orphanLogicalIds.filter((id) => !(id in state.resources));
|
|
33544
|
+
if (missing.length > 0) {
|
|
33545
|
+
const have = Object.keys(state.resources).join(", ");
|
|
32819
33546
|
throw new Error(
|
|
32820
|
-
`
|
|
33547
|
+
`Resource(s) not in state for stack '${stackInfo.stackName}' (${targetRegion}): ${missing.join(", ")}.
|
|
33548
|
+
Available logical IDs: ${have}`
|
|
32821
33549
|
);
|
|
32822
33550
|
}
|
|
32823
|
-
|
|
32824
|
-
|
|
32825
|
-
|
|
32826
|
-
|
|
32827
|
-
|
|
32828
|
-
|
|
32829
|
-
|
|
32830
|
-
|
|
32831
|
-
|
|
32832
|
-
|
|
33551
|
+
const providerRegistry = new ProviderRegistry();
|
|
33552
|
+
registerAllProviders(providerRegistry);
|
|
33553
|
+
const rewriteResult = await rewriteResourceReferences(
|
|
33554
|
+
state,
|
|
33555
|
+
orphanLogicalIds,
|
|
33556
|
+
providerRegistry,
|
|
33557
|
+
{ force: options.force }
|
|
33558
|
+
);
|
|
33559
|
+
printRewriteSummary(rewriteResult.rewrites, orphanLogicalIds);
|
|
33560
|
+
if (rewriteResult.unresolvable.length > 0 && !options.force) {
|
|
33561
|
+
printUnresolvable(rewriteResult.unresolvable);
|
|
33562
|
+
throw new Error(
|
|
33563
|
+
`Orphan aborted: ${rewriteResult.unresolvable.length} reference(s) could not be resolved.
|
|
33564
|
+
Re-run with --force to fall back to cached attribute values from state, or fix the underlying provider/AWS issue and retry.`
|
|
33565
|
+
);
|
|
32833
33566
|
}
|
|
32834
|
-
if (
|
|
32835
|
-
|
|
32836
|
-
|
|
32837
|
-
|
|
32838
|
-
WARNING: This removes cdkd's state record for [${targetList}] only. AWS resources will NOT be deleted.
|
|
32839
|
-
Use 'cdkd destroy ${stackName}' if you want to delete the actual resources.
|
|
32840
|
-
|
|
32841
|
-
`
|
|
33567
|
+
if (rewriteResult.unresolvable.length > 0) {
|
|
33568
|
+
printUnresolvable(rewriteResult.unresolvable);
|
|
33569
|
+
logger.warn(
|
|
33570
|
+
`--force: continuing despite ${rewriteResult.unresolvable.length} unresolved reference(s); the original intrinsic was left in place where the cache also lacked the value.`
|
|
32842
33571
|
);
|
|
32843
|
-
|
|
32844
|
-
|
|
32845
|
-
|
|
32846
|
-
|
|
32847
|
-
|
|
32848
|
-
|
|
33572
|
+
}
|
|
33573
|
+
if (options.dryRun) {
|
|
33574
|
+
logger.info("--dry-run: state will NOT be written. Re-run without --dry-run to apply.");
|
|
33575
|
+
return;
|
|
33576
|
+
}
|
|
33577
|
+
if (!options.yes && !options.force) {
|
|
33578
|
+
const ok = await confirmPrompt(
|
|
33579
|
+
`Orphan ${orphanLogicalIds.length} resource(s) from cdkd state for ${stackInfo.stackName} (${targetRegion})? AWS resources will NOT be deleted.`
|
|
32849
33580
|
);
|
|
32850
|
-
|
|
32851
|
-
|
|
32852
|
-
|
|
32853
|
-
logger.info(`Cancelled orphan of stack: ${stackName}`);
|
|
32854
|
-
continue;
|
|
33581
|
+
if (!ok) {
|
|
33582
|
+
logger.info("Orphan cancelled.");
|
|
33583
|
+
return;
|
|
32855
33584
|
}
|
|
32856
33585
|
}
|
|
32857
|
-
|
|
32858
|
-
|
|
32859
|
-
|
|
32860
|
-
|
|
32861
|
-
|
|
32862
|
-
|
|
32863
|
-
|
|
32864
|
-
|
|
32865
|
-
|
|
33586
|
+
await stateBackend.saveState(stackInfo.stackName, targetRegion, rewriteResult.state, {
|
|
33587
|
+
expectedEtag: etag,
|
|
33588
|
+
...migrationPending && { migrateLegacy: true }
|
|
33589
|
+
});
|
|
33590
|
+
logger.info(
|
|
33591
|
+
`Orphaned ${orphanLogicalIds.length} resource(s) from state: ${stackInfo.stackName} (${targetRegion}). AWS resources are still in AWS; cdkd will no longer manage them.`
|
|
33592
|
+
);
|
|
33593
|
+
} finally {
|
|
33594
|
+
if (!options.dryRun) {
|
|
33595
|
+
await lockManager.releaseLock(stackInfo.stackName, targetRegion).catch((err) => {
|
|
33596
|
+
logger.warn(
|
|
33597
|
+
`Failed to release lock: ${err instanceof Error ? err.message : String(err)}`
|
|
33598
|
+
);
|
|
33599
|
+
});
|
|
32866
33600
|
}
|
|
32867
33601
|
}
|
|
32868
33602
|
} finally {
|
|
32869
33603
|
awsClients.destroy();
|
|
32870
33604
|
}
|
|
32871
33605
|
}
|
|
33606
|
+
function resolveConstructPaths(paths, stacks) {
|
|
33607
|
+
const byStackName = /* @__PURE__ */ new Map();
|
|
33608
|
+
const byDisplayName = /* @__PURE__ */ new Map();
|
|
33609
|
+
for (const s of stacks) {
|
|
33610
|
+
byStackName.set(s.stackName, s);
|
|
33611
|
+
byDisplayName.set(s.displayName, s);
|
|
33612
|
+
}
|
|
33613
|
+
let stack;
|
|
33614
|
+
const logicalIds = [];
|
|
33615
|
+
for (const p of paths) {
|
|
33616
|
+
const slash = p.indexOf("/");
|
|
33617
|
+
if (slash <= 0 || slash === p.length - 1) {
|
|
33618
|
+
throw new Error(`Invalid construct path '${p}'. Expected '<StackName>/<Path/To/Resource>'.`);
|
|
33619
|
+
}
|
|
33620
|
+
const head = p.slice(0, slash);
|
|
33621
|
+
const candidate = byDisplayName.get(head) ?? byStackName.get(head);
|
|
33622
|
+
if (!candidate) {
|
|
33623
|
+
const available = stacks.map((s) => s.displayName ?? s.stackName).join(", ");
|
|
33624
|
+
throw new Error(
|
|
33625
|
+
`Construct path '${p}': stack '${head}' not found in synthesized app. Available: ${available}`
|
|
33626
|
+
);
|
|
33627
|
+
}
|
|
33628
|
+
if (stack === void 0) {
|
|
33629
|
+
stack = candidate;
|
|
33630
|
+
} else if (stack.stackName !== candidate.stackName) {
|
|
33631
|
+
throw new Error(
|
|
33632
|
+
`All construct paths must reference the same stack. Got '${stack.stackName}' and '${candidate.stackName}'. Run 'cdkd orphan' once per stack.`
|
|
33633
|
+
);
|
|
33634
|
+
}
|
|
33635
|
+
const cdkPath = p;
|
|
33636
|
+
const index = buildCdkPathIndex(candidate.template);
|
|
33637
|
+
const logicalId = index.get(cdkPath);
|
|
33638
|
+
if (!logicalId) {
|
|
33639
|
+
const available = [...index.keys()].sort().join("\n ");
|
|
33640
|
+
throw new Error(
|
|
33641
|
+
`Construct path '${cdkPath}' not found in template for stack '${candidate.stackName}'.
|
|
33642
|
+
Available paths:
|
|
33643
|
+
${available}`
|
|
33644
|
+
);
|
|
33645
|
+
}
|
|
33646
|
+
if (!logicalIds.includes(logicalId)) {
|
|
33647
|
+
logicalIds.push(logicalId);
|
|
33648
|
+
}
|
|
33649
|
+
}
|
|
33650
|
+
if (!stack) {
|
|
33651
|
+
throw new Error("No construct paths supplied.");
|
|
33652
|
+
}
|
|
33653
|
+
return { stack, logicalIds };
|
|
33654
|
+
}
|
|
33655
|
+
async function pickStackRegion(stateBackend, stackName, synthRegion, flag) {
|
|
33656
|
+
const refs = (await stateBackend.listStacks()).filter((r) => r.stackName === stackName);
|
|
33657
|
+
if (refs.length === 0) {
|
|
33658
|
+
if (flag)
|
|
33659
|
+
return flag;
|
|
33660
|
+
if (synthRegion)
|
|
33661
|
+
return synthRegion;
|
|
33662
|
+
throw new Error(
|
|
33663
|
+
`No state found for stack '${stackName}'. Run 'cdkd state list' to see available stacks.`
|
|
33664
|
+
);
|
|
33665
|
+
}
|
|
33666
|
+
if (flag) {
|
|
33667
|
+
const found = refs.find((r) => r.region === flag);
|
|
33668
|
+
if (!found) {
|
|
33669
|
+
const seen = refs.map((r) => r.region ?? "(legacy)").join(", ");
|
|
33670
|
+
throw new Error(
|
|
33671
|
+
`No state found for stack '${stackName}' in region '${flag}'. Available regions: ${seen}.`
|
|
33672
|
+
);
|
|
33673
|
+
}
|
|
33674
|
+
return flag;
|
|
33675
|
+
}
|
|
33676
|
+
if (synthRegion) {
|
|
33677
|
+
const found = refs.find((r) => r.region === synthRegion);
|
|
33678
|
+
if (found)
|
|
33679
|
+
return synthRegion;
|
|
33680
|
+
}
|
|
33681
|
+
if (refs.length === 1) {
|
|
33682
|
+
return refs[0].region ?? synthRegion ?? "";
|
|
33683
|
+
}
|
|
33684
|
+
const regions = refs.map((r) => r.region ?? "(legacy)").join(", ");
|
|
33685
|
+
throw new Error(
|
|
33686
|
+
`Stack '${stackName}' has state in multiple regions: ${regions}. Re-run with --stack-region <region> to disambiguate.`
|
|
33687
|
+
);
|
|
33688
|
+
}
|
|
33689
|
+
function printRewriteSummary(rewrites, orphanLogicalIds) {
|
|
33690
|
+
const logger = getLogger();
|
|
33691
|
+
logger.info("");
|
|
33692
|
+
logger.info(`Orphaning ${orphanLogicalIds.length} resource(s): ${orphanLogicalIds.join(", ")}`);
|
|
33693
|
+
if (rewrites.length === 0) {
|
|
33694
|
+
logger.info(" No sibling references \u2014 every reference was already to a non-orphan resource.");
|
|
33695
|
+
return;
|
|
33696
|
+
}
|
|
33697
|
+
logger.info(`Applied ${rewrites.length} rewrite(s):`);
|
|
33698
|
+
for (const r of rewrites) {
|
|
33699
|
+
const before = stringifyForAudit(r.before);
|
|
33700
|
+
const after = r.kind === "dependency" ? "(dropped)" : stringifyForAudit(r.after);
|
|
33701
|
+
logger.info(` [${r.kind}] ${r.logicalId}.${r.path}: ${before} \u2192 ${after}`);
|
|
33702
|
+
}
|
|
33703
|
+
}
|
|
33704
|
+
function printUnresolvable(unresolvable) {
|
|
33705
|
+
const logger = getLogger();
|
|
33706
|
+
logger.error(`${unresolvable.length} reference(s) could not be resolved:`);
|
|
33707
|
+
for (const u of unresolvable) {
|
|
33708
|
+
logger.error(` ${u.logicalId}.${u.path}: ${u.orphanLogicalId}.${u.attribute} \u2014 ${u.reason}`);
|
|
33709
|
+
}
|
|
33710
|
+
}
|
|
33711
|
+
function stringifyForAudit(value) {
|
|
33712
|
+
if (typeof value === "string")
|
|
33713
|
+
return JSON.stringify(value);
|
|
33714
|
+
return JSON.stringify(value);
|
|
33715
|
+
}
|
|
33716
|
+
async function confirmPrompt(prompt) {
|
|
33717
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
33718
|
+
try {
|
|
33719
|
+
const ans = await rl.question(`${prompt} [y/N] `);
|
|
33720
|
+
return /^y(es)?$/i.test(ans.trim());
|
|
33721
|
+
} finally {
|
|
33722
|
+
rl.close();
|
|
33723
|
+
}
|
|
33724
|
+
}
|
|
32872
33725
|
function createOrphanCommand() {
|
|
32873
33726
|
const cmd = new Command7("orphan").description(
|
|
32874
|
-
"Remove
|
|
33727
|
+
"Remove one or more resources from cdkd state by construct path (does NOT delete AWS resources). Mirrors aws-cdk-cli's 'cdk orphan --unstable=orphan'. Synth-driven; for the previous whole-stack-orphan behavior, use 'cdkd state orphan <stack>'."
|
|
32875
33728
|
).argument(
|
|
32876
|
-
"
|
|
32877
|
-
"
|
|
32878
|
-
).option(
|
|
33729
|
+
"<paths...>",
|
|
33730
|
+
"Construct paths to orphan, e.g. 'MyStack/MyTable'. Multiple paths must reference the same stack."
|
|
33731
|
+
).option(
|
|
32879
33732
|
"--stack-region <region>",
|
|
32880
33733
|
"Region of the stack record to operate on. Required when the same stack name has state in multiple regions."
|
|
33734
|
+
).option(
|
|
33735
|
+
"--dry-run",
|
|
33736
|
+
"Compute and print the rewrite audit table without acquiring a lock or saving state.",
|
|
33737
|
+
false
|
|
32881
33738
|
).action(withErrorHandling(orphanCommand));
|
|
32882
33739
|
[
|
|
32883
33740
|
...commonOptions,
|
|
32884
33741
|
...appOptions,
|
|
32885
33742
|
...stateOptions,
|
|
32886
|
-
...stackOptions,
|
|
32887
33743
|
...destroyOptions,
|
|
33744
|
+
// adds -f / --force (escape hatch for unresolvable references + skip confirm)
|
|
32888
33745
|
...contextOptions
|
|
32889
33746
|
].forEach((opt) => cmd.addOption(opt));
|
|
32890
33747
|
cmd.addOption(deprecatedRegionOption);
|
|
@@ -33076,7 +33933,7 @@ async function stateMigrateCommand(options) {
|
|
|
33076
33933
|
}
|
|
33077
33934
|
if (!options.yes) {
|
|
33078
33935
|
const action = options.removeLegacy ? "and DELETE the source bucket" : "(source bucket will be kept)";
|
|
33079
|
-
const ok = await
|
|
33936
|
+
const ok = await confirmPrompt2(
|
|
33080
33937
|
`Copy ${sourceObjects.length} object(s) from ${legacyBucket} -> ${newBucket} ${action}?`
|
|
33081
33938
|
);
|
|
33082
33939
|
if (!ok) {
|
|
@@ -33281,7 +34138,7 @@ async function emptyBucketAllVersions(s3, bucket) {
|
|
|
33281
34138
|
versionIdMarker = resp.NextVersionIdMarker;
|
|
33282
34139
|
} while (keyMarker || versionIdMarker);
|
|
33283
34140
|
}
|
|
33284
|
-
async function
|
|
34141
|
+
async function confirmPrompt2(prompt) {
|
|
33285
34142
|
const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
|
|
33286
34143
|
try {
|
|
33287
34144
|
const ans = await rl.question(`${prompt} [y/N] `);
|
|
@@ -33536,7 +34393,7 @@ function formatAttributeValue(value) {
|
|
|
33536
34393
|
}
|
|
33537
34394
|
return JSON.stringify(value);
|
|
33538
34395
|
}
|
|
33539
|
-
function
|
|
34396
|
+
function formatDuration2(ms) {
|
|
33540
34397
|
const seconds = Math.floor(ms / 1e3);
|
|
33541
34398
|
if (seconds < 60)
|
|
33542
34399
|
return `${seconds}s`;
|
|
@@ -33549,7 +34406,7 @@ function formatLockSummary(lockInfo) {
|
|
|
33549
34406
|
return "unlocked";
|
|
33550
34407
|
const opStr = lockInfo.operation ? ` (operation: ${lockInfo.operation})` : "";
|
|
33551
34408
|
const expiresInMs = lockInfo.expiresAt - Date.now();
|
|
33552
|
-
const expiresStr = expiresInMs > 0 ? `expires in ${
|
|
34409
|
+
const expiresStr = expiresInMs > 0 ? `expires in ${formatDuration2(expiresInMs)}` : `expired ${formatDuration2(-expiresInMs)} ago`;
|
|
33553
34410
|
return `locked by ${lockInfo.owner}${opStr}, ${expiresStr}`;
|
|
33554
34411
|
}
|
|
33555
34412
|
function createStateResourcesCommand() {
|
|
@@ -33737,6 +34594,10 @@ async function stateDestroyCommand(stackArgs, options) {
|
|
|
33737
34594
|
logger.setLevel("debug");
|
|
33738
34595
|
process.env["CDKD_NO_LIVE"] = "1";
|
|
33739
34596
|
}
|
|
34597
|
+
validateResourceTimeouts({
|
|
34598
|
+
...options.resourceWarnAfter && { resourceWarnAfter: options.resourceWarnAfter },
|
|
34599
|
+
...options.resourceTimeout && { resourceTimeout: options.resourceTimeout }
|
|
34600
|
+
});
|
|
33740
34601
|
if (!options.all && stackArgs.length === 0) {
|
|
33741
34602
|
throw new Error(
|
|
33742
34603
|
"Stack name is required. Usage: cdkd state destroy <stack> [<stack>...] | --all"
|
|
@@ -33836,7 +34697,19 @@ Preparing to destroy stack: ${stackName}${ref.region ? ` (${ref.region})` : ""}`
|
|
|
33836
34697
|
// and the per-stack prompt inside the runner. Per-stack prompts are
|
|
33837
34698
|
// skipped when `options.yes` is set OR `--all` was set (the user
|
|
33838
34699
|
// already accepted the batch prompt).
|
|
33839
|
-
skipConfirmation: options.yes || options.all === true
|
|
34700
|
+
skipConfirmation: options.yes || options.all === true,
|
|
34701
|
+
...options.resourceWarnAfter?.globalMs !== void 0 && {
|
|
34702
|
+
resourceWarnAfterMs: options.resourceWarnAfter.globalMs
|
|
34703
|
+
},
|
|
34704
|
+
...options.resourceTimeout?.globalMs !== void 0 && {
|
|
34705
|
+
resourceTimeoutMs: options.resourceTimeout.globalMs
|
|
34706
|
+
},
|
|
34707
|
+
...options.resourceWarnAfter?.perTypeMs && {
|
|
34708
|
+
resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs
|
|
34709
|
+
},
|
|
34710
|
+
...options.resourceTimeout?.perTypeMs && {
|
|
34711
|
+
resourceTimeoutByType: options.resourceTimeout.perTypeMs
|
|
34712
|
+
}
|
|
33840
34713
|
});
|
|
33841
34714
|
totalErrors += result.errorCount;
|
|
33842
34715
|
}
|
|
@@ -33867,7 +34740,9 @@ function createStateDestroyCommand() {
|
|
|
33867
34740
|
"For removing only the state record (keeping AWS resources intact), use 'cdkd state orphan'."
|
|
33868
34741
|
].join("\n")
|
|
33869
34742
|
).action(withErrorHandling(stateDestroyCommand));
|
|
33870
|
-
[...commonOptions, ...stateOptions].forEach(
|
|
34743
|
+
[...commonOptions, ...stateOptions, ...resourceTimeoutOptions].forEach(
|
|
34744
|
+
(opt) => cmd.addOption(opt)
|
|
34745
|
+
);
|
|
33871
34746
|
cmd.addOption(deprecatedRegionOption);
|
|
33872
34747
|
return cmd;
|
|
33873
34748
|
}
|
|
@@ -34002,7 +34877,7 @@ function createStateCommand() {
|
|
|
34002
34877
|
}
|
|
34003
34878
|
|
|
34004
34879
|
// src/cli/commands/import.ts
|
|
34005
|
-
import { readFileSync as readFileSync5 } from "node:fs";
|
|
34880
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
34006
34881
|
import * as readline5 from "node:readline/promises";
|
|
34007
34882
|
import { Command as Command12 } from "commander";
|
|
34008
34883
|
init_aws_clients();
|
|
@@ -34121,6 +34996,9 @@ async function importCommand(stackArg, options) {
|
|
|
34121
34996
|
rows.push(outcome);
|
|
34122
34997
|
}
|
|
34123
34998
|
printSummary(rows);
|
|
34999
|
+
if (options.recordResourceMapping) {
|
|
35000
|
+
writeRecordedMapping(options.recordResourceMapping, rows);
|
|
35001
|
+
}
|
|
34124
35002
|
if (options.dryRun) {
|
|
34125
35003
|
logger.info("--dry-run: state will NOT be written. Re-run without --dry-run to apply.");
|
|
34126
35004
|
return;
|
|
@@ -34131,7 +35009,7 @@ async function importCommand(stackArg, options) {
|
|
|
34131
35009
|
return;
|
|
34132
35010
|
}
|
|
34133
35011
|
if (!options.yes) {
|
|
34134
|
-
const ok = await
|
|
35012
|
+
const ok = await confirmPrompt3(
|
|
34135
35013
|
`Write state for ${stackInfo.stackName} (${targetRegion}) with ${importedRows.length} resource(s)?`
|
|
34136
35014
|
);
|
|
34137
35015
|
if (!ok) {
|
|
@@ -34274,12 +35152,24 @@ function parseMappingJson(raw, source) {
|
|
|
34274
35152
|
}
|
|
34275
35153
|
return out;
|
|
34276
35154
|
}
|
|
34277
|
-
function
|
|
34278
|
-
const
|
|
34279
|
-
|
|
34280
|
-
|
|
34281
|
-
|
|
34282
|
-
|
|
35155
|
+
function writeRecordedMapping(filePath, rows) {
|
|
35156
|
+
const logger = getLogger();
|
|
35157
|
+
const map = {};
|
|
35158
|
+
for (const row of rows) {
|
|
35159
|
+
if (row.outcome === "imported" && row.physicalId) {
|
|
35160
|
+
map[row.logicalId] = row.physicalId;
|
|
35161
|
+
}
|
|
35162
|
+
}
|
|
35163
|
+
const body = JSON.stringify(map, null, 2) + "\n";
|
|
35164
|
+
try {
|
|
35165
|
+
writeFileSync4(filePath, body, "utf-8");
|
|
35166
|
+
logger.info(`Wrote resolved mapping to ${filePath} (${Object.keys(map).length} entry(ies))`);
|
|
35167
|
+
} catch (err) {
|
|
35168
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
35169
|
+
logger.error(
|
|
35170
|
+
`Failed to write --record-resource-mapping file '${filePath}': ${msg}. Continuing \u2014 the import already resolved every physical id in memory.`
|
|
35171
|
+
);
|
|
35172
|
+
}
|
|
34283
35173
|
}
|
|
34284
35174
|
function collectImportableResources(template) {
|
|
34285
35175
|
const out = [];
|
|
@@ -34352,7 +35242,7 @@ function formatOutcome(outcome) {
|
|
|
34352
35242
|
return "\u2717";
|
|
34353
35243
|
}
|
|
34354
35244
|
}
|
|
34355
|
-
async function
|
|
35245
|
+
async function confirmPrompt3(prompt) {
|
|
34356
35246
|
const rl = readline5.createInterface({ input: process.stdin, output: process.stdout });
|
|
34357
35247
|
try {
|
|
34358
35248
|
const ans = await rl.question(`${prompt} [y/N] `);
|
|
@@ -34378,6 +35268,9 @@ function createImportCommand() {
|
|
|
34378
35268
|
).option(
|
|
34379
35269
|
"--resource-mapping-inline <json>",
|
|
34380
35270
|
"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."
|
|
35271
|
+
).option(
|
|
35272
|
+
"--record-resource-mapping <file>",
|
|
35273
|
+
'After cdkd resolves every logical ID (via --resource / --resource-mapping / tag-based auto-lookup), write the resulting {logicalId: physicalId} map to <file> as JSON. Useful in auto / hybrid mode for capturing the tag-resolved mapping and feeding it back as --resource-mapping in non-interactive CI re-runs. Written before the confirmation prompt (so the user can review the file before saying "yes") and even when the user says "no". Mirrors `cdk import --record-resource-mapping`.'
|
|
34381
35274
|
).option(
|
|
34382
35275
|
"--auto",
|
|
34383
35276
|
"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).",
|
|
@@ -34423,7 +35316,7 @@ function reorderArgs(argv) {
|
|
|
34423
35316
|
}
|
|
34424
35317
|
async function main() {
|
|
34425
35318
|
const program = new Command13();
|
|
34426
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
35319
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.26.0");
|
|
34427
35320
|
program.addCommand(createBootstrapCommand());
|
|
34428
35321
|
program.addCommand(createSynthCommand());
|
|
34429
35322
|
program.addCommand(createListCommand());
|