@go-to-k/cdkd 0.25.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 +59 -16
- package/dist/cli.js +775 -165
- package/dist/cli.js.map +4 -4
- package/dist/go-to-k-cdkd-0.26.0.tgz +0 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +2 -2
- package/package.json +1 -1
- package/dist/go-to-k-cdkd-0.25.0.tgz +0 -0
package/dist/cli.js
CHANGED
|
@@ -528,29 +528,82 @@ function parseDuration(value) {
|
|
|
528
528
|
const multiplier = unit === "s" ? 1e3 : unit === "m" ? 6e4 : 36e5;
|
|
529
529
|
return Math.round(num * multiplier);
|
|
530
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
|
+
}
|
|
531
565
|
var resourceTimeoutOptions = [
|
|
532
|
-
// Default
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
// `
|
|
537
|
-
//
|
|
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.
|
|
538
572
|
new Option(
|
|
539
|
-
"--resource-warn-after <duration>",
|
|
540
|
-
"Warn when a single resource operation exceeds this wall-clock duration (e.g. 5m
|
|
541
|
-
).default(
|
|
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")),
|
|
542
576
|
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(
|
|
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"))
|
|
546
580
|
];
|
|
547
581
|
function validateResourceTimeouts(opts) {
|
|
548
582
|
const warn = opts.resourceWarnAfter;
|
|
549
583
|
const timeout = opts.resourceTimeout;
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
)
|
|
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
|
+
}
|
|
554
607
|
}
|
|
555
608
|
}
|
|
556
609
|
var deployOptions = [
|
|
@@ -31580,8 +31633,8 @@ var DeployEngine = class {
|
|
|
31580
31633
|
const baseLabel = `${verb} ${logicalId} (${resourceType})`;
|
|
31581
31634
|
renderer.addTask(logicalId, baseLabel);
|
|
31582
31635
|
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;
|
|
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;
|
|
31585
31638
|
try {
|
|
31586
31639
|
await withResourceDeadline(
|
|
31587
31640
|
async () => {
|
|
@@ -32085,8 +32138,8 @@ async function deployCommand(stacks, options) {
|
|
|
32085
32138
|
}
|
|
32086
32139
|
warnIfDeprecatedRegion(options);
|
|
32087
32140
|
validateResourceTimeouts({
|
|
32088
|
-
resourceWarnAfter: options.resourceWarnAfter,
|
|
32089
|
-
resourceTimeout: options.resourceTimeout
|
|
32141
|
+
...options.resourceWarnAfter && { resourceWarnAfter: options.resourceWarnAfter },
|
|
32142
|
+
...options.resourceTimeout && { resourceTimeout: options.resourceTimeout }
|
|
32090
32143
|
});
|
|
32091
32144
|
if (!options.wait) {
|
|
32092
32145
|
process.env["CDKD_NO_WAIT"] = "true";
|
|
@@ -32281,8 +32334,18 @@ Deploying stack: ${stackInfo.stackName}${stackRegion !== baseRegion ? ` (region:
|
|
|
32281
32334
|
concurrency: options.concurrency,
|
|
32282
32335
|
dryRun: options.dryRun,
|
|
32283
32336
|
noRollback: !options.rollback,
|
|
32284
|
-
|
|
32285
|
-
|
|
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
|
+
}
|
|
32286
32349
|
},
|
|
32287
32350
|
stackRegion
|
|
32288
32351
|
);
|
|
@@ -32692,8 +32755,6 @@ Acquiring lock for stack ${stackName}...`);
|
|
|
32692
32755
|
logger.debug(
|
|
32693
32756
|
`Deletion level ${executionLevels.length - levelIndex}/${executionLevels.length} (${level.length} resources)`
|
|
32694
32757
|
);
|
|
32695
|
-
const warnAfterMs = ctx.resourceWarnAfterMs ?? DEFAULT_RESOURCE_WARN_AFTER_MS;
|
|
32696
|
-
const timeoutMs = ctx.resourceTimeoutMs ?? DEFAULT_RESOURCE_TIMEOUT_MS;
|
|
32697
32758
|
const stackRegion2 = state.region ?? ctx.baseRegion;
|
|
32698
32759
|
const deletePromises = level.map(async (logicalId) => {
|
|
32699
32760
|
const resource = state.resources[logicalId];
|
|
@@ -32701,6 +32762,8 @@ Acquiring lock for stack ${stackName}...`);
|
|
|
32701
32762
|
logger.warn(`Resource ${logicalId} not found in state, skipping`);
|
|
32702
32763
|
return;
|
|
32703
32764
|
}
|
|
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;
|
|
32704
32767
|
const baseLabel = `Deleting ${logicalId} (${resource.resourceType})`;
|
|
32705
32768
|
renderer.addTask(logicalId, baseLabel);
|
|
32706
32769
|
try {
|
|
@@ -32821,8 +32884,8 @@ async function destroyCommand(stackArgs, options) {
|
|
|
32821
32884
|
}
|
|
32822
32885
|
warnIfDeprecatedRegion(options);
|
|
32823
32886
|
validateResourceTimeouts({
|
|
32824
|
-
resourceWarnAfter: options.resourceWarnAfter,
|
|
32825
|
-
resourceTimeout: options.resourceTimeout
|
|
32887
|
+
...options.resourceWarnAfter && { resourceWarnAfter: options.resourceWarnAfter },
|
|
32888
|
+
...options.resourceTimeout && { resourceTimeout: options.resourceTimeout }
|
|
32826
32889
|
});
|
|
32827
32890
|
const region = options.region || process.env["AWS_REGION"] || "us-east-1";
|
|
32828
32891
|
const stateBucket = await resolveStateBucketWithDefault(options.stateBucket, region);
|
|
@@ -32956,8 +33019,18 @@ Preparing to destroy stack: ${stackName}`);
|
|
|
32956
33019
|
...options.profile && { profile: options.profile },
|
|
32957
33020
|
stateBucket,
|
|
32958
33021
|
skipConfirmation: options.yes || options.force,
|
|
32959
|
-
|
|
32960
|
-
|
|
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
|
+
}
|
|
32961
33034
|
});
|
|
32962
33035
|
}
|
|
32963
33036
|
} finally {
|
|
@@ -32986,15 +33059,432 @@ function createDestroyCommand() {
|
|
|
32986
33059
|
import * as readline2 from "node:readline/promises";
|
|
32987
33060
|
import { Command as Command7 } from "commander";
|
|
32988
33061
|
init_aws_clients();
|
|
32989
|
-
|
|
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) {
|
|
32990
33467
|
const logger = getLogger();
|
|
32991
33468
|
if (options.verbose)
|
|
32992
33469
|
logger.setLevel("debug");
|
|
32993
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
|
+
}
|
|
32994
33486
|
const region = options.region || process.env["AWS_REGION"] || "us-east-1";
|
|
32995
33487
|
const stateBucket = await resolveStateBucketWithDefault(options.stateBucket, region);
|
|
32996
|
-
logger.info("Starting stack orphan...");
|
|
32997
|
-
logger.debug("Options:", options);
|
|
32998
33488
|
if (options.region) {
|
|
32999
33489
|
process.env["AWS_REGION"] = options.region;
|
|
33000
33490
|
process.env["AWS_DEFAULT_REGION"] = options.region;
|
|
@@ -33005,10 +33495,7 @@ async function orphanCommand(stackArgs, options) {
|
|
|
33005
33495
|
});
|
|
33006
33496
|
setAwsClients(awsClients);
|
|
33007
33497
|
try {
|
|
33008
|
-
const stateConfig = {
|
|
33009
|
-
bucket: stateBucket,
|
|
33010
|
-
prefix: options.statePrefix
|
|
33011
|
-
};
|
|
33498
|
+
const stateConfig = { bucket: stateBucket, prefix: options.statePrefix };
|
|
33012
33499
|
const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
|
|
33013
33500
|
...options.region && { region: options.region },
|
|
33014
33501
|
...options.profile && { profile: options.profile }
|
|
@@ -33016,150 +33503,245 @@ async function orphanCommand(stackArgs, options) {
|
|
|
33016
33503
|
await stateBackend.verifyBucketExists();
|
|
33017
33504
|
const lockManager = new LockManager(awsClients.s3, stateConfig);
|
|
33018
33505
|
const appCmd = options.app || resolveApp();
|
|
33019
|
-
|
|
33020
|
-
if (appCmd) {
|
|
33021
|
-
try {
|
|
33022
|
-
const synthesizer = new Synthesizer();
|
|
33023
|
-
const context = parseContextOptions(options.context);
|
|
33024
|
-
const result = await synthesizer.synthesize({
|
|
33025
|
-
app: appCmd,
|
|
33026
|
-
output: options.output || "cdk.out",
|
|
33027
|
-
...Object.keys(context).length > 0 && { context }
|
|
33028
|
-
});
|
|
33029
|
-
appStacks = result.stacks.map((s) => ({
|
|
33030
|
-
stackName: s.stackName,
|
|
33031
|
-
displayName: s.displayName,
|
|
33032
|
-
...s.region && { region: s.region }
|
|
33033
|
-
}));
|
|
33034
|
-
} catch {
|
|
33035
|
-
logger.debug("Could not synthesize app, falling back to state-based stack list");
|
|
33036
|
-
}
|
|
33037
|
-
}
|
|
33038
|
-
const allStateRefs = await stateBackend.listStacks();
|
|
33039
|
-
let candidateStacks;
|
|
33040
|
-
if (appStacks.length > 0) {
|
|
33041
|
-
const stateNames = new Set(allStateRefs.map((r) => r.stackName));
|
|
33042
|
-
candidateStacks = appStacks.filter((s) => stateNames.has(s.stackName));
|
|
33043
|
-
} else if (stackArgs.length > 0 || options.stack || options.all) {
|
|
33044
|
-
const seen = /* @__PURE__ */ new Set();
|
|
33045
|
-
candidateStacks = [];
|
|
33046
|
-
for (const ref of allStateRefs) {
|
|
33047
|
-
if (seen.has(ref.stackName))
|
|
33048
|
-
continue;
|
|
33049
|
-
seen.add(ref.stackName);
|
|
33050
|
-
candidateStacks.push({ stackName: ref.stackName });
|
|
33051
|
-
}
|
|
33052
|
-
} else {
|
|
33053
|
-
throw new Error(
|
|
33054
|
-
"Could not determine which stacks belong to this app. Specify stack names explicitly, use --all, or ensure --app / cdk.json is configured."
|
|
33055
|
-
);
|
|
33056
|
-
}
|
|
33057
|
-
const stackPatterns = stackArgs.length > 0 ? stackArgs : options.stack ? [options.stack] : [];
|
|
33058
|
-
let stackNames;
|
|
33059
|
-
if (options.all) {
|
|
33060
|
-
stackNames = candidateStacks.map((s) => s.stackName);
|
|
33061
|
-
} else if (stackPatterns.length > 0) {
|
|
33062
|
-
stackNames = matchStacks(candidateStacks, stackPatterns).map((s) => s.stackName);
|
|
33063
|
-
} else if (candidateStacks.length === 1) {
|
|
33064
|
-
stackNames = candidateStacks.map((s) => s.stackName);
|
|
33065
|
-
} else if (candidateStacks.length === 0) {
|
|
33066
|
-
logger.info("No stacks found in state");
|
|
33067
|
-
return;
|
|
33068
|
-
} else {
|
|
33506
|
+
if (!appCmd) {
|
|
33069
33507
|
throw new Error(
|
|
33070
|
-
|
|
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."
|
|
33071
33509
|
);
|
|
33072
33510
|
}
|
|
33073
|
-
|
|
33074
|
-
|
|
33075
|
-
|
|
33076
|
-
|
|
33077
|
-
|
|
33078
|
-
|
|
33079
|
-
|
|
33080
|
-
|
|
33081
|
-
|
|
33082
|
-
|
|
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");
|
|
33083
33534
|
}
|
|
33084
|
-
|
|
33085
|
-
|
|
33086
|
-
|
|
33087
|
-
|
|
33088
|
-
|
|
33089
|
-
|
|
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
|
+
);
|
|
33090
33541
|
}
|
|
33091
|
-
const
|
|
33092
|
-
|
|
33093
|
-
|
|
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(", ");
|
|
33094
33546
|
throw new Error(
|
|
33095
|
-
`
|
|
33547
|
+
`Resource(s) not in state for stack '${stackInfo.stackName}' (${targetRegion}): ${missing.join(", ")}.
|
|
33548
|
+
Available logical IDs: ${have}`
|
|
33096
33549
|
);
|
|
33097
33550
|
}
|
|
33098
|
-
|
|
33099
|
-
|
|
33100
|
-
|
|
33101
|
-
|
|
33102
|
-
|
|
33103
|
-
|
|
33104
|
-
|
|
33105
|
-
|
|
33106
|
-
|
|
33107
|
-
|
|
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
|
+
);
|
|
33108
33566
|
}
|
|
33109
|
-
if (
|
|
33110
|
-
|
|
33111
|
-
|
|
33112
|
-
|
|
33113
|
-
WARNING: This removes cdkd's state record for [${targetList}] only. AWS resources will NOT be deleted.
|
|
33114
|
-
Use 'cdkd destroy ${stackName}' if you want to delete the actual resources.
|
|
33115
|
-
|
|
33116
|
-
`
|
|
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.`
|
|
33117
33571
|
);
|
|
33118
|
-
|
|
33119
|
-
|
|
33120
|
-
|
|
33121
|
-
|
|
33122
|
-
|
|
33123
|
-
|
|
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.`
|
|
33124
33580
|
);
|
|
33125
|
-
|
|
33126
|
-
|
|
33127
|
-
|
|
33128
|
-
logger.info(`Cancelled orphan of stack: ${stackName}`);
|
|
33129
|
-
continue;
|
|
33581
|
+
if (!ok) {
|
|
33582
|
+
logger.info("Orphan cancelled.");
|
|
33583
|
+
return;
|
|
33130
33584
|
}
|
|
33131
33585
|
}
|
|
33132
|
-
|
|
33133
|
-
|
|
33134
|
-
|
|
33135
|
-
|
|
33136
|
-
|
|
33137
|
-
|
|
33138
|
-
|
|
33139
|
-
|
|
33140
|
-
|
|
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
|
+
});
|
|
33141
33600
|
}
|
|
33142
33601
|
}
|
|
33143
33602
|
} finally {
|
|
33144
33603
|
awsClients.destroy();
|
|
33145
33604
|
}
|
|
33146
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
|
+
}
|
|
33147
33725
|
function createOrphanCommand() {
|
|
33148
33726
|
const cmd = new Command7("orphan").description(
|
|
33149
|
-
"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>'."
|
|
33150
33728
|
).argument(
|
|
33151
|
-
"
|
|
33152
|
-
"
|
|
33153
|
-
).option(
|
|
33729
|
+
"<paths...>",
|
|
33730
|
+
"Construct paths to orphan, e.g. 'MyStack/MyTable'. Multiple paths must reference the same stack."
|
|
33731
|
+
).option(
|
|
33154
33732
|
"--stack-region <region>",
|
|
33155
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
|
|
33156
33738
|
).action(withErrorHandling(orphanCommand));
|
|
33157
33739
|
[
|
|
33158
33740
|
...commonOptions,
|
|
33159
33741
|
...appOptions,
|
|
33160
33742
|
...stateOptions,
|
|
33161
|
-
...stackOptions,
|
|
33162
33743
|
...destroyOptions,
|
|
33744
|
+
// adds -f / --force (escape hatch for unresolvable references + skip confirm)
|
|
33163
33745
|
...contextOptions
|
|
33164
33746
|
].forEach((opt) => cmd.addOption(opt));
|
|
33165
33747
|
cmd.addOption(deprecatedRegionOption);
|
|
@@ -33351,7 +33933,7 @@ async function stateMigrateCommand(options) {
|
|
|
33351
33933
|
}
|
|
33352
33934
|
if (!options.yes) {
|
|
33353
33935
|
const action = options.removeLegacy ? "and DELETE the source bucket" : "(source bucket will be kept)";
|
|
33354
|
-
const ok = await
|
|
33936
|
+
const ok = await confirmPrompt2(
|
|
33355
33937
|
`Copy ${sourceObjects.length} object(s) from ${legacyBucket} -> ${newBucket} ${action}?`
|
|
33356
33938
|
);
|
|
33357
33939
|
if (!ok) {
|
|
@@ -33556,7 +34138,7 @@ async function emptyBucketAllVersions(s3, bucket) {
|
|
|
33556
34138
|
versionIdMarker = resp.NextVersionIdMarker;
|
|
33557
34139
|
} while (keyMarker || versionIdMarker);
|
|
33558
34140
|
}
|
|
33559
|
-
async function
|
|
34141
|
+
async function confirmPrompt2(prompt) {
|
|
33560
34142
|
const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
|
|
33561
34143
|
try {
|
|
33562
34144
|
const ans = await rl.question(`${prompt} [y/N] `);
|
|
@@ -34013,8 +34595,8 @@ async function stateDestroyCommand(stackArgs, options) {
|
|
|
34013
34595
|
process.env["CDKD_NO_LIVE"] = "1";
|
|
34014
34596
|
}
|
|
34015
34597
|
validateResourceTimeouts({
|
|
34016
|
-
resourceWarnAfter: options.resourceWarnAfter,
|
|
34017
|
-
resourceTimeout: options.resourceTimeout
|
|
34598
|
+
...options.resourceWarnAfter && { resourceWarnAfter: options.resourceWarnAfter },
|
|
34599
|
+
...options.resourceTimeout && { resourceTimeout: options.resourceTimeout }
|
|
34018
34600
|
});
|
|
34019
34601
|
if (!options.all && stackArgs.length === 0) {
|
|
34020
34602
|
throw new Error(
|
|
@@ -34116,8 +34698,18 @@ Preparing to destroy stack: ${stackName}${ref.region ? ` (${ref.region})` : ""}`
|
|
|
34116
34698
|
// skipped when `options.yes` is set OR `--all` was set (the user
|
|
34117
34699
|
// already accepted the batch prompt).
|
|
34118
34700
|
skipConfirmation: options.yes || options.all === true,
|
|
34119
|
-
|
|
34120
|
-
|
|
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
|
+
}
|
|
34121
34713
|
});
|
|
34122
34714
|
totalErrors += result.errorCount;
|
|
34123
34715
|
}
|
|
@@ -34285,7 +34877,7 @@ function createStateCommand() {
|
|
|
34285
34877
|
}
|
|
34286
34878
|
|
|
34287
34879
|
// src/cli/commands/import.ts
|
|
34288
|
-
import { readFileSync as readFileSync5 } from "node:fs";
|
|
34880
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
34289
34881
|
import * as readline5 from "node:readline/promises";
|
|
34290
34882
|
import { Command as Command12 } from "commander";
|
|
34291
34883
|
init_aws_clients();
|
|
@@ -34404,6 +34996,9 @@ async function importCommand(stackArg, options) {
|
|
|
34404
34996
|
rows.push(outcome);
|
|
34405
34997
|
}
|
|
34406
34998
|
printSummary(rows);
|
|
34999
|
+
if (options.recordResourceMapping) {
|
|
35000
|
+
writeRecordedMapping(options.recordResourceMapping, rows);
|
|
35001
|
+
}
|
|
34407
35002
|
if (options.dryRun) {
|
|
34408
35003
|
logger.info("--dry-run: state will NOT be written. Re-run without --dry-run to apply.");
|
|
34409
35004
|
return;
|
|
@@ -34414,7 +35009,7 @@ async function importCommand(stackArg, options) {
|
|
|
34414
35009
|
return;
|
|
34415
35010
|
}
|
|
34416
35011
|
if (!options.yes) {
|
|
34417
|
-
const ok = await
|
|
35012
|
+
const ok = await confirmPrompt3(
|
|
34418
35013
|
`Write state for ${stackInfo.stackName} (${targetRegion}) with ${importedRows.length} resource(s)?`
|
|
34419
35014
|
);
|
|
34420
35015
|
if (!ok) {
|
|
@@ -34557,12 +35152,24 @@ function parseMappingJson(raw, source) {
|
|
|
34557
35152
|
}
|
|
34558
35153
|
return out;
|
|
34559
35154
|
}
|
|
34560
|
-
function
|
|
34561
|
-
const
|
|
34562
|
-
|
|
34563
|
-
|
|
34564
|
-
|
|
34565
|
-
|
|
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
|
+
}
|
|
34566
35173
|
}
|
|
34567
35174
|
function collectImportableResources(template) {
|
|
34568
35175
|
const out = [];
|
|
@@ -34635,7 +35242,7 @@ function formatOutcome(outcome) {
|
|
|
34635
35242
|
return "\u2717";
|
|
34636
35243
|
}
|
|
34637
35244
|
}
|
|
34638
|
-
async function
|
|
35245
|
+
async function confirmPrompt3(prompt) {
|
|
34639
35246
|
const rl = readline5.createInterface({ input: process.stdin, output: process.stdout });
|
|
34640
35247
|
try {
|
|
34641
35248
|
const ans = await rl.question(`${prompt} [y/N] `);
|
|
@@ -34661,6 +35268,9 @@ function createImportCommand() {
|
|
|
34661
35268
|
).option(
|
|
34662
35269
|
"--resource-mapping-inline <json>",
|
|
34663
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`.'
|
|
34664
35274
|
).option(
|
|
34665
35275
|
"--auto",
|
|
34666
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).",
|
|
@@ -34706,7 +35316,7 @@ function reorderArgs(argv) {
|
|
|
34706
35316
|
}
|
|
34707
35317
|
async function main() {
|
|
34708
35318
|
const program = new Command13();
|
|
34709
|
-
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");
|
|
34710
35320
|
program.addCommand(createBootstrapCommand());
|
|
34711
35321
|
program.addCommand(createSynthCommand());
|
|
34712
35322
|
program.addCommand(createListCommand());
|