@go-to-k/cdkd 0.235.1 → 0.236.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/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, f as withSkipPrefix, i as runStackBuffered, n as getLogger, o as PATTERN_B_NAME_OPTIONS, p as withStackName, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-BzO-joNR.js";
|
|
3
3
|
import { A as withErrorHandling, C as StackHasActiveImportsError, S as ResourceUpdateNotSupportedError, _ as MissingCdkCliError, a as normalizeAwsTagsToCfn, b as ProvisioningError, f as LocalInvokeBuildError$1, i as matchesCdkPath, k as normalizeAwsError, l as CdkdError, m as LocalStartServiceError, n as isTerminationProtectionPropagationError, o as resolveExplicitPhysicalId, p as LocalMigrateError, r as CDK_PATH_TAG, s as assertRegionMatch, t as disableInstanceApiTermination, v as NestedStackChildDirectDestroyError, w as StackTerminationProtectionError, x as ResourceTimeoutError, y as PartialFailureError } from "./ec2-termination-protection-q-WGWgxa.js";
|
|
4
4
|
import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-B15NAPbL.js";
|
|
5
|
-
import { $ as
|
|
5
|
+
import { $ as runDockerStreaming, A as DiffCalculator, B as buildAssetRedirectMap, C as findActionableSilentDrops, D as refStateLookupFromResource, E as cfnRefValueFromPhysicalId, F as rebuildClientForBucketRegion, G as BOOTSTRAP_MARKER_PREFIX, H as loadPublishableAssetManifest, I as shouldRetainResource, J as parseBootstrapMarker, K as ensureAssetStorage, L as AssetPublisher, M as TemplateParser, N as LockManager, O as WAFv2WebACLProvider, P as S3StateBackend, Q as runDockerForeground, R as stringifyValue, S as ProviderRegistry, T as IntrinsicFunctionResolver, U as rewriteTemplateAssetReferences, V as createAssetRedirectResolver, W as AssetModeResolver, X as formatDockerLoginError, Y as buildDockerImage, Z as getDockerCmd, _ as green, _t as findLargeInlineResources, a as withRetry, at as getLegacyStateBucketName, b as IAMRoleProvider, c as computeImplicitDeleteEdges, ct as resolveCaptureObservedState, d as isStatefulRecreateTargetSync, dt as resolveStateBucketWithDefaultAndSource, et as AssetManifestLoader, f as renderStatefulReason, ft as resolveUseCdkBootstrapAssets, g as gray, gt as MIGRATE_TMP_PREFIX, h as cyan, ht as CFN_TEMPLATE_URL_LIMIT, i as withResourceDeadline, it as getDefaultStateBucketName, j as DagBuilder, k as applyRoleArnIfSet, l as extractDeploymentEventError, lt as resolveSkipPrefix, m as bold, mt as CFN_TEMPLATE_BODY_LIMIT, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as Synthesizer, o as isRetryableTransientError, ot as resolveApp, p as formatResourceLine, pt as warnDeprecatedNoPrefixCliFlag, q as getBootstrapMarkerKey, r as DeployEngine, rt as synthesisStatusMessage, s as IMPLICIT_DELETE_DEPENDENCIES, st as resolveAutoAssetStorage, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as getDockerImageBySourceHash, u as MULTI_REGION_RECREATE_BLOCKED_TYPES, ut as resolveStateBucketWithDefault, v as red, vt as uploadCfnTemplate, w as CloudControlProvider, x as collectInlinePolicyNamesManagedBySiblings, xt as resolveBucketRegion, y as yellow, yt as AssemblyReader, z as WorkGraph } from "./deploy-engine-DqAIXGA_.js";
|
|
6
6
|
import { t as expectedOwnerParam } from "./expected-bucket-owner-BjEfUnkA.js";
|
|
7
7
|
import { t as ASGProvider } from "./asg-provider-Dbl1_mwT.js";
|
|
8
8
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
@@ -40,9 +40,9 @@ import graphlib from "graphlib";
|
|
|
40
40
|
import { AddTagsToResourceCommand as AddTagsToResourceCommand$1, CreateDBClusterCommand, CreateDBInstanceCommand, CreateDBProxyCommand, CreateDBProxyEndpointCommand, CreateDBSubnetGroupCommand, DBProxyEndpointNotFoundFault, DBProxyNotFoundFault, DBProxyTargetGroupNotFoundFault, DBProxyTargetNotFoundFault, DeleteDBClusterCommand, DeleteDBInstanceCommand, DeleteDBProxyCommand, DeleteDBProxyEndpointCommand, DeleteDBSubnetGroupCommand, DeregisterDBProxyTargetsCommand, DescribeDBClustersCommand, DescribeDBInstancesCommand, DescribeDBProxiesCommand, DescribeDBProxyEndpointsCommand, DescribeDBProxyTargetGroupsCommand, DescribeDBProxyTargetsCommand, DescribeDBSubnetGroupsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$8, ModifyDBClusterCommand, ModifyDBInstanceCommand, ModifyDBProxyCommand, ModifyDBProxyEndpointCommand, ModifyDBProxyTargetGroupCommand, ModifyDBSubnetGroupCommand, RDSClient, RegisterDBProxyTargetsCommand, RemoveTagsFromResourceCommand as RemoveTagsFromResourceCommand$1 } from "@aws-sdk/client-rds";
|
|
41
41
|
import { AddTagsToResourceCommand as AddTagsToResourceCommand$2, CreateCacheClusterCommand, CreateCacheSubnetGroupCommand, DeleteCacheClusterCommand, DeleteCacheSubnetGroupCommand, DescribeCacheClustersCommand, DescribeCacheSubnetGroupsCommand, ElastiCacheClient, ListTagsForResourceCommand as ListTagsForResourceCommand$9, ModifyCacheClusterCommand, ModifyCacheSubnetGroupCommand, RemoveTagsFromResourceCommand as RemoveTagsFromResourceCommand$2 } from "@aws-sdk/client-elasticache";
|
|
42
42
|
import { Command, Option } from "commander";
|
|
43
|
-
import { writeFileSync as writeFileSync$1 } from "fs";
|
|
44
43
|
import * as readline$1 from "node:readline/promises";
|
|
45
44
|
import readline from "node:readline/promises";
|
|
45
|
+
import { writeFileSync as writeFileSync$1 } from "fs";
|
|
46
46
|
import * as zlib from "node:zlib";
|
|
47
47
|
import { ApplicationAutoScalingClient, DeleteScalingPolicyCommand, DeregisterScalableTargetCommand, DescribeScalableTargetsCommand, DescribeScalingPoliciesCommand, PutScalingPolicyCommand, RegisterScalableTargetCommand } from "@aws-sdk/client-application-auto-scaling";
|
|
48
48
|
import { ApiGatewayV2Client, CreateApiCommand, CreateAuthorizerCommand as CreateAuthorizerCommand$1, CreateIntegrationCommand, CreateRouteCommand as CreateRouteCommand$1, CreateStageCommand as CreateStageCommand$1, DeleteApiCommand, DeleteAuthorizerCommand as DeleteAuthorizerCommand$1, DeleteIntegrationCommand, DeleteRouteCommand as DeleteRouteCommand$1, DeleteStageCommand as DeleteStageCommand$1, GetApiCommand, GetApisCommand, GetAuthorizerCommand as GetAuthorizerCommand$1, GetIntegrationCommand, GetRouteCommand, GetStageCommand as GetStageCommand$1, NotFoundException as NotFoundException$3, UpdateApiCommand, UpdateAuthorizerCommand as UpdateAuthorizerCommand$1, UpdateIntegrationCommand, UpdateRouteCommand, UpdateStageCommand as UpdateStageCommand$1 } from "@aws-sdk/client-apigatewayv2";
|
|
@@ -610,6 +610,310 @@ const destroyOptions = [
|
|
|
610
610
|
allowUnsupportedTypesOption
|
|
611
611
|
];
|
|
612
612
|
|
|
613
|
+
//#endregion
|
|
614
|
+
//#region src/cli/commands/bootstrap-destroy.ts
|
|
615
|
+
/**
|
|
616
|
+
* `cdkd bootstrap --destroy` — teardown of cdkd-created account resources
|
|
617
|
+
* (issue #1010, the reverse of `cdkd bootstrap` / `ensureAssetStorage`).
|
|
618
|
+
*
|
|
619
|
+
* Scope per invocation is ONE region's asset storage (`--region`, same
|
|
620
|
+
* option the create side uses): empty + delete the asset bucket, force-
|
|
621
|
+
* delete the container-asset ECR repo, then delete the region's bootstrap
|
|
622
|
+
* marker LAST — the mirror image of the create side's marker-written-last
|
|
623
|
+
* ordering, so a crash mid-teardown leaves the region still consistently
|
|
624
|
+
* opted in (marker present → deploys keep hard-erroring at the missing
|
|
625
|
+
* resources with a re-bootstrap hint, never a silent legacy fallback).
|
|
626
|
+
*
|
|
627
|
+
* The asset bucket / repo NAMES are read from the region's bootstrap
|
|
628
|
+
* marker, NOT recomputed from the naming convention — the marker is the
|
|
629
|
+
* source of truth for names (design §4.1), which keeps this teardown
|
|
630
|
+
* compatible with custom asset-storage names (issue #1011).
|
|
631
|
+
*
|
|
632
|
+
* The state bucket is NOT deleted by default: it is the account's source
|
|
633
|
+
* of truth. `--include-state-bucket` opts in, and even then the deletion
|
|
634
|
+
* is refused while ANY stack state exists in the bucket or any OTHER
|
|
635
|
+
* region is still opted in to asset storage (deleting their markers would
|
|
636
|
+
* silently flip those regions back to legacy mode).
|
|
637
|
+
*/
|
|
638
|
+
/** S3 key prefix for state files — same fixed value the create side uses. */
|
|
639
|
+
const STATE_PREFIX = "cdkd";
|
|
640
|
+
/**
|
|
641
|
+
* Every cdkd state file ends with this suffix, regardless of the
|
|
642
|
+
* `--state-prefix` it was deployed under (`{prefix}/{stack}/{region}/state.json`,
|
|
643
|
+
* or legacy `{prefix}/{stack}/state.json`).
|
|
644
|
+
*/
|
|
645
|
+
const STATE_FILE_SUFFIX = "/state.json";
|
|
646
|
+
/** `us-east-1` / `ap-northeast-1` / `us-gov-west-1` — a region-shaped segment. */
|
|
647
|
+
const REGION_SEGMENT = /^[a-z]{2}(-[a-z]+)+-\d+$/;
|
|
648
|
+
/**
|
|
649
|
+
* List every state file in the bucket — the WHOLE bucket, not just the
|
|
650
|
+
* default `cdkd/` prefix. Other commands accept `--state-prefix`, so live
|
|
651
|
+
* stack state may exist under ANY prefix in this bucket; scoping this
|
|
652
|
+
* listing to the default prefix would let the reference scan and the
|
|
653
|
+
* `--include-state-bucket` guard silently miss those stacks and delete
|
|
654
|
+
* live state.
|
|
655
|
+
*/
|
|
656
|
+
async function listAllStateKeys(stateBackend) {
|
|
657
|
+
return (await stateBackend.listRawKeys("")).filter((k) => k.endsWith(STATE_FILE_SUFFIX));
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Interactive confirmation for the teardown. Follows the repo's
|
|
661
|
+
* destructive-prompt convention (see `recreate-confirm-prompt.ts`, the
|
|
662
|
+
* same pattern family as `confirm-prompt.ts`): print the deletion plan as
|
|
663
|
+
* a WARN block, `--yes` skips the prompt, a non-TTY stdin without `--yes`
|
|
664
|
+
* is a hard error (never hang / never silently decline in CI), and the
|
|
665
|
+
* prompt itself defaults to NO because the side effect is destructive.
|
|
666
|
+
*/
|
|
667
|
+
async function promptBootstrapDestroyConfirm(input) {
|
|
668
|
+
const logger = getLogger();
|
|
669
|
+
logger.warn("");
|
|
670
|
+
logger.warn("cdkd bootstrap --destroy will delete the following:");
|
|
671
|
+
for (const line of input.planLines) logger.warn(` - ${line}`);
|
|
672
|
+
if (input.yes) return true;
|
|
673
|
+
if (process.stdin.isTTY !== true) throw new CdkdError("The bootstrap --destroy confirmation prompt cannot run in a non-interactive environment. Pass --yes / -y to confirm the teardown, or run the command from a real terminal.", "NON_INTERACTIVE_CONFIRM");
|
|
674
|
+
const rl = readline.createInterface({
|
|
675
|
+
input: process.stdin,
|
|
676
|
+
output: process.stdout
|
|
677
|
+
});
|
|
678
|
+
try {
|
|
679
|
+
const trimmed = (await rl.question("\nContinue? (y/N): ")).trim().toLowerCase();
|
|
680
|
+
return trimmed === "y" || trimmed === "yes";
|
|
681
|
+
} finally {
|
|
682
|
+
rl.close();
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Scan every state file in the state bucket for references to the target
|
|
687
|
+
* region's asset bucket / container repo. A pragmatic string scan of the
|
|
688
|
+
* serialized state (design rationale in issue #1010): asset references
|
|
689
|
+
* appear in many property shapes (`Code.S3Bucket`, `ImageUri`, nested
|
|
690
|
+
* `TemplateURL`, `s3.Asset` env-var URLs, …) but all of them carry the
|
|
691
|
+
* bucket / repo NAME verbatim, so a substring match over the raw JSON body
|
|
692
|
+
* catches every shape — including ones future providers add — with zero
|
|
693
|
+
* per-shape maintenance. Covers both key layouts (region-prefixed and
|
|
694
|
+
* legacy), nested-stack children, and stacks deployed under a custom
|
|
695
|
+
* `--state-prefix` (the listing spans the whole bucket, not just the
|
|
696
|
+
* default `cdkd/` prefix).
|
|
697
|
+
*
|
|
698
|
+
* Returns a human-readable `stack (region)` descriptor per referencing
|
|
699
|
+
* state file.
|
|
700
|
+
*/
|
|
701
|
+
async function scanStateReferences(stateBackend, names) {
|
|
702
|
+
const stateKeys = await listAllStateKeys(stateBackend);
|
|
703
|
+
const referencing = [];
|
|
704
|
+
for (const key of stateKeys) {
|
|
705
|
+
const body = await stateBackend.getRawObject(key);
|
|
706
|
+
if (body === null) continue;
|
|
707
|
+
if (names.some((name) => body.includes(name))) referencing.push(describeStateKey(key));
|
|
708
|
+
}
|
|
709
|
+
return referencing;
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* `{prefix}/{stack}/{region}/state.json` → `stack (region)`; legacy
|
|
713
|
+
* `{prefix}/{stack}/state.json` → `stack`. The prefix is arbitrary (custom
|
|
714
|
+
* `--state-prefix` values included), so the stack/region pair is derived
|
|
715
|
+
* from the key's TAIL: the last segment is treated as a region only when
|
|
716
|
+
* it is region-shaped.
|
|
717
|
+
*/
|
|
718
|
+
function describeStateKey(key) {
|
|
719
|
+
const segments = key.slice(0, -11).split("/");
|
|
720
|
+
const last = segments[segments.length - 1] ?? key;
|
|
721
|
+
const secondLast = segments[segments.length - 2];
|
|
722
|
+
if (secondLast !== void 0 && REGION_SEGMENT.test(last)) return `${secondLast} (${last})`;
|
|
723
|
+
return last;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Empty (all versions + delete markers) and delete an S3 bucket, with
|
|
727
|
+
* `ExpectedBucketOwner` pinned on every call — deleting a foreign bucket
|
|
728
|
+
* that squatted the predictable name would be catastrophic, so the same
|
|
729
|
+
* ownership defense as the create side applies in reverse. Returns `false`
|
|
730
|
+
* (skip, info line) when the bucket does not exist — the idempotency
|
|
731
|
+
* mirror of `ensureAssetStorage`.
|
|
732
|
+
*/
|
|
733
|
+
async function emptyAndDeleteBucket(s3Client, bucket, accountId, label, logger) {
|
|
734
|
+
try {
|
|
735
|
+
await s3Client.send(new HeadBucketCommand({
|
|
736
|
+
Bucket: bucket,
|
|
737
|
+
ExpectedBucketOwner: accountId
|
|
738
|
+
}));
|
|
739
|
+
} catch (error) {
|
|
740
|
+
const err = error;
|
|
741
|
+
if (err.name === "NotFound" || err.name === "NoSuchBucket") {
|
|
742
|
+
logger.info(`${label} ${bucket} does not exist — skipping`);
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
if (err.$metadata?.httpStatusCode === 403) throw new CdkdError(`${label} '${bucket}' exists but is not owned by account ${accountId} (or access is denied). Refusing to delete it.`, "ASSET_STORAGE_FOREIGN_BUCKET", error);
|
|
746
|
+
throw normalizeAwsError(error, {
|
|
747
|
+
bucket,
|
|
748
|
+
operation: "HeadBucket"
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
logger.info(`Emptying ${label.toLowerCase()}: ${bucket}`);
|
|
752
|
+
let keyMarker;
|
|
753
|
+
let versionIdMarker;
|
|
754
|
+
do {
|
|
755
|
+
const page = await s3Client.send(new ListObjectVersionsCommand({
|
|
756
|
+
Bucket: bucket,
|
|
757
|
+
ExpectedBucketOwner: accountId,
|
|
758
|
+
...keyMarker && { KeyMarker: keyMarker },
|
|
759
|
+
...versionIdMarker && { VersionIdMarker: versionIdMarker }
|
|
760
|
+
}));
|
|
761
|
+
const entries = [...page.Versions ?? [], ...page.DeleteMarkers ?? []].filter((v) => v.Key).map((v) => ({
|
|
762
|
+
Key: v.Key,
|
|
763
|
+
...v.VersionId && { VersionId: v.VersionId }
|
|
764
|
+
}));
|
|
765
|
+
if (entries.length > 0) {
|
|
766
|
+
const failures = ((await s3Client.send(new DeleteObjectsCommand({
|
|
767
|
+
Bucket: bucket,
|
|
768
|
+
ExpectedBucketOwner: accountId,
|
|
769
|
+
Delete: {
|
|
770
|
+
Objects: entries,
|
|
771
|
+
Quiet: true
|
|
772
|
+
}
|
|
773
|
+
}))).Errors ?? []).map((e) => `${e.Key ?? "<unknown>"} (${e.Code ?? "Error"}: ${e.Message ?? ""})`);
|
|
774
|
+
if (failures.length > 0) throw new CdkdError(`Failed to delete ${failures.length} object(s) from ${label.toLowerCase()} '${bucket}': ${failures.join("; ")}`, "BUCKET_EMPTY_FAILED");
|
|
775
|
+
}
|
|
776
|
+
keyMarker = page.IsTruncated ? page.NextKeyMarker : void 0;
|
|
777
|
+
versionIdMarker = page.IsTruncated ? page.NextVersionIdMarker : void 0;
|
|
778
|
+
} while (keyMarker || versionIdMarker);
|
|
779
|
+
await s3Client.send(new DeleteBucketCommand({
|
|
780
|
+
Bucket: bucket,
|
|
781
|
+
ExpectedBucketOwner: accountId
|
|
782
|
+
}));
|
|
783
|
+
logger.info(`✓ Deleted ${label.toLowerCase()}: ${bucket}`);
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Force-delete the container-asset ECR repository. Missing repo is an
|
|
788
|
+
* idempotent skip with an info line.
|
|
789
|
+
*/
|
|
790
|
+
async function deleteContainerRepo(containerRepo, region, profile, logger) {
|
|
791
|
+
const ecrClient = new ECRClient({
|
|
792
|
+
region,
|
|
793
|
+
...profile && { profile }
|
|
794
|
+
});
|
|
795
|
+
try {
|
|
796
|
+
await ecrClient.send(new DeleteRepositoryCommand({
|
|
797
|
+
repositoryName: containerRepo,
|
|
798
|
+
force: true
|
|
799
|
+
}));
|
|
800
|
+
logger.info(`✓ Deleted container-asset ECR repository: ${containerRepo}`);
|
|
801
|
+
} catch (error) {
|
|
802
|
+
if (error.name === "RepositoryNotFoundException") logger.info(`Container-asset repository ${containerRepo} does not exist — skipping`);
|
|
803
|
+
else throw normalizeAwsError(error, { operation: "DeleteRepository" });
|
|
804
|
+
} finally {
|
|
805
|
+
ecrClient.destroy();
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* List OTHER regions that still have a bootstrap marker in the state
|
|
810
|
+
* bucket (i.e. are still opted in to cdkd asset storage).
|
|
811
|
+
*/
|
|
812
|
+
async function listOtherBootstrapRegions(stateBackend, region) {
|
|
813
|
+
return (await stateBackend.listRawKeys(BOOTSTRAP_MARKER_PREFIX)).filter((k) => k.endsWith(".json")).map((k) => k.slice(BOOTSTRAP_MARKER_PREFIX.length, -5)).filter((r) => r.length > 0 && r !== region);
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* `cdkd bootstrap --destroy` implementation. Dispatched from the bootstrap
|
|
817
|
+
* command action when `--destroy` is passed.
|
|
818
|
+
*/
|
|
819
|
+
async function bootstrapDestroyCommand(options) {
|
|
820
|
+
const logger = getLogger();
|
|
821
|
+
if (options.verbose) logger.setLevel("debug");
|
|
822
|
+
logger.info("Starting cdkd bootstrap --destroy...");
|
|
823
|
+
logger.debug("Options:", options);
|
|
824
|
+
await applyRoleArnIfSet({
|
|
825
|
+
roleArn: options.roleArn,
|
|
826
|
+
region: options.region
|
|
827
|
+
});
|
|
828
|
+
const awsClients = new AwsClients({
|
|
829
|
+
...options.region && { region: options.region },
|
|
830
|
+
...options.profile && { profile: options.profile }
|
|
831
|
+
});
|
|
832
|
+
setAwsClients(awsClients);
|
|
833
|
+
const region = options.region || process.env["AWS_REGION"] || "us-east-1";
|
|
834
|
+
const accountId = (await awsClients.sts.send(new GetCallerIdentityCommand({}))).Account;
|
|
835
|
+
const bucketName = options.stateBucket ?? getDefaultStateBucketName(accountId);
|
|
836
|
+
const markerS3Client = new S3Client({
|
|
837
|
+
region,
|
|
838
|
+
...options.profile && { profile: options.profile }
|
|
839
|
+
});
|
|
840
|
+
const stateBackend = new S3StateBackend(markerS3Client, {
|
|
841
|
+
bucket: bucketName,
|
|
842
|
+
prefix: STATE_PREFIX
|
|
843
|
+
}, {
|
|
844
|
+
region,
|
|
845
|
+
...options.profile && { profile: options.profile }
|
|
846
|
+
});
|
|
847
|
+
try {
|
|
848
|
+
const markerKey = getBootstrapMarkerKey(region);
|
|
849
|
+
let markerBody;
|
|
850
|
+
try {
|
|
851
|
+
markerBody = await stateBackend.getRawObject(markerKey);
|
|
852
|
+
} catch (error) {
|
|
853
|
+
if (error.name === "NoSuchBucket") {
|
|
854
|
+
logger.info(`State bucket '${bucketName}' does not exist — this account/region was never bootstrapped; nothing to delete.`);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
throw error;
|
|
858
|
+
}
|
|
859
|
+
let marker;
|
|
860
|
+
if (markerBody === null) {
|
|
861
|
+
logger.info(`No bootstrap marker for region '${region}' (${markerKey}) — asset storage is not opted in for this region (or was already destroyed); nothing to delete.`);
|
|
862
|
+
if (!options.includeStateBucket) {
|
|
863
|
+
logger.info(`If the asset bucket / ECR repo still exist without a marker, re-run 'cdkd bootstrap --region ${region}' to recreate the marker, then destroy again.`);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
} else marker = parseBootstrapMarker(markerBody, markerKey);
|
|
867
|
+
if (marker && !options.force) {
|
|
868
|
+
logger.info("Scanning stack state for references to the asset storage...");
|
|
869
|
+
const referencing = await scanStateReferences(stateBackend, [marker.assetBucket, marker.containerRepo]);
|
|
870
|
+
if (referencing.length > 0) throw new CdkdError(`Refusing to destroy asset storage for region '${region}': ${referencing.length} deployed stack(s) still reference '${marker.assetBucket}' / '${marker.containerRepo}':\n` + referencing.map((s) => ` - ${s}`).join("\n") + "\nDestroy (or re-deploy off the storage) these stacks first, or pass --force to delete anyway.", "ASSET_STORAGE_IN_USE");
|
|
871
|
+
}
|
|
872
|
+
if (options.includeStateBucket) {
|
|
873
|
+
const stateKeys = await listAllStateKeys(stateBackend);
|
|
874
|
+
if (stateKeys.length > 0) {
|
|
875
|
+
const listing = stateKeys.map((k) => ` - ${describeStateKey(k)} [${k}]`).join("\n");
|
|
876
|
+
throw new CdkdError(`Refusing to delete state bucket '${bucketName}': ${stateKeys.length} stack(s) still have state in it:\n${listing}\nDestroy every stack ('cdkd destroy' / 'cdkd state destroy') before deleting the state bucket.`, "STATE_BUCKET_NOT_EMPTY");
|
|
877
|
+
}
|
|
878
|
+
const otherRegions = await listOtherBootstrapRegions(stateBackend, region);
|
|
879
|
+
if (otherRegions.length > 0) throw new CdkdError(`Refusing to delete state bucket '${bucketName}': region(s) ${otherRegions.join(", ")} are still opted in to cdkd asset storage (their bootstrap markers live in this bucket). Run 'cdkd bootstrap --destroy --region <r>' for each first.`, "STATE_BUCKET_HOLDS_MARKERS");
|
|
880
|
+
}
|
|
881
|
+
const planLines = [];
|
|
882
|
+
if (marker) {
|
|
883
|
+
planLines.push(`Asset bucket: s3://${marker.assetBucket} (region ${region}, all contents)`);
|
|
884
|
+
planLines.push(`Container-asset ECR repository: ${marker.containerRepo} (region ${region}, all images)`);
|
|
885
|
+
planLines.push(`Bootstrap marker: s3://${bucketName}/${markerKey} (deleted last)`);
|
|
886
|
+
}
|
|
887
|
+
if (options.includeStateBucket) planLines.push(`State bucket: s3://${bucketName} (ALL contents, including all versions)`);
|
|
888
|
+
if (!await promptBootstrapDestroyConfirm({
|
|
889
|
+
planLines,
|
|
890
|
+
yes: options.yes
|
|
891
|
+
})) {
|
|
892
|
+
logger.info("Bootstrap teardown cancelled — nothing deleted.");
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (marker) {
|
|
896
|
+
await emptyAndDeleteBucket(awsClients.s3, marker.assetBucket, accountId, "Asset bucket", logger);
|
|
897
|
+
await deleteContainerRepo(marker.containerRepo, region, options.profile, logger);
|
|
898
|
+
await stateBackend.deleteRawObjects([markerKey]);
|
|
899
|
+
logger.info(`✓ Deleted bootstrap marker (${markerKey})`);
|
|
900
|
+
logger.info(`\ncdkd asset storage is now OFF for region ${region}: future deploys in this region fall back to legacy mode (CDK bootstrap destinations) unless the region is bootstrapped again.`);
|
|
901
|
+
}
|
|
902
|
+
if (options.includeStateBucket) {
|
|
903
|
+
const rebuiltStateBucketClient = await rebuildClientForBucketRegion(awsClients.s3, bucketName, { ...options.profile && { profile: options.profile } });
|
|
904
|
+
try {
|
|
905
|
+
await emptyAndDeleteBucket(rebuiltStateBucketClient ?? awsClients.s3, bucketName, accountId, "State bucket", logger);
|
|
906
|
+
} finally {
|
|
907
|
+
rebuiltStateBucketClient?.destroy();
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
logger.info("\n✓ Bootstrap teardown completed");
|
|
911
|
+
} finally {
|
|
912
|
+
markerS3Client.destroy();
|
|
913
|
+
awsClients.destroy();
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
613
917
|
//#endregion
|
|
614
918
|
//#region src/cli/commands/bootstrap.ts
|
|
615
919
|
/**
|
|
@@ -772,7 +1076,24 @@ async function bootstrapCommand(options) {
|
|
|
772
1076
|
* Create bootstrap command
|
|
773
1077
|
*/
|
|
774
1078
|
function createBootstrapCommand() {
|
|
775
|
-
const cmd = new Command("bootstrap").description("Bootstrap cdkd by creating the S3 state bucket plus cdkd-owned asset storage (asset bucket + container-asset ECR repo) for the region").option("--state-bucket <bucket>", "Name of S3 bucket to create for state storage (default: cdkd-state-{accountId})").option("--force", "
|
|
1079
|
+
const cmd = new Command("bootstrap").description("Bootstrap cdkd by creating the S3 state bucket plus cdkd-owned asset storage (asset bucket + container-asset ECR repo) for the region. With --destroy, tear down the region's asset storage instead (issue #1010).").option("--state-bucket <bucket>", "Name of S3 bucket to create for state storage (default: cdkd-state-{accountId})").option("--force", "Create: force reconfiguration of an existing bucket. Destroy: delete the asset storage even when deployed stacks still reference it", false).option("--no-assets", "Skip cdkd asset storage (asset bucket / ECR repo / marker) — deploys keep publishing assets to the CDK bootstrap bucket/repo").option("--destroy", "Tear down the region's cdkd asset storage (empty + delete the asset bucket, force-delete the container-asset ECR repo, then delete the bootstrap marker last). The state bucket is kept unless --include-state-bucket is also passed", false).option("--include-state-bucket", "With --destroy: also delete the S3 state bucket. Refused while any stack state exists or any other region is still opted in to asset storage", false).addOption(new Option("--region <region>", "AWS region in which to create the state bucket (defaults to AWS_REGION env or us-east-1)")).action(withErrorHandling(async (options) => {
|
|
1080
|
+
if (options.destroy) {
|
|
1081
|
+
if (!options.assets) throw new CdkdError("--no-assets cannot be combined with --destroy. The teardown scope is the asset storage itself; use --include-state-bucket to also delete the state bucket.", "INVALID_OPTIONS");
|
|
1082
|
+
await bootstrapDestroyCommand({
|
|
1083
|
+
...options.stateBucket && { stateBucket: options.stateBucket },
|
|
1084
|
+
...options.region && { region: options.region },
|
|
1085
|
+
...options.profile && { profile: options.profile },
|
|
1086
|
+
...options.roleArn && { roleArn: options.roleArn },
|
|
1087
|
+
force: options.force,
|
|
1088
|
+
includeStateBucket: options.includeStateBucket,
|
|
1089
|
+
yes: options.yes,
|
|
1090
|
+
verbose: options.verbose
|
|
1091
|
+
});
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (options.includeStateBucket) throw new CdkdError("--include-state-bucket requires --destroy (it opts the state bucket into the teardown).", "INVALID_OPTIONS");
|
|
1095
|
+
await bootstrapCommand(options);
|
|
1096
|
+
}));
|
|
776
1097
|
commonOptions.forEach((opt) => cmd.addOption(opt));
|
|
777
1098
|
return cmd;
|
|
778
1099
|
}
|
|
@@ -1534,7 +1855,7 @@ const FLUSH_INTERVAL_MS = 2e3;
|
|
|
1534
1855
|
const FLUSH_EVENT_THRESHOLD = 50;
|
|
1535
1856
|
/** Build-time cdkd version, with a dev fallback for non-built contexts. */
|
|
1536
1857
|
function getCdkdVersion() {
|
|
1537
|
-
return "0.
|
|
1858
|
+
return "0.236.0";
|
|
1538
1859
|
}
|
|
1539
1860
|
/**
|
|
1540
1861
|
* Generate a time-sortable unique run id, e.g.
|
|
@@ -56083,7 +56404,7 @@ function reorderArgs(argv) {
|
|
|
56083
56404
|
async function main() {
|
|
56084
56405
|
installPipeCloseHandler();
|
|
56085
56406
|
const program = new Command();
|
|
56086
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
56407
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.236.0");
|
|
56087
56408
|
program.addCommand(createBootstrapCommand());
|
|
56088
56409
|
program.addCommand(createSynthCommand());
|
|
56089
56410
|
program.addCommand(createListCommand());
|