@go-to-k/cdkd 0.231.11 → 0.232.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 +16 -4
- package/dist/cli.js +455 -51
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-CINi8mJy.js → deploy-engine-BkPZar1R.js} +9 -5
- package/dist/deploy-engine-BkPZar1R.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-CINi8mJy.js.map +0 -1
package/README.md
CHANGED
|
@@ -14,6 +14,8 @@ Drop-in CDK CLI for existing CDK apps — faster deploys via AWS SDK instead of
|
|
|
14
14
|
|
|
15
15
|
**cdkd complements the AWS CDK CLI rather than replacing it.** Use cdkd in dev/test for rapid iteration and SAM-style local execution; use the AWS CDK CLI in production for full CloudFormation tooling. Install cdkd alongside an existing `cdk deploy` workflow — no migration needed, `cdkd local *` reads deployed state directly via `--from-cfn-stack`. Bidirectional migration is also supported — [import](#importing-existing-resources) into cdkd or [export](#exporting-a-stack-back-to-cloudformation) back to CloudFormation when ready.
|
|
16
16
|
|
|
17
|
+
**A natural fit for AI-driven development.** AI coding agents iterate in tight spin-up / tear-down loops — and cdkd keeps each turn short, with fast deploys and an equally fast `cdkd destroy` that deletes via direct SDK calls instead of polling a CloudFormation stack-delete.
|
|
18
|
+
|
|
17
19
|
> [!IMPORTANT]
|
|
18
20
|
> cdkd is for dev/test workflows only — early in development, not yet production-ready.
|
|
19
21
|
|
|
@@ -109,10 +111,16 @@ Reproduce the first two with `./tests/benchmark/run-benchmark.sh all`. See [test
|
|
|
109
111
|
│
|
|
110
112
|
▼
|
|
111
113
|
┌─────────────────┐
|
|
114
|
+
│ Asset Build & │ S3 ZIP upload / ECR image build & push
|
|
115
|
+
│ Publish │
|
|
116
|
+
└────────┬────────┘
|
|
117
|
+
│
|
|
118
|
+
▼
|
|
119
|
+
┌─────────────────┐
|
|
112
120
|
│ cdkd Engine │
|
|
113
121
|
│ - DAG Analysis │ Dependency graph construction
|
|
114
122
|
│ - Diff Calc │ Compare with existing resources
|
|
115
|
-
│ - Parallel Exec │
|
|
123
|
+
│ - Parallel Exec │ Dispatch on deps complete (no level barrier)
|
|
116
124
|
└────────┬────────┘
|
|
117
125
|
│
|
|
118
126
|
┌────┴────┐
|
|
@@ -131,7 +139,7 @@ parsing → synthesis → asset publishing → per-stack deploy), see
|
|
|
131
139
|
## Prerequisites
|
|
132
140
|
|
|
133
141
|
- **Node.js** >= 20.0.0
|
|
134
|
-
- **AWS CDK Bootstrap**: You must run `cdk bootstrap` before using cdkd. cdkd uses CDK's bootstrap bucket (`cdk-hnb659fds-assets-*`) for asset uploads (Lambda code, Docker images). Custom bootstrap qualifiers are supported — CDK embeds the correct bucket/repo names in the asset manifest during synthesis.
|
|
142
|
+
- **AWS CDK Bootstrap**: You must run `cdk bootstrap` before using cdkd. cdkd uses CDK's bootstrap bucket (`cdk-hnb659fds-assets-*`) for asset uploads (Lambda code, Docker images). Custom bootstrap qualifiers are supported — CDK embeds the correct bucket/repo names in the asset manifest during synthesis. Note: `cdk gc` cannot see cdkd-deployed stacks (no CloudFormation stack to scan) and may garbage-collect cdkd-published assets from the CDK bootstrap storage — `cdkd bootstrap` therefore also creates cdkd-owned asset storage that asset publishing is moving onto (issue [#1002](https://github.com/go-to-k/cdkd/issues/1002); design in [docs/design/1002-cdkd-asset-storage.md](docs/design/1002-cdkd-asset-storage.md); the publish redirection ships in a follow-up release).
|
|
135
143
|
- **AWS credentials with admin-equivalent permissions** for the resources being deployed. cdkd does NOT route through CloudFormation, so CDK CLI's `cdk-hnb659fds-deploy-role-*` is NOT sufficient — see [`--role-arn`](docs/cli-reference.md).
|
|
136
144
|
|
|
137
145
|
## Installation
|
|
@@ -148,12 +156,16 @@ The installed binary is `cdkd`.
|
|
|
148
156
|
> **First-time setup**: cdkd requires a one-time `cdkd bootstrap` per AWS
|
|
149
157
|
> account before any other command will work — it creates the S3 state
|
|
150
158
|
> bucket (`cdkd-state-{accountId}`) that cdkd uses to track deployed
|
|
151
|
-
> resources
|
|
159
|
+
> resources, plus cdkd-owned asset storage for the region
|
|
160
|
+
> (`cdkd-assets-{accountId}-{region}` bucket +
|
|
161
|
+
> `cdkd-container-assets-{accountId}-{region}` ECR repo — skip with
|
|
162
|
+
> `--no-assets`; see [`cdkd bootstrap`](docs/cli-reference.md#cdkd-bootstrap)).
|
|
163
|
+
> This is separate from `cdk bootstrap` (which sets up the
|
|
152
164
|
> CDK asset bucket / ECR repo and is also required — see
|
|
153
165
|
> [Prerequisites](#prerequisites)).
|
|
154
166
|
|
|
155
167
|
```bash
|
|
156
|
-
# Bootstrap (creates S3 state bucket — one-time setup
|
|
168
|
+
# Bootstrap (creates S3 state bucket + asset storage — one-time setup per AWS account)
|
|
157
169
|
cdkd bootstrap
|
|
158
170
|
|
|
159
171
|
# List stacks in the CDK app
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { A as withErrorHandling, B as generateResourceName, C as StackHasActiveImportsError, F as getLiveRenderer, H as withSkipPrefix, I as PATTERN_B_NAME_OPTIONS, L as PATTERN_B_NAME_PROPERTIES, M as getLogger, P as runStackBuffered, R as PATTERN_B_RESOURCE_TYPES, S as ResourceUpdateNotSupportedError, U as withStackName, V as generateResourceNameWithFallback, _ 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-uvVnteGl.js";
|
|
3
3
|
import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-pjPwZz1r.js";
|
|
4
|
-
import { $ as resolveSkipPrefix, A as DiffCalculator, B as buildDockerImage, C as findActionableSilentDrops, D as refStateLookupFromResource, E as cfnRefValueFromPhysicalId, F as rebuildClientForBucketRegion, G as AssetManifestLoader, H as getDockerCmd, I as shouldRetainResource, J as synthesisStatusMessage, K as getDockerImageBySourceHash, L as AssetPublisher, M as TemplateParser, N as LockManager, O as WAFv2WebACLProvider, P as S3StateBackend, Q as resolveCaptureObservedState, R as stringifyValue, S as ProviderRegistry, T as IntrinsicFunctionResolver, U as runDockerForeground, V as formatDockerLoginError, W as runDockerStreaming, X as getLegacyStateBucketName, Y as getDefaultStateBucketName, Z as resolveApp, _ as green, a as withRetry, at as MIGRATE_TMP_PREFIX, b as IAMRoleProvider, c as computeImplicitDeleteEdges, ct as AssemblyReader, d as isStatefulRecreateTargetSync, et as resolveStateBucketWithDefault, f as renderStatefulReason, g as gray, h as cyan, i as withResourceDeadline, it as CFN_TEMPLATE_URL_LIMIT, j as DagBuilder, k as applyRoleArnIfSet, l as extractDeploymentEventError, m as bold, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as warnDeprecatedNoPrefixCliFlag, o as isRetryableTransientError, ot as findLargeInlineResources, p as formatResourceLine, q as Synthesizer, r as DeployEngine, rt as CFN_TEMPLATE_BODY_LIMIT, s as IMPLICIT_DELETE_DEPENDENCIES, st as uploadCfnTemplate, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as resolveStateBucketWithDefaultAndSource, u as MULTI_REGION_RECREATE_BLOCKED_TYPES, ut as resolveBucketRegion, v as red, w as CloudControlProvider, x as collectInlinePolicyNamesManagedBySiblings, y as yellow, z as WorkGraph } from "./deploy-engine-
|
|
4
|
+
import { $ as resolveSkipPrefix, A as DiffCalculator, B as buildDockerImage, C as findActionableSilentDrops, D as refStateLookupFromResource, E as cfnRefValueFromPhysicalId, F as rebuildClientForBucketRegion, G as AssetManifestLoader, H as getDockerCmd, I as shouldRetainResource, J as synthesisStatusMessage, K as getDockerImageBySourceHash, L as AssetPublisher, M as TemplateParser, N as LockManager, O as WAFv2WebACLProvider, P as S3StateBackend, Q as resolveCaptureObservedState, R as stringifyValue, S as ProviderRegistry, T as IntrinsicFunctionResolver, U as runDockerForeground, V as formatDockerLoginError, W as runDockerStreaming, X as getLegacyStateBucketName, Y as getDefaultStateBucketName, Z as resolveApp, _ as green, a as withRetry, at as MIGRATE_TMP_PREFIX, b as IAMRoleProvider, c as computeImplicitDeleteEdges, ct as AssemblyReader, d as isStatefulRecreateTargetSync, et as resolveStateBucketWithDefault, f as renderStatefulReason, g as gray, h as cyan, i as withResourceDeadline, it as CFN_TEMPLATE_URL_LIMIT, j as DagBuilder, k as applyRoleArnIfSet, l as extractDeploymentEventError, m as bold, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as warnDeprecatedNoPrefixCliFlag, o as isRetryableTransientError, ot as findLargeInlineResources, p as formatResourceLine, q as Synthesizer, r as DeployEngine, rt as CFN_TEMPLATE_BODY_LIMIT, s as IMPLICIT_DELETE_DEPENDENCIES, st as uploadCfnTemplate, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as resolveStateBucketWithDefaultAndSource, u as MULTI_REGION_RECREATE_BLOCKED_TYPES, ut as resolveBucketRegion, v as red, w as CloudControlProvider, x as collectInlinePolicyNamesManagedBySiblings, y as yellow, z as WorkGraph } from "./deploy-engine-BkPZar1R.js";
|
|
5
5
|
import { t as ASGProvider } from "./asg-provider-Dd9Xze-T.js";
|
|
6
6
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
7
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
@@ -598,12 +598,320 @@ const destroyOptions = [
|
|
|
598
598
|
allowUnsupportedTypesOption
|
|
599
599
|
];
|
|
600
600
|
|
|
601
|
+
//#endregion
|
|
602
|
+
//#region src/assets/asset-storage.ts
|
|
603
|
+
/**
|
|
604
|
+
* cdkd-owned asset storage — naming, bootstrap marker, and deploy-time
|
|
605
|
+
* asset-mode detection (issue #1002, design at
|
|
606
|
+
* docs/design/1002-cdkd-asset-storage.md).
|
|
607
|
+
*
|
|
608
|
+
* Why this exists: cdkd publishes assets to the CDK bootstrap bucket / ECR
|
|
609
|
+
* repo, but `cdk gc` decides "in use" by scanning CloudFormation stack
|
|
610
|
+
* templates — cdkd-deployed stacks have no CFn stack, so every cdkd-published
|
|
611
|
+
* asset looks isolated and gets deleted. The fix is cdkd-owned asset storage
|
|
612
|
+
* created by `cdkd bootstrap` (structurally out of `cdk gc`'s reach because
|
|
613
|
+
* gc only discovers storage from the CDK bootstrap stack's outputs).
|
|
614
|
+
*
|
|
615
|
+
* The upgrade is strictly transparent: deploys behave byte-identically to
|
|
616
|
+
* before until the user re-runs `cdkd bootstrap` for a region, which writes a
|
|
617
|
+
* per-region marker object into the state bucket. Deploy reads the marker to
|
|
618
|
+
* pick the mode; PR 2 of the phasing wires the actual publish redirection +
|
|
619
|
+
* template rewrite onto the `cdkd-assets` mode resolved here.
|
|
620
|
+
*/
|
|
621
|
+
/** Marker schema version, bumped if the marker shape ever changes. */
|
|
622
|
+
const ASSET_SUPPORT_VERSION = 1;
|
|
623
|
+
/**
|
|
624
|
+
* S3 key prefix for bootstrap markers in the state bucket. Deliberately
|
|
625
|
+
* outside the `cdkd/` state prefix so `state list` key-listing never mistakes
|
|
626
|
+
* a marker for a stack, and concurrent bootstraps in two regions cannot race
|
|
627
|
+
* last-writer-wins on a shared object (design §12.1).
|
|
628
|
+
*/
|
|
629
|
+
const BOOTSTRAP_MARKER_PREFIX = "cdkd-bootstrap/";
|
|
630
|
+
/**
|
|
631
|
+
* Name of the cdkd-owned asset bucket for an (account, region) pair.
|
|
632
|
+
* Per-region because Lambda requires the code bucket to be in the function's
|
|
633
|
+
* region — the same reason CDK's asset bucket is per-region (design §3).
|
|
634
|
+
*/
|
|
635
|
+
function getCdkdAssetBucketName(accountId, region) {
|
|
636
|
+
return `cdkd-assets-${accountId}-${region}`;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Name of the cdkd-owned container-asset ECR repository for an
|
|
640
|
+
* (account, region) pair. ECR repos are account+region scoped by ARN, so the
|
|
641
|
+
* suffix is not strictly needed — the CDK-parallel shape keeps the future
|
|
642
|
+
* template rewrite uniform and the names self-describing (design §3).
|
|
643
|
+
*/
|
|
644
|
+
function getCdkdContainerRepoName(accountId, region) {
|
|
645
|
+
return `cdkd-container-assets-${accountId}-${region}`;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* State-bucket key of the bootstrap marker for a region.
|
|
649
|
+
*/
|
|
650
|
+
function getBootstrapMarkerKey(region) {
|
|
651
|
+
return `${BOOTSTRAP_MARKER_PREFIX}${region}.json`;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Parse and validate a bootstrap marker body.
|
|
655
|
+
*
|
|
656
|
+
* Throws on malformed JSON or missing required fields — a corrupt marker is
|
|
657
|
+
* treated like the marker-present-but-resources-missing case (hard error,
|
|
658
|
+
* never a silent legacy fallback that would flip-flop stack properties
|
|
659
|
+
* between deploys — design §4.1).
|
|
660
|
+
*/
|
|
661
|
+
function parseBootstrapMarker(body, markerKey) {
|
|
662
|
+
let parsed;
|
|
663
|
+
try {
|
|
664
|
+
parsed = JSON.parse(body);
|
|
665
|
+
} catch (error) {
|
|
666
|
+
throw new CdkdError(`Bootstrap marker '${markerKey}' in the state bucket is not valid JSON. Re-run 'cdkd bootstrap' for this region to rewrite it.`, "INVALID_BOOTSTRAP_MARKER", error);
|
|
667
|
+
}
|
|
668
|
+
const marker = parsed;
|
|
669
|
+
if (typeof marker.assetBucket !== "string" || marker.assetBucket.length === 0 || typeof marker.containerRepo !== "string" || marker.containerRepo.length === 0 || typeof marker.assetSupportVersion !== "number") throw new CdkdError(`Bootstrap marker '${markerKey}' in the state bucket is malformed (missing assetBucket / containerRepo / assetSupportVersion). Re-run 'cdkd bootstrap' for this region to rewrite it.`, "INVALID_BOOTSTRAP_MARKER");
|
|
670
|
+
if (marker.assetSupportVersion > 1) throw new CdkdError(`Bootstrap marker '${markerKey}' has assetSupportVersion ${marker.assetSupportVersion}, but this cdkd only understands up to ${1}. Upgrade cdkd to deploy in this region.`, "UNSUPPORTED_BOOTSTRAP_MARKER_VERSION");
|
|
671
|
+
return {
|
|
672
|
+
assetBucket: marker.assetBucket,
|
|
673
|
+
containerRepo: marker.containerRepo,
|
|
674
|
+
assetSupportVersion: marker.assetSupportVersion,
|
|
675
|
+
createdAt: typeof marker.createdAt === "string" ? marker.createdAt : ""
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Verify that the asset bucket + ECR repo named by a marker actually exist.
|
|
680
|
+
*
|
|
681
|
+
* Called when a marker is present: the user opted the region in, so missing
|
|
682
|
+
* resources (user deleted them) are a hard error naming the missing resource
|
|
683
|
+
* and the `cdkd bootstrap` fix — never a silent fallback to CDK bootstrap
|
|
684
|
+
* storage (design §4.1).
|
|
685
|
+
*
|
|
686
|
+
* Every S3 call passes `ExpectedBucketOwner` so a marker pointing at a
|
|
687
|
+
* since-recreated foreign bucket can never leak assets cross-account
|
|
688
|
+
* (bucket-squatting defense, design §5).
|
|
689
|
+
*/
|
|
690
|
+
async function verifyAssetStorageExists(marker, accountId, region, opts = {}) {
|
|
691
|
+
const rebootstrapHint = `Run 'cdkd bootstrap --region ${region}' to recreate it. cdkd never silently falls back to CDK bootstrap asset storage once a region is opted in.`;
|
|
692
|
+
const clientOpts = {
|
|
693
|
+
region,
|
|
694
|
+
...opts.profile && { profile: opts.profile }
|
|
695
|
+
};
|
|
696
|
+
const s3Client = new S3Client(clientOpts);
|
|
697
|
+
const ecrClient = new ECRClient(clientOpts);
|
|
698
|
+
try {
|
|
699
|
+
try {
|
|
700
|
+
await s3Client.send(new HeadBucketCommand({
|
|
701
|
+
Bucket: marker.assetBucket,
|
|
702
|
+
ExpectedBucketOwner: accountId
|
|
703
|
+
}));
|
|
704
|
+
} catch (error) {
|
|
705
|
+
const err = error;
|
|
706
|
+
if (err.name === "NotFound" || err.name === "NoSuchBucket") throw new CdkdError(`cdkd asset storage is bootstrapped for region '${region}' but the asset bucket '${marker.assetBucket}' is missing. ${rebootstrapHint}`, "ASSET_STORAGE_MISSING");
|
|
707
|
+
if (err.$metadata?.httpStatusCode === 403) throw new CdkdError(`Asset bucket '${marker.assetBucket}' exists but is not owned by account ${accountId} (or access is denied). Refusing to use it. ${rebootstrapHint}`, "ASSET_STORAGE_FOREIGN_BUCKET", error);
|
|
708
|
+
throw error;
|
|
709
|
+
}
|
|
710
|
+
try {
|
|
711
|
+
await ecrClient.send(new DescribeRepositoriesCommand({ repositoryNames: [marker.containerRepo] }));
|
|
712
|
+
} catch (error) {
|
|
713
|
+
if (error.name === "RepositoryNotFoundException") throw new CdkdError(`cdkd asset storage is bootstrapped for region '${region}' but the container-asset ECR repository '${marker.containerRepo}' is missing. ${rebootstrapHint}`, "ASSET_STORAGE_MISSING");
|
|
714
|
+
throw error;
|
|
715
|
+
}
|
|
716
|
+
} finally {
|
|
717
|
+
s3Client.destroy();
|
|
718
|
+
ecrClient.destroy();
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Create (or adopt, when already owned by this account) the cdkd asset
|
|
723
|
+
* bucket + container-asset ECR repo for a region, then write the bootstrap
|
|
724
|
+
* marker. Called by `cdkd bootstrap` unless `--no-assets` is passed.
|
|
725
|
+
*
|
|
726
|
+
* Idempotent like the state-bucket path: existing resources are left as-is
|
|
727
|
+
* unless `force` re-applies their configuration. The marker is written LAST,
|
|
728
|
+
* only after both resources exist (design §5) — a crash mid-bootstrap leaves
|
|
729
|
+
* no marker, so deploys stay in legacy mode.
|
|
730
|
+
*
|
|
731
|
+
* Bucket-squatting defense: the bucket name is predictable (same weakness as
|
|
732
|
+
* CDK's bootstrap bucket), so this refuses to adopt a bucket this account
|
|
733
|
+
* does not own, and the `HeadBucket` probe passes `ExpectedBucketOwner`.
|
|
734
|
+
*/
|
|
735
|
+
async function ensureAssetStorage(options) {
|
|
736
|
+
const logger = getLogger();
|
|
737
|
+
const { s3Client, ecrClient, stateBackend, accountId, region, force } = options;
|
|
738
|
+
const assetBucket = getCdkdAssetBucketName(accountId, region);
|
|
739
|
+
const containerRepo = getCdkdContainerRepoName(accountId, region);
|
|
740
|
+
let bucketExists = false;
|
|
741
|
+
try {
|
|
742
|
+
await s3Client.send(new HeadBucketCommand({
|
|
743
|
+
Bucket: assetBucket,
|
|
744
|
+
ExpectedBucketOwner: accountId
|
|
745
|
+
}));
|
|
746
|
+
bucketExists = true;
|
|
747
|
+
logger.info(`Asset bucket ${assetBucket} already exists`);
|
|
748
|
+
} catch (error) {
|
|
749
|
+
const err = error;
|
|
750
|
+
if (err.name === "NotFound" || err.name === "NoSuchBucket") {} else if (err.$metadata?.httpStatusCode === 403) throw new CdkdError(`Asset bucket name '${assetBucket}' is already taken by a bucket this account does not own (or access is denied). Refusing to adopt it — resolve the naming conflict before re-running 'cdkd bootstrap'.`, "ASSET_STORAGE_FOREIGN_BUCKET", error);
|
|
751
|
+
else throw normalizeAwsError(error, {
|
|
752
|
+
bucket: assetBucket,
|
|
753
|
+
operation: "HeadBucket"
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
if (!bucketExists) {
|
|
757
|
+
logger.info(`Creating asset bucket: ${assetBucket} in region ${region}`);
|
|
758
|
+
try {
|
|
759
|
+
await s3Client.send(new CreateBucketCommand({
|
|
760
|
+
Bucket: assetBucket,
|
|
761
|
+
...region !== "us-east-1" && { CreateBucketConfiguration: { LocationConstraint: region } }
|
|
762
|
+
}));
|
|
763
|
+
logger.info(`✓ Created asset bucket: ${assetBucket}`);
|
|
764
|
+
} catch (error) {
|
|
765
|
+
const err = error;
|
|
766
|
+
if (err.name === "BucketAlreadyOwnedByYou") logger.info(`Asset bucket ${assetBucket} already exists`);
|
|
767
|
+
else if (err.name === "BucketAlreadyExists") throw new CdkdError(`Asset bucket name '${assetBucket}' is already taken by another AWS account. Refusing to adopt it — resolve the naming conflict before re-running 'cdkd bootstrap'.`, "ASSET_STORAGE_FOREIGN_BUCKET", error);
|
|
768
|
+
else throw normalizeAwsError(error, {
|
|
769
|
+
bucket: assetBucket,
|
|
770
|
+
operation: "CreateBucket"
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (!bucketExists || force) {
|
|
775
|
+
await s3Client.send(new PutBucketEncryptionCommand({
|
|
776
|
+
Bucket: assetBucket,
|
|
777
|
+
ExpectedBucketOwner: accountId,
|
|
778
|
+
ServerSideEncryptionConfiguration: { Rules: [{
|
|
779
|
+
ApplyServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" },
|
|
780
|
+
BucketKeyEnabled: true
|
|
781
|
+
}] }
|
|
782
|
+
}));
|
|
783
|
+
await s3Client.send(new PutPublicAccessBlockCommand({
|
|
784
|
+
Bucket: assetBucket,
|
|
785
|
+
ExpectedBucketOwner: accountId,
|
|
786
|
+
PublicAccessBlockConfiguration: {
|
|
787
|
+
BlockPublicAcls: true,
|
|
788
|
+
BlockPublicPolicy: true,
|
|
789
|
+
IgnorePublicAcls: true,
|
|
790
|
+
RestrictPublicBuckets: true
|
|
791
|
+
}
|
|
792
|
+
}));
|
|
793
|
+
await s3Client.send(new PutBucketPolicyCommand({
|
|
794
|
+
Bucket: assetBucket,
|
|
795
|
+
ExpectedBucketOwner: accountId,
|
|
796
|
+
Policy: JSON.stringify({
|
|
797
|
+
Version: "2012-10-17",
|
|
798
|
+
Statement: [{
|
|
799
|
+
Sid: "DenyExternalAccess",
|
|
800
|
+
Effect: "Deny",
|
|
801
|
+
Principal: "*",
|
|
802
|
+
Action: "s3:*",
|
|
803
|
+
Resource: [`arn:aws:s3:::${assetBucket}`, `arn:aws:s3:::${assetBucket}/*`],
|
|
804
|
+
Condition: { StringNotEquals: { "aws:PrincipalAccount": accountId } }
|
|
805
|
+
}]
|
|
806
|
+
})
|
|
807
|
+
}));
|
|
808
|
+
logger.info("✓ Configured asset bucket (AES-256 encryption, public access block, deny external access)");
|
|
809
|
+
}
|
|
810
|
+
let repoExists = false;
|
|
811
|
+
try {
|
|
812
|
+
await ecrClient.send(new DescribeRepositoriesCommand({ repositoryNames: [containerRepo] }));
|
|
813
|
+
repoExists = true;
|
|
814
|
+
logger.info(`Container-asset repository ${containerRepo} already exists`);
|
|
815
|
+
} catch (error) {
|
|
816
|
+
if (error.name !== "RepositoryNotFoundException") throw error;
|
|
817
|
+
}
|
|
818
|
+
if (!repoExists) {
|
|
819
|
+
logger.info(`Creating container-asset ECR repository: ${containerRepo}`);
|
|
820
|
+
try {
|
|
821
|
+
await ecrClient.send(new CreateRepositoryCommand({
|
|
822
|
+
repositoryName: containerRepo,
|
|
823
|
+
imageTagMutability: "IMMUTABLE"
|
|
824
|
+
}));
|
|
825
|
+
logger.info(`✓ Created container-asset ECR repository: ${containerRepo}`);
|
|
826
|
+
} catch (error) {
|
|
827
|
+
if (error.name === "RepositoryAlreadyExistsException") logger.info(`Container-asset repository ${containerRepo} already exists`);
|
|
828
|
+
else throw error;
|
|
829
|
+
}
|
|
830
|
+
} else if (force) {
|
|
831
|
+
await ecrClient.send(new PutImageTagMutabilityCommand({
|
|
832
|
+
repositoryName: containerRepo,
|
|
833
|
+
imageTagMutability: "IMMUTABLE"
|
|
834
|
+
}));
|
|
835
|
+
logger.info("✓ Configured container-asset repository (immutable tags)");
|
|
836
|
+
}
|
|
837
|
+
const marker = {
|
|
838
|
+
assetBucket,
|
|
839
|
+
containerRepo,
|
|
840
|
+
assetSupportVersion: 1,
|
|
841
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
842
|
+
};
|
|
843
|
+
await stateBackend.putRawObject(getBootstrapMarkerKey(region), JSON.stringify(marker, null, 2));
|
|
844
|
+
logger.info(`✓ Wrote bootstrap marker (${getBootstrapMarkerKey(region)})`);
|
|
845
|
+
return {
|
|
846
|
+
assetBucket,
|
|
847
|
+
containerRepo
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Deploy-time asset-mode resolver. One instance per CLI invocation; results
|
|
852
|
+
* are cached per region for the process lifetime (the marker read is one
|
|
853
|
+
* GetObject against a bucket every deploy already reads — design §4.1).
|
|
854
|
+
*
|
|
855
|
+
* In legacy mode, one `logger.info` line per invocation mentions the
|
|
856
|
+
* `cdk gc` hazard and the `cdkd bootstrap` opt-in (info, not warn — existing
|
|
857
|
+
* users are not doing anything wrong; design §12.2).
|
|
858
|
+
*/
|
|
859
|
+
var AssetModeResolver = class {
|
|
860
|
+
logger = getLogger().child("AssetMode");
|
|
861
|
+
cache = /* @__PURE__ */ new Map();
|
|
862
|
+
legacyNoticeShown = false;
|
|
863
|
+
stateBackend;
|
|
864
|
+
accountId;
|
|
865
|
+
profile;
|
|
866
|
+
constructor(stateBackend, accountId, opts = {}) {
|
|
867
|
+
this.stateBackend = stateBackend;
|
|
868
|
+
this.accountId = accountId;
|
|
869
|
+
this.profile = opts.profile;
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Resolve the asset mode for a deploy region. Concurrent callers for the
|
|
873
|
+
* same region share one in-flight resolution.
|
|
874
|
+
*/
|
|
875
|
+
resolve(region) {
|
|
876
|
+
const cached = this.cache.get(region);
|
|
877
|
+
if (cached) return cached;
|
|
878
|
+
const inFlight = this.doResolve(region).catch((error) => {
|
|
879
|
+
this.cache.delete(region);
|
|
880
|
+
throw error;
|
|
881
|
+
});
|
|
882
|
+
this.cache.set(region, inFlight);
|
|
883
|
+
return inFlight;
|
|
884
|
+
}
|
|
885
|
+
async doResolve(region) {
|
|
886
|
+
const markerKey = getBootstrapMarkerKey(region);
|
|
887
|
+
const body = await this.stateBackend.getRawObject(markerKey);
|
|
888
|
+
if (body === null) {
|
|
889
|
+
if (!this.legacyNoticeShown) {
|
|
890
|
+
this.legacyNoticeShown = true;
|
|
891
|
+
this.logger.info("Assets are published to the CDK bootstrap bucket/repo, which 'cdk gc' may garbage-collect (cdkd-deployed stacks have no CloudFormation stack for gc to scan). Run 'cdkd bootstrap' to create cdkd-owned asset storage that 'cdk gc' never touches.");
|
|
892
|
+
}
|
|
893
|
+
return { mode: "legacy" };
|
|
894
|
+
}
|
|
895
|
+
const marker = parseBootstrapMarker(body, markerKey);
|
|
896
|
+
await verifyAssetStorageExists(marker, this.accountId, region, { ...this.profile && { profile: this.profile } });
|
|
897
|
+
this.logger.debug(`cdkd asset storage active for region '${region}': ${marker.assetBucket} / ${marker.containerRepo}`);
|
|
898
|
+
return {
|
|
899
|
+
mode: "cdkd-assets",
|
|
900
|
+
marker
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
|
|
601
905
|
//#endregion
|
|
602
906
|
//#region src/cli/commands/bootstrap.ts
|
|
603
907
|
/**
|
|
604
908
|
* Bootstrap command implementation
|
|
605
909
|
*
|
|
606
|
-
* Creates S3 bucket for state management
|
|
910
|
+
* Creates the S3 bucket for state management and (unless --no-assets) the
|
|
911
|
+
* cdkd-owned asset storage for the region: asset bucket + container-asset
|
|
912
|
+
* ECR repo + the per-region bootstrap marker that flips deploys in this
|
|
913
|
+
* region from legacy (CDK bootstrap destinations) to cdkd-assets mode
|
|
914
|
+
* (issue #1002, design at docs/design/1002-cdkd-asset-storage.md).
|
|
607
915
|
*/
|
|
608
916
|
async function bootstrapCommand(options) {
|
|
609
917
|
const logger = getLogger();
|
|
@@ -646,54 +954,96 @@ async function bootstrapCommand(options) {
|
|
|
646
954
|
operation: "HeadBucket"
|
|
647
955
|
});
|
|
648
956
|
}
|
|
649
|
-
|
|
650
|
-
|
|
957
|
+
let skipStateBucketConfig = false;
|
|
958
|
+
if (bucketExists) if (!options.force) {
|
|
959
|
+
if (!options.assets) {
|
|
651
960
|
logger.warn(`Bucket ${bucketName} already exists. Use --force to reconfigure (this will not delete existing state)`);
|
|
652
961
|
return;
|
|
653
962
|
}
|
|
654
|
-
logger.info(
|
|
655
|
-
|
|
963
|
+
logger.info(`State bucket ${bucketName} already exists — skipping reconfiguration (use --force to reconfigure)`);
|
|
964
|
+
skipStateBucketConfig = true;
|
|
965
|
+
} else logger.info("--force specified, continuing with existing bucket");
|
|
966
|
+
else {
|
|
656
967
|
logger.info(`Creating S3 bucket: ${bucketName} in region ${region}`);
|
|
657
968
|
const createBucketParams = { Bucket: bucketName };
|
|
658
969
|
if (region !== "us-east-1") createBucketParams.CreateBucketConfiguration = { LocationConstraint: region };
|
|
659
970
|
await s3Client.send(new CreateBucketCommand(createBucketParams));
|
|
660
971
|
logger.info(`✓ Created S3 bucket: ${bucketName}`);
|
|
661
972
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
973
|
+
if (!skipStateBucketConfig) {
|
|
974
|
+
logger.debug("Enabling bucket versioning...");
|
|
975
|
+
await s3Client.send(new PutBucketVersioningCommand({
|
|
976
|
+
Bucket: bucketName,
|
|
977
|
+
VersioningConfiguration: { Status: "Enabled" }
|
|
978
|
+
}));
|
|
979
|
+
logger.info("✓ Enabled bucket versioning");
|
|
980
|
+
logger.debug("Enabling bucket encryption...");
|
|
981
|
+
await s3Client.send(new PutBucketEncryptionCommand({
|
|
982
|
+
Bucket: bucketName,
|
|
983
|
+
ServerSideEncryptionConfiguration: { Rules: [{
|
|
984
|
+
ApplyServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" },
|
|
985
|
+
BucketKeyEnabled: true
|
|
986
|
+
}] }
|
|
987
|
+
}));
|
|
988
|
+
logger.info("✓ Enabled bucket encryption (AES-256)");
|
|
989
|
+
logger.debug("Setting bucket policy...");
|
|
990
|
+
const bucketPolicy = {
|
|
991
|
+
Version: "2012-10-17",
|
|
992
|
+
Statement: [{
|
|
993
|
+
Sid: "DenyExternalAccess",
|
|
994
|
+
Effect: "Deny",
|
|
995
|
+
Principal: "*",
|
|
996
|
+
Action: "s3:*",
|
|
997
|
+
Resource: [`arn:aws:s3:::${bucketName}`, `arn:aws:s3:::${bucketName}/*`],
|
|
998
|
+
Condition: { StringNotEquals: { "aws:PrincipalAccount": accountId } }
|
|
999
|
+
}]
|
|
1000
|
+
};
|
|
1001
|
+
await s3Client.send(new PutBucketPolicyCommand({
|
|
1002
|
+
Bucket: bucketName,
|
|
1003
|
+
Policy: JSON.stringify(bucketPolicy)
|
|
1004
|
+
}));
|
|
1005
|
+
logger.info("✓ Set bucket policy (deny external access)");
|
|
1006
|
+
}
|
|
1007
|
+
let assetStorage;
|
|
1008
|
+
if (options.assets) {
|
|
1009
|
+
logger.info("\nSetting up cdkd asset storage...");
|
|
1010
|
+
const ecrClient = new ECRClient({
|
|
1011
|
+
region,
|
|
1012
|
+
...options.profile && { profile: options.profile }
|
|
1013
|
+
});
|
|
1014
|
+
const markerS3Client = new S3Client({
|
|
1015
|
+
region,
|
|
1016
|
+
...options.profile && { profile: options.profile }
|
|
1017
|
+
});
|
|
1018
|
+
const stateBackend = new S3StateBackend(markerS3Client, {
|
|
1019
|
+
bucket: bucketName,
|
|
1020
|
+
prefix: "cdkd"
|
|
1021
|
+
}, {
|
|
1022
|
+
region,
|
|
1023
|
+
...options.profile && { profile: options.profile }
|
|
1024
|
+
});
|
|
1025
|
+
try {
|
|
1026
|
+
assetStorage = await ensureAssetStorage({
|
|
1027
|
+
s3Client,
|
|
1028
|
+
ecrClient,
|
|
1029
|
+
stateBackend,
|
|
1030
|
+
accountId,
|
|
1031
|
+
region,
|
|
1032
|
+
force: options.force
|
|
1033
|
+
});
|
|
1034
|
+
} finally {
|
|
1035
|
+
ecrClient.destroy();
|
|
1036
|
+
markerS3Client.destroy();
|
|
1037
|
+
}
|
|
1038
|
+
} else logger.info("\n--no-assets specified — skipping cdkd asset storage. Deploys in this region keep publishing assets to the CDK bootstrap bucket/repo.");
|
|
694
1039
|
logger.info("\n✓ Bootstrap completed successfully");
|
|
695
1040
|
logger.info(`\nState bucket: ${bucketName}`);
|
|
1041
|
+
if (assetStorage) {
|
|
1042
|
+
logger.info(`Asset bucket: ${assetStorage.assetBucket}`);
|
|
1043
|
+
logger.info(`Container-asset repository: ${assetStorage.containerRepo}`);
|
|
1044
|
+
}
|
|
696
1045
|
logger.info(`Region: ${region}`);
|
|
1046
|
+
if (assetStorage) logger.info(`\ncdkd asset storage is now ON for region ${region}: deploys in this region publish assets to the cdkd-owned bucket/repo (out of 'cdk gc' reach) instead of the CDK bootstrap storage. The first deploy of each existing stack with assets will show a one-time UPDATE re-pointing asset references — content is identical, no replacement.`);
|
|
697
1047
|
logger.info("\nYou can now use cdkd deploy with:");
|
|
698
1048
|
logger.info(` --state-bucket ${bucketName}`);
|
|
699
1049
|
logger.info(` --region ${region}`);
|
|
@@ -705,7 +1055,7 @@ async function bootstrapCommand(options) {
|
|
|
705
1055
|
* Create bootstrap command
|
|
706
1056
|
*/
|
|
707
1057
|
function createBootstrapCommand() {
|
|
708
|
-
const cmd = new Command("bootstrap").description("Bootstrap cdkd by creating
|
|
1058
|
+
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", "Force reconfiguration of existing bucket", false).option("--no-assets", "Skip cdkd asset storage (asset bucket / ECR repo / marker) — deploys keep publishing assets to the CDK bootstrap bucket/repo").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(bootstrapCommand));
|
|
709
1059
|
commonOptions.forEach((opt) => cmd.addOption(opt));
|
|
710
1060
|
return cmd;
|
|
711
1061
|
}
|
|
@@ -1445,7 +1795,7 @@ const FLUSH_INTERVAL_MS = 2e3;
|
|
|
1445
1795
|
const FLUSH_EVENT_THRESHOLD = 50;
|
|
1446
1796
|
/** Build-time cdkd version, with a dev fallback for non-built contexts. */
|
|
1447
1797
|
function getCdkdVersion() {
|
|
1448
|
-
return "0.
|
|
1798
|
+
return "0.232.0";
|
|
1449
1799
|
}
|
|
1450
1800
|
/**
|
|
1451
1801
|
* Generate a time-sortable unique run id, e.g.
|
|
@@ -37644,6 +37994,7 @@ async function deployCommand(stacks, options) {
|
|
|
37644
37994
|
const accountId = (await stsClient.send(new GetCallerIdentityCommand({}))).Account;
|
|
37645
37995
|
stsClient.destroy();
|
|
37646
37996
|
const assetPublisher = new AssetPublisher();
|
|
37997
|
+
const assetModeResolver = new AssetModeResolver(preflightStateBackend, accountId, { ...options.profile && { profile: options.profile } });
|
|
37647
37998
|
const stateConfig = {
|
|
37648
37999
|
bucket: stateBucket,
|
|
37649
38000
|
prefix: options.statePrefix
|
|
@@ -37660,17 +38011,24 @@ async function deployCommand(stacks, options) {
|
|
|
37660
38011
|
for (const stack of targetStacks) {
|
|
37661
38012
|
const stackNodeId = `stack:${stack.stackName}`;
|
|
37662
38013
|
const stackDeps = /* @__PURE__ */ new Set();
|
|
37663
|
-
if (!options.skipAssets && stack.assetManifestPath)
|
|
38014
|
+
if (!options.skipAssets && stack.assetManifestPath) {
|
|
37664
38015
|
const assetRegion = stack.region || baseRegion;
|
|
37665
|
-
|
|
37666
|
-
|
|
37667
|
-
|
|
37668
|
-
|
|
37669
|
-
|
|
37670
|
-
|
|
38016
|
+
let nodeIds = [];
|
|
38017
|
+
try {
|
|
38018
|
+
nodeIds = assetPublisher.addAssetsToGraph(workGraph, stack.assetManifestPath, {
|
|
38019
|
+
accountId,
|
|
38020
|
+
region: assetRegion,
|
|
38021
|
+
...options.profile && { profile: options.profile },
|
|
38022
|
+
nodePrefix: `${stack.stackName}:`
|
|
38023
|
+
});
|
|
38024
|
+
} catch (error) {
|
|
38025
|
+
if (error.code !== "ENOENT") throw error;
|
|
38026
|
+
}
|
|
38027
|
+
if (nodeIds.length > 0) {
|
|
38028
|
+
const assetMode = await assetModeResolver.resolve(assetRegion);
|
|
38029
|
+
logger.debug(`Asset mode for region ${assetRegion}: ${assetMode.mode} (stack ${stack.stackName})`);
|
|
38030
|
+
}
|
|
37671
38031
|
for (const id of nodeIds) stackDeps.add(id);
|
|
37672
|
-
} catch (error) {
|
|
37673
|
-
if (error.code !== "ENOENT") throw error;
|
|
37674
38032
|
}
|
|
37675
38033
|
for (const depName of effectiveStackDeps(stack.stackName, stack.dependencyNames)) if (stackMap.has(depName)) stackDeps.add(`stack:${depName}`);
|
|
37676
38034
|
workGraph.addNode({
|
|
@@ -45188,6 +45546,45 @@ async function readSchemaVersion(awsClients, bucket, keys) {
|
|
|
45188
45546
|
}
|
|
45189
45547
|
}
|
|
45190
45548
|
/**
|
|
45549
|
+
* List every region's bootstrap marker under `cdkd-bootstrap/` in the state
|
|
45550
|
+
* bucket. Malformed markers are skipped with a warning — `state info` is a
|
|
45551
|
+
* cosmetic command and should not crash on an unexpected payload (deploy
|
|
45552
|
+
* hard-errors on the same marker instead).
|
|
45553
|
+
*/
|
|
45554
|
+
async function listAssetStorageMarkers(awsClients, bucket) {
|
|
45555
|
+
const logger = getLogger();
|
|
45556
|
+
const entries = [];
|
|
45557
|
+
let continuationToken;
|
|
45558
|
+
const keys = [];
|
|
45559
|
+
do {
|
|
45560
|
+
const resp = await awsClients.s3.send(new ListObjectsV2Command({
|
|
45561
|
+
Bucket: bucket,
|
|
45562
|
+
Prefix: BOOTSTRAP_MARKER_PREFIX,
|
|
45563
|
+
...continuationToken && { ContinuationToken: continuationToken }
|
|
45564
|
+
}));
|
|
45565
|
+
for (const obj of resp.Contents ?? []) if (typeof obj.Key === "string" && obj.Key.startsWith("cdkd-bootstrap/") && obj.Key.endsWith(".json")) keys.push(obj.Key);
|
|
45566
|
+
continuationToken = resp.NextContinuationToken;
|
|
45567
|
+
} while (continuationToken);
|
|
45568
|
+
for (const key of keys) {
|
|
45569
|
+
const region = key.slice(BOOTSTRAP_MARKER_PREFIX.length, -5);
|
|
45570
|
+
try {
|
|
45571
|
+
const marker = parseBootstrapMarker(await (await awsClients.s3.send(new GetObjectCommand({
|
|
45572
|
+
Bucket: bucket,
|
|
45573
|
+
Key: key
|
|
45574
|
+
}))).Body?.transformToString() ?? "", key);
|
|
45575
|
+
entries.push({
|
|
45576
|
+
region,
|
|
45577
|
+
assetBucket: marker.assetBucket,
|
|
45578
|
+
containerRepo: marker.containerRepo,
|
|
45579
|
+
createdAt: marker.createdAt
|
|
45580
|
+
});
|
|
45581
|
+
} catch (error) {
|
|
45582
|
+
logger.warn(`Skipping malformed/unreadable bootstrap marker '${key}': ${error.message}`);
|
|
45583
|
+
}
|
|
45584
|
+
}
|
|
45585
|
+
return entries.sort((a, b) => a.region.localeCompare(b.region));
|
|
45586
|
+
}
|
|
45587
|
+
/**
|
|
45191
45588
|
* `cdkd state info` command implementation.
|
|
45192
45589
|
*
|
|
45193
45590
|
* Prints the state-bucket information that used to appear as a banner on
|
|
@@ -45229,6 +45626,7 @@ async function stateInfoCommand(options) {
|
|
|
45229
45626
|
const detectedRegion = await detectBucketRegion(awsClients, bucket);
|
|
45230
45627
|
const stateFileKeys = await listStateFileKeys(awsClients, bucket, prefix);
|
|
45231
45628
|
const schemaVersion = await readSchemaVersion(awsClients, bucket, stateFileKeys);
|
|
45629
|
+
const assetStorage = await listAssetStorageMarkers(awsClients, bucket);
|
|
45232
45630
|
if (options.json) {
|
|
45233
45631
|
const json = {
|
|
45234
45632
|
bucket,
|
|
@@ -45236,7 +45634,8 @@ async function stateInfoCommand(options) {
|
|
|
45236
45634
|
regionSource: detectedRegion ? "auto-detected" : "unknown",
|
|
45237
45635
|
bucketSource: resolved.source,
|
|
45238
45636
|
schemaVersion,
|
|
45239
|
-
stackCount: stateFileKeys.length
|
|
45637
|
+
stackCount: stateFileKeys.length,
|
|
45638
|
+
assetStorage
|
|
45240
45639
|
};
|
|
45241
45640
|
process.stdout.write(`${JSON.stringify(json, null, 2)}\n`);
|
|
45242
45641
|
return;
|
|
@@ -45248,6 +45647,11 @@ async function stateInfoCommand(options) {
|
|
|
45248
45647
|
lines.push(`Source: ${formatBucketSource(resolved.source)}`);
|
|
45249
45648
|
lines.push(`Schema version: ${schemaVersion}`);
|
|
45250
45649
|
lines.push(`Stacks: ${stateFileKeys.length}`);
|
|
45650
|
+
if (assetStorage.length === 0) lines.push("Asset storage: legacy (CDK bootstrap) — run cdkd bootstrap to opt in");
|
|
45651
|
+
else {
|
|
45652
|
+
lines.push(`Asset storage: cdkd-assets mode in ${assetStorage.length} region(s)`);
|
|
45653
|
+
for (const entry of assetStorage) lines.push(` ${entry.region}: ${entry.assetBucket} / ${entry.containerRepo}`);
|
|
45654
|
+
}
|
|
45251
45655
|
process.stdout.write(`${lines.join("\n")}\n`);
|
|
45252
45656
|
} finally {
|
|
45253
45657
|
awsClients.destroy();
|
|
@@ -55779,7 +56183,7 @@ function reorderArgs(argv) {
|
|
|
55779
56183
|
async function main() {
|
|
55780
56184
|
installPipeCloseHandler();
|
|
55781
56185
|
const program = new Command();
|
|
55782
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
56186
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.232.0");
|
|
55783
56187
|
program.addCommand(createBootstrapCommand());
|
|
55784
56188
|
program.addCommand(createSynthCommand());
|
|
55785
56189
|
program.addCommand(createListCommand());
|