@go-to-k/cdkd 0.10.0 → 0.12.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 +15 -4
- package/dist/cli.js +447 -204
- package/dist/cli.js.map +4 -4
- package/dist/go-to-k-cdkd-0.12.0.tgz +0 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/go-to-k-cdkd-0.10.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>",
|
|
@@ -971,7 +981,10 @@ function resolveStateBucket(cliBucket) {
|
|
|
971
981
|
const bucket = cdkdContext?.["stateBucket"];
|
|
972
982
|
return typeof bucket === "string" ? bucket : void 0;
|
|
973
983
|
}
|
|
974
|
-
function getDefaultStateBucketName(accountId
|
|
984
|
+
function getDefaultStateBucketName(accountId) {
|
|
985
|
+
return `cdkd-state-${accountId}`;
|
|
986
|
+
}
|
|
987
|
+
function getLegacyStateBucketName(accountId, region) {
|
|
975
988
|
return `cdkd-state-${accountId}-${region}`;
|
|
976
989
|
}
|
|
977
990
|
async function resolveStateBucketWithDefault(cliBucket, region) {
|
|
@@ -981,13 +994,48 @@ async function resolveStateBucketWithDefault(cliBucket, region) {
|
|
|
981
994
|
const logger = getLogger();
|
|
982
995
|
logger.debug("No state bucket specified, resolving default from account...");
|
|
983
996
|
const { GetCallerIdentityCommand: GetCallerIdentityCommand8 } = await import("@aws-sdk/client-sts");
|
|
997
|
+
const { S3Client: S3Client10 } = await import("@aws-sdk/client-s3");
|
|
984
998
|
const { getAwsClients: getAwsClients2 } = await Promise.resolve().then(() => (init_aws_clients(), aws_clients_exports));
|
|
985
999
|
const awsClients = getAwsClients2();
|
|
986
1000
|
const identity = await awsClients.sts.send(new GetCallerIdentityCommand8({}));
|
|
987
1001
|
const accountId = identity.Account;
|
|
988
|
-
const
|
|
989
|
-
|
|
990
|
-
|
|
1002
|
+
const newName = getDefaultStateBucketName(accountId);
|
|
1003
|
+
const legacyName = getLegacyStateBucketName(accountId, region);
|
|
1004
|
+
const probe = new S3Client10({ region: "us-east-1" });
|
|
1005
|
+
try {
|
|
1006
|
+
if (await bucketExists(probe, newName)) {
|
|
1007
|
+
logger.info(`State bucket: ${newName}`);
|
|
1008
|
+
return newName;
|
|
1009
|
+
}
|
|
1010
|
+
if (await bucketExists(probe, legacyName)) {
|
|
1011
|
+
logger.warn(
|
|
1012
|
+
`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).`
|
|
1013
|
+
);
|
|
1014
|
+
return legacyName;
|
|
1015
|
+
}
|
|
1016
|
+
throw new Error(
|
|
1017
|
+
`No cdkd state bucket found for account ${accountId}. Looked for '${newName}' (current default) and '${legacyName}' (legacy default). Run 'cdkd bootstrap' to create '${newName}'.`
|
|
1018
|
+
);
|
|
1019
|
+
} finally {
|
|
1020
|
+
probe.destroy();
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
async function bucketExists(client, bucketName) {
|
|
1024
|
+
const { HeadBucketCommand: HeadBucketCommand3 } = await import("@aws-sdk/client-s3");
|
|
1025
|
+
try {
|
|
1026
|
+
await client.send(new HeadBucketCommand3({ Bucket: bucketName }));
|
|
1027
|
+
return true;
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
const err = error;
|
|
1030
|
+
const status = err.$metadata?.httpStatusCode;
|
|
1031
|
+
if (err.name === "NotFound" || err.name === "NoSuchBucket" || status === 404) {
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
if (status === 301 || status === 403) {
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
throw error;
|
|
1038
|
+
}
|
|
991
1039
|
}
|
|
992
1040
|
|
|
993
1041
|
// src/cli/commands/bootstrap.ts
|
|
@@ -1015,14 +1063,14 @@ async function bootstrapCommand(options) {
|
|
|
1015
1063
|
logger.info("No --state-bucket specified, resolving default bucket name...");
|
|
1016
1064
|
const identity = await awsClients.sts.send(new GetCallerIdentityCommand({}));
|
|
1017
1065
|
accountId = identity.Account;
|
|
1018
|
-
bucketName = getDefaultStateBucketName(accountId
|
|
1066
|
+
bucketName = getDefaultStateBucketName(accountId);
|
|
1019
1067
|
logger.info(`Using default state bucket: ${bucketName}`);
|
|
1020
1068
|
}
|
|
1021
1069
|
try {
|
|
1022
|
-
let
|
|
1070
|
+
let bucketExists2 = false;
|
|
1023
1071
|
try {
|
|
1024
1072
|
await s3Client.send(new HeadBucketCommand({ Bucket: bucketName }));
|
|
1025
|
-
|
|
1073
|
+
bucketExists2 = true;
|
|
1026
1074
|
logger.info(`Bucket ${bucketName} already exists`);
|
|
1027
1075
|
} catch (error) {
|
|
1028
1076
|
const err = error;
|
|
@@ -1032,7 +1080,7 @@ async function bootstrapCommand(options) {
|
|
|
1032
1080
|
throw normalizeAwsError(error, { bucket: bucketName, operation: "HeadBucket" });
|
|
1033
1081
|
}
|
|
1034
1082
|
}
|
|
1035
|
-
if (
|
|
1083
|
+
if (bucketExists2) {
|
|
1036
1084
|
if (!options.force) {
|
|
1037
1085
|
logger.warn(
|
|
1038
1086
|
`Bucket ${bucketName} already exists. Use --force to reconfigure (this will not delete existing state)`
|
|
@@ -1119,8 +1167,17 @@ State bucket: ${bucketName}`);
|
|
|
1119
1167
|
function createBootstrapCommand() {
|
|
1120
1168
|
const cmd = new Command("bootstrap").description("Bootstrap cdkd by creating required S3 bucket for state management").option(
|
|
1121
1169
|
"--state-bucket <bucket>",
|
|
1122
|
-
"Name of S3 bucket to create for state storage (default: cdkd-state-{accountId}
|
|
1123
|
-
).option("--force", "Force reconfiguration of existing bucket", false).
|
|
1170
|
+
"Name of S3 bucket to create for state storage (default: cdkd-state-{accountId})"
|
|
1171
|
+
).option("--force", "Force reconfiguration of existing bucket", false).addOption(
|
|
1172
|
+
// Bootstrap-specific: needs to know which region to create the bucket
|
|
1173
|
+
// in. After PR 5, `--region` is removed from `commonOptions` and only
|
|
1174
|
+
// re-added explicitly here — every other command resolves the region
|
|
1175
|
+
// from `AWS_REGION` / profile.
|
|
1176
|
+
new Option2(
|
|
1177
|
+
"--region <region>",
|
|
1178
|
+
"AWS region in which to create the state bucket (defaults to AWS_REGION env or us-east-1)"
|
|
1179
|
+
)
|
|
1180
|
+
).action(withErrorHandling(bootstrapCommand));
|
|
1124
1181
|
commonOptions.forEach((opt) => cmd.addOption(opt));
|
|
1125
1182
|
return cmd;
|
|
1126
1183
|
}
|
|
@@ -2455,6 +2512,7 @@ async function synthCommand(options) {
|
|
|
2455
2512
|
if (options.verbose) {
|
|
2456
2513
|
logger.setLevel("debug");
|
|
2457
2514
|
}
|
|
2515
|
+
warnIfDeprecatedRegion(options);
|
|
2458
2516
|
const app = resolveApp(options.app);
|
|
2459
2517
|
if (!app) {
|
|
2460
2518
|
throw new Error(
|
|
@@ -2502,6 +2560,7 @@ Output: ${assemblyDir}`);
|
|
|
2502
2560
|
function createSynthCommand() {
|
|
2503
2561
|
const cmd = new Command2("synth").description("Synthesize CDK app to CloudFormation template").action(withErrorHandling(synthCommand));
|
|
2504
2562
|
[...commonOptions, ...appOptions, ...contextOptions].forEach((opt) => cmd.addOption(opt));
|
|
2563
|
+
cmd.addOption(deprecatedRegionOption);
|
|
2505
2564
|
return cmd;
|
|
2506
2565
|
}
|
|
2507
2566
|
|
|
@@ -2584,6 +2643,7 @@ async function listCommand(patterns, options) {
|
|
|
2584
2643
|
if (options.verbose) {
|
|
2585
2644
|
logger.setLevel("debug");
|
|
2586
2645
|
}
|
|
2646
|
+
warnIfDeprecatedRegion(options);
|
|
2587
2647
|
const app = resolveApp(options.app);
|
|
2588
2648
|
if (!app) {
|
|
2589
2649
|
throw new Error(
|
|
@@ -2649,6 +2709,7 @@ function createListCommand() {
|
|
|
2649
2709
|
"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/*')."
|
|
2650
2710
|
).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));
|
|
2651
2711
|
[...commonOptions, ...appOptions, ...contextOptions].forEach((opt) => cmd.addOption(opt));
|
|
2712
|
+
cmd.addOption(deprecatedRegionOption);
|
|
2652
2713
|
return cmd;
|
|
2653
2714
|
}
|
|
2654
2715
|
|
|
@@ -28548,6 +28609,7 @@ async function deployCommand(stacks, options) {
|
|
|
28548
28609
|
logger.setLevel("debug");
|
|
28549
28610
|
process.env["CDKD_NO_LIVE"] = "1";
|
|
28550
28611
|
}
|
|
28612
|
+
warnIfDeprecatedRegion(options);
|
|
28551
28613
|
if (!options.wait) {
|
|
28552
28614
|
process.env["CDKD_NO_WAIT"] = "true";
|
|
28553
28615
|
}
|
|
@@ -28797,6 +28859,7 @@ function createDeployCommand() {
|
|
|
28797
28859
|
...deployOptions,
|
|
28798
28860
|
...contextOptions
|
|
28799
28861
|
].forEach((opt) => cmd.addOption(opt));
|
|
28862
|
+
cmd.addOption(deprecatedRegionOption);
|
|
28800
28863
|
return cmd;
|
|
28801
28864
|
}
|
|
28802
28865
|
|
|
@@ -28865,6 +28928,7 @@ async function diffCommand(stacks, options) {
|
|
|
28865
28928
|
if (options.verbose) {
|
|
28866
28929
|
logger.setLevel("debug");
|
|
28867
28930
|
}
|
|
28931
|
+
warnIfDeprecatedRegion(options);
|
|
28868
28932
|
const app = resolveApp(options.app);
|
|
28869
28933
|
if (!app) {
|
|
28870
28934
|
throw new Error(
|
|
@@ -29008,19 +29072,219 @@ function createDiffCommand() {
|
|
|
29008
29072
|
[...commonOptions, ...appOptions, ...stateOptions, ...stackOptions, ...contextOptions].forEach(
|
|
29009
29073
|
(opt) => cmd.addOption(opt)
|
|
29010
29074
|
);
|
|
29075
|
+
cmd.addOption(deprecatedRegionOption);
|
|
29011
29076
|
return cmd;
|
|
29012
29077
|
}
|
|
29013
29078
|
|
|
29014
29079
|
// src/cli/commands/destroy.ts
|
|
29015
29080
|
import { Command as Command6 } from "commander";
|
|
29016
29081
|
init_aws_clients();
|
|
29082
|
+
|
|
29083
|
+
// src/cli/commands/destroy-runner.ts
|
|
29017
29084
|
import * as readline from "node:readline/promises";
|
|
29085
|
+
init_aws_clients();
|
|
29086
|
+
async function runDestroyForStack(stackName, state, ctx) {
|
|
29087
|
+
const logger = getLogger();
|
|
29088
|
+
const result = {
|
|
29089
|
+
stackName,
|
|
29090
|
+
cancelled: false,
|
|
29091
|
+
skippedEmpty: false,
|
|
29092
|
+
deletedCount: 0,
|
|
29093
|
+
errorCount: 0
|
|
29094
|
+
};
|
|
29095
|
+
const resourceCount = Object.keys(state.resources).length;
|
|
29096
|
+
const regionForState = state.region ?? ctx.baseRegion;
|
|
29097
|
+
if (resourceCount === 0) {
|
|
29098
|
+
logger.info(`Stack ${stackName} has no resources, cleaning up state...`);
|
|
29099
|
+
await ctx.stateBackend.deleteState(stackName, regionForState);
|
|
29100
|
+
logger.info("\u2713 State deleted");
|
|
29101
|
+
result.skippedEmpty = true;
|
|
29102
|
+
return result;
|
|
29103
|
+
}
|
|
29104
|
+
logger.info(`
|
|
29105
|
+
Resources to be deleted (${resourceCount}):`);
|
|
29106
|
+
for (const [logicalId, resource] of Object.entries(state.resources)) {
|
|
29107
|
+
logger.info(` - ${logicalId} (${resource.resourceType})`);
|
|
29108
|
+
}
|
|
29109
|
+
if (!ctx.skipConfirmation) {
|
|
29110
|
+
const rl = readline.createInterface({
|
|
29111
|
+
input: process.stdin,
|
|
29112
|
+
output: process.stdout
|
|
29113
|
+
});
|
|
29114
|
+
const answer = await rl.question(
|
|
29115
|
+
`
|
|
29116
|
+
Are you sure you want to destroy stack "${stackName}" and delete all ${resourceCount} resources? (Y/n): `
|
|
29117
|
+
);
|
|
29118
|
+
rl.close();
|
|
29119
|
+
const trimmed = answer.trim().toLowerCase();
|
|
29120
|
+
if (trimmed === "n" || trimmed === "no") {
|
|
29121
|
+
logger.info("Destroy cancelled");
|
|
29122
|
+
result.cancelled = true;
|
|
29123
|
+
return result;
|
|
29124
|
+
}
|
|
29125
|
+
}
|
|
29126
|
+
const stackRegion = state.region;
|
|
29127
|
+
let destroyProviderRegistry = ctx.providerRegistry;
|
|
29128
|
+
let destroyAwsClients;
|
|
29129
|
+
if (stackRegion && stackRegion !== ctx.baseRegion) {
|
|
29130
|
+
logger.info(`Stack region: ${stackRegion}`);
|
|
29131
|
+
process.env["AWS_REGION"] = stackRegion;
|
|
29132
|
+
process.env["AWS_DEFAULT_REGION"] = stackRegion;
|
|
29133
|
+
destroyAwsClients = new AwsClients({
|
|
29134
|
+
region: stackRegion,
|
|
29135
|
+
...ctx.profile && { profile: ctx.profile }
|
|
29136
|
+
});
|
|
29137
|
+
setAwsClients(destroyAwsClients);
|
|
29138
|
+
destroyProviderRegistry = new ProviderRegistry();
|
|
29139
|
+
registerAllProviders(destroyProviderRegistry);
|
|
29140
|
+
destroyProviderRegistry.setCustomResourceResponseBucket(ctx.stateBucket);
|
|
29141
|
+
}
|
|
29142
|
+
logger.info(`
|
|
29143
|
+
Acquiring lock for stack ${stackName}...`);
|
|
29144
|
+
await ctx.lockManager.acquireLock(stackName, regionForState, void 0, "destroy");
|
|
29145
|
+
const renderer = getLiveRenderer();
|
|
29146
|
+
renderer.start();
|
|
29147
|
+
try {
|
|
29148
|
+
logger.info("Building dependency graph...");
|
|
29149
|
+
const template = {
|
|
29150
|
+
AWSTemplateFormatVersion: "2010-09-09",
|
|
29151
|
+
Resources: {}
|
|
29152
|
+
};
|
|
29153
|
+
for (const [logicalId, resource] of Object.entries(state.resources)) {
|
|
29154
|
+
template.Resources[logicalId] = {
|
|
29155
|
+
Type: resource.resourceType,
|
|
29156
|
+
Properties: resource.properties || {},
|
|
29157
|
+
...resource.dependencies && resource.dependencies.length > 0 && {
|
|
29158
|
+
DependsOn: resource.dependencies
|
|
29159
|
+
}
|
|
29160
|
+
};
|
|
29161
|
+
}
|
|
29162
|
+
const typeToLogicalIds = /* @__PURE__ */ new Map();
|
|
29163
|
+
for (const [logicalId, resource] of Object.entries(state.resources)) {
|
|
29164
|
+
const ids = typeToLogicalIds.get(resource.resourceType) ?? [];
|
|
29165
|
+
ids.push(logicalId);
|
|
29166
|
+
typeToLogicalIds.set(resource.resourceType, ids);
|
|
29167
|
+
}
|
|
29168
|
+
for (const [logicalId, resource] of Object.entries(state.resources)) {
|
|
29169
|
+
const mustDeleteAfter = IMPLICIT_DELETE_DEPENDENCIES[resource.resourceType];
|
|
29170
|
+
if (!mustDeleteAfter)
|
|
29171
|
+
continue;
|
|
29172
|
+
for (const depType of mustDeleteAfter) {
|
|
29173
|
+
const depIds = typeToLogicalIds.get(depType);
|
|
29174
|
+
if (!depIds)
|
|
29175
|
+
continue;
|
|
29176
|
+
for (const depId of depIds) {
|
|
29177
|
+
const existing = template.Resources[depId]?.DependsOn ?? [];
|
|
29178
|
+
const depsArray = Array.isArray(existing) ? existing : [existing];
|
|
29179
|
+
if (!depsArray.includes(logicalId)) {
|
|
29180
|
+
template.Resources[depId] = {
|
|
29181
|
+
...template.Resources[depId],
|
|
29182
|
+
DependsOn: [...depsArray, logicalId]
|
|
29183
|
+
};
|
|
29184
|
+
logger.debug(
|
|
29185
|
+
`Implicit delete dependency: ${depId} (${depType}) must be deleted before ${logicalId} (${resource.resourceType})`
|
|
29186
|
+
);
|
|
29187
|
+
}
|
|
29188
|
+
}
|
|
29189
|
+
}
|
|
29190
|
+
}
|
|
29191
|
+
const dagBuilder = new DagBuilder();
|
|
29192
|
+
const graph = dagBuilder.buildGraph(template);
|
|
29193
|
+
const executionLevels = dagBuilder.getExecutionLevels(graph);
|
|
29194
|
+
logger.debug(`Dependency graph: ${executionLevels.length} level(s)`);
|
|
29195
|
+
for (let levelIndex = executionLevels.length - 1; levelIndex >= 0; levelIndex--) {
|
|
29196
|
+
const level = executionLevels[levelIndex];
|
|
29197
|
+
if (!level)
|
|
29198
|
+
continue;
|
|
29199
|
+
logger.debug(
|
|
29200
|
+
`Deletion level ${executionLevels.length - levelIndex}/${executionLevels.length} (${level.length} resources)`
|
|
29201
|
+
);
|
|
29202
|
+
const deletePromises = level.map(async (logicalId) => {
|
|
29203
|
+
const resource = state.resources[logicalId];
|
|
29204
|
+
if (!resource) {
|
|
29205
|
+
logger.warn(`Resource ${logicalId} not found in state, skipping`);
|
|
29206
|
+
return;
|
|
29207
|
+
}
|
|
29208
|
+
renderer.addTask(logicalId, `Deleting ${logicalId} (${resource.resourceType})`);
|
|
29209
|
+
try {
|
|
29210
|
+
const provider = destroyProviderRegistry.getProvider(resource.resourceType);
|
|
29211
|
+
let lastDeleteError;
|
|
29212
|
+
for (let attempt = 0; attempt <= 3; attempt++) {
|
|
29213
|
+
try {
|
|
29214
|
+
await provider.delete(
|
|
29215
|
+
logicalId,
|
|
29216
|
+
resource.physicalId,
|
|
29217
|
+
resource.resourceType,
|
|
29218
|
+
resource.properties
|
|
29219
|
+
);
|
|
29220
|
+
lastDeleteError = null;
|
|
29221
|
+
break;
|
|
29222
|
+
} catch (retryError) {
|
|
29223
|
+
lastDeleteError = retryError;
|
|
29224
|
+
const msg = retryError instanceof Error ? retryError.message : String(retryError);
|
|
29225
|
+
const isRetryable = msg.includes("Too Many Requests") || msg.includes("has dependencies") || msg.includes("can't be deleted since") || msg.includes("DependencyViolation");
|
|
29226
|
+
if (!isRetryable || attempt >= 3)
|
|
29227
|
+
break;
|
|
29228
|
+
const delay = 5e3 * Math.pow(2, attempt);
|
|
29229
|
+
logger.debug(
|
|
29230
|
+
` \u23F3 Retrying delete ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/3)`
|
|
29231
|
+
);
|
|
29232
|
+
await new Promise((resolve4) => setTimeout(resolve4, delay));
|
|
29233
|
+
}
|
|
29234
|
+
}
|
|
29235
|
+
if (lastDeleteError)
|
|
29236
|
+
throw lastDeleteError;
|
|
29237
|
+
renderer.removeTask(logicalId);
|
|
29238
|
+
logger.info(` \u2705 ${logicalId} (${resource.resourceType}) deleted`);
|
|
29239
|
+
result.deletedCount++;
|
|
29240
|
+
} catch (error) {
|
|
29241
|
+
renderer.removeTask(logicalId);
|
|
29242
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
29243
|
+
if (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException")) {
|
|
29244
|
+
logger.debug(` ${logicalId} already deleted, removing from state`);
|
|
29245
|
+
result.deletedCount++;
|
|
29246
|
+
} else {
|
|
29247
|
+
logger.error(` \u2717 Failed to delete ${logicalId}:`, String(error));
|
|
29248
|
+
result.errorCount++;
|
|
29249
|
+
}
|
|
29250
|
+
} finally {
|
|
29251
|
+
renderer.removeTask(logicalId);
|
|
29252
|
+
}
|
|
29253
|
+
});
|
|
29254
|
+
await Promise.all(deletePromises);
|
|
29255
|
+
}
|
|
29256
|
+
if (result.errorCount === 0) {
|
|
29257
|
+
await ctx.stateBackend.deleteState(stackName, regionForState);
|
|
29258
|
+
logger.debug("State deleted");
|
|
29259
|
+
} else {
|
|
29260
|
+
logger.warn(`${result.errorCount} resource(s) failed to delete. State preserved.`);
|
|
29261
|
+
}
|
|
29262
|
+
logger.info(
|
|
29263
|
+
`
|
|
29264
|
+
\u2713 Stack ${stackName} destroyed (${result.deletedCount} deleted, ${result.errorCount} errors)`
|
|
29265
|
+
);
|
|
29266
|
+
} finally {
|
|
29267
|
+
renderer.stop();
|
|
29268
|
+
logger.debug("Releasing lock...");
|
|
29269
|
+
await ctx.lockManager.releaseLock(stackName, regionForState);
|
|
29270
|
+
if (destroyAwsClients) {
|
|
29271
|
+
destroyAwsClients.destroy();
|
|
29272
|
+
process.env["AWS_REGION"] = ctx.baseRegion;
|
|
29273
|
+
process.env["AWS_DEFAULT_REGION"] = ctx.baseRegion;
|
|
29274
|
+
setAwsClients(ctx.baseAwsClients);
|
|
29275
|
+
}
|
|
29276
|
+
}
|
|
29277
|
+
return result;
|
|
29278
|
+
}
|
|
29279
|
+
|
|
29280
|
+
// src/cli/commands/destroy.ts
|
|
29018
29281
|
async function destroyCommand(stackArgs, options) {
|
|
29019
29282
|
const logger = getLogger();
|
|
29020
29283
|
if (options.verbose) {
|
|
29021
29284
|
logger.setLevel("debug");
|
|
29022
29285
|
process.env["CDKD_NO_LIVE"] = "1";
|
|
29023
29286
|
}
|
|
29287
|
+
warnIfDeprecatedRegion(options);
|
|
29024
29288
|
const region = options.region || process.env["AWS_REGION"] || "us-east-1";
|
|
29025
29289
|
const stateBucket = await resolveStateBucketWithDefault(options.stateBucket, region);
|
|
29026
29290
|
logger.info("Starting stack destruction...");
|
|
@@ -29045,7 +29309,6 @@ async function destroyCommand(stackArgs, options) {
|
|
|
29045
29309
|
});
|
|
29046
29310
|
await stateBackend.verifyBucketExists();
|
|
29047
29311
|
const lockManager = new LockManager(awsClients.s3, stateConfig);
|
|
29048
|
-
const dagBuilder = new DagBuilder();
|
|
29049
29312
|
const providerRegistry = new ProviderRegistry();
|
|
29050
29313
|
registerAllProviders(providerRegistry);
|
|
29051
29314
|
providerRegistry.setCustomResourceResponseBucket(stateBucket);
|
|
@@ -29145,189 +29408,16 @@ Preparing to destroy stack: ${stackName}`);
|
|
|
29145
29408
|
logger.warn(`No state found for stack ${stackName}, skipping`);
|
|
29146
29409
|
continue;
|
|
29147
29410
|
}
|
|
29148
|
-
|
|
29149
|
-
|
|
29150
|
-
|
|
29151
|
-
|
|
29152
|
-
|
|
29153
|
-
|
|
29154
|
-
|
|
29155
|
-
|
|
29156
|
-
|
|
29157
|
-
|
|
29158
|
-
for (const [logicalId, resource] of Object.entries(currentState.resources)) {
|
|
29159
|
-
logger.info(` - ${logicalId} (${resource.resourceType})`);
|
|
29160
|
-
}
|
|
29161
|
-
if (!options.yes && !options.force) {
|
|
29162
|
-
const rl = readline.createInterface({
|
|
29163
|
-
input: process.stdin,
|
|
29164
|
-
output: process.stdout
|
|
29165
|
-
});
|
|
29166
|
-
const answer = await rl.question(
|
|
29167
|
-
`
|
|
29168
|
-
Are you sure you want to destroy stack "${stackName}" and delete all ${resourceCount} resources? (Y/n): `
|
|
29169
|
-
);
|
|
29170
|
-
rl.close();
|
|
29171
|
-
const trimmed = answer.trim().toLowerCase();
|
|
29172
|
-
if (trimmed === "n" || trimmed === "no") {
|
|
29173
|
-
logger.info("Destroy cancelled");
|
|
29174
|
-
continue;
|
|
29175
|
-
}
|
|
29176
|
-
}
|
|
29177
|
-
const stackRegion = stackTargetRegion;
|
|
29178
|
-
let destroyProviderRegistry = providerRegistry;
|
|
29179
|
-
let destroyAwsClients;
|
|
29180
|
-
if (stackRegion && stackRegion !== region) {
|
|
29181
|
-
logger.info(`Stack region: ${stackRegion}`);
|
|
29182
|
-
process.env["AWS_REGION"] = stackRegion;
|
|
29183
|
-
process.env["AWS_DEFAULT_REGION"] = stackRegion;
|
|
29184
|
-
destroyAwsClients = new AwsClients({
|
|
29185
|
-
region: stackRegion,
|
|
29186
|
-
...options.profile && { profile: options.profile }
|
|
29187
|
-
});
|
|
29188
|
-
setAwsClients(destroyAwsClients);
|
|
29189
|
-
destroyProviderRegistry = new ProviderRegistry();
|
|
29190
|
-
registerAllProviders(destroyProviderRegistry);
|
|
29191
|
-
destroyProviderRegistry.setCustomResourceResponseBucket(stateBucket);
|
|
29192
|
-
}
|
|
29193
|
-
logger.info(`
|
|
29194
|
-
Acquiring lock for stack ${stackName}...`);
|
|
29195
|
-
await lockManager.acquireLock(stackName, stackRegion, void 0, "destroy");
|
|
29196
|
-
const renderer = getLiveRenderer();
|
|
29197
|
-
renderer.start();
|
|
29198
|
-
try {
|
|
29199
|
-
logger.info("Building dependency graph...");
|
|
29200
|
-
const template = {
|
|
29201
|
-
AWSTemplateFormatVersion: "2010-09-09",
|
|
29202
|
-
Resources: {}
|
|
29203
|
-
};
|
|
29204
|
-
for (const [logicalId, resource] of Object.entries(currentState.resources)) {
|
|
29205
|
-
template.Resources[logicalId] = {
|
|
29206
|
-
Type: resource.resourceType,
|
|
29207
|
-
Properties: resource.properties || {},
|
|
29208
|
-
...resource.dependencies && resource.dependencies.length > 0 && {
|
|
29209
|
-
DependsOn: resource.dependencies
|
|
29210
|
-
}
|
|
29211
|
-
};
|
|
29212
|
-
}
|
|
29213
|
-
const typeToLogicalIds = /* @__PURE__ */ new Map();
|
|
29214
|
-
for (const [logicalId, resource] of Object.entries(currentState.resources)) {
|
|
29215
|
-
const ids = typeToLogicalIds.get(resource.resourceType) ?? [];
|
|
29216
|
-
ids.push(logicalId);
|
|
29217
|
-
typeToLogicalIds.set(resource.resourceType, ids);
|
|
29218
|
-
}
|
|
29219
|
-
for (const [logicalId, resource] of Object.entries(currentState.resources)) {
|
|
29220
|
-
const mustDeleteAfter = IMPLICIT_DELETE_DEPENDENCIES[resource.resourceType];
|
|
29221
|
-
if (!mustDeleteAfter)
|
|
29222
|
-
continue;
|
|
29223
|
-
for (const depType of mustDeleteAfter) {
|
|
29224
|
-
const depIds = typeToLogicalIds.get(depType);
|
|
29225
|
-
if (!depIds)
|
|
29226
|
-
continue;
|
|
29227
|
-
for (const depId of depIds) {
|
|
29228
|
-
const existing = template.Resources[depId]?.DependsOn ?? [];
|
|
29229
|
-
const depsArray = Array.isArray(existing) ? existing : [existing];
|
|
29230
|
-
if (!depsArray.includes(logicalId)) {
|
|
29231
|
-
template.Resources[depId] = {
|
|
29232
|
-
...template.Resources[depId],
|
|
29233
|
-
DependsOn: [...depsArray, logicalId]
|
|
29234
|
-
};
|
|
29235
|
-
logger.debug(
|
|
29236
|
-
`Implicit delete dependency: ${depId} (${depType}) must be deleted before ${logicalId} (${resource.resourceType})`
|
|
29237
|
-
);
|
|
29238
|
-
}
|
|
29239
|
-
}
|
|
29240
|
-
}
|
|
29241
|
-
}
|
|
29242
|
-
const graph = dagBuilder.buildGraph(template);
|
|
29243
|
-
const executionLevels = dagBuilder.getExecutionLevels(graph);
|
|
29244
|
-
logger.debug(`Dependency graph: ${executionLevels.length} level(s)`);
|
|
29245
|
-
let deletedCount = 0;
|
|
29246
|
-
let errorCount = 0;
|
|
29247
|
-
for (let levelIndex = executionLevels.length - 1; levelIndex >= 0; levelIndex--) {
|
|
29248
|
-
const level = executionLevels[levelIndex];
|
|
29249
|
-
if (!level) {
|
|
29250
|
-
continue;
|
|
29251
|
-
}
|
|
29252
|
-
logger.debug(
|
|
29253
|
-
`Deletion level ${executionLevels.length - levelIndex}/${executionLevels.length} (${level.length} resources)`
|
|
29254
|
-
);
|
|
29255
|
-
const deletePromises = level.map(async (logicalId) => {
|
|
29256
|
-
const resource = currentState.resources[logicalId];
|
|
29257
|
-
if (!resource) {
|
|
29258
|
-
logger.warn(`Resource ${logicalId} not found in state, skipping`);
|
|
29259
|
-
return;
|
|
29260
|
-
}
|
|
29261
|
-
renderer.addTask(logicalId, `Deleting ${logicalId} (${resource.resourceType})`);
|
|
29262
|
-
try {
|
|
29263
|
-
const provider = destroyProviderRegistry.getProvider(resource.resourceType);
|
|
29264
|
-
let lastDeleteError;
|
|
29265
|
-
for (let attempt = 0; attempt <= 3; attempt++) {
|
|
29266
|
-
try {
|
|
29267
|
-
await provider.delete(
|
|
29268
|
-
logicalId,
|
|
29269
|
-
resource.physicalId,
|
|
29270
|
-
resource.resourceType,
|
|
29271
|
-
resource.properties,
|
|
29272
|
-
{ expectedRegion: currentState.region }
|
|
29273
|
-
);
|
|
29274
|
-
lastDeleteError = null;
|
|
29275
|
-
break;
|
|
29276
|
-
} catch (retryError) {
|
|
29277
|
-
lastDeleteError = retryError;
|
|
29278
|
-
const msg = retryError instanceof Error ? retryError.message : String(retryError);
|
|
29279
|
-
const isRetryable = msg.includes("Too Many Requests") || msg.includes("has dependencies") || msg.includes("can't be deleted since") || msg.includes("DependencyViolation");
|
|
29280
|
-
if (!isRetryable || attempt >= 3)
|
|
29281
|
-
break;
|
|
29282
|
-
const delay = 5e3 * Math.pow(2, attempt);
|
|
29283
|
-
logger.debug(
|
|
29284
|
-
` \u23F3 Retrying delete ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/3)`
|
|
29285
|
-
);
|
|
29286
|
-
await new Promise((resolve4) => setTimeout(resolve4, delay));
|
|
29287
|
-
}
|
|
29288
|
-
}
|
|
29289
|
-
if (lastDeleteError)
|
|
29290
|
-
throw lastDeleteError;
|
|
29291
|
-
renderer.removeTask(logicalId);
|
|
29292
|
-
logger.info(` \u2705 ${logicalId} (${resource.resourceType}) deleted`);
|
|
29293
|
-
deletedCount++;
|
|
29294
|
-
} catch (error) {
|
|
29295
|
-
renderer.removeTask(logicalId);
|
|
29296
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
29297
|
-
if (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException")) {
|
|
29298
|
-
logger.debug(` ${logicalId} already deleted, removing from state`);
|
|
29299
|
-
deletedCount++;
|
|
29300
|
-
} else {
|
|
29301
|
-
logger.error(` \u2717 Failed to delete ${logicalId}:`, String(error));
|
|
29302
|
-
errorCount++;
|
|
29303
|
-
}
|
|
29304
|
-
} finally {
|
|
29305
|
-
renderer.removeTask(logicalId);
|
|
29306
|
-
}
|
|
29307
|
-
});
|
|
29308
|
-
await Promise.all(deletePromises);
|
|
29309
|
-
}
|
|
29310
|
-
if (errorCount === 0) {
|
|
29311
|
-
await stateBackend.deleteState(stackName, stackRegion);
|
|
29312
|
-
logger.debug("State deleted");
|
|
29313
|
-
} else {
|
|
29314
|
-
logger.warn(`${errorCount} resource(s) failed to delete. State preserved.`);
|
|
29315
|
-
}
|
|
29316
|
-
logger.info(
|
|
29317
|
-
`
|
|
29318
|
-
\u2713 Stack ${stackName} destroyed (${deletedCount} deleted, ${errorCount} errors)`
|
|
29319
|
-
);
|
|
29320
|
-
} finally {
|
|
29321
|
-
renderer.stop();
|
|
29322
|
-
logger.debug("Releasing lock...");
|
|
29323
|
-
await lockManager.releaseLock(stackName, stackRegion);
|
|
29324
|
-
if (destroyAwsClients) {
|
|
29325
|
-
destroyAwsClients.destroy();
|
|
29326
|
-
process.env["AWS_REGION"] = region;
|
|
29327
|
-
process.env["AWS_DEFAULT_REGION"] = region;
|
|
29328
|
-
setAwsClients(awsClients);
|
|
29329
|
-
}
|
|
29330
|
-
}
|
|
29411
|
+
await runDestroyForStack(stackName, stateResult.state, {
|
|
29412
|
+
stateBackend,
|
|
29413
|
+
lockManager,
|
|
29414
|
+
providerRegistry,
|
|
29415
|
+
baseAwsClients: awsClients,
|
|
29416
|
+
baseRegion: region,
|
|
29417
|
+
...options.profile && { profile: options.profile },
|
|
29418
|
+
stateBucket,
|
|
29419
|
+
skipConfirmation: options.yes || options.force
|
|
29420
|
+
});
|
|
29331
29421
|
}
|
|
29332
29422
|
} finally {
|
|
29333
29423
|
awsClients.destroy();
|
|
@@ -29346,16 +29436,18 @@ function createDestroyCommand() {
|
|
|
29346
29436
|
...destroyOptions,
|
|
29347
29437
|
...contextOptions
|
|
29348
29438
|
].forEach((opt) => cmd.addOption(opt));
|
|
29439
|
+
cmd.addOption(deprecatedRegionOption);
|
|
29349
29440
|
return cmd;
|
|
29350
29441
|
}
|
|
29351
29442
|
|
|
29352
29443
|
// src/cli/commands/publish-assets.ts
|
|
29353
|
-
import { Option as
|
|
29444
|
+
import { Option as Option3, Command as Command7 } from "commander";
|
|
29354
29445
|
async function publishAssetsCommand(options) {
|
|
29355
29446
|
const logger = getLogger();
|
|
29356
29447
|
if (options.verbose) {
|
|
29357
29448
|
logger.setLevel("debug");
|
|
29358
29449
|
}
|
|
29450
|
+
warnIfDeprecatedRegion(options);
|
|
29359
29451
|
logger.info("Publishing assets...");
|
|
29360
29452
|
logger.debug("Asset manifest path:", options.path);
|
|
29361
29453
|
const publisher = new AssetPublisher();
|
|
@@ -29369,25 +29461,27 @@ async function publishAssetsCommand(options) {
|
|
|
29369
29461
|
}
|
|
29370
29462
|
function createPublishAssetsCommand() {
|
|
29371
29463
|
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(
|
|
29372
|
-
new
|
|
29464
|
+
new Option3(
|
|
29373
29465
|
"--asset-publish-concurrency <number>",
|
|
29374
29466
|
"Maximum concurrent asset publish operations"
|
|
29375
29467
|
).default(8).argParser((value) => parseInt(value, 10))
|
|
29376
29468
|
).addOption(
|
|
29377
|
-
new
|
|
29469
|
+
new Option3("--image-build-concurrency <number>", "Maximum concurrent Docker image builds").default(4).argParser((value) => parseInt(value, 10))
|
|
29378
29470
|
).action(withErrorHandling(publishAssetsCommand));
|
|
29379
29471
|
commonOptions.forEach((opt) => cmd.addOption(opt));
|
|
29472
|
+
cmd.addOption(deprecatedRegionOption);
|
|
29380
29473
|
return cmd;
|
|
29381
29474
|
}
|
|
29382
29475
|
|
|
29383
29476
|
// src/cli/commands/force-unlock.ts
|
|
29384
|
-
import { Command as Command8, Option as
|
|
29477
|
+
import { Command as Command8, Option as Option4 } from "commander";
|
|
29385
29478
|
init_aws_clients();
|
|
29386
29479
|
async function forceUnlockCommand(stackArgs, options) {
|
|
29387
29480
|
const logger = getLogger();
|
|
29388
29481
|
if (options.verbose) {
|
|
29389
29482
|
logger.setLevel("debug");
|
|
29390
29483
|
}
|
|
29484
|
+
warnIfDeprecatedRegion(options);
|
|
29391
29485
|
const stackPatterns = stackArgs.length > 0 ? stackArgs : options.stack ? [options.stack] : [];
|
|
29392
29486
|
if (stackPatterns.length === 0) {
|
|
29393
29487
|
throw new Error("Stack name is required. Usage: cdkd force-unlock <stack-name>");
|
|
@@ -29441,18 +29535,19 @@ async function forceUnlockCommand(stackArgs, options) {
|
|
|
29441
29535
|
}
|
|
29442
29536
|
function createForceUnlockCommand() {
|
|
29443
29537
|
const cmd = new Command8("force-unlock").description("Force-release a stale lock on a stack").argument("[stacks...]", "Stack name(s) to unlock").addOption(
|
|
29444
|
-
new
|
|
29538
|
+
new Option4(
|
|
29445
29539
|
"--stack-region <region>",
|
|
29446
29540
|
"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."
|
|
29447
29541
|
)
|
|
29448
29542
|
).action(withErrorHandling(forceUnlockCommand));
|
|
29449
29543
|
[...commonOptions, ...stateOptions, ...stackOptions].forEach((opt) => cmd.addOption(opt));
|
|
29544
|
+
cmd.addOption(deprecatedRegionOption);
|
|
29450
29545
|
return cmd;
|
|
29451
29546
|
}
|
|
29452
29547
|
|
|
29453
29548
|
// src/cli/commands/state.ts
|
|
29454
29549
|
import * as readline2 from "node:readline/promises";
|
|
29455
|
-
import { Command as Command9, Option as
|
|
29550
|
+
import { Command as Command9, Option as Option5 } from "commander";
|
|
29456
29551
|
init_aws_clients();
|
|
29457
29552
|
function formatStackRef(ref) {
|
|
29458
29553
|
return ref.region ? `${ref.stackName} (${ref.region})` : ref.stackName;
|
|
@@ -29482,6 +29577,7 @@ function resolveSingleRegion(stackName, refs, requestedRegion) {
|
|
|
29482
29577
|
);
|
|
29483
29578
|
}
|
|
29484
29579
|
async function setupStateBackend(options) {
|
|
29580
|
+
warnIfDeprecatedRegion(options);
|
|
29485
29581
|
const awsClients = new AwsClients({
|
|
29486
29582
|
...options.region && { region: options.region },
|
|
29487
29583
|
...options.profile && { profile: options.profile }
|
|
@@ -29500,6 +29596,8 @@ async function setupStateBackend(options) {
|
|
|
29500
29596
|
return {
|
|
29501
29597
|
stateBackend,
|
|
29502
29598
|
lockManager,
|
|
29599
|
+
awsClients,
|
|
29600
|
+
region,
|
|
29503
29601
|
bucket,
|
|
29504
29602
|
prefix,
|
|
29505
29603
|
dispose: () => awsClients.destroy()
|
|
@@ -29590,6 +29688,7 @@ async function stateListCommand(options) {
|
|
|
29590
29688
|
function createStateListCommand() {
|
|
29591
29689
|
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));
|
|
29592
29690
|
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
29691
|
+
cmd.addOption(deprecatedRegionOption);
|
|
29593
29692
|
return cmd;
|
|
29594
29693
|
}
|
|
29595
29694
|
async function stateResourcesCommand(stackName, options) {
|
|
@@ -29693,6 +29792,7 @@ function formatLockSummary(lockInfo) {
|
|
|
29693
29792
|
function createStateResourcesCommand() {
|
|
29694
29793
|
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));
|
|
29695
29794
|
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
29795
|
+
cmd.addOption(deprecatedRegionOption);
|
|
29696
29796
|
return cmd;
|
|
29697
29797
|
}
|
|
29698
29798
|
async function stateShowCommand(stackName, options) {
|
|
@@ -29780,6 +29880,7 @@ async function stateShowCommand(stackName, options) {
|
|
|
29780
29880
|
function createStateShowCommand() {
|
|
29781
29881
|
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));
|
|
29782
29882
|
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
29883
|
+
cmd.addOption(deprecatedRegionOption);
|
|
29783
29884
|
return cmd;
|
|
29784
29885
|
}
|
|
29785
29886
|
async function stateRmCommand(stackArgs, options) {
|
|
@@ -29854,7 +29955,7 @@ Use 'cdkd destroy ${stackName}' if you want to delete the actual resources.
|
|
|
29854
29955
|
}
|
|
29855
29956
|
}
|
|
29856
29957
|
function stackRegionOption() {
|
|
29857
|
-
return new
|
|
29958
|
+
return new Option5(
|
|
29858
29959
|
"--stack-region <region>",
|
|
29859
29960
|
"Region of the stack record to operate on. Required when the same stack name has state in multiple regions."
|
|
29860
29961
|
);
|
|
@@ -29862,6 +29963,147 @@ function stackRegionOption() {
|
|
|
29862
29963
|
function createStateRmCommand() {
|
|
29863
29964
|
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));
|
|
29864
29965
|
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
29966
|
+
cmd.addOption(deprecatedRegionOption);
|
|
29967
|
+
return cmd;
|
|
29968
|
+
}
|
|
29969
|
+
async function stateDestroyCommand(stackArgs, options) {
|
|
29970
|
+
const logger = getLogger();
|
|
29971
|
+
if (options.verbose) {
|
|
29972
|
+
logger.setLevel("debug");
|
|
29973
|
+
process.env["CDKD_NO_LIVE"] = "1";
|
|
29974
|
+
}
|
|
29975
|
+
if (!options.all && stackArgs.length === 0) {
|
|
29976
|
+
throw new Error(
|
|
29977
|
+
"Stack name is required. Usage: cdkd state destroy <stack> [<stack>...] | --all"
|
|
29978
|
+
);
|
|
29979
|
+
}
|
|
29980
|
+
const setup = await setupStateBackend(options);
|
|
29981
|
+
const providerRegistry = new ProviderRegistry();
|
|
29982
|
+
registerAllProviders(providerRegistry);
|
|
29983
|
+
providerRegistry.setCustomResourceResponseBucket(setup.bucket);
|
|
29984
|
+
try {
|
|
29985
|
+
const stateRefs = await setup.stateBackend.listStacks();
|
|
29986
|
+
const knownStackNames = new Set(stateRefs.map((r) => r.stackName));
|
|
29987
|
+
let stackNames;
|
|
29988
|
+
if (options.all) {
|
|
29989
|
+
stackNames = [...knownStackNames].sort();
|
|
29990
|
+
if (stackNames.length === 0) {
|
|
29991
|
+
logger.info("No stacks found in state");
|
|
29992
|
+
return;
|
|
29993
|
+
}
|
|
29994
|
+
} else {
|
|
29995
|
+
const missing = stackArgs.filter((name) => !knownStackNames.has(name));
|
|
29996
|
+
if (missing.length > 0) {
|
|
29997
|
+
throw new Error(
|
|
29998
|
+
`No state found for stack(s): ${missing.join(", ")}. Run 'cdkd state list' to see available stacks.`
|
|
29999
|
+
);
|
|
30000
|
+
}
|
|
30001
|
+
stackNames = stackArgs;
|
|
30002
|
+
}
|
|
30003
|
+
if (options.all && !options.yes) {
|
|
30004
|
+
process.stdout.write(
|
|
30005
|
+
`
|
|
30006
|
+
WARNING: This destroys ${stackNames.length} stack(s) and removes their state records:
|
|
30007
|
+
`
|
|
30008
|
+
);
|
|
30009
|
+
for (const name of stackNames) {
|
|
30010
|
+
process.stdout.write(` - ${name}
|
|
30011
|
+
`);
|
|
30012
|
+
}
|
|
30013
|
+
process.stdout.write("\n");
|
|
30014
|
+
const rl = readline2.createInterface({
|
|
30015
|
+
input: process.stdin,
|
|
30016
|
+
output: process.stdout
|
|
30017
|
+
});
|
|
30018
|
+
const answer = await rl.question(`Destroy all ${stackNames.length} stack(s)? (y/N): `);
|
|
30019
|
+
rl.close();
|
|
30020
|
+
const trimmed = answer.trim().toLowerCase();
|
|
30021
|
+
if (trimmed !== "y" && trimmed !== "yes") {
|
|
30022
|
+
logger.info("Destroy cancelled");
|
|
30023
|
+
return;
|
|
30024
|
+
}
|
|
30025
|
+
}
|
|
30026
|
+
logger.info(`Found ${stackNames.length} stack(s) to destroy: ${stackNames.join(", ")}`);
|
|
30027
|
+
let totalErrors = 0;
|
|
30028
|
+
for (const stackName of stackNames) {
|
|
30029
|
+
const refs = stateRefs.filter((r) => r.stackName === stackName);
|
|
30030
|
+
let targets;
|
|
30031
|
+
if (options.stackRegion) {
|
|
30032
|
+
targets = refs.filter((r) => r.region === options.stackRegion || !r.region);
|
|
30033
|
+
if (targets.length === 0) {
|
|
30034
|
+
logger.warn(
|
|
30035
|
+
`Skipping ${stackName}: no state record matches --stack-region '${options.stackRegion}'`
|
|
30036
|
+
);
|
|
30037
|
+
continue;
|
|
30038
|
+
}
|
|
30039
|
+
} else if (refs.length === 1) {
|
|
30040
|
+
targets = refs;
|
|
30041
|
+
} else {
|
|
30042
|
+
const regions = refs.map((r) => r.region ?? "(legacy)").join(", ");
|
|
30043
|
+
throw new Error(
|
|
30044
|
+
`Stack '${stackName}' has state in multiple regions: ${regions}. Use --region <region> to pick one.`
|
|
30045
|
+
);
|
|
30046
|
+
}
|
|
30047
|
+
for (const ref of targets) {
|
|
30048
|
+
logger.info(
|
|
30049
|
+
`
|
|
30050
|
+
Preparing to destroy stack: ${stackName}${ref.region ? ` (${ref.region})` : ""}`
|
|
30051
|
+
);
|
|
30052
|
+
const stateResult = await setup.stateBackend.getState(
|
|
30053
|
+
stackName,
|
|
30054
|
+
ref.region ?? setup.region
|
|
30055
|
+
);
|
|
30056
|
+
if (!stateResult) {
|
|
30057
|
+
logger.warn(
|
|
30058
|
+
`No state found for stack ${stackName}${ref.region ? ` in ${ref.region}` : ""}, skipping`
|
|
30059
|
+
);
|
|
30060
|
+
continue;
|
|
30061
|
+
}
|
|
30062
|
+
const result = await runDestroyForStack(stackName, stateResult.state, {
|
|
30063
|
+
stateBackend: setup.stateBackend,
|
|
30064
|
+
lockManager: setup.lockManager,
|
|
30065
|
+
providerRegistry,
|
|
30066
|
+
baseAwsClients: setup.awsClients,
|
|
30067
|
+
baseRegion: setup.region,
|
|
30068
|
+
...options.profile && { profile: options.profile },
|
|
30069
|
+
stateBucket: setup.bucket,
|
|
30070
|
+
// --yes covers both the --all batch prompt above (already consumed)
|
|
30071
|
+
// and the per-stack prompt inside the runner. Per-stack prompts are
|
|
30072
|
+
// skipped when `options.yes` is set OR `--all` was set (the user
|
|
30073
|
+
// already accepted the batch prompt).
|
|
30074
|
+
skipConfirmation: options.yes || options.all === true
|
|
30075
|
+
});
|
|
30076
|
+
totalErrors += result.errorCount;
|
|
30077
|
+
}
|
|
30078
|
+
}
|
|
30079
|
+
if (totalErrors > 0) {
|
|
30080
|
+
throw new Error(
|
|
30081
|
+
`Destroy completed with ${totalErrors} resource error(s). Inspect 'cdkd state show <stack>' and re-run.`
|
|
30082
|
+
);
|
|
30083
|
+
}
|
|
30084
|
+
} finally {
|
|
30085
|
+
setup.dispose();
|
|
30086
|
+
}
|
|
30087
|
+
}
|
|
30088
|
+
function createStateDestroyCommand() {
|
|
30089
|
+
const cmd = new Command9("destroy").description(
|
|
30090
|
+
"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'."
|
|
30091
|
+
).argument("[stacks...]", "Stack name(s) to destroy (physical CloudFormation names)").option("--all", "Destroy every stack in the state bucket", false).addOption(stackRegionOption()).addHelpText(
|
|
30092
|
+
"after",
|
|
30093
|
+
[
|
|
30094
|
+
"",
|
|
30095
|
+
"Examples:",
|
|
30096
|
+
" cdkd state destroy MyStack",
|
|
30097
|
+
" cdkd state destroy MyStack OtherStack",
|
|
30098
|
+
" cdkd state destroy --all -y",
|
|
30099
|
+
" cdkd state destroy MyStack --state-bucket cdkd-state-test",
|
|
30100
|
+
" cdkd state destroy MyStack --stack-region us-west-2",
|
|
30101
|
+
"",
|
|
30102
|
+
"For removing only the state record (keeping AWS resources intact), use 'cdkd state rm'."
|
|
30103
|
+
].join("\n")
|
|
30104
|
+
).action(withErrorHandling(stateDestroyCommand));
|
|
30105
|
+
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
30106
|
+
cmd.addOption(deprecatedRegionOption);
|
|
29865
30107
|
return cmd;
|
|
29866
30108
|
}
|
|
29867
30109
|
function createStateCommand() {
|
|
@@ -29870,6 +30112,7 @@ function createStateCommand() {
|
|
|
29870
30112
|
cmd.addCommand(createStateResourcesCommand());
|
|
29871
30113
|
cmd.addCommand(createStateShowCommand());
|
|
29872
30114
|
cmd.addCommand(createStateRmCommand());
|
|
30115
|
+
cmd.addCommand(createStateDestroyCommand());
|
|
29873
30116
|
return cmd;
|
|
29874
30117
|
}
|
|
29875
30118
|
|
|
@@ -29898,7 +30141,7 @@ function reorderArgs(argv) {
|
|
|
29898
30141
|
}
|
|
29899
30142
|
async function main() {
|
|
29900
30143
|
const program = new Command10();
|
|
29901
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
30144
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.12.0");
|
|
29902
30145
|
program.addCommand(createBootstrapCommand());
|
|
29903
30146
|
program.addCommand(createSynthCommand());
|
|
29904
30147
|
program.addCommand(createListCommand());
|