@go-to-k/cdkd 0.12.0 → 0.14.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 +8 -1
- package/dist/cli.js +475 -34
- package/dist/cli.js.map +4 -4
- package/dist/go-to-k-cdkd-0.14.0.tgz +0 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/go-to-k-cdkd-0.12.0.tgz +0 -0
package/dist/cli.js
CHANGED
|
@@ -447,7 +447,7 @@ var init_aws_clients = __esm({
|
|
|
447
447
|
});
|
|
448
448
|
|
|
449
449
|
// src/cli/index.ts
|
|
450
|
-
import { Command as
|
|
450
|
+
import { Command as Command11 } from "commander";
|
|
451
451
|
|
|
452
452
|
// src/cli/commands/bootstrap.ts
|
|
453
453
|
import { Command, Option as Option2 } from "commander";
|
|
@@ -970,16 +970,18 @@ function resolveApp(cliApp) {
|
|
|
970
970
|
const cdkJson = loadCdkJson();
|
|
971
971
|
return cdkJson?.app ?? void 0;
|
|
972
972
|
}
|
|
973
|
-
function
|
|
973
|
+
function resolveStateBucketWithSource(cliBucket) {
|
|
974
974
|
if (cliBucket)
|
|
975
|
-
return cliBucket;
|
|
975
|
+
return { bucket: cliBucket, source: "cli-flag" };
|
|
976
976
|
const envBucket = process.env["CDKD_STATE_BUCKET"];
|
|
977
977
|
if (envBucket)
|
|
978
|
-
return envBucket;
|
|
978
|
+
return { bucket: envBucket, source: "env" };
|
|
979
979
|
const cdkJson = loadCdkJson();
|
|
980
980
|
const cdkdContext = cdkJson?.context?.["cdkd"];
|
|
981
981
|
const bucket = cdkdContext?.["stateBucket"];
|
|
982
|
-
|
|
982
|
+
if (typeof bucket === "string")
|
|
983
|
+
return { bucket, source: "cdk.json" };
|
|
984
|
+
return void 0;
|
|
983
985
|
}
|
|
984
986
|
function getDefaultStateBucketName(accountId) {
|
|
985
987
|
return `cdkd-state-${accountId}`;
|
|
@@ -988,30 +990,37 @@ function getLegacyStateBucketName(accountId, region) {
|
|
|
988
990
|
return `cdkd-state-${accountId}-${region}`;
|
|
989
991
|
}
|
|
990
992
|
async function resolveStateBucketWithDefault(cliBucket, region) {
|
|
991
|
-
|
|
993
|
+
return (await resolveStateBucketWithDefaultAndSource(cliBucket, region)).bucket;
|
|
994
|
+
}
|
|
995
|
+
async function resolveStateBucketWithDefaultAndSource(cliBucket, region) {
|
|
996
|
+
const syncResult = resolveStateBucketWithSource(cliBucket);
|
|
992
997
|
if (syncResult)
|
|
993
998
|
return syncResult;
|
|
994
999
|
const logger = getLogger();
|
|
995
1000
|
logger.debug("No state bucket specified, resolving default from account...");
|
|
996
|
-
const { GetCallerIdentityCommand:
|
|
997
|
-
const { S3Client:
|
|
1001
|
+
const { GetCallerIdentityCommand: GetCallerIdentityCommand9 } = await import("@aws-sdk/client-sts");
|
|
1002
|
+
const { S3Client: S3Client11 } = await import("@aws-sdk/client-s3");
|
|
998
1003
|
const { getAwsClients: getAwsClients2 } = await Promise.resolve().then(() => (init_aws_clients(), aws_clients_exports));
|
|
999
1004
|
const awsClients = getAwsClients2();
|
|
1000
|
-
const identity = await awsClients.sts.send(new
|
|
1005
|
+
const identity = await awsClients.sts.send(new GetCallerIdentityCommand9({}));
|
|
1001
1006
|
const accountId = identity.Account;
|
|
1002
1007
|
const newName = getDefaultStateBucketName(accountId);
|
|
1003
1008
|
const legacyName = getLegacyStateBucketName(accountId, region);
|
|
1004
|
-
const probe = new
|
|
1009
|
+
const probe = new S3Client11({ region: "us-east-1" });
|
|
1005
1010
|
try {
|
|
1006
1011
|
if (await bucketExists(probe, newName)) {
|
|
1007
|
-
logger.
|
|
1008
|
-
return newName;
|
|
1012
|
+
logger.debug(`State bucket: ${newName}`);
|
|
1013
|
+
return { bucket: newName, source: "default" };
|
|
1009
1014
|
}
|
|
1010
1015
|
if (await bucketExists(probe, legacyName)) {
|
|
1011
1016
|
logger.warn(
|
|
1012
|
-
`Using legacy state bucket name '${legacyName}'. The default has changed to '${newName}'.
|
|
1017
|
+
`Using legacy state bucket name '${legacyName}'. The default has changed to '${newName}'. To migrate, run:
|
|
1018
|
+
|
|
1019
|
+
cdkd state migrate-bucket --region ${region}
|
|
1020
|
+
|
|
1021
|
+
(add --remove-legacy to delete the legacy bucket after a successful copy; legacy support will be dropped in a future release.)`
|
|
1013
1022
|
);
|
|
1014
|
-
return legacyName;
|
|
1023
|
+
return { bucket: legacyName, source: "default-legacy" };
|
|
1015
1024
|
}
|
|
1016
1025
|
throw new Error(
|
|
1017
1026
|
`No cdkd state bucket found for account ${accountId}. Looked for '${newName}' (current default) and '${legacyName}' (legacy default). Run 'cdkd bootstrap' to create '${newName}'.`
|
|
@@ -1021,9 +1030,9 @@ async function resolveStateBucketWithDefault(cliBucket, region) {
|
|
|
1021
1030
|
}
|
|
1022
1031
|
}
|
|
1023
1032
|
async function bucketExists(client, bucketName) {
|
|
1024
|
-
const { HeadBucketCommand:
|
|
1033
|
+
const { HeadBucketCommand: HeadBucketCommand4 } = await import("@aws-sdk/client-s3");
|
|
1025
1034
|
try {
|
|
1026
|
-
await client.send(new
|
|
1035
|
+
await client.send(new HeadBucketCommand4({ Bucket: bucketName }));
|
|
1027
1036
|
return true;
|
|
1028
1037
|
} catch (error) {
|
|
1029
1038
|
const err = error;
|
|
@@ -1067,10 +1076,10 @@ async function bootstrapCommand(options) {
|
|
|
1067
1076
|
logger.info(`Using default state bucket: ${bucketName}`);
|
|
1068
1077
|
}
|
|
1069
1078
|
try {
|
|
1070
|
-
let
|
|
1079
|
+
let bucketExists3 = false;
|
|
1071
1080
|
try {
|
|
1072
1081
|
await s3Client.send(new HeadBucketCommand({ Bucket: bucketName }));
|
|
1073
|
-
|
|
1082
|
+
bucketExists3 = true;
|
|
1074
1083
|
logger.info(`Bucket ${bucketName} already exists`);
|
|
1075
1084
|
} catch (error) {
|
|
1076
1085
|
const err = error;
|
|
@@ -1080,7 +1089,7 @@ async function bootstrapCommand(options) {
|
|
|
1080
1089
|
throw normalizeAwsError(error, { bucket: bucketName, operation: "HeadBucket" });
|
|
1081
1090
|
}
|
|
1082
1091
|
}
|
|
1083
|
-
if (
|
|
1092
|
+
if (bucketExists3) {
|
|
1084
1093
|
if (!options.force) {
|
|
1085
1094
|
logger.warn(
|
|
1086
1095
|
`Bucket ${bucketName} already exists. Use --force to reconfigure (this will not delete existing state)`
|
|
@@ -3234,9 +3243,9 @@ var AssetPublisher = class {
|
|
|
3234
3243
|
const region = options.region || process.env["AWS_REGION"] || "us-east-1";
|
|
3235
3244
|
let accountId = options.accountId;
|
|
3236
3245
|
if (!accountId) {
|
|
3237
|
-
const { STSClient: STSClient7, GetCallerIdentityCommand:
|
|
3246
|
+
const { STSClient: STSClient7, GetCallerIdentityCommand: GetCallerIdentityCommand9 } = await import("@aws-sdk/client-sts");
|
|
3238
3247
|
const stsClient = new STSClient7({ region });
|
|
3239
|
-
const identity = await stsClient.send(new
|
|
3248
|
+
const identity = await stsClient.send(new GetCallerIdentityCommand9({}));
|
|
3240
3249
|
accountId = identity.Account;
|
|
3241
3250
|
stsClient.destroy();
|
|
3242
3251
|
}
|
|
@@ -28711,11 +28720,11 @@ async function deployCommand(stacks, options) {
|
|
|
28711
28720
|
addDependencies(stack.stackName);
|
|
28712
28721
|
}
|
|
28713
28722
|
}
|
|
28714
|
-
const { STSClient: STSClient7, GetCallerIdentityCommand:
|
|
28723
|
+
const { STSClient: STSClient7, GetCallerIdentityCommand: GetCallerIdentityCommand9 } = await import("@aws-sdk/client-sts");
|
|
28715
28724
|
const stsClient = new STSClient7({
|
|
28716
28725
|
region: options.region || process.env["AWS_REGION"] || "us-east-1"
|
|
28717
28726
|
});
|
|
28718
|
-
const callerIdentity = await stsClient.send(new
|
|
28727
|
+
const callerIdentity = await stsClient.send(new GetCallerIdentityCommand9({}));
|
|
28719
28728
|
const accountId = callerIdentity.Account;
|
|
28720
28729
|
stsClient.destroy();
|
|
28721
28730
|
const assetPublisher = new AssetPublisher();
|
|
@@ -29546,9 +29555,321 @@ function createForceUnlockCommand() {
|
|
|
29546
29555
|
}
|
|
29547
29556
|
|
|
29548
29557
|
// src/cli/commands/state.ts
|
|
29558
|
+
import * as readline3 from "node:readline/promises";
|
|
29559
|
+
import { Command as Command10, Option as Option5 } from "commander";
|
|
29560
|
+
import {
|
|
29561
|
+
GetBucketLocationCommand as GetBucketLocationCommand2,
|
|
29562
|
+
GetObjectCommand as GetObjectCommand4,
|
|
29563
|
+
ListObjectsV2Command as ListObjectsV2Command4
|
|
29564
|
+
} from "@aws-sdk/client-s3";
|
|
29565
|
+
init_aws_clients();
|
|
29566
|
+
|
|
29567
|
+
// src/cli/commands/state-migrate-bucket.ts
|
|
29549
29568
|
import * as readline2 from "node:readline/promises";
|
|
29550
|
-
import { Command as Command9
|
|
29569
|
+
import { Command as Command9 } from "commander";
|
|
29570
|
+
import {
|
|
29571
|
+
CopyObjectCommand,
|
|
29572
|
+
CreateBucketCommand as CreateBucketCommand4,
|
|
29573
|
+
DeleteBucketCommand as DeleteBucketCommand3,
|
|
29574
|
+
DeleteObjectsCommand as DeleteObjectsCommand3,
|
|
29575
|
+
HeadBucketCommand as HeadBucketCommand3,
|
|
29576
|
+
ListObjectVersionsCommand as ListObjectVersionsCommand2,
|
|
29577
|
+
ListObjectsV2Command as ListObjectsV2Command3,
|
|
29578
|
+
PutBucketEncryptionCommand as PutBucketEncryptionCommand3,
|
|
29579
|
+
PutBucketPolicyCommand as PutBucketPolicyCommand3,
|
|
29580
|
+
PutBucketVersioningCommand as PutBucketVersioningCommand3,
|
|
29581
|
+
S3Client as S3Client10
|
|
29582
|
+
} from "@aws-sdk/client-s3";
|
|
29583
|
+
import { GetCallerIdentityCommand as GetCallerIdentityCommand8 } from "@aws-sdk/client-sts";
|
|
29551
29584
|
init_aws_clients();
|
|
29585
|
+
async function stateMigrateBucketCommand(options) {
|
|
29586
|
+
const logger = getLogger();
|
|
29587
|
+
if (options.verbose)
|
|
29588
|
+
logger.setLevel("debug");
|
|
29589
|
+
const region = options.region || process.env["AWS_REGION"] || "us-east-1";
|
|
29590
|
+
const awsClients = new AwsClients({
|
|
29591
|
+
region,
|
|
29592
|
+
...options.profile && { profile: options.profile }
|
|
29593
|
+
});
|
|
29594
|
+
setAwsClients(awsClients);
|
|
29595
|
+
try {
|
|
29596
|
+
const identity = await awsClients.sts.send(new GetCallerIdentityCommand8({}));
|
|
29597
|
+
const accountId = identity.Account;
|
|
29598
|
+
if (!accountId) {
|
|
29599
|
+
throw new Error("STS GetCallerIdentity returned no Account id.");
|
|
29600
|
+
}
|
|
29601
|
+
const legacyBucket = options.legacyBucket ?? getLegacyStateBucketName(accountId, region);
|
|
29602
|
+
const newBucket = options.newBucket ?? getDefaultStateBucketName(accountId);
|
|
29603
|
+
if (legacyBucket === newBucket) {
|
|
29604
|
+
logger.warn(
|
|
29605
|
+
`Source and destination resolve to the same bucket (${legacyBucket}); nothing to do.`
|
|
29606
|
+
);
|
|
29607
|
+
return;
|
|
29608
|
+
}
|
|
29609
|
+
logger.info("Migrating state bucket:");
|
|
29610
|
+
logger.info(` source: ${legacyBucket} (resolved for --region ${region})`);
|
|
29611
|
+
logger.info(` destination: ${newBucket}`);
|
|
29612
|
+
const probeRegion = "us-east-1";
|
|
29613
|
+
const probe = new S3Client10({ region: probeRegion });
|
|
29614
|
+
let sourceExists;
|
|
29615
|
+
try {
|
|
29616
|
+
sourceExists = await bucketExists2(probe, legacyBucket);
|
|
29617
|
+
} finally {
|
|
29618
|
+
probe.destroy();
|
|
29619
|
+
}
|
|
29620
|
+
if (!sourceExists) {
|
|
29621
|
+
throw new Error(
|
|
29622
|
+
`Source bucket '${legacyBucket}' does not exist. Nothing to migrate. (Tip: run \`cdkd state info\` to confirm which bucket cdkd is reading from.)`
|
|
29623
|
+
);
|
|
29624
|
+
}
|
|
29625
|
+
const legacyRegion = await resolveBucketRegion(legacyBucket);
|
|
29626
|
+
logger.info(` source bucket actual region: ${legacyRegion}`);
|
|
29627
|
+
const legacyS3 = new S3Client10({ region: legacyRegion });
|
|
29628
|
+
try {
|
|
29629
|
+
await assertNoActiveLocks(legacyS3, legacyBucket);
|
|
29630
|
+
const sourceObjects = await listAllObjects(legacyS3, legacyBucket);
|
|
29631
|
+
logger.info(` source object count: ${sourceObjects.length}`);
|
|
29632
|
+
if (sourceObjects.length === 0) {
|
|
29633
|
+
logger.info("Source bucket is empty \u2014 no objects to copy.");
|
|
29634
|
+
}
|
|
29635
|
+
if (!options.yes) {
|
|
29636
|
+
const action = options.removeLegacy ? "and DELETE the source bucket" : "(source bucket will be kept)";
|
|
29637
|
+
const ok = await confirmPrompt(
|
|
29638
|
+
`Copy ${sourceObjects.length} object(s) from ${legacyBucket} -> ${newBucket} ${action}?`
|
|
29639
|
+
);
|
|
29640
|
+
if (!ok) {
|
|
29641
|
+
logger.info("Migration cancelled.");
|
|
29642
|
+
return;
|
|
29643
|
+
}
|
|
29644
|
+
}
|
|
29645
|
+
if (options.dryRun) {
|
|
29646
|
+
logger.info("--dry-run: no changes will be made. Stopping here.");
|
|
29647
|
+
return;
|
|
29648
|
+
}
|
|
29649
|
+
const newS3 = await ensureDestinationBucket(newBucket, legacyRegion, accountId, logger);
|
|
29650
|
+
try {
|
|
29651
|
+
let copied = 0;
|
|
29652
|
+
for (const obj of sourceObjects) {
|
|
29653
|
+
if (!obj.Key)
|
|
29654
|
+
continue;
|
|
29655
|
+
await newS3.send(
|
|
29656
|
+
new CopyObjectCommand({
|
|
29657
|
+
Bucket: newBucket,
|
|
29658
|
+
Key: obj.Key,
|
|
29659
|
+
// CopySource needs encoding for slashes inside the key path.
|
|
29660
|
+
CopySource: encodeURIComponent(`${legacyBucket}/${obj.Key}`)
|
|
29661
|
+
})
|
|
29662
|
+
);
|
|
29663
|
+
copied++;
|
|
29664
|
+
logger.debug(` copied ${obj.Key}`);
|
|
29665
|
+
}
|
|
29666
|
+
logger.info(`\u2713 Copied ${copied} object(s) to ${newBucket}`);
|
|
29667
|
+
const destObjects = await listAllObjects(newS3, newBucket);
|
|
29668
|
+
if (destObjects.length < sourceObjects.length) {
|
|
29669
|
+
throw new Error(
|
|
29670
|
+
`Migration verification failed: source has ${sourceObjects.length} object(s), destination has ${destObjects.length}. Aborting before any source-bucket cleanup.`
|
|
29671
|
+
);
|
|
29672
|
+
}
|
|
29673
|
+
logger.info("\u2713 Object count verified at destination");
|
|
29674
|
+
if (options.removeLegacy) {
|
|
29675
|
+
logger.info(`Emptying source bucket ${legacyBucket} (all versions + delete markers)...`);
|
|
29676
|
+
await emptyBucketAllVersions(legacyS3, legacyBucket);
|
|
29677
|
+
logger.info(`Deleting source bucket ${legacyBucket}...`);
|
|
29678
|
+
await legacyS3.send(new DeleteBucketCommand3({ Bucket: legacyBucket }));
|
|
29679
|
+
logger.info(`\u2713 Deleted source bucket: ${legacyBucket}`);
|
|
29680
|
+
} else {
|
|
29681
|
+
logger.info(
|
|
29682
|
+
`Source bucket ${legacyBucket} kept. Pass --remove-legacy on a future run to delete it.`
|
|
29683
|
+
);
|
|
29684
|
+
}
|
|
29685
|
+
logger.info(`\u2713 Migration complete: ${legacyBucket} -> ${newBucket}`);
|
|
29686
|
+
} finally {
|
|
29687
|
+
newS3.destroy();
|
|
29688
|
+
}
|
|
29689
|
+
} finally {
|
|
29690
|
+
legacyS3.destroy();
|
|
29691
|
+
}
|
|
29692
|
+
} finally {
|
|
29693
|
+
awsClients.destroy();
|
|
29694
|
+
}
|
|
29695
|
+
}
|
|
29696
|
+
async function bucketExists2(s3, bucketName) {
|
|
29697
|
+
try {
|
|
29698
|
+
await s3.send(new HeadBucketCommand3({ Bucket: bucketName }));
|
|
29699
|
+
return true;
|
|
29700
|
+
} catch (error) {
|
|
29701
|
+
const err = error;
|
|
29702
|
+
const status = err.$metadata?.httpStatusCode;
|
|
29703
|
+
if (err.name === "NotFound" || err.name === "NoSuchBucket" || status === 404) {
|
|
29704
|
+
return false;
|
|
29705
|
+
}
|
|
29706
|
+
if (status === 301 || status === 403)
|
|
29707
|
+
return true;
|
|
29708
|
+
throw error;
|
|
29709
|
+
}
|
|
29710
|
+
}
|
|
29711
|
+
async function listAllObjects(s3, bucket) {
|
|
29712
|
+
const all = [];
|
|
29713
|
+
let continuationToken;
|
|
29714
|
+
do {
|
|
29715
|
+
const resp = await s3.send(
|
|
29716
|
+
new ListObjectsV2Command3({
|
|
29717
|
+
Bucket: bucket,
|
|
29718
|
+
...continuationToken && { ContinuationToken: continuationToken }
|
|
29719
|
+
})
|
|
29720
|
+
);
|
|
29721
|
+
if (resp.Contents)
|
|
29722
|
+
all.push(...resp.Contents);
|
|
29723
|
+
continuationToken = resp.NextContinuationToken;
|
|
29724
|
+
} while (continuationToken);
|
|
29725
|
+
return all;
|
|
29726
|
+
}
|
|
29727
|
+
async function assertNoActiveLocks(s3, bucket) {
|
|
29728
|
+
const all = await listAllObjects(s3, bucket);
|
|
29729
|
+
const locks = all.map((o) => o.Key).filter((k) => typeof k === "string" && k.endsWith("/lock.json"));
|
|
29730
|
+
if (locks.length > 0) {
|
|
29731
|
+
const sample = locks.slice(0, 3).join(", ");
|
|
29732
|
+
const more = locks.length > 3 ? ` (+${locks.length - 3} more)` : "";
|
|
29733
|
+
throw new Error(
|
|
29734
|
+
`Refusing to migrate: ${locks.length} active lock file(s) found in '${bucket}': ${sample}${more}. Wait for in-flight cdkd operations to complete, or run 'cdkd force-unlock <stack>' if a lock is stale.`
|
|
29735
|
+
);
|
|
29736
|
+
}
|
|
29737
|
+
}
|
|
29738
|
+
async function ensureDestinationBucket(bucketName, region, accountId, logger) {
|
|
29739
|
+
const probe = new S3Client10({ region });
|
|
29740
|
+
let exists;
|
|
29741
|
+
try {
|
|
29742
|
+
exists = await bucketExists2(probe, bucketName);
|
|
29743
|
+
} finally {
|
|
29744
|
+
probe.destroy();
|
|
29745
|
+
}
|
|
29746
|
+
if (exists) {
|
|
29747
|
+
logger.info(`Destination bucket ${bucketName} already exists; reusing it.`);
|
|
29748
|
+
const actual = await resolveBucketRegion(bucketName);
|
|
29749
|
+
if (actual !== region) {
|
|
29750
|
+
logger.warn(
|
|
29751
|
+
`Destination bucket lives in ${actual}, but source is in ${region}. Cross-region copy is supported but slower; objects will be replicated to ${actual}.`
|
|
29752
|
+
);
|
|
29753
|
+
}
|
|
29754
|
+
return new S3Client10({ region: actual });
|
|
29755
|
+
}
|
|
29756
|
+
logger.info(`Creating destination bucket ${bucketName} in ${region}...`);
|
|
29757
|
+
const s3 = new S3Client10({ region });
|
|
29758
|
+
const createParams = { Bucket: bucketName };
|
|
29759
|
+
if (region !== "us-east-1") {
|
|
29760
|
+
createParams.CreateBucketConfiguration = {
|
|
29761
|
+
LocationConstraint: region
|
|
29762
|
+
};
|
|
29763
|
+
}
|
|
29764
|
+
await s3.send(new CreateBucketCommand4(createParams));
|
|
29765
|
+
logger.info(`\u2713 Created destination bucket: ${bucketName}`);
|
|
29766
|
+
await s3.send(
|
|
29767
|
+
new PutBucketVersioningCommand3({
|
|
29768
|
+
Bucket: bucketName,
|
|
29769
|
+
VersioningConfiguration: { Status: "Enabled" }
|
|
29770
|
+
})
|
|
29771
|
+
);
|
|
29772
|
+
await s3.send(
|
|
29773
|
+
new PutBucketEncryptionCommand3({
|
|
29774
|
+
Bucket: bucketName,
|
|
29775
|
+
ServerSideEncryptionConfiguration: {
|
|
29776
|
+
Rules: [
|
|
29777
|
+
{
|
|
29778
|
+
ApplyServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" },
|
|
29779
|
+
BucketKeyEnabled: true
|
|
29780
|
+
}
|
|
29781
|
+
]
|
|
29782
|
+
}
|
|
29783
|
+
})
|
|
29784
|
+
);
|
|
29785
|
+
await s3.send(
|
|
29786
|
+
new PutBucketPolicyCommand3({
|
|
29787
|
+
Bucket: bucketName,
|
|
29788
|
+
Policy: JSON.stringify({
|
|
29789
|
+
Version: "2012-10-17",
|
|
29790
|
+
Statement: [
|
|
29791
|
+
{
|
|
29792
|
+
Sid: "DenyExternalAccess",
|
|
29793
|
+
Effect: "Deny",
|
|
29794
|
+
Principal: "*",
|
|
29795
|
+
Action: "s3:*",
|
|
29796
|
+
Resource: [`arn:aws:s3:::${bucketName}`, `arn:aws:s3:::${bucketName}/*`],
|
|
29797
|
+
Condition: { StringNotEquals: { "aws:PrincipalAccount": accountId } }
|
|
29798
|
+
}
|
|
29799
|
+
]
|
|
29800
|
+
})
|
|
29801
|
+
})
|
|
29802
|
+
);
|
|
29803
|
+
logger.info("\u2713 Applied versioning, encryption, and account-only access policy");
|
|
29804
|
+
return s3;
|
|
29805
|
+
}
|
|
29806
|
+
async function emptyBucketAllVersions(s3, bucket) {
|
|
29807
|
+
let keyMarker;
|
|
29808
|
+
let versionIdMarker;
|
|
29809
|
+
do {
|
|
29810
|
+
const resp = await s3.send(
|
|
29811
|
+
new ListObjectVersionsCommand2({
|
|
29812
|
+
Bucket: bucket,
|
|
29813
|
+
...keyMarker && { KeyMarker: keyMarker },
|
|
29814
|
+
...versionIdMarker && { VersionIdMarker: versionIdMarker }
|
|
29815
|
+
})
|
|
29816
|
+
);
|
|
29817
|
+
const ids = [];
|
|
29818
|
+
for (const v of resp.Versions ?? []) {
|
|
29819
|
+
if (v.Key && v.VersionId)
|
|
29820
|
+
ids.push({ Key: v.Key, VersionId: v.VersionId });
|
|
29821
|
+
}
|
|
29822
|
+
for (const dm of resp.DeleteMarkers ?? []) {
|
|
29823
|
+
if (dm.Key && dm.VersionId)
|
|
29824
|
+
ids.push({ Key: dm.Key, VersionId: dm.VersionId });
|
|
29825
|
+
}
|
|
29826
|
+
for (let i = 0; i < ids.length; i += 1e3) {
|
|
29827
|
+
const batch = ids.slice(i, i + 1e3);
|
|
29828
|
+
await s3.send(
|
|
29829
|
+
new DeleteObjectsCommand3({
|
|
29830
|
+
Bucket: bucket,
|
|
29831
|
+
Delete: {
|
|
29832
|
+
Objects: batch,
|
|
29833
|
+
Quiet: true
|
|
29834
|
+
}
|
|
29835
|
+
})
|
|
29836
|
+
);
|
|
29837
|
+
}
|
|
29838
|
+
keyMarker = resp.NextKeyMarker;
|
|
29839
|
+
versionIdMarker = resp.NextVersionIdMarker;
|
|
29840
|
+
} while (keyMarker || versionIdMarker);
|
|
29841
|
+
}
|
|
29842
|
+
async function confirmPrompt(prompt) {
|
|
29843
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
29844
|
+
try {
|
|
29845
|
+
const ans = await rl.question(`${prompt} [y/N] `);
|
|
29846
|
+
return /^y(es)?$/i.test(ans.trim());
|
|
29847
|
+
} finally {
|
|
29848
|
+
rl.close();
|
|
29849
|
+
}
|
|
29850
|
+
}
|
|
29851
|
+
function createStateMigrateBucketCommand() {
|
|
29852
|
+
const cmd = new Command9("migrate-bucket").description(
|
|
29853
|
+
"Migrate state from the legacy region-suffixed bucket (cdkd-state-{account}-{region}) to the new region-free default (cdkd-state-{account}). Source bucket is kept by default; pass --remove-legacy to delete it after a successful migration."
|
|
29854
|
+
).option(
|
|
29855
|
+
"--region <region>",
|
|
29856
|
+
"Region of the legacy bucket to migrate. Defaults to AWS_REGION or us-east-1. Run once per region for multi-region setups."
|
|
29857
|
+
).option(
|
|
29858
|
+
"--legacy-bucket <name>",
|
|
29859
|
+
"Override the legacy (source) bucket name (default: derived from STS account + --region)."
|
|
29860
|
+
).option(
|
|
29861
|
+
"--new-bucket <name>",
|
|
29862
|
+
"Override the new (destination) bucket name (default: cdkd-state-{accountId})."
|
|
29863
|
+
).option("--dry-run", "Show planned actions without making changes", false).option(
|
|
29864
|
+
"--remove-legacy",
|
|
29865
|
+
"Delete the source bucket after successful migration. Default: keep it.",
|
|
29866
|
+
false
|
|
29867
|
+
).action(withErrorHandling(stateMigrateBucketCommand));
|
|
29868
|
+
commonOptions.forEach((o) => cmd.addOption(o));
|
|
29869
|
+
return cmd;
|
|
29870
|
+
}
|
|
29871
|
+
|
|
29872
|
+
// src/cli/commands/state.ts
|
|
29552
29873
|
function formatStackRef(ref) {
|
|
29553
29874
|
return ref.region ? `${ref.stackName} (${ref.region})` : ref.stackName;
|
|
29554
29875
|
}
|
|
@@ -29686,7 +30007,7 @@ async function stateListCommand(options) {
|
|
|
29686
30007
|
}
|
|
29687
30008
|
}
|
|
29688
30009
|
function createStateListCommand() {
|
|
29689
|
-
const cmd = new
|
|
30010
|
+
const cmd = new Command10("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));
|
|
29690
30011
|
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
29691
30012
|
cmd.addOption(deprecatedRegionOption);
|
|
29692
30013
|
return cmd;
|
|
@@ -29790,7 +30111,7 @@ function formatLockSummary(lockInfo) {
|
|
|
29790
30111
|
return `locked by ${lockInfo.owner}${opStr}, ${expiresStr}`;
|
|
29791
30112
|
}
|
|
29792
30113
|
function createStateResourcesCommand() {
|
|
29793
|
-
const cmd = new
|
|
30114
|
+
const cmd = new Command10("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));
|
|
29794
30115
|
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
29795
30116
|
cmd.addOption(deprecatedRegionOption);
|
|
29796
30117
|
return cmd;
|
|
@@ -29878,7 +30199,7 @@ async function stateShowCommand(stackName, options) {
|
|
|
29878
30199
|
}
|
|
29879
30200
|
}
|
|
29880
30201
|
function createStateShowCommand() {
|
|
29881
|
-
const cmd = new
|
|
30202
|
+
const cmd = new Command10("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));
|
|
29882
30203
|
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
29883
30204
|
cmd.addOption(deprecatedRegionOption);
|
|
29884
30205
|
return cmd;
|
|
@@ -29926,7 +30247,7 @@ Use 'cdkd destroy ${stackName}' if you want to delete the actual resources.
|
|
|
29926
30247
|
|
|
29927
30248
|
`
|
|
29928
30249
|
);
|
|
29929
|
-
const rl =
|
|
30250
|
+
const rl = readline3.createInterface({
|
|
29930
30251
|
input: process.stdin,
|
|
29931
30252
|
output: process.stdout
|
|
29932
30253
|
});
|
|
@@ -29961,7 +30282,7 @@ function stackRegionOption() {
|
|
|
29961
30282
|
);
|
|
29962
30283
|
}
|
|
29963
30284
|
function createStateRmCommand() {
|
|
29964
|
-
const cmd = new
|
|
30285
|
+
const cmd = new Command10("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));
|
|
29965
30286
|
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
29966
30287
|
cmd.addOption(deprecatedRegionOption);
|
|
29967
30288
|
return cmd;
|
|
@@ -30011,7 +30332,7 @@ WARNING: This destroys ${stackNames.length} stack(s) and removes their state rec
|
|
|
30011
30332
|
`);
|
|
30012
30333
|
}
|
|
30013
30334
|
process.stdout.write("\n");
|
|
30014
|
-
const rl =
|
|
30335
|
+
const rl = readline3.createInterface({
|
|
30015
30336
|
input: process.stdin,
|
|
30016
30337
|
output: process.stdout
|
|
30017
30338
|
});
|
|
@@ -30086,7 +30407,7 @@ Preparing to destroy stack: ${stackName}${ref.region ? ` (${ref.region})` : ""}`
|
|
|
30086
30407
|
}
|
|
30087
30408
|
}
|
|
30088
30409
|
function createStateDestroyCommand() {
|
|
30089
|
-
const cmd = new
|
|
30410
|
+
const cmd = new Command10("destroy").description(
|
|
30090
30411
|
"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
30412
|
).argument("[stacks...]", "Stack name(s) to destroy (physical CloudFormation names)").option("--all", "Destroy every stack in the state bucket", false).addOption(stackRegionOption()).addHelpText(
|
|
30092
30413
|
"after",
|
|
@@ -30106,13 +30427,133 @@ function createStateDestroyCommand() {
|
|
|
30106
30427
|
cmd.addOption(deprecatedRegionOption);
|
|
30107
30428
|
return cmd;
|
|
30108
30429
|
}
|
|
30430
|
+
function formatBucketSource(source) {
|
|
30431
|
+
switch (source) {
|
|
30432
|
+
case "cli-flag":
|
|
30433
|
+
return "--state-bucket flag";
|
|
30434
|
+
case "env":
|
|
30435
|
+
return "CDKD_STATE_BUCKET env";
|
|
30436
|
+
case "cdk.json":
|
|
30437
|
+
return "cdk.json (context.cdkd.stateBucket)";
|
|
30438
|
+
case "default":
|
|
30439
|
+
return "default (account ID from STS)";
|
|
30440
|
+
case "default-legacy":
|
|
30441
|
+
return "default (legacy region-suffixed name; cdkd state migrate-bucket recommended)";
|
|
30442
|
+
}
|
|
30443
|
+
}
|
|
30444
|
+
async function detectBucketRegion(awsClients, bucket) {
|
|
30445
|
+
try {
|
|
30446
|
+
const resp = await awsClients.s3.send(new GetBucketLocationCommand2({ Bucket: bucket }));
|
|
30447
|
+
const constraint = resp.LocationConstraint;
|
|
30448
|
+
if (!constraint)
|
|
30449
|
+
return "us-east-1";
|
|
30450
|
+
if (constraint === "EU")
|
|
30451
|
+
return "eu-west-1";
|
|
30452
|
+
return constraint;
|
|
30453
|
+
} catch {
|
|
30454
|
+
return void 0;
|
|
30455
|
+
}
|
|
30456
|
+
}
|
|
30457
|
+
async function listStateFileKeys(awsClients, bucket, prefix) {
|
|
30458
|
+
const keys = [];
|
|
30459
|
+
let continuationToken;
|
|
30460
|
+
const searchPrefix = `${prefix}/`;
|
|
30461
|
+
do {
|
|
30462
|
+
const resp = await awsClients.s3.send(
|
|
30463
|
+
new ListObjectsV2Command4({
|
|
30464
|
+
Bucket: bucket,
|
|
30465
|
+
Prefix: searchPrefix,
|
|
30466
|
+
...continuationToken && { ContinuationToken: continuationToken }
|
|
30467
|
+
})
|
|
30468
|
+
);
|
|
30469
|
+
for (const obj of resp.Contents ?? []) {
|
|
30470
|
+
const key = obj.Key;
|
|
30471
|
+
if (typeof key === "string" && key.endsWith("/state.json")) {
|
|
30472
|
+
keys.push(key);
|
|
30473
|
+
}
|
|
30474
|
+
}
|
|
30475
|
+
continuationToken = resp.NextContinuationToken;
|
|
30476
|
+
} while (continuationToken);
|
|
30477
|
+
return keys;
|
|
30478
|
+
}
|
|
30479
|
+
async function readSchemaVersion(awsClients, bucket, keys) {
|
|
30480
|
+
if (keys.length === 0)
|
|
30481
|
+
return "unknown";
|
|
30482
|
+
try {
|
|
30483
|
+
const resp = await awsClients.s3.send(new GetObjectCommand4({ Bucket: bucket, Key: keys[0] }));
|
|
30484
|
+
if (!resp.Body)
|
|
30485
|
+
return "unknown";
|
|
30486
|
+
const body = await resp.Body.transformToString();
|
|
30487
|
+
const parsed = JSON.parse(body);
|
|
30488
|
+
return typeof parsed.version === "number" ? parsed.version : "unknown";
|
|
30489
|
+
} catch {
|
|
30490
|
+
return "unknown";
|
|
30491
|
+
}
|
|
30492
|
+
}
|
|
30493
|
+
async function stateInfoCommand(options) {
|
|
30494
|
+
const logger = getLogger();
|
|
30495
|
+
if (options.verbose)
|
|
30496
|
+
logger.setLevel("debug");
|
|
30497
|
+
const awsClients = new AwsClients({
|
|
30498
|
+
...options.region && { region: options.region },
|
|
30499
|
+
...options.profile && { profile: options.profile }
|
|
30500
|
+
});
|
|
30501
|
+
setAwsClients(awsClients);
|
|
30502
|
+
try {
|
|
30503
|
+
const region = options.region || process.env["AWS_REGION"] || "us-east-1";
|
|
30504
|
+
const resolved = await resolveStateBucketWithDefaultAndSource(options.stateBucket, region);
|
|
30505
|
+
const bucket = resolved.bucket;
|
|
30506
|
+
const prefix = options.statePrefix;
|
|
30507
|
+
const stateBackend = new S3StateBackend(awsClients.s3, { bucket, prefix });
|
|
30508
|
+
await stateBackend.verifyBucketExists();
|
|
30509
|
+
const detectedRegion = await detectBucketRegion(awsClients, bucket);
|
|
30510
|
+
const stateFileKeys = await listStateFileKeys(awsClients, bucket, prefix);
|
|
30511
|
+
const schemaVersion = await readSchemaVersion(awsClients, bucket, stateFileKeys);
|
|
30512
|
+
if (options.json) {
|
|
30513
|
+
const json = {
|
|
30514
|
+
bucket,
|
|
30515
|
+
region: detectedRegion ?? null,
|
|
30516
|
+
regionSource: detectedRegion ? "auto-detected" : "unknown",
|
|
30517
|
+
bucketSource: resolved.source,
|
|
30518
|
+
schemaVersion,
|
|
30519
|
+
stackCount: stateFileKeys.length
|
|
30520
|
+
};
|
|
30521
|
+
process.stdout.write(`${JSON.stringify(json, null, 2)}
|
|
30522
|
+
`);
|
|
30523
|
+
return;
|
|
30524
|
+
}
|
|
30525
|
+
const lines = [];
|
|
30526
|
+
lines.push(`State bucket: ${bucket}`);
|
|
30527
|
+
if (detectedRegion) {
|
|
30528
|
+
lines.push(`Region: ${detectedRegion} (auto-detected via GetBucketLocation)`);
|
|
30529
|
+
} else {
|
|
30530
|
+
lines.push("Region: unknown (GetBucketLocation failed or denied)");
|
|
30531
|
+
}
|
|
30532
|
+
lines.push(`Source: ${formatBucketSource(resolved.source)}`);
|
|
30533
|
+
lines.push(`Schema version: ${schemaVersion}`);
|
|
30534
|
+
lines.push(`Stacks: ${stateFileKeys.length}`);
|
|
30535
|
+
process.stdout.write(`${lines.join("\n")}
|
|
30536
|
+
`);
|
|
30537
|
+
} finally {
|
|
30538
|
+
awsClients.destroy();
|
|
30539
|
+
}
|
|
30540
|
+
}
|
|
30541
|
+
function createStateInfoCommand() {
|
|
30542
|
+
const cmd = new Command10("info").description(
|
|
30543
|
+
"Show cdkd state bucket info (bucket name, region, source, schema version, stack count)"
|
|
30544
|
+
).option("--json", "Output as JSON", false).action(withErrorHandling(stateInfoCommand));
|
|
30545
|
+
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
30546
|
+
return cmd;
|
|
30547
|
+
}
|
|
30109
30548
|
function createStateCommand() {
|
|
30110
|
-
const cmd = new
|
|
30549
|
+
const cmd = new Command10("state").description("Manage cdkd state stored in S3");
|
|
30550
|
+
cmd.addCommand(createStateInfoCommand());
|
|
30111
30551
|
cmd.addCommand(createStateListCommand());
|
|
30112
30552
|
cmd.addCommand(createStateResourcesCommand());
|
|
30113
30553
|
cmd.addCommand(createStateShowCommand());
|
|
30114
30554
|
cmd.addCommand(createStateRmCommand());
|
|
30115
30555
|
cmd.addCommand(createStateDestroyCommand());
|
|
30556
|
+
cmd.addCommand(createStateMigrateBucketCommand());
|
|
30116
30557
|
return cmd;
|
|
30117
30558
|
}
|
|
30118
30559
|
|
|
@@ -30140,8 +30581,8 @@ function reorderArgs(argv) {
|
|
|
30140
30581
|
return [...prefix, ...cmdAndAfter, ...beforeCmd];
|
|
30141
30582
|
}
|
|
30142
30583
|
async function main() {
|
|
30143
|
-
const program = new
|
|
30144
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
30584
|
+
const program = new Command11();
|
|
30585
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.14.0");
|
|
30145
30586
|
program.addCommand(createBootstrapCommand());
|
|
30146
30587
|
program.addCommand(createSynthCommand());
|
|
30147
30588
|
program.addCommand(createListCommand());
|