@go-to-k/cdkd 0.11.0 → 0.13.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 +19 -1
- package/dist/cli.js +537 -203
- package/dist/cli.js.map +4 -4
- package/dist/go-to-k-cdkd-0.13.0.tgz +0 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/go-to-k-cdkd-0.11.0.tgz +0 -0
package/dist/cli.js
CHANGED
|
@@ -450,7 +450,7 @@ var init_aws_clients = __esm({
|
|
|
450
450
|
import { Command as Command10 } from "commander";
|
|
451
451
|
|
|
452
452
|
// src/cli/commands/bootstrap.ts
|
|
453
|
-
import { Command } from "commander";
|
|
453
|
+
import { Command, Option as Option2 } from "commander";
|
|
454
454
|
import {
|
|
455
455
|
CreateBucketCommand,
|
|
456
456
|
HeadBucketCommand,
|
|
@@ -476,13 +476,23 @@ function parseContextOptions(contextArgs) {
|
|
|
476
476
|
}
|
|
477
477
|
var commonOptions = [
|
|
478
478
|
new Option("--verbose", "Enable verbose logging").default(false),
|
|
479
|
-
new Option("--region <region>", "AWS region"),
|
|
480
479
|
new Option("--profile <profile>", "AWS profile"),
|
|
481
480
|
new Option(
|
|
482
481
|
"-y, --yes",
|
|
483
482
|
"Automatically answer interactive prompts with the recommended response (e.g. confirm destroy)"
|
|
484
483
|
).default(false)
|
|
485
484
|
];
|
|
485
|
+
var deprecatedRegionOption = new Option(
|
|
486
|
+
"--region <region>",
|
|
487
|
+
"[deprecated] No effect on this command; use AWS_REGION or your AWS profile"
|
|
488
|
+
).hideHelp();
|
|
489
|
+
function warnIfDeprecatedRegion(options) {
|
|
490
|
+
if (options.region !== void 0) {
|
|
491
|
+
process.stderr.write(
|
|
492
|
+
"Warning: --region is deprecated for this command and has no effect. Use the AWS_REGION environment variable or your AWS profile to override the SDK default region.\n"
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
486
496
|
var appOptions = [
|
|
487
497
|
new Option(
|
|
488
498
|
"-a, --app <command>",
|
|
@@ -960,16 +970,18 @@ function resolveApp(cliApp) {
|
|
|
960
970
|
const cdkJson = loadCdkJson();
|
|
961
971
|
return cdkJson?.app ?? void 0;
|
|
962
972
|
}
|
|
963
|
-
function
|
|
973
|
+
function resolveStateBucketWithSource(cliBucket) {
|
|
964
974
|
if (cliBucket)
|
|
965
|
-
return cliBucket;
|
|
975
|
+
return { bucket: cliBucket, source: "cli-flag" };
|
|
966
976
|
const envBucket = process.env["CDKD_STATE_BUCKET"];
|
|
967
977
|
if (envBucket)
|
|
968
|
-
return envBucket;
|
|
978
|
+
return { bucket: envBucket, source: "env" };
|
|
969
979
|
const cdkJson = loadCdkJson();
|
|
970
980
|
const cdkdContext = cdkJson?.context?.["cdkd"];
|
|
971
981
|
const bucket = cdkdContext?.["stateBucket"];
|
|
972
|
-
|
|
982
|
+
if (typeof bucket === "string")
|
|
983
|
+
return { bucket, source: "cdk.json" };
|
|
984
|
+
return void 0;
|
|
973
985
|
}
|
|
974
986
|
function getDefaultStateBucketName(accountId) {
|
|
975
987
|
return `cdkd-state-${accountId}`;
|
|
@@ -978,7 +990,10 @@ function getLegacyStateBucketName(accountId, region) {
|
|
|
978
990
|
return `cdkd-state-${accountId}-${region}`;
|
|
979
991
|
}
|
|
980
992
|
async function resolveStateBucketWithDefault(cliBucket, region) {
|
|
981
|
-
|
|
993
|
+
return (await resolveStateBucketWithDefaultAndSource(cliBucket, region)).bucket;
|
|
994
|
+
}
|
|
995
|
+
async function resolveStateBucketWithDefaultAndSource(cliBucket, region) {
|
|
996
|
+
const syncResult = resolveStateBucketWithSource(cliBucket);
|
|
982
997
|
if (syncResult)
|
|
983
998
|
return syncResult;
|
|
984
999
|
const logger = getLogger();
|
|
@@ -994,14 +1009,14 @@ async function resolveStateBucketWithDefault(cliBucket, region) {
|
|
|
994
1009
|
const probe = new S3Client10({ region: "us-east-1" });
|
|
995
1010
|
try {
|
|
996
1011
|
if (await bucketExists(probe, newName)) {
|
|
997
|
-
logger.
|
|
998
|
-
return newName;
|
|
1012
|
+
logger.debug(`State bucket: ${newName}`);
|
|
1013
|
+
return { bucket: newName, source: "default" };
|
|
999
1014
|
}
|
|
1000
1015
|
if (await bucketExists(probe, legacyName)) {
|
|
1001
1016
|
logger.warn(
|
|
1002
1017
|
`Using legacy state bucket name '${legacyName}'. The default has changed to '${newName}'. Future cdkd versions will drop legacy support; consider migrating with cdkd state migrate-bucket (coming in a future release).`
|
|
1003
1018
|
);
|
|
1004
|
-
return legacyName;
|
|
1019
|
+
return { bucket: legacyName, source: "default-legacy" };
|
|
1005
1020
|
}
|
|
1006
1021
|
throw new Error(
|
|
1007
1022
|
`No cdkd state bucket found for account ${accountId}. Looked for '${newName}' (current default) and '${legacyName}' (legacy default). Run 'cdkd bootstrap' to create '${newName}'.`
|
|
@@ -1158,7 +1173,16 @@ function createBootstrapCommand() {
|
|
|
1158
1173
|
const cmd = new Command("bootstrap").description("Bootstrap cdkd by creating required S3 bucket for state management").option(
|
|
1159
1174
|
"--state-bucket <bucket>",
|
|
1160
1175
|
"Name of S3 bucket to create for state storage (default: cdkd-state-{accountId})"
|
|
1161
|
-
).option("--force", "Force reconfiguration of existing bucket", false).
|
|
1176
|
+
).option("--force", "Force reconfiguration of existing bucket", false).addOption(
|
|
1177
|
+
// Bootstrap-specific: needs to know which region to create the bucket
|
|
1178
|
+
// in. After PR 5, `--region` is removed from `commonOptions` and only
|
|
1179
|
+
// re-added explicitly here — every other command resolves the region
|
|
1180
|
+
// from `AWS_REGION` / profile.
|
|
1181
|
+
new Option2(
|
|
1182
|
+
"--region <region>",
|
|
1183
|
+
"AWS region in which to create the state bucket (defaults to AWS_REGION env or us-east-1)"
|
|
1184
|
+
)
|
|
1185
|
+
).action(withErrorHandling(bootstrapCommand));
|
|
1162
1186
|
commonOptions.forEach((opt) => cmd.addOption(opt));
|
|
1163
1187
|
return cmd;
|
|
1164
1188
|
}
|
|
@@ -2493,6 +2517,7 @@ async function synthCommand(options) {
|
|
|
2493
2517
|
if (options.verbose) {
|
|
2494
2518
|
logger.setLevel("debug");
|
|
2495
2519
|
}
|
|
2520
|
+
warnIfDeprecatedRegion(options);
|
|
2496
2521
|
const app = resolveApp(options.app);
|
|
2497
2522
|
if (!app) {
|
|
2498
2523
|
throw new Error(
|
|
@@ -2540,6 +2565,7 @@ Output: ${assemblyDir}`);
|
|
|
2540
2565
|
function createSynthCommand() {
|
|
2541
2566
|
const cmd = new Command2("synth").description("Synthesize CDK app to CloudFormation template").action(withErrorHandling(synthCommand));
|
|
2542
2567
|
[...commonOptions, ...appOptions, ...contextOptions].forEach((opt) => cmd.addOption(opt));
|
|
2568
|
+
cmd.addOption(deprecatedRegionOption);
|
|
2543
2569
|
return cmd;
|
|
2544
2570
|
}
|
|
2545
2571
|
|
|
@@ -2622,6 +2648,7 @@ async function listCommand(patterns, options) {
|
|
|
2622
2648
|
if (options.verbose) {
|
|
2623
2649
|
logger.setLevel("debug");
|
|
2624
2650
|
}
|
|
2651
|
+
warnIfDeprecatedRegion(options);
|
|
2625
2652
|
const app = resolveApp(options.app);
|
|
2626
2653
|
if (!app) {
|
|
2627
2654
|
throw new Error(
|
|
@@ -2687,6 +2714,7 @@ function createListCommand() {
|
|
|
2687
2714
|
"Stack name pattern(s). Accepts physical CloudFormation names (e.g. 'MyStage-Api') or CDK display paths (e.g. 'MyStage/Api'). Supports wildcards (e.g. 'MyStage/*')."
|
|
2688
2715
|
).option("-l, --long", "Display environment information for each stack", false).option("-d, --show-dependencies", "Display stack dependency information for each stack", false).option("--json", "Output as JSON instead of YAML for --long / --show-dependencies", false).action(withErrorHandling(listCommand));
|
|
2689
2716
|
[...commonOptions, ...appOptions, ...contextOptions].forEach((opt) => cmd.addOption(opt));
|
|
2717
|
+
cmd.addOption(deprecatedRegionOption);
|
|
2690
2718
|
return cmd;
|
|
2691
2719
|
}
|
|
2692
2720
|
|
|
@@ -28586,6 +28614,7 @@ async function deployCommand(stacks, options) {
|
|
|
28586
28614
|
logger.setLevel("debug");
|
|
28587
28615
|
process.env["CDKD_NO_LIVE"] = "1";
|
|
28588
28616
|
}
|
|
28617
|
+
warnIfDeprecatedRegion(options);
|
|
28589
28618
|
if (!options.wait) {
|
|
28590
28619
|
process.env["CDKD_NO_WAIT"] = "true";
|
|
28591
28620
|
}
|
|
@@ -28835,6 +28864,7 @@ function createDeployCommand() {
|
|
|
28835
28864
|
...deployOptions,
|
|
28836
28865
|
...contextOptions
|
|
28837
28866
|
].forEach((opt) => cmd.addOption(opt));
|
|
28867
|
+
cmd.addOption(deprecatedRegionOption);
|
|
28838
28868
|
return cmd;
|
|
28839
28869
|
}
|
|
28840
28870
|
|
|
@@ -28903,6 +28933,7 @@ async function diffCommand(stacks, options) {
|
|
|
28903
28933
|
if (options.verbose) {
|
|
28904
28934
|
logger.setLevel("debug");
|
|
28905
28935
|
}
|
|
28936
|
+
warnIfDeprecatedRegion(options);
|
|
28906
28937
|
const app = resolveApp(options.app);
|
|
28907
28938
|
if (!app) {
|
|
28908
28939
|
throw new Error(
|
|
@@ -29046,19 +29077,219 @@ function createDiffCommand() {
|
|
|
29046
29077
|
[...commonOptions, ...appOptions, ...stateOptions, ...stackOptions, ...contextOptions].forEach(
|
|
29047
29078
|
(opt) => cmd.addOption(opt)
|
|
29048
29079
|
);
|
|
29080
|
+
cmd.addOption(deprecatedRegionOption);
|
|
29049
29081
|
return cmd;
|
|
29050
29082
|
}
|
|
29051
29083
|
|
|
29052
29084
|
// src/cli/commands/destroy.ts
|
|
29053
29085
|
import { Command as Command6 } from "commander";
|
|
29054
29086
|
init_aws_clients();
|
|
29087
|
+
|
|
29088
|
+
// src/cli/commands/destroy-runner.ts
|
|
29055
29089
|
import * as readline from "node:readline/promises";
|
|
29090
|
+
init_aws_clients();
|
|
29091
|
+
async function runDestroyForStack(stackName, state, ctx) {
|
|
29092
|
+
const logger = getLogger();
|
|
29093
|
+
const result = {
|
|
29094
|
+
stackName,
|
|
29095
|
+
cancelled: false,
|
|
29096
|
+
skippedEmpty: false,
|
|
29097
|
+
deletedCount: 0,
|
|
29098
|
+
errorCount: 0
|
|
29099
|
+
};
|
|
29100
|
+
const resourceCount = Object.keys(state.resources).length;
|
|
29101
|
+
const regionForState = state.region ?? ctx.baseRegion;
|
|
29102
|
+
if (resourceCount === 0) {
|
|
29103
|
+
logger.info(`Stack ${stackName} has no resources, cleaning up state...`);
|
|
29104
|
+
await ctx.stateBackend.deleteState(stackName, regionForState);
|
|
29105
|
+
logger.info("\u2713 State deleted");
|
|
29106
|
+
result.skippedEmpty = true;
|
|
29107
|
+
return result;
|
|
29108
|
+
}
|
|
29109
|
+
logger.info(`
|
|
29110
|
+
Resources to be deleted (${resourceCount}):`);
|
|
29111
|
+
for (const [logicalId, resource] of Object.entries(state.resources)) {
|
|
29112
|
+
logger.info(` - ${logicalId} (${resource.resourceType})`);
|
|
29113
|
+
}
|
|
29114
|
+
if (!ctx.skipConfirmation) {
|
|
29115
|
+
const rl = readline.createInterface({
|
|
29116
|
+
input: process.stdin,
|
|
29117
|
+
output: process.stdout
|
|
29118
|
+
});
|
|
29119
|
+
const answer = await rl.question(
|
|
29120
|
+
`
|
|
29121
|
+
Are you sure you want to destroy stack "${stackName}" and delete all ${resourceCount} resources? (Y/n): `
|
|
29122
|
+
);
|
|
29123
|
+
rl.close();
|
|
29124
|
+
const trimmed = answer.trim().toLowerCase();
|
|
29125
|
+
if (trimmed === "n" || trimmed === "no") {
|
|
29126
|
+
logger.info("Destroy cancelled");
|
|
29127
|
+
result.cancelled = true;
|
|
29128
|
+
return result;
|
|
29129
|
+
}
|
|
29130
|
+
}
|
|
29131
|
+
const stackRegion = state.region;
|
|
29132
|
+
let destroyProviderRegistry = ctx.providerRegistry;
|
|
29133
|
+
let destroyAwsClients;
|
|
29134
|
+
if (stackRegion && stackRegion !== ctx.baseRegion) {
|
|
29135
|
+
logger.info(`Stack region: ${stackRegion}`);
|
|
29136
|
+
process.env["AWS_REGION"] = stackRegion;
|
|
29137
|
+
process.env["AWS_DEFAULT_REGION"] = stackRegion;
|
|
29138
|
+
destroyAwsClients = new AwsClients({
|
|
29139
|
+
region: stackRegion,
|
|
29140
|
+
...ctx.profile && { profile: ctx.profile }
|
|
29141
|
+
});
|
|
29142
|
+
setAwsClients(destroyAwsClients);
|
|
29143
|
+
destroyProviderRegistry = new ProviderRegistry();
|
|
29144
|
+
registerAllProviders(destroyProviderRegistry);
|
|
29145
|
+
destroyProviderRegistry.setCustomResourceResponseBucket(ctx.stateBucket);
|
|
29146
|
+
}
|
|
29147
|
+
logger.info(`
|
|
29148
|
+
Acquiring lock for stack ${stackName}...`);
|
|
29149
|
+
await ctx.lockManager.acquireLock(stackName, regionForState, void 0, "destroy");
|
|
29150
|
+
const renderer = getLiveRenderer();
|
|
29151
|
+
renderer.start();
|
|
29152
|
+
try {
|
|
29153
|
+
logger.info("Building dependency graph...");
|
|
29154
|
+
const template = {
|
|
29155
|
+
AWSTemplateFormatVersion: "2010-09-09",
|
|
29156
|
+
Resources: {}
|
|
29157
|
+
};
|
|
29158
|
+
for (const [logicalId, resource] of Object.entries(state.resources)) {
|
|
29159
|
+
template.Resources[logicalId] = {
|
|
29160
|
+
Type: resource.resourceType,
|
|
29161
|
+
Properties: resource.properties || {},
|
|
29162
|
+
...resource.dependencies && resource.dependencies.length > 0 && {
|
|
29163
|
+
DependsOn: resource.dependencies
|
|
29164
|
+
}
|
|
29165
|
+
};
|
|
29166
|
+
}
|
|
29167
|
+
const typeToLogicalIds = /* @__PURE__ */ new Map();
|
|
29168
|
+
for (const [logicalId, resource] of Object.entries(state.resources)) {
|
|
29169
|
+
const ids = typeToLogicalIds.get(resource.resourceType) ?? [];
|
|
29170
|
+
ids.push(logicalId);
|
|
29171
|
+
typeToLogicalIds.set(resource.resourceType, ids);
|
|
29172
|
+
}
|
|
29173
|
+
for (const [logicalId, resource] of Object.entries(state.resources)) {
|
|
29174
|
+
const mustDeleteAfter = IMPLICIT_DELETE_DEPENDENCIES[resource.resourceType];
|
|
29175
|
+
if (!mustDeleteAfter)
|
|
29176
|
+
continue;
|
|
29177
|
+
for (const depType of mustDeleteAfter) {
|
|
29178
|
+
const depIds = typeToLogicalIds.get(depType);
|
|
29179
|
+
if (!depIds)
|
|
29180
|
+
continue;
|
|
29181
|
+
for (const depId of depIds) {
|
|
29182
|
+
const existing = template.Resources[depId]?.DependsOn ?? [];
|
|
29183
|
+
const depsArray = Array.isArray(existing) ? existing : [existing];
|
|
29184
|
+
if (!depsArray.includes(logicalId)) {
|
|
29185
|
+
template.Resources[depId] = {
|
|
29186
|
+
...template.Resources[depId],
|
|
29187
|
+
DependsOn: [...depsArray, logicalId]
|
|
29188
|
+
};
|
|
29189
|
+
logger.debug(
|
|
29190
|
+
`Implicit delete dependency: ${depId} (${depType}) must be deleted before ${logicalId} (${resource.resourceType})`
|
|
29191
|
+
);
|
|
29192
|
+
}
|
|
29193
|
+
}
|
|
29194
|
+
}
|
|
29195
|
+
}
|
|
29196
|
+
const dagBuilder = new DagBuilder();
|
|
29197
|
+
const graph = dagBuilder.buildGraph(template);
|
|
29198
|
+
const executionLevels = dagBuilder.getExecutionLevels(graph);
|
|
29199
|
+
logger.debug(`Dependency graph: ${executionLevels.length} level(s)`);
|
|
29200
|
+
for (let levelIndex = executionLevels.length - 1; levelIndex >= 0; levelIndex--) {
|
|
29201
|
+
const level = executionLevels[levelIndex];
|
|
29202
|
+
if (!level)
|
|
29203
|
+
continue;
|
|
29204
|
+
logger.debug(
|
|
29205
|
+
`Deletion level ${executionLevels.length - levelIndex}/${executionLevels.length} (${level.length} resources)`
|
|
29206
|
+
);
|
|
29207
|
+
const deletePromises = level.map(async (logicalId) => {
|
|
29208
|
+
const resource = state.resources[logicalId];
|
|
29209
|
+
if (!resource) {
|
|
29210
|
+
logger.warn(`Resource ${logicalId} not found in state, skipping`);
|
|
29211
|
+
return;
|
|
29212
|
+
}
|
|
29213
|
+
renderer.addTask(logicalId, `Deleting ${logicalId} (${resource.resourceType})`);
|
|
29214
|
+
try {
|
|
29215
|
+
const provider = destroyProviderRegistry.getProvider(resource.resourceType);
|
|
29216
|
+
let lastDeleteError;
|
|
29217
|
+
for (let attempt = 0; attempt <= 3; attempt++) {
|
|
29218
|
+
try {
|
|
29219
|
+
await provider.delete(
|
|
29220
|
+
logicalId,
|
|
29221
|
+
resource.physicalId,
|
|
29222
|
+
resource.resourceType,
|
|
29223
|
+
resource.properties
|
|
29224
|
+
);
|
|
29225
|
+
lastDeleteError = null;
|
|
29226
|
+
break;
|
|
29227
|
+
} catch (retryError) {
|
|
29228
|
+
lastDeleteError = retryError;
|
|
29229
|
+
const msg = retryError instanceof Error ? retryError.message : String(retryError);
|
|
29230
|
+
const isRetryable = msg.includes("Too Many Requests") || msg.includes("has dependencies") || msg.includes("can't be deleted since") || msg.includes("DependencyViolation");
|
|
29231
|
+
if (!isRetryable || attempt >= 3)
|
|
29232
|
+
break;
|
|
29233
|
+
const delay = 5e3 * Math.pow(2, attempt);
|
|
29234
|
+
logger.debug(
|
|
29235
|
+
` \u23F3 Retrying delete ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/3)`
|
|
29236
|
+
);
|
|
29237
|
+
await new Promise((resolve4) => setTimeout(resolve4, delay));
|
|
29238
|
+
}
|
|
29239
|
+
}
|
|
29240
|
+
if (lastDeleteError)
|
|
29241
|
+
throw lastDeleteError;
|
|
29242
|
+
renderer.removeTask(logicalId);
|
|
29243
|
+
logger.info(` \u2705 ${logicalId} (${resource.resourceType}) deleted`);
|
|
29244
|
+
result.deletedCount++;
|
|
29245
|
+
} catch (error) {
|
|
29246
|
+
renderer.removeTask(logicalId);
|
|
29247
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
29248
|
+
if (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException")) {
|
|
29249
|
+
logger.debug(` ${logicalId} already deleted, removing from state`);
|
|
29250
|
+
result.deletedCount++;
|
|
29251
|
+
} else {
|
|
29252
|
+
logger.error(` \u2717 Failed to delete ${logicalId}:`, String(error));
|
|
29253
|
+
result.errorCount++;
|
|
29254
|
+
}
|
|
29255
|
+
} finally {
|
|
29256
|
+
renderer.removeTask(logicalId);
|
|
29257
|
+
}
|
|
29258
|
+
});
|
|
29259
|
+
await Promise.all(deletePromises);
|
|
29260
|
+
}
|
|
29261
|
+
if (result.errorCount === 0) {
|
|
29262
|
+
await ctx.stateBackend.deleteState(stackName, regionForState);
|
|
29263
|
+
logger.debug("State deleted");
|
|
29264
|
+
} else {
|
|
29265
|
+
logger.warn(`${result.errorCount} resource(s) failed to delete. State preserved.`);
|
|
29266
|
+
}
|
|
29267
|
+
logger.info(
|
|
29268
|
+
`
|
|
29269
|
+
\u2713 Stack ${stackName} destroyed (${result.deletedCount} deleted, ${result.errorCount} errors)`
|
|
29270
|
+
);
|
|
29271
|
+
} finally {
|
|
29272
|
+
renderer.stop();
|
|
29273
|
+
logger.debug("Releasing lock...");
|
|
29274
|
+
await ctx.lockManager.releaseLock(stackName, regionForState);
|
|
29275
|
+
if (destroyAwsClients) {
|
|
29276
|
+
destroyAwsClients.destroy();
|
|
29277
|
+
process.env["AWS_REGION"] = ctx.baseRegion;
|
|
29278
|
+
process.env["AWS_DEFAULT_REGION"] = ctx.baseRegion;
|
|
29279
|
+
setAwsClients(ctx.baseAwsClients);
|
|
29280
|
+
}
|
|
29281
|
+
}
|
|
29282
|
+
return result;
|
|
29283
|
+
}
|
|
29284
|
+
|
|
29285
|
+
// src/cli/commands/destroy.ts
|
|
29056
29286
|
async function destroyCommand(stackArgs, options) {
|
|
29057
29287
|
const logger = getLogger();
|
|
29058
29288
|
if (options.verbose) {
|
|
29059
29289
|
logger.setLevel("debug");
|
|
29060
29290
|
process.env["CDKD_NO_LIVE"] = "1";
|
|
29061
29291
|
}
|
|
29292
|
+
warnIfDeprecatedRegion(options);
|
|
29062
29293
|
const region = options.region || process.env["AWS_REGION"] || "us-east-1";
|
|
29063
29294
|
const stateBucket = await resolveStateBucketWithDefault(options.stateBucket, region);
|
|
29064
29295
|
logger.info("Starting stack destruction...");
|
|
@@ -29083,7 +29314,6 @@ async function destroyCommand(stackArgs, options) {
|
|
|
29083
29314
|
});
|
|
29084
29315
|
await stateBackend.verifyBucketExists();
|
|
29085
29316
|
const lockManager = new LockManager(awsClients.s3, stateConfig);
|
|
29086
|
-
const dagBuilder = new DagBuilder();
|
|
29087
29317
|
const providerRegistry = new ProviderRegistry();
|
|
29088
29318
|
registerAllProviders(providerRegistry);
|
|
29089
29319
|
providerRegistry.setCustomResourceResponseBucket(stateBucket);
|
|
@@ -29183,189 +29413,16 @@ Preparing to destroy stack: ${stackName}`);
|
|
|
29183
29413
|
logger.warn(`No state found for stack ${stackName}, skipping`);
|
|
29184
29414
|
continue;
|
|
29185
29415
|
}
|
|
29186
|
-
|
|
29187
|
-
|
|
29188
|
-
|
|
29189
|
-
|
|
29190
|
-
|
|
29191
|
-
|
|
29192
|
-
|
|
29193
|
-
|
|
29194
|
-
|
|
29195
|
-
|
|
29196
|
-
for (const [logicalId, resource] of Object.entries(currentState.resources)) {
|
|
29197
|
-
logger.info(` - ${logicalId} (${resource.resourceType})`);
|
|
29198
|
-
}
|
|
29199
|
-
if (!options.yes && !options.force) {
|
|
29200
|
-
const rl = readline.createInterface({
|
|
29201
|
-
input: process.stdin,
|
|
29202
|
-
output: process.stdout
|
|
29203
|
-
});
|
|
29204
|
-
const answer = await rl.question(
|
|
29205
|
-
`
|
|
29206
|
-
Are you sure you want to destroy stack "${stackName}" and delete all ${resourceCount} resources? (Y/n): `
|
|
29207
|
-
);
|
|
29208
|
-
rl.close();
|
|
29209
|
-
const trimmed = answer.trim().toLowerCase();
|
|
29210
|
-
if (trimmed === "n" || trimmed === "no") {
|
|
29211
|
-
logger.info("Destroy cancelled");
|
|
29212
|
-
continue;
|
|
29213
|
-
}
|
|
29214
|
-
}
|
|
29215
|
-
const stackRegion = stackTargetRegion;
|
|
29216
|
-
let destroyProviderRegistry = providerRegistry;
|
|
29217
|
-
let destroyAwsClients;
|
|
29218
|
-
if (stackRegion && stackRegion !== region) {
|
|
29219
|
-
logger.info(`Stack region: ${stackRegion}`);
|
|
29220
|
-
process.env["AWS_REGION"] = stackRegion;
|
|
29221
|
-
process.env["AWS_DEFAULT_REGION"] = stackRegion;
|
|
29222
|
-
destroyAwsClients = new AwsClients({
|
|
29223
|
-
region: stackRegion,
|
|
29224
|
-
...options.profile && { profile: options.profile }
|
|
29225
|
-
});
|
|
29226
|
-
setAwsClients(destroyAwsClients);
|
|
29227
|
-
destroyProviderRegistry = new ProviderRegistry();
|
|
29228
|
-
registerAllProviders(destroyProviderRegistry);
|
|
29229
|
-
destroyProviderRegistry.setCustomResourceResponseBucket(stateBucket);
|
|
29230
|
-
}
|
|
29231
|
-
logger.info(`
|
|
29232
|
-
Acquiring lock for stack ${stackName}...`);
|
|
29233
|
-
await lockManager.acquireLock(stackName, stackRegion, void 0, "destroy");
|
|
29234
|
-
const renderer = getLiveRenderer();
|
|
29235
|
-
renderer.start();
|
|
29236
|
-
try {
|
|
29237
|
-
logger.info("Building dependency graph...");
|
|
29238
|
-
const template = {
|
|
29239
|
-
AWSTemplateFormatVersion: "2010-09-09",
|
|
29240
|
-
Resources: {}
|
|
29241
|
-
};
|
|
29242
|
-
for (const [logicalId, resource] of Object.entries(currentState.resources)) {
|
|
29243
|
-
template.Resources[logicalId] = {
|
|
29244
|
-
Type: resource.resourceType,
|
|
29245
|
-
Properties: resource.properties || {},
|
|
29246
|
-
...resource.dependencies && resource.dependencies.length > 0 && {
|
|
29247
|
-
DependsOn: resource.dependencies
|
|
29248
|
-
}
|
|
29249
|
-
};
|
|
29250
|
-
}
|
|
29251
|
-
const typeToLogicalIds = /* @__PURE__ */ new Map();
|
|
29252
|
-
for (const [logicalId, resource] of Object.entries(currentState.resources)) {
|
|
29253
|
-
const ids = typeToLogicalIds.get(resource.resourceType) ?? [];
|
|
29254
|
-
ids.push(logicalId);
|
|
29255
|
-
typeToLogicalIds.set(resource.resourceType, ids);
|
|
29256
|
-
}
|
|
29257
|
-
for (const [logicalId, resource] of Object.entries(currentState.resources)) {
|
|
29258
|
-
const mustDeleteAfter = IMPLICIT_DELETE_DEPENDENCIES[resource.resourceType];
|
|
29259
|
-
if (!mustDeleteAfter)
|
|
29260
|
-
continue;
|
|
29261
|
-
for (const depType of mustDeleteAfter) {
|
|
29262
|
-
const depIds = typeToLogicalIds.get(depType);
|
|
29263
|
-
if (!depIds)
|
|
29264
|
-
continue;
|
|
29265
|
-
for (const depId of depIds) {
|
|
29266
|
-
const existing = template.Resources[depId]?.DependsOn ?? [];
|
|
29267
|
-
const depsArray = Array.isArray(existing) ? existing : [existing];
|
|
29268
|
-
if (!depsArray.includes(logicalId)) {
|
|
29269
|
-
template.Resources[depId] = {
|
|
29270
|
-
...template.Resources[depId],
|
|
29271
|
-
DependsOn: [...depsArray, logicalId]
|
|
29272
|
-
};
|
|
29273
|
-
logger.debug(
|
|
29274
|
-
`Implicit delete dependency: ${depId} (${depType}) must be deleted before ${logicalId} (${resource.resourceType})`
|
|
29275
|
-
);
|
|
29276
|
-
}
|
|
29277
|
-
}
|
|
29278
|
-
}
|
|
29279
|
-
}
|
|
29280
|
-
const graph = dagBuilder.buildGraph(template);
|
|
29281
|
-
const executionLevels = dagBuilder.getExecutionLevels(graph);
|
|
29282
|
-
logger.debug(`Dependency graph: ${executionLevels.length} level(s)`);
|
|
29283
|
-
let deletedCount = 0;
|
|
29284
|
-
let errorCount = 0;
|
|
29285
|
-
for (let levelIndex = executionLevels.length - 1; levelIndex >= 0; levelIndex--) {
|
|
29286
|
-
const level = executionLevels[levelIndex];
|
|
29287
|
-
if (!level) {
|
|
29288
|
-
continue;
|
|
29289
|
-
}
|
|
29290
|
-
logger.debug(
|
|
29291
|
-
`Deletion level ${executionLevels.length - levelIndex}/${executionLevels.length} (${level.length} resources)`
|
|
29292
|
-
);
|
|
29293
|
-
const deletePromises = level.map(async (logicalId) => {
|
|
29294
|
-
const resource = currentState.resources[logicalId];
|
|
29295
|
-
if (!resource) {
|
|
29296
|
-
logger.warn(`Resource ${logicalId} not found in state, skipping`);
|
|
29297
|
-
return;
|
|
29298
|
-
}
|
|
29299
|
-
renderer.addTask(logicalId, `Deleting ${logicalId} (${resource.resourceType})`);
|
|
29300
|
-
try {
|
|
29301
|
-
const provider = destroyProviderRegistry.getProvider(resource.resourceType);
|
|
29302
|
-
let lastDeleteError;
|
|
29303
|
-
for (let attempt = 0; attempt <= 3; attempt++) {
|
|
29304
|
-
try {
|
|
29305
|
-
await provider.delete(
|
|
29306
|
-
logicalId,
|
|
29307
|
-
resource.physicalId,
|
|
29308
|
-
resource.resourceType,
|
|
29309
|
-
resource.properties,
|
|
29310
|
-
{ expectedRegion: currentState.region }
|
|
29311
|
-
);
|
|
29312
|
-
lastDeleteError = null;
|
|
29313
|
-
break;
|
|
29314
|
-
} catch (retryError) {
|
|
29315
|
-
lastDeleteError = retryError;
|
|
29316
|
-
const msg = retryError instanceof Error ? retryError.message : String(retryError);
|
|
29317
|
-
const isRetryable = msg.includes("Too Many Requests") || msg.includes("has dependencies") || msg.includes("can't be deleted since") || msg.includes("DependencyViolation");
|
|
29318
|
-
if (!isRetryable || attempt >= 3)
|
|
29319
|
-
break;
|
|
29320
|
-
const delay = 5e3 * Math.pow(2, attempt);
|
|
29321
|
-
logger.debug(
|
|
29322
|
-
` \u23F3 Retrying delete ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/3)`
|
|
29323
|
-
);
|
|
29324
|
-
await new Promise((resolve4) => setTimeout(resolve4, delay));
|
|
29325
|
-
}
|
|
29326
|
-
}
|
|
29327
|
-
if (lastDeleteError)
|
|
29328
|
-
throw lastDeleteError;
|
|
29329
|
-
renderer.removeTask(logicalId);
|
|
29330
|
-
logger.info(` \u2705 ${logicalId} (${resource.resourceType}) deleted`);
|
|
29331
|
-
deletedCount++;
|
|
29332
|
-
} catch (error) {
|
|
29333
|
-
renderer.removeTask(logicalId);
|
|
29334
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
29335
|
-
if (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException")) {
|
|
29336
|
-
logger.debug(` ${logicalId} already deleted, removing from state`);
|
|
29337
|
-
deletedCount++;
|
|
29338
|
-
} else {
|
|
29339
|
-
logger.error(` \u2717 Failed to delete ${logicalId}:`, String(error));
|
|
29340
|
-
errorCount++;
|
|
29341
|
-
}
|
|
29342
|
-
} finally {
|
|
29343
|
-
renderer.removeTask(logicalId);
|
|
29344
|
-
}
|
|
29345
|
-
});
|
|
29346
|
-
await Promise.all(deletePromises);
|
|
29347
|
-
}
|
|
29348
|
-
if (errorCount === 0) {
|
|
29349
|
-
await stateBackend.deleteState(stackName, stackRegion);
|
|
29350
|
-
logger.debug("State deleted");
|
|
29351
|
-
} else {
|
|
29352
|
-
logger.warn(`${errorCount} resource(s) failed to delete. State preserved.`);
|
|
29353
|
-
}
|
|
29354
|
-
logger.info(
|
|
29355
|
-
`
|
|
29356
|
-
\u2713 Stack ${stackName} destroyed (${deletedCount} deleted, ${errorCount} errors)`
|
|
29357
|
-
);
|
|
29358
|
-
} finally {
|
|
29359
|
-
renderer.stop();
|
|
29360
|
-
logger.debug("Releasing lock...");
|
|
29361
|
-
await lockManager.releaseLock(stackName, stackRegion);
|
|
29362
|
-
if (destroyAwsClients) {
|
|
29363
|
-
destroyAwsClients.destroy();
|
|
29364
|
-
process.env["AWS_REGION"] = region;
|
|
29365
|
-
process.env["AWS_DEFAULT_REGION"] = region;
|
|
29366
|
-
setAwsClients(awsClients);
|
|
29367
|
-
}
|
|
29368
|
-
}
|
|
29416
|
+
await runDestroyForStack(stackName, stateResult.state, {
|
|
29417
|
+
stateBackend,
|
|
29418
|
+
lockManager,
|
|
29419
|
+
providerRegistry,
|
|
29420
|
+
baseAwsClients: awsClients,
|
|
29421
|
+
baseRegion: region,
|
|
29422
|
+
...options.profile && { profile: options.profile },
|
|
29423
|
+
stateBucket,
|
|
29424
|
+
skipConfirmation: options.yes || options.force
|
|
29425
|
+
});
|
|
29369
29426
|
}
|
|
29370
29427
|
} finally {
|
|
29371
29428
|
awsClients.destroy();
|
|
@@ -29384,16 +29441,18 @@ function createDestroyCommand() {
|
|
|
29384
29441
|
...destroyOptions,
|
|
29385
29442
|
...contextOptions
|
|
29386
29443
|
].forEach((opt) => cmd.addOption(opt));
|
|
29444
|
+
cmd.addOption(deprecatedRegionOption);
|
|
29387
29445
|
return cmd;
|
|
29388
29446
|
}
|
|
29389
29447
|
|
|
29390
29448
|
// src/cli/commands/publish-assets.ts
|
|
29391
|
-
import { Option as
|
|
29449
|
+
import { Option as Option3, Command as Command7 } from "commander";
|
|
29392
29450
|
async function publishAssetsCommand(options) {
|
|
29393
29451
|
const logger = getLogger();
|
|
29394
29452
|
if (options.verbose) {
|
|
29395
29453
|
logger.setLevel("debug");
|
|
29396
29454
|
}
|
|
29455
|
+
warnIfDeprecatedRegion(options);
|
|
29397
29456
|
logger.info("Publishing assets...");
|
|
29398
29457
|
logger.debug("Asset manifest path:", options.path);
|
|
29399
29458
|
const publisher = new AssetPublisher();
|
|
@@ -29407,25 +29466,27 @@ async function publishAssetsCommand(options) {
|
|
|
29407
29466
|
}
|
|
29408
29467
|
function createPublishAssetsCommand() {
|
|
29409
29468
|
const cmd = new Command7("publish-assets").description("Publish assets to S3/ECR from asset manifest").requiredOption("--path <path>", "Path to asset manifest file or directory").addOption(
|
|
29410
|
-
new
|
|
29469
|
+
new Option3(
|
|
29411
29470
|
"--asset-publish-concurrency <number>",
|
|
29412
29471
|
"Maximum concurrent asset publish operations"
|
|
29413
29472
|
).default(8).argParser((value) => parseInt(value, 10))
|
|
29414
29473
|
).addOption(
|
|
29415
|
-
new
|
|
29474
|
+
new Option3("--image-build-concurrency <number>", "Maximum concurrent Docker image builds").default(4).argParser((value) => parseInt(value, 10))
|
|
29416
29475
|
).action(withErrorHandling(publishAssetsCommand));
|
|
29417
29476
|
commonOptions.forEach((opt) => cmd.addOption(opt));
|
|
29477
|
+
cmd.addOption(deprecatedRegionOption);
|
|
29418
29478
|
return cmd;
|
|
29419
29479
|
}
|
|
29420
29480
|
|
|
29421
29481
|
// src/cli/commands/force-unlock.ts
|
|
29422
|
-
import { Command as Command8, Option as
|
|
29482
|
+
import { Command as Command8, Option as Option4 } from "commander";
|
|
29423
29483
|
init_aws_clients();
|
|
29424
29484
|
async function forceUnlockCommand(stackArgs, options) {
|
|
29425
29485
|
const logger = getLogger();
|
|
29426
29486
|
if (options.verbose) {
|
|
29427
29487
|
logger.setLevel("debug");
|
|
29428
29488
|
}
|
|
29489
|
+
warnIfDeprecatedRegion(options);
|
|
29429
29490
|
const stackPatterns = stackArgs.length > 0 ? stackArgs : options.stack ? [options.stack] : [];
|
|
29430
29491
|
if (stackPatterns.length === 0) {
|
|
29431
29492
|
throw new Error("Stack name is required. Usage: cdkd force-unlock <stack-name>");
|
|
@@ -29479,18 +29540,24 @@ async function forceUnlockCommand(stackArgs, options) {
|
|
|
29479
29540
|
}
|
|
29480
29541
|
function createForceUnlockCommand() {
|
|
29481
29542
|
const cmd = new Command8("force-unlock").description("Force-release a stale lock on a stack").argument("[stacks...]", "Stack name(s) to unlock").addOption(
|
|
29482
|
-
new
|
|
29543
|
+
new Option4(
|
|
29483
29544
|
"--stack-region <region>",
|
|
29484
29545
|
"Stack region whose lock to release (use when the same stack name has locks in multiple regions). Defaults to all regions where the stack has state."
|
|
29485
29546
|
)
|
|
29486
29547
|
).action(withErrorHandling(forceUnlockCommand));
|
|
29487
29548
|
[...commonOptions, ...stateOptions, ...stackOptions].forEach((opt) => cmd.addOption(opt));
|
|
29549
|
+
cmd.addOption(deprecatedRegionOption);
|
|
29488
29550
|
return cmd;
|
|
29489
29551
|
}
|
|
29490
29552
|
|
|
29491
29553
|
// src/cli/commands/state.ts
|
|
29492
29554
|
import * as readline2 from "node:readline/promises";
|
|
29493
|
-
import { Command as Command9, Option as
|
|
29555
|
+
import { Command as Command9, Option as Option5 } from "commander";
|
|
29556
|
+
import {
|
|
29557
|
+
GetBucketLocationCommand as GetBucketLocationCommand2,
|
|
29558
|
+
GetObjectCommand as GetObjectCommand4,
|
|
29559
|
+
ListObjectsV2Command as ListObjectsV2Command3
|
|
29560
|
+
} from "@aws-sdk/client-s3";
|
|
29494
29561
|
init_aws_clients();
|
|
29495
29562
|
function formatStackRef(ref) {
|
|
29496
29563
|
return ref.region ? `${ref.stackName} (${ref.region})` : ref.stackName;
|
|
@@ -29520,6 +29587,7 @@ function resolveSingleRegion(stackName, refs, requestedRegion) {
|
|
|
29520
29587
|
);
|
|
29521
29588
|
}
|
|
29522
29589
|
async function setupStateBackend(options) {
|
|
29590
|
+
warnIfDeprecatedRegion(options);
|
|
29523
29591
|
const awsClients = new AwsClients({
|
|
29524
29592
|
...options.region && { region: options.region },
|
|
29525
29593
|
...options.profile && { profile: options.profile }
|
|
@@ -29538,6 +29606,8 @@ async function setupStateBackend(options) {
|
|
|
29538
29606
|
return {
|
|
29539
29607
|
stateBackend,
|
|
29540
29608
|
lockManager,
|
|
29609
|
+
awsClients,
|
|
29610
|
+
region,
|
|
29541
29611
|
bucket,
|
|
29542
29612
|
prefix,
|
|
29543
29613
|
dispose: () => awsClients.destroy()
|
|
@@ -29628,6 +29698,7 @@ async function stateListCommand(options) {
|
|
|
29628
29698
|
function createStateListCommand() {
|
|
29629
29699
|
const cmd = new Command9("list").alias("ls").description("List stacks registered in the cdkd state bucket").option("-l, --long", "Show resource count, last-modified time, and lock status", false).option("--json", "Output as JSON", false).action(withErrorHandling(stateListCommand));
|
|
29630
29700
|
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
29701
|
+
cmd.addOption(deprecatedRegionOption);
|
|
29631
29702
|
return cmd;
|
|
29632
29703
|
}
|
|
29633
29704
|
async function stateResourcesCommand(stackName, options) {
|
|
@@ -29731,6 +29802,7 @@ function formatLockSummary(lockInfo) {
|
|
|
29731
29802
|
function createStateResourcesCommand() {
|
|
29732
29803
|
const cmd = new Command9("resources").description("List resources recorded in a stack's state").argument("<stack>", "Stack name (physical CloudFormation name)").option("-l, --long", "Include dependencies and attributes per resource", false).option("--json", "Output as JSON", false).addOption(stackRegionOption()).action(withErrorHandling(stateResourcesCommand));
|
|
29733
29804
|
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
29805
|
+
cmd.addOption(deprecatedRegionOption);
|
|
29734
29806
|
return cmd;
|
|
29735
29807
|
}
|
|
29736
29808
|
async function stateShowCommand(stackName, options) {
|
|
@@ -29818,6 +29890,7 @@ async function stateShowCommand(stackName, options) {
|
|
|
29818
29890
|
function createStateShowCommand() {
|
|
29819
29891
|
const cmd = new Command9("show").description("Show the full cdkd state record for a stack (metadata, outputs, resources)").argument("<stack>", "Stack name (physical CloudFormation name)").option("--json", "Output the raw state and lock as JSON", false).addOption(stackRegionOption()).action(withErrorHandling(stateShowCommand));
|
|
29820
29892
|
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
29893
|
+
cmd.addOption(deprecatedRegionOption);
|
|
29821
29894
|
return cmd;
|
|
29822
29895
|
}
|
|
29823
29896
|
async function stateRmCommand(stackArgs, options) {
|
|
@@ -29892,7 +29965,7 @@ Use 'cdkd destroy ${stackName}' if you want to delete the actual resources.
|
|
|
29892
29965
|
}
|
|
29893
29966
|
}
|
|
29894
29967
|
function stackRegionOption() {
|
|
29895
|
-
return new
|
|
29968
|
+
return new Option5(
|
|
29896
29969
|
"--stack-region <region>",
|
|
29897
29970
|
"Region of the stack record to operate on. Required when the same stack name has state in multiple regions."
|
|
29898
29971
|
);
|
|
@@ -29900,14 +29973,275 @@ function stackRegionOption() {
|
|
|
29900
29973
|
function createStateRmCommand() {
|
|
29901
29974
|
const cmd = new Command9("rm").description("Remove cdkd state for one or more stacks (does NOT delete AWS resources)").argument("<stacks...>", "Stack name(s) to remove from state").option("-f, --force", "Skip confirmation and remove even if the stack is locked", false).addOption(stackRegionOption()).action(withErrorHandling(stateRmCommand));
|
|
29902
29975
|
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
29976
|
+
cmd.addOption(deprecatedRegionOption);
|
|
29977
|
+
return cmd;
|
|
29978
|
+
}
|
|
29979
|
+
async function stateDestroyCommand(stackArgs, options) {
|
|
29980
|
+
const logger = getLogger();
|
|
29981
|
+
if (options.verbose) {
|
|
29982
|
+
logger.setLevel("debug");
|
|
29983
|
+
process.env["CDKD_NO_LIVE"] = "1";
|
|
29984
|
+
}
|
|
29985
|
+
if (!options.all && stackArgs.length === 0) {
|
|
29986
|
+
throw new Error(
|
|
29987
|
+
"Stack name is required. Usage: cdkd state destroy <stack> [<stack>...] | --all"
|
|
29988
|
+
);
|
|
29989
|
+
}
|
|
29990
|
+
const setup = await setupStateBackend(options);
|
|
29991
|
+
const providerRegistry = new ProviderRegistry();
|
|
29992
|
+
registerAllProviders(providerRegistry);
|
|
29993
|
+
providerRegistry.setCustomResourceResponseBucket(setup.bucket);
|
|
29994
|
+
try {
|
|
29995
|
+
const stateRefs = await setup.stateBackend.listStacks();
|
|
29996
|
+
const knownStackNames = new Set(stateRefs.map((r) => r.stackName));
|
|
29997
|
+
let stackNames;
|
|
29998
|
+
if (options.all) {
|
|
29999
|
+
stackNames = [...knownStackNames].sort();
|
|
30000
|
+
if (stackNames.length === 0) {
|
|
30001
|
+
logger.info("No stacks found in state");
|
|
30002
|
+
return;
|
|
30003
|
+
}
|
|
30004
|
+
} else {
|
|
30005
|
+
const missing = stackArgs.filter((name) => !knownStackNames.has(name));
|
|
30006
|
+
if (missing.length > 0) {
|
|
30007
|
+
throw new Error(
|
|
30008
|
+
`No state found for stack(s): ${missing.join(", ")}. Run 'cdkd state list' to see available stacks.`
|
|
30009
|
+
);
|
|
30010
|
+
}
|
|
30011
|
+
stackNames = stackArgs;
|
|
30012
|
+
}
|
|
30013
|
+
if (options.all && !options.yes) {
|
|
30014
|
+
process.stdout.write(
|
|
30015
|
+
`
|
|
30016
|
+
WARNING: This destroys ${stackNames.length} stack(s) and removes their state records:
|
|
30017
|
+
`
|
|
30018
|
+
);
|
|
30019
|
+
for (const name of stackNames) {
|
|
30020
|
+
process.stdout.write(` - ${name}
|
|
30021
|
+
`);
|
|
30022
|
+
}
|
|
30023
|
+
process.stdout.write("\n");
|
|
30024
|
+
const rl = readline2.createInterface({
|
|
30025
|
+
input: process.stdin,
|
|
30026
|
+
output: process.stdout
|
|
30027
|
+
});
|
|
30028
|
+
const answer = await rl.question(`Destroy all ${stackNames.length} stack(s)? (y/N): `);
|
|
30029
|
+
rl.close();
|
|
30030
|
+
const trimmed = answer.trim().toLowerCase();
|
|
30031
|
+
if (trimmed !== "y" && trimmed !== "yes") {
|
|
30032
|
+
logger.info("Destroy cancelled");
|
|
30033
|
+
return;
|
|
30034
|
+
}
|
|
30035
|
+
}
|
|
30036
|
+
logger.info(`Found ${stackNames.length} stack(s) to destroy: ${stackNames.join(", ")}`);
|
|
30037
|
+
let totalErrors = 0;
|
|
30038
|
+
for (const stackName of stackNames) {
|
|
30039
|
+
const refs = stateRefs.filter((r) => r.stackName === stackName);
|
|
30040
|
+
let targets;
|
|
30041
|
+
if (options.stackRegion) {
|
|
30042
|
+
targets = refs.filter((r) => r.region === options.stackRegion || !r.region);
|
|
30043
|
+
if (targets.length === 0) {
|
|
30044
|
+
logger.warn(
|
|
30045
|
+
`Skipping ${stackName}: no state record matches --stack-region '${options.stackRegion}'`
|
|
30046
|
+
);
|
|
30047
|
+
continue;
|
|
30048
|
+
}
|
|
30049
|
+
} else if (refs.length === 1) {
|
|
30050
|
+
targets = refs;
|
|
30051
|
+
} else {
|
|
30052
|
+
const regions = refs.map((r) => r.region ?? "(legacy)").join(", ");
|
|
30053
|
+
throw new Error(
|
|
30054
|
+
`Stack '${stackName}' has state in multiple regions: ${regions}. Use --region <region> to pick one.`
|
|
30055
|
+
);
|
|
30056
|
+
}
|
|
30057
|
+
for (const ref of targets) {
|
|
30058
|
+
logger.info(
|
|
30059
|
+
`
|
|
30060
|
+
Preparing to destroy stack: ${stackName}${ref.region ? ` (${ref.region})` : ""}`
|
|
30061
|
+
);
|
|
30062
|
+
const stateResult = await setup.stateBackend.getState(
|
|
30063
|
+
stackName,
|
|
30064
|
+
ref.region ?? setup.region
|
|
30065
|
+
);
|
|
30066
|
+
if (!stateResult) {
|
|
30067
|
+
logger.warn(
|
|
30068
|
+
`No state found for stack ${stackName}${ref.region ? ` in ${ref.region}` : ""}, skipping`
|
|
30069
|
+
);
|
|
30070
|
+
continue;
|
|
30071
|
+
}
|
|
30072
|
+
const result = await runDestroyForStack(stackName, stateResult.state, {
|
|
30073
|
+
stateBackend: setup.stateBackend,
|
|
30074
|
+
lockManager: setup.lockManager,
|
|
30075
|
+
providerRegistry,
|
|
30076
|
+
baseAwsClients: setup.awsClients,
|
|
30077
|
+
baseRegion: setup.region,
|
|
30078
|
+
...options.profile && { profile: options.profile },
|
|
30079
|
+
stateBucket: setup.bucket,
|
|
30080
|
+
// --yes covers both the --all batch prompt above (already consumed)
|
|
30081
|
+
// and the per-stack prompt inside the runner. Per-stack prompts are
|
|
30082
|
+
// skipped when `options.yes` is set OR `--all` was set (the user
|
|
30083
|
+
// already accepted the batch prompt).
|
|
30084
|
+
skipConfirmation: options.yes || options.all === true
|
|
30085
|
+
});
|
|
30086
|
+
totalErrors += result.errorCount;
|
|
30087
|
+
}
|
|
30088
|
+
}
|
|
30089
|
+
if (totalErrors > 0) {
|
|
30090
|
+
throw new Error(
|
|
30091
|
+
`Destroy completed with ${totalErrors} resource error(s). Inspect 'cdkd state show <stack>' and re-run.`
|
|
30092
|
+
);
|
|
30093
|
+
}
|
|
30094
|
+
} finally {
|
|
30095
|
+
setup.dispose();
|
|
30096
|
+
}
|
|
30097
|
+
}
|
|
30098
|
+
function createStateDestroyCommand() {
|
|
30099
|
+
const cmd = new Command9("destroy").description(
|
|
30100
|
+
"Destroy a stack's AWS resources and remove its state record without requiring the CDK app. For removing only the state record (keeping AWS resources intact), use 'cdkd state rm'."
|
|
30101
|
+
).argument("[stacks...]", "Stack name(s) to destroy (physical CloudFormation names)").option("--all", "Destroy every stack in the state bucket", false).addOption(stackRegionOption()).addHelpText(
|
|
30102
|
+
"after",
|
|
30103
|
+
[
|
|
30104
|
+
"",
|
|
30105
|
+
"Examples:",
|
|
30106
|
+
" cdkd state destroy MyStack",
|
|
30107
|
+
" cdkd state destroy MyStack OtherStack",
|
|
30108
|
+
" cdkd state destroy --all -y",
|
|
30109
|
+
" cdkd state destroy MyStack --state-bucket cdkd-state-test",
|
|
30110
|
+
" cdkd state destroy MyStack --stack-region us-west-2",
|
|
30111
|
+
"",
|
|
30112
|
+
"For removing only the state record (keeping AWS resources intact), use 'cdkd state rm'."
|
|
30113
|
+
].join("\n")
|
|
30114
|
+
).action(withErrorHandling(stateDestroyCommand));
|
|
30115
|
+
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
30116
|
+
cmd.addOption(deprecatedRegionOption);
|
|
30117
|
+
return cmd;
|
|
30118
|
+
}
|
|
30119
|
+
function formatBucketSource(source) {
|
|
30120
|
+
switch (source) {
|
|
30121
|
+
case "cli-flag":
|
|
30122
|
+
return "--state-bucket flag";
|
|
30123
|
+
case "env":
|
|
30124
|
+
return "CDKD_STATE_BUCKET env";
|
|
30125
|
+
case "cdk.json":
|
|
30126
|
+
return "cdk.json (context.cdkd.stateBucket)";
|
|
30127
|
+
case "default":
|
|
30128
|
+
return "default (account ID from STS)";
|
|
30129
|
+
case "default-legacy":
|
|
30130
|
+
return "default (legacy region-suffixed name; cdkd state migrate-bucket recommended)";
|
|
30131
|
+
}
|
|
30132
|
+
}
|
|
30133
|
+
async function detectBucketRegion(awsClients, bucket) {
|
|
30134
|
+
try {
|
|
30135
|
+
const resp = await awsClients.s3.send(new GetBucketLocationCommand2({ Bucket: bucket }));
|
|
30136
|
+
const constraint = resp.LocationConstraint;
|
|
30137
|
+
if (!constraint)
|
|
30138
|
+
return "us-east-1";
|
|
30139
|
+
if (constraint === "EU")
|
|
30140
|
+
return "eu-west-1";
|
|
30141
|
+
return constraint;
|
|
30142
|
+
} catch {
|
|
30143
|
+
return void 0;
|
|
30144
|
+
}
|
|
30145
|
+
}
|
|
30146
|
+
async function listStateFileKeys(awsClients, bucket, prefix) {
|
|
30147
|
+
const keys = [];
|
|
30148
|
+
let continuationToken;
|
|
30149
|
+
const searchPrefix = `${prefix}/`;
|
|
30150
|
+
do {
|
|
30151
|
+
const resp = await awsClients.s3.send(
|
|
30152
|
+
new ListObjectsV2Command3({
|
|
30153
|
+
Bucket: bucket,
|
|
30154
|
+
Prefix: searchPrefix,
|
|
30155
|
+
...continuationToken && { ContinuationToken: continuationToken }
|
|
30156
|
+
})
|
|
30157
|
+
);
|
|
30158
|
+
for (const obj of resp.Contents ?? []) {
|
|
30159
|
+
const key = obj.Key;
|
|
30160
|
+
if (typeof key === "string" && key.endsWith("/state.json")) {
|
|
30161
|
+
keys.push(key);
|
|
30162
|
+
}
|
|
30163
|
+
}
|
|
30164
|
+
continuationToken = resp.NextContinuationToken;
|
|
30165
|
+
} while (continuationToken);
|
|
30166
|
+
return keys;
|
|
30167
|
+
}
|
|
30168
|
+
async function readSchemaVersion(awsClients, bucket, keys) {
|
|
30169
|
+
if (keys.length === 0)
|
|
30170
|
+
return "unknown";
|
|
30171
|
+
try {
|
|
30172
|
+
const resp = await awsClients.s3.send(new GetObjectCommand4({ Bucket: bucket, Key: keys[0] }));
|
|
30173
|
+
if (!resp.Body)
|
|
30174
|
+
return "unknown";
|
|
30175
|
+
const body = await resp.Body.transformToString();
|
|
30176
|
+
const parsed = JSON.parse(body);
|
|
30177
|
+
return typeof parsed.version === "number" ? parsed.version : "unknown";
|
|
30178
|
+
} catch {
|
|
30179
|
+
return "unknown";
|
|
30180
|
+
}
|
|
30181
|
+
}
|
|
30182
|
+
async function stateInfoCommand(options) {
|
|
30183
|
+
const logger = getLogger();
|
|
30184
|
+
if (options.verbose)
|
|
30185
|
+
logger.setLevel("debug");
|
|
30186
|
+
const awsClients = new AwsClients({
|
|
30187
|
+
...options.region && { region: options.region },
|
|
30188
|
+
...options.profile && { profile: options.profile }
|
|
30189
|
+
});
|
|
30190
|
+
setAwsClients(awsClients);
|
|
30191
|
+
try {
|
|
30192
|
+
const region = options.region || process.env["AWS_REGION"] || "us-east-1";
|
|
30193
|
+
const resolved = await resolveStateBucketWithDefaultAndSource(options.stateBucket, region);
|
|
30194
|
+
const bucket = resolved.bucket;
|
|
30195
|
+
const prefix = options.statePrefix;
|
|
30196
|
+
const stateBackend = new S3StateBackend(awsClients.s3, { bucket, prefix });
|
|
30197
|
+
await stateBackend.verifyBucketExists();
|
|
30198
|
+
const detectedRegion = await detectBucketRegion(awsClients, bucket);
|
|
30199
|
+
const stateFileKeys = await listStateFileKeys(awsClients, bucket, prefix);
|
|
30200
|
+
const schemaVersion = await readSchemaVersion(awsClients, bucket, stateFileKeys);
|
|
30201
|
+
if (options.json) {
|
|
30202
|
+
const json = {
|
|
30203
|
+
bucket,
|
|
30204
|
+
region: detectedRegion ?? null,
|
|
30205
|
+
regionSource: detectedRegion ? "auto-detected" : "unknown",
|
|
30206
|
+
bucketSource: resolved.source,
|
|
30207
|
+
schemaVersion,
|
|
30208
|
+
stackCount: stateFileKeys.length
|
|
30209
|
+
};
|
|
30210
|
+
process.stdout.write(`${JSON.stringify(json, null, 2)}
|
|
30211
|
+
`);
|
|
30212
|
+
return;
|
|
30213
|
+
}
|
|
30214
|
+
const lines = [];
|
|
30215
|
+
lines.push(`State bucket: ${bucket}`);
|
|
30216
|
+
if (detectedRegion) {
|
|
30217
|
+
lines.push(`Region: ${detectedRegion} (auto-detected via GetBucketLocation)`);
|
|
30218
|
+
} else {
|
|
30219
|
+
lines.push("Region: unknown (GetBucketLocation failed or denied)");
|
|
30220
|
+
}
|
|
30221
|
+
lines.push(`Source: ${formatBucketSource(resolved.source)}`);
|
|
30222
|
+
lines.push(`Schema version: ${schemaVersion}`);
|
|
30223
|
+
lines.push(`Stacks: ${stateFileKeys.length}`);
|
|
30224
|
+
process.stdout.write(`${lines.join("\n")}
|
|
30225
|
+
`);
|
|
30226
|
+
} finally {
|
|
30227
|
+
awsClients.destroy();
|
|
30228
|
+
}
|
|
30229
|
+
}
|
|
30230
|
+
function createStateInfoCommand() {
|
|
30231
|
+
const cmd = new Command9("info").description(
|
|
30232
|
+
"Show cdkd state bucket info (bucket name, region, source, schema version, stack count)"
|
|
30233
|
+
).option("--json", "Output as JSON", false).action(withErrorHandling(stateInfoCommand));
|
|
30234
|
+
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
29903
30235
|
return cmd;
|
|
29904
30236
|
}
|
|
29905
30237
|
function createStateCommand() {
|
|
29906
30238
|
const cmd = new Command9("state").description("Manage cdkd state stored in S3");
|
|
30239
|
+
cmd.addCommand(createStateInfoCommand());
|
|
29907
30240
|
cmd.addCommand(createStateListCommand());
|
|
29908
30241
|
cmd.addCommand(createStateResourcesCommand());
|
|
29909
30242
|
cmd.addCommand(createStateShowCommand());
|
|
29910
30243
|
cmd.addCommand(createStateRmCommand());
|
|
30244
|
+
cmd.addCommand(createStateDestroyCommand());
|
|
29911
30245
|
return cmd;
|
|
29912
30246
|
}
|
|
29913
30247
|
|
|
@@ -29936,7 +30270,7 @@ function reorderArgs(argv) {
|
|
|
29936
30270
|
}
|
|
29937
30271
|
async function main() {
|
|
29938
30272
|
const program = new Command10();
|
|
29939
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
30273
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.13.0");
|
|
29940
30274
|
program.addCommand(createBootstrapCommand());
|
|
29941
30275
|
program.addCommand(createSynthCommand());
|
|
29942
30276
|
program.addCommand(createListCommand());
|