@go-to-k/cdkd 0.133.0 → 0.135.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 +1118 -32
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-Dn7oV5rA.js → deploy-engine-CX1x5ug1.js} +34 -2
- package/dist/deploy-engine-CX1x5ug1.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-Dn7oV5rA.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { _ as withSkipPrefix, a as runDockerStreaming, c as getLogger, d as getLiveRenderer, f as PATTERN_B_NAME_PROPERTIES, g as generateResourceNameWithFallback, h as generateResourceName, i as runDockerForeground, n as formatDockerLoginError, p as PATTERN_B_RESOURCE_TYPES, r as getDockerCmd, u as runStackBuffered, v as withStackName } from "./docker-cmd-EtWSTAje.js";
|
|
3
|
-
import {
|
|
3
|
+
import { A as AssetPublisher, B as resolveStateBucketWithDefault, C as applyRoleArnIfSet, D as LockManager, E as TemplateParser, F as getDefaultStateBucketName, G as resolveBucketRegion, H as warnDeprecatedNoPrefixCliFlag, I as getLegacyStateBucketName, L as resolveApp, M as WorkGraph, N as buildDockerImage, O as S3StateBackend, P as Synthesizer, Q as LocalStartServiceError, R as resolveCaptureObservedState, S as IntrinsicFunctionResolver, T as DagBuilder, U as AssemblyReader, V as resolveStateBucketWithDefaultAndSource, X as LocalInvokeBuildError, Z as LocalMigrateError, _ as normalizeAwsTagsToCfn, a as withRetry, at as RouteDiscoveryError, b as CloudControlProvider, c as cyan, d as red, et as MissingCdkCliError, f as yellow, ft as normalizeAwsError, g as matchesCdkPath, h as CDK_PATH_TAG, i as withResourceDeadline, it as ResourceUpdateNotSupportedError, j as stringifyValue, k as shouldRetainResource, l as gray, m as collectInlinePolicyNamesManagedBySiblings, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as ProvisioningError, o as IMPLICIT_DELETE_DEPENDENCIES, ot as StackHasActiveImportsError, p as IAMRoleProvider, pt as withErrorHandling, q as CdkdError, r as DeployEngine, rt as ResourceTimeoutError, s as bold, st as StackTerminationProtectionError, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as PartialFailureError, u as green, v as resolveExplicitPhysicalId, w as DiffCalculator, x as assertRegionMatch, y as ProviderRegistry, z as resolveSkipPrefix } from "./deploy-engine-CX1x5ug1.js";
|
|
4
4
|
import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-BF03Alpe.js";
|
|
5
5
|
import { createHash, createHmac, createPublicKey, createVerify, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
6
6
|
import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListBucketsCommand, ListDirectoryBucketsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
|
|
@@ -20,7 +20,7 @@ import { CloudFrontClient, CreateCloudFrontOriginAccessIdentityCommand, CreateDi
|
|
|
20
20
|
import { CloudWatchClient, DeleteAlarmsCommand, DescribeAlarmsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$4, PutMetricAlarmCommand, TagResourceCommand as TagResourceCommand$6, UntagResourceCommand as UntagResourceCommand$6 } from "@aws-sdk/client-cloudwatch";
|
|
21
21
|
import { CloudWatchLogsClient, CreateLogGroupCommand, DeleteDataProtectionPolicyCommand, DeleteIndexPolicyCommand, DeleteLogGroupCommand, DeleteRetentionPolicyCommand, DescribeIndexPoliciesCommand, DescribeLogGroupsCommand, GetDataProtectionPolicyCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$5, PutBearerTokenAuthenticationCommand, PutDataProtectionPolicyCommand, PutIndexPolicyCommand, PutLogGroupDeletionProtectionCommand, PutRetentionPolicyCommand, ResourceAlreadyExistsException, ResourceNotFoundException as ResourceNotFoundException$4, TagResourceCommand as TagResourceCommand$7, UntagResourceCommand as UntagResourceCommand$7 } from "@aws-sdk/client-cloudwatch-logs";
|
|
22
22
|
import { BedrockAgentCoreControlClient, CreateAgentRuntimeCommand, DeleteAgentRuntimeCommand, GetAgentRuntimeCommand, ResourceNotFoundException as ResourceNotFoundException$5, UpdateAgentRuntimeCommand } from "@aws-sdk/client-bedrock-agentcore-control";
|
|
23
|
-
import { cpSync, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { cpSync, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
24
24
|
import * as path from "node:path";
|
|
25
25
|
import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
|
|
26
26
|
import { execFile, spawn } from "node:child_process";
|
|
@@ -34750,6 +34750,31 @@ function createStateCommand() {
|
|
|
34750
34750
|
return cmd;
|
|
34751
34751
|
}
|
|
34752
34752
|
|
|
34753
|
+
//#endregion
|
|
34754
|
+
//#region src/cli/cfn-stack-states.ts
|
|
34755
|
+
/**
|
|
34756
|
+
* Stable terminal CloudFormation stack states.
|
|
34757
|
+
*
|
|
34758
|
+
* Sourced from the AWS CloudFormation API documentation: a stack in any
|
|
34759
|
+
* of these states has settled (success or rolled-back) and is safe to
|
|
34760
|
+
* read / mutate. Every other status (`*_IN_PROGRESS`, `*_FAILED`,
|
|
34761
|
+
* `REVIEW_IN_PROGRESS`) means the stack is mid-operation or in an
|
|
34762
|
+
* unhealthy state — callers gate AWS-side mutations behind this set so
|
|
34763
|
+
* the user can settle the source before paying for further work.
|
|
34764
|
+
*
|
|
34765
|
+
* Single source of truth — consumed by both `cdkd migrate`'s pre-flight
|
|
34766
|
+
* check (`src/cli/commands/migrate/cfn-stack-prefetch.ts`) and the
|
|
34767
|
+
* `cdkd import --migrate-from-cloudformation` retirement flow
|
|
34768
|
+
* (`src/cli/commands/retire-cfn-stack.ts`).
|
|
34769
|
+
*/
|
|
34770
|
+
const STABLE_TERMINAL_STATUSES = new Set([
|
|
34771
|
+
"CREATE_COMPLETE",
|
|
34772
|
+
"UPDATE_COMPLETE",
|
|
34773
|
+
"UPDATE_ROLLBACK_COMPLETE",
|
|
34774
|
+
"IMPORT_COMPLETE",
|
|
34775
|
+
"IMPORT_ROLLBACK_COMPLETE"
|
|
34776
|
+
]);
|
|
34777
|
+
|
|
34753
34778
|
//#endregion
|
|
34754
34779
|
//#region src/cli/upload-cfn-template.ts
|
|
34755
34780
|
/**
|
|
@@ -35191,18 +35216,6 @@ function parseCfnTemplateWithFormat(text) {
|
|
|
35191
35216
|
//#endregion
|
|
35192
35217
|
//#region src/cli/commands/retire-cfn-stack.ts
|
|
35193
35218
|
/**
|
|
35194
|
-
* Stack states from which an UpdateStack call is safe. Anything else (an
|
|
35195
|
-
* IN_PROGRESS, FAILED, or REVIEW_IN_PROGRESS state) means the stack is
|
|
35196
|
-
* mid-operation or in an unhealthy state we should not touch.
|
|
35197
|
-
*/
|
|
35198
|
-
const STABLE_TERMINAL_STATUSES = new Set([
|
|
35199
|
-
"CREATE_COMPLETE",
|
|
35200
|
-
"UPDATE_COMPLETE",
|
|
35201
|
-
"UPDATE_ROLLBACK_COMPLETE",
|
|
35202
|
-
"IMPORT_COMPLETE",
|
|
35203
|
-
"IMPORT_ROLLBACK_COMPLETE"
|
|
35204
|
-
]);
|
|
35205
|
-
/**
|
|
35206
35219
|
* UpdateStack TemplateBody hard limit (51,200 bytes). Templates larger than
|
|
35207
35220
|
* this are uploaded to cdkd's state S3 bucket and submitted via `TemplateURL`
|
|
35208
35221
|
* instead — see {@link uploadTemplateForUpdateStack}.
|
|
@@ -35291,10 +35304,15 @@ async function retireCloudFormationStack(options) {
|
|
|
35291
35304
|
logger.info(`[3/4] Updating CloudFormation stack with Retain policies...`);
|
|
35292
35305
|
let updateRan = false;
|
|
35293
35306
|
try {
|
|
35307
|
+
const previousParameters = (stack.Parameters ?? []).map((p) => ({
|
|
35308
|
+
ParameterKey: p.ParameterKey,
|
|
35309
|
+
UsePreviousValue: true
|
|
35310
|
+
}));
|
|
35294
35311
|
await cfnClient.send(new UpdateStackCommand({
|
|
35295
35312
|
StackName: cfnStackName,
|
|
35296
35313
|
...updateInput,
|
|
35297
|
-
Capabilities: capabilities
|
|
35314
|
+
Capabilities: capabilities,
|
|
35315
|
+
...previousParameters.length > 0 && { Parameters: previousParameters }
|
|
35298
35316
|
}));
|
|
35299
35317
|
updateRan = true;
|
|
35300
35318
|
} catch (err) {
|
|
@@ -35435,6 +35453,9 @@ async function confirmPrompt$2(prompt) {
|
|
|
35435
35453
|
|
|
35436
35454
|
//#endregion
|
|
35437
35455
|
//#region src/cli/commands/import.ts
|
|
35456
|
+
async function runImport(stackArg, options) {
|
|
35457
|
+
return importCommand(stackArg, options);
|
|
35458
|
+
}
|
|
35438
35459
|
async function importCommand(stackArg, options) {
|
|
35439
35460
|
const logger = getLogger();
|
|
35440
35461
|
if (options.verbose) {
|
|
@@ -38748,7 +38769,7 @@ function resolveRuntimeCodeMountPath(runtime) {
|
|
|
38748
38769
|
|
|
38749
38770
|
//#endregion
|
|
38750
38771
|
//#region src/local/docker-runner.ts
|
|
38751
|
-
const execFileAsync$
|
|
38772
|
+
const execFileAsync$3 = promisify(execFile);
|
|
38752
38773
|
/**
|
|
38753
38774
|
* Wraps `docker pull` / `docker run` / `docker rm` for `cdkd local invoke`.
|
|
38754
38775
|
*
|
|
@@ -38840,7 +38861,7 @@ async function runDetached(opts) {
|
|
|
38840
38861
|
args.push(opts.image, ...entryPointTail, ...opts.cmd);
|
|
38841
38862
|
getLogger().child("docker").debug(`${getDockerCmd()} ${redactAwsCredentialsInArgs(args).join(" ")}`);
|
|
38842
38863
|
try {
|
|
38843
|
-
const { stdout } = await execFileAsync$
|
|
38864
|
+
const { stdout } = await execFileAsync$3(getDockerCmd(), args, { maxBuffer: 10 * 1024 * 1024 });
|
|
38844
38865
|
return stdout.trim();
|
|
38845
38866
|
} catch (error) {
|
|
38846
38867
|
const err = error;
|
|
@@ -38877,7 +38898,7 @@ async function removeContainer(containerId) {
|
|
|
38877
38898
|
if (!containerId) return;
|
|
38878
38899
|
const logger = getLogger().child("docker");
|
|
38879
38900
|
try {
|
|
38880
|
-
await execFileAsync$
|
|
38901
|
+
await execFileAsync$3(getDockerCmd(), [
|
|
38881
38902
|
"rm",
|
|
38882
38903
|
"-f",
|
|
38883
38904
|
containerId
|
|
@@ -38896,7 +38917,7 @@ async function removeContainer(containerId) {
|
|
|
38896
38917
|
async function ensureDockerAvailable() {
|
|
38897
38918
|
const cmd = getDockerCmd();
|
|
38898
38919
|
try {
|
|
38899
|
-
await execFileAsync$
|
|
38920
|
+
await execFileAsync$3(cmd, [
|
|
38900
38921
|
"version",
|
|
38901
38922
|
"--format",
|
|
38902
38923
|
"{{.Server.Version}}"
|
|
@@ -48039,7 +48060,7 @@ function createLocalStartApiCommand() {
|
|
|
48039
48060
|
|
|
48040
48061
|
//#endregion
|
|
48041
48062
|
//#region src/local/ecs-network.ts
|
|
48042
|
-
const execFileAsync$
|
|
48063
|
+
const execFileAsync$2 = promisify(execFile);
|
|
48043
48064
|
/**
|
|
48044
48065
|
* Docker network + AWS-published metadata-endpoints sidecar lifecycle for
|
|
48045
48066
|
* `cdkd local run-task`. The sidecar (a small Go binary maintained by
|
|
@@ -48102,7 +48123,7 @@ async function createTaskNetwork(options = {}) {
|
|
|
48102
48123
|
await pullImage(METADATA_ENDPOINT_IMAGE, options.skipPull ?? false);
|
|
48103
48124
|
logger.info(`Creating docker network ${networkName} (subnet ${cidr})...`);
|
|
48104
48125
|
try {
|
|
48105
|
-
await execFileAsync$
|
|
48126
|
+
await execFileAsync$2(getDockerCmd(), [
|
|
48106
48127
|
"network",
|
|
48107
48128
|
"create",
|
|
48108
48129
|
"--driver",
|
|
@@ -48138,7 +48159,7 @@ async function createTaskNetwork(options = {}) {
|
|
|
48138
48159
|
logger.info(`Starting ECS local-container-endpoints sidecar at ${sidecarIp}...`);
|
|
48139
48160
|
let sidecarContainerId;
|
|
48140
48161
|
try {
|
|
48141
|
-
const { stdout } = await execFileAsync$
|
|
48162
|
+
const { stdout } = await execFileAsync$2(getDockerCmd(), sidecarArgs, { maxBuffer: 10 * 1024 * 1024 });
|
|
48142
48163
|
sidecarContainerId = stdout.trim();
|
|
48143
48164
|
} catch (err) {
|
|
48144
48165
|
await destroyNetworkOnly(networkName);
|
|
@@ -48186,7 +48207,7 @@ async function destroyNetworkOnly(networkName) {
|
|
|
48186
48207
|
if (!networkName) return;
|
|
48187
48208
|
const logger = getLogger().child("ecs-network");
|
|
48188
48209
|
try {
|
|
48189
|
-
await execFileAsync$
|
|
48210
|
+
await execFileAsync$2(getDockerCmd(), [
|
|
48190
48211
|
"network",
|
|
48191
48212
|
"rm",
|
|
48192
48213
|
networkName
|
|
@@ -48319,7 +48340,7 @@ async function resolveSsm(entry, shape, client) {
|
|
|
48319
48340
|
|
|
48320
48341
|
//#endregion
|
|
48321
48342
|
//#region src/local/ecs-task-runner.ts
|
|
48322
|
-
const execFileAsync = promisify(execFile);
|
|
48343
|
+
const execFileAsync$1 = promisify(execFile);
|
|
48323
48344
|
/**
|
|
48324
48345
|
* Top-level orchestrator for `cdkd local run-task`. Coordinates image
|
|
48325
48346
|
* preparation, secret resolution, docker-network bring-up, container
|
|
@@ -48379,7 +48400,7 @@ async function cleanupEcsRun(state, options) {
|
|
|
48379
48400
|
await destroyTaskNetwork(state.network);
|
|
48380
48401
|
state.network = void 0;
|
|
48381
48402
|
for (const v of state.dockerVolumeNames) try {
|
|
48382
|
-
await execFileAsync(getDockerCmd(), [
|
|
48403
|
+
await execFileAsync$1(getDockerCmd(), [
|
|
48383
48404
|
"volume",
|
|
48384
48405
|
"rm",
|
|
48385
48406
|
v
|
|
@@ -48445,7 +48466,7 @@ async function runEcsTask(task, options, state) {
|
|
|
48445
48466
|
logger.info(`Starting container '${container.name}' (image=${imagePlan.get(container.name)})`);
|
|
48446
48467
|
let id;
|
|
48447
48468
|
try {
|
|
48448
|
-
const { stdout } = await execFileAsync(getDockerCmd(), args, { maxBuffer: 10 * 1024 * 1024 });
|
|
48469
|
+
const { stdout } = await execFileAsync$1(getDockerCmd(), args, { maxBuffer: 10 * 1024 * 1024 });
|
|
48449
48470
|
id = stdout.trim();
|
|
48450
48471
|
} catch (err) {
|
|
48451
48472
|
const e = err;
|
|
@@ -48554,7 +48575,7 @@ async function waitForContainerHealthy(containerId, displayName) {
|
|
|
48554
48575
|
let lastStatus = "";
|
|
48555
48576
|
while (Date.now() < deadline) {
|
|
48556
48577
|
try {
|
|
48557
|
-
const { stdout } = await execFileAsync(getDockerCmd(), [
|
|
48578
|
+
const { stdout } = await execFileAsync$1(getDockerCmd(), [
|
|
48558
48579
|
"inspect",
|
|
48559
48580
|
"--format",
|
|
48560
48581
|
"{{.State.Health.Status}}",
|
|
@@ -48577,7 +48598,7 @@ async function waitForContainerHealthy(containerId, displayName) {
|
|
|
48577
48598
|
}
|
|
48578
48599
|
async function waitForContainerExit(containerId) {
|
|
48579
48600
|
try {
|
|
48580
|
-
const { stdout } = await execFileAsync(getDockerCmd(), ["wait", containerId], { maxBuffer: 1024 * 1024 });
|
|
48601
|
+
const { stdout } = await execFileAsync$1(getDockerCmd(), ["wait", containerId], { maxBuffer: 1024 * 1024 });
|
|
48581
48602
|
const code = Number.parseInt(stdout.trim(), 10);
|
|
48582
48603
|
return Number.isFinite(code) ? code : 1;
|
|
48583
48604
|
} catch (err) {
|
|
@@ -48587,7 +48608,7 @@ async function waitForContainerExit(containerId) {
|
|
|
48587
48608
|
}
|
|
48588
48609
|
async function stopContainer(containerId, graceSeconds) {
|
|
48589
48610
|
try {
|
|
48590
|
-
await execFileAsync(getDockerCmd(), [
|
|
48611
|
+
await execFileAsync$1(getDockerCmd(), [
|
|
48591
48612
|
"stop",
|
|
48592
48613
|
"-t",
|
|
48593
48614
|
String(graceSeconds),
|
|
@@ -48712,7 +48733,7 @@ async function realizeDockerVolumes(volumes, state) {
|
|
|
48712
48733
|
const dockerVolumeName = `cdkd-local-${v.name}-${randHex(4)}`;
|
|
48713
48734
|
args.push(dockerVolumeName);
|
|
48714
48735
|
try {
|
|
48715
|
-
await execFileAsync(getDockerCmd(), args);
|
|
48736
|
+
await execFileAsync$1(getDockerCmd(), args);
|
|
48716
48737
|
state.dockerVolumeNames.push(dockerVolumeName);
|
|
48717
48738
|
logger.debug(`Created docker volume ${dockerVolumeName} for task volume '${v.name}'`);
|
|
48718
48739
|
} catch (err) {
|
|
@@ -51846,6 +51867,1069 @@ function createExportCommand() {
|
|
|
51846
51867
|
return cmd;
|
|
51847
51868
|
}
|
|
51848
51869
|
|
|
51870
|
+
//#endregion
|
|
51871
|
+
//#region src/cli/commands/migrate/cdk-cli-check.ts
|
|
51872
|
+
const execFileAsync = promisify(execFile);
|
|
51873
|
+
/**
|
|
51874
|
+
* Minimum aws-cdk CLI version where `cdk migrate --from-stack` is
|
|
51875
|
+
* considered stable (per the design doc at docs/design/465-cfn-migrate.md
|
|
51876
|
+
* §4 — "Version requirement"). Below this we WARN but do not block,
|
|
51877
|
+
* because users may have a working older release and a hard rejection
|
|
51878
|
+
* would be more disruptive than a warning.
|
|
51879
|
+
*/
|
|
51880
|
+
const RECOMMENDED_MIN_VERSION = "2.124.0";
|
|
51881
|
+
/**
|
|
51882
|
+
* Verify that the upstream `cdk` CLI is available on PATH (or at the
|
|
51883
|
+
* override path passed via `--cdk-bin`) and report its version.
|
|
51884
|
+
*
|
|
51885
|
+
* Hard-errors with {@link MissingCdkCliError} when `cdk` cannot be
|
|
51886
|
+
* spawned (ENOENT, permission denied, etc.). Emits a `warn` field on
|
|
51887
|
+
* the result when the version string is below the recommended minimum
|
|
51888
|
+
* — the caller decides how to surface it.
|
|
51889
|
+
*
|
|
51890
|
+
* `cdk --version` output format empirically (verified 2026-05-22
|
|
51891
|
+
* against cdk@2.1112.0): `<MAJOR.MINOR.PATCH> (build <id>)`. The
|
|
51892
|
+
* version is the first whitespace-separated token; the trailing
|
|
51893
|
+
* `(build <id>)` is informational and ignored.
|
|
51894
|
+
*
|
|
51895
|
+
* @param cdkBinPath - Path to the `cdk` binary. Defaults to `'cdk'`
|
|
51896
|
+
* which uses the system PATH for resolution.
|
|
51897
|
+
*/
|
|
51898
|
+
async function verifyCdkCliAvailable(cdkBinPath = "cdk") {
|
|
51899
|
+
let stdout;
|
|
51900
|
+
try {
|
|
51901
|
+
stdout = (await execFileAsync(cdkBinPath, ["--version"])).stdout ?? "";
|
|
51902
|
+
} catch (err) {
|
|
51903
|
+
throw new MissingCdkCliError(`Failed to run '${cdkBinPath} --version': ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0);
|
|
51904
|
+
}
|
|
51905
|
+
const version = parseCdkVersion(stdout);
|
|
51906
|
+
if (!version) throw new MissingCdkCliError(`'${cdkBinPath} --version' produced unexpected output: ${JSON.stringify(stdout.trim())}`);
|
|
51907
|
+
if (compareSemver(version, RECOMMENDED_MIN_VERSION) < 0) return {
|
|
51908
|
+
version,
|
|
51909
|
+
warn: `cdk CLI version ${version} is older than the recommended minimum ${RECOMMENDED_MIN_VERSION}. 'cdkd migrate' relies on the stabilized 'cdk migrate --from-stack' flag; upgrade with 'npm install -g aws-cdk@latest' if you hit codegen issues.`
|
|
51910
|
+
};
|
|
51911
|
+
return { version };
|
|
51912
|
+
}
|
|
51913
|
+
/**
|
|
51914
|
+
* Parse the first semver-shaped token (`<MAJOR>.<MINOR>.<PATCH>`) out of
|
|
51915
|
+
* `cdk --version` stdout. Tolerates a trailing `(build <id>)` suffix.
|
|
51916
|
+
*
|
|
51917
|
+
* Returns `undefined` when no semver-shaped token is present so the
|
|
51918
|
+
* caller can surface the raw stdout in the error message.
|
|
51919
|
+
*/
|
|
51920
|
+
function parseCdkVersion(stdout) {
|
|
51921
|
+
const match = stdout.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
51922
|
+
if (!match) return void 0;
|
|
51923
|
+
return `${match[1]}.${match[2]}.${match[3]}`;
|
|
51924
|
+
}
|
|
51925
|
+
/**
|
|
51926
|
+
* Compare two semver strings (`MAJOR.MINOR.PATCH`). Returns a negative
|
|
51927
|
+
* number when `a < b`, zero when `a === b`, positive when `a > b`.
|
|
51928
|
+
*
|
|
51929
|
+
* Strictly numeric comparison per component — no pre-release / build
|
|
51930
|
+
* suffix handling needed for this use case (`cdk --version` always
|
|
51931
|
+
* emits a stable `X.Y.Z` triple).
|
|
51932
|
+
*/
|
|
51933
|
+
function compareSemver(a, b) {
|
|
51934
|
+
const aParts = a.split(".").map((n) => Number.parseInt(n, 10));
|
|
51935
|
+
const bParts = b.split(".").map((n) => Number.parseInt(n, 10));
|
|
51936
|
+
for (let i = 0; i < 3; i++) {
|
|
51937
|
+
const aN = aParts[i] ?? 0;
|
|
51938
|
+
const bN = bParts[i] ?? 0;
|
|
51939
|
+
if (aN !== bN) return aN - bN;
|
|
51940
|
+
}
|
|
51941
|
+
return 0;
|
|
51942
|
+
}
|
|
51943
|
+
|
|
51944
|
+
//#endregion
|
|
51945
|
+
//#region src/cli/commands/migrate/cfn-stack-prefetch.ts
|
|
51946
|
+
/**
|
|
51947
|
+
* Resource types that `cdkd migrate` refuses to adopt under any
|
|
51948
|
+
* circumstances. The rationale per type:
|
|
51949
|
+
*
|
|
51950
|
+
* - `AWS::CloudFormation::CustomResource` — Lambda-backed Custom
|
|
51951
|
+
* Resources whose response protocol (cfn-response over a
|
|
51952
|
+
* pre-signed S3 URL) is incompatible with cdkd's import flow. The
|
|
51953
|
+
* backing Lambda's onCreate handler would need to be re-invoked
|
|
51954
|
+
* for a cdkd-side adoption to make sense, and that's structurally
|
|
51955
|
+
* different from the metadata-transfer migration this command
|
|
51956
|
+
* provides.
|
|
51957
|
+
*
|
|
51958
|
+
* - `AWS::CloudFormation::Stack` — nested stacks. cdkd has no
|
|
51959
|
+
* provider for this type, and the matching `cdk migrate` output
|
|
51960
|
+
* flattens nested stacks into separate generated apps in a way
|
|
51961
|
+
* that doesn't round-trip cleanly. Out of scope for #465.
|
|
51962
|
+
*
|
|
51963
|
+
* - `Custom::*` — any user-defined Custom Resource type prefix.
|
|
51964
|
+
* Same rationale as `AWS::CloudFormation::CustomResource`.
|
|
51965
|
+
*/
|
|
51966
|
+
const HARD_REJECT_RESOURCE_TYPES = new Set(["AWS::CloudFormation::CustomResource", "AWS::CloudFormation::Stack"]);
|
|
51967
|
+
/**
|
|
51968
|
+
* Fetch the source CFn stack's current state + resources + transform
|
|
51969
|
+
* info. Issues 3 read-only AWS API calls (`DescribeStacks`,
|
|
51970
|
+
* `DescribeStackResources`, `GetTemplate(Stage=Original)`); none of
|
|
51971
|
+
* them mutate AWS state. The result is fed into
|
|
51972
|
+
* {@link validatePrefetchResult} for the hard-reject check and
|
|
51973
|
+
* surfaced to {@link runMigrateLibrary}'s caller (PR B) as part of
|
|
51974
|
+
* `RunMigrateLibraryResult`.
|
|
51975
|
+
*
|
|
51976
|
+
* `DescribeStackResources` is preferred over `ListStackResources`
|
|
51977
|
+
* because the response is unpaginated up to 500 resources (CFn's hard
|
|
51978
|
+
* stack cap) — no pagination loop needed for the migration use case.
|
|
51979
|
+
*/
|
|
51980
|
+
async function prefetchCfnStack(stackName, cfnClient) {
|
|
51981
|
+
const stack = (await cfnClient.send(new DescribeStacksCommand({ StackName: stackName }))).Stacks?.[0];
|
|
51982
|
+
if (!stack) throw new LocalMigrateError(`CloudFormation stack '${stackName}' not found.`);
|
|
51983
|
+
const stackStatus = stack.StackStatus ?? "";
|
|
51984
|
+
const resourcesResp = await cfnClient.send(new DescribeStackResourcesCommand({ StackName: stackName }));
|
|
51985
|
+
const resources = [];
|
|
51986
|
+
for (const r of resourcesResp.StackResources ?? []) {
|
|
51987
|
+
if (!r.LogicalResourceId || !r.PhysicalResourceId || !r.ResourceType) continue;
|
|
51988
|
+
resources.push({
|
|
51989
|
+
LogicalResourceId: r.LogicalResourceId,
|
|
51990
|
+
PhysicalResourceId: r.PhysicalResourceId,
|
|
51991
|
+
ResourceType: r.ResourceType
|
|
51992
|
+
});
|
|
51993
|
+
}
|
|
51994
|
+
let transformInfo = {
|
|
51995
|
+
hasSamTransform: false,
|
|
51996
|
+
hasIncludeTransform: false
|
|
51997
|
+
};
|
|
51998
|
+
let sourceCfnTemplate = null;
|
|
51999
|
+
try {
|
|
52000
|
+
const tplResp = await cfnClient.send(new GetTemplateCommand({
|
|
52001
|
+
StackName: stackName,
|
|
52002
|
+
TemplateStage: "Original"
|
|
52003
|
+
}));
|
|
52004
|
+
if (tplResp.TemplateBody) {
|
|
52005
|
+
try {
|
|
52006
|
+
sourceCfnTemplate = parseCfnTemplate(tplResp.TemplateBody);
|
|
52007
|
+
} catch {
|
|
52008
|
+
sourceCfnTemplate = null;
|
|
52009
|
+
}
|
|
52010
|
+
transformInfo = detectTransformsFromParsed(sourceCfnTemplate);
|
|
52011
|
+
}
|
|
52012
|
+
} catch (err) {
|
|
52013
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
52014
|
+
getLogger().warn(`[migrate] GetTemplate failed for '${stackName}': ${detail}. Skipping transform detection.`);
|
|
52015
|
+
}
|
|
52016
|
+
return {
|
|
52017
|
+
stackStatus,
|
|
52018
|
+
resources,
|
|
52019
|
+
transformInfo,
|
|
52020
|
+
sourceCfnTemplate
|
|
52021
|
+
};
|
|
52022
|
+
}
|
|
52023
|
+
/**
|
|
52024
|
+
* Pre-flight reject when the source CFn stack contains any
|
|
52025
|
+
* un-migratable resource type OR is in a non-terminal state. Surfaces
|
|
52026
|
+
* SAM / Include transforms via the result's `transformInfo` instead —
|
|
52027
|
+
* the caller is responsible for emitting INFO logs.
|
|
52028
|
+
*
|
|
52029
|
+
* Per #465 Q2 (parent-session decision): every hard-reject case
|
|
52030
|
+
* routes the user to the standalone `cdk migrate --from-stack <name>
|
|
52031
|
+
* --output-dir ./tmp` flow so they can manually inspect the generated
|
|
52032
|
+
* code and decide how to proceed (e.g. delete the Custom Resource
|
|
52033
|
+
* from the source CFn stack before re-running cdkd migrate).
|
|
52034
|
+
*/
|
|
52035
|
+
function validatePrefetchResult(result) {
|
|
52036
|
+
if (!STABLE_TERMINAL_STATUSES.has(result.stackStatus)) throw new LocalMigrateError(`CloudFormation stack is in status '${result.stackStatus}', which is not a stable terminal state. Wait for the stack to settle (or roll back) before running 'cdkd migrate'.`);
|
|
52037
|
+
const offenders = [];
|
|
52038
|
+
for (const r of result.resources) if (isHardRejectType(r.ResourceType)) offenders.push({
|
|
52039
|
+
LogicalResourceId: r.LogicalResourceId,
|
|
52040
|
+
ResourceType: r.ResourceType
|
|
52041
|
+
});
|
|
52042
|
+
if (offenders.length === 0) return;
|
|
52043
|
+
throw new LocalMigrateError(`Source CloudFormation stack contains resource types that 'cdkd migrate' cannot adopt:\n${offenders.map((o) => ` - ${o.LogicalResourceId} (${o.ResourceType})`).join("\n")}\n\nLambda-backed Custom Resources and nested CloudFormation stacks are out of scope for #465.\nTo migrate the rest of the stack, either:\n 1. Delete the unsupported resources from the source CFn stack first, OR\n 2. Run upstream 'cdk migrate' standalone to inspect the generated code:\n cdk migrate --from-stack --stack-name <source-stack> --output-path ./tmp\n then hand-author CDK constructs to replace the Custom Resource semantics.`);
|
|
52044
|
+
}
|
|
52045
|
+
/**
|
|
52046
|
+
* Whether the given resource type is in the hard-reject set. Matches
|
|
52047
|
+
* literal types from {@link HARD_REJECT_RESOURCE_TYPES} AND the
|
|
52048
|
+
* `Custom::*` prefix pattern (user-defined Custom Resource types).
|
|
52049
|
+
*/
|
|
52050
|
+
function isHardRejectType(resourceType) {
|
|
52051
|
+
if (HARD_REJECT_RESOURCE_TYPES.has(resourceType)) return true;
|
|
52052
|
+
if (resourceType.startsWith("Custom::")) return true;
|
|
52053
|
+
return false;
|
|
52054
|
+
}
|
|
52055
|
+
/**
|
|
52056
|
+
* Scan an already-parsed CFn template object for stack-level
|
|
52057
|
+
* Transform blocks. Accepts both the scalar form (`Transform:
|
|
52058
|
+
* AWS::Serverless-2016-10-31`) and the array form (`Transform:
|
|
52059
|
+
* [AWS::Serverless-2016-10-31, ...]`).
|
|
52060
|
+
*
|
|
52061
|
+
* The caller is responsible for parsing the raw template body via the
|
|
52062
|
+
* CFn-aware codec; this helper operates on the parsed object so the
|
|
52063
|
+
* `GetTemplate` response can be parsed once and reused.
|
|
52064
|
+
*/
|
|
52065
|
+
function detectTransformsFromParsed(parsed) {
|
|
52066
|
+
if (!parsed || typeof parsed !== "object") return {
|
|
52067
|
+
hasSamTransform: false,
|
|
52068
|
+
hasIncludeTransform: false
|
|
52069
|
+
};
|
|
52070
|
+
const transformRaw = parsed["Transform"];
|
|
52071
|
+
const transforms = Array.isArray(transformRaw) ? transformRaw.map((t) => String(t)) : typeof transformRaw === "string" ? [transformRaw] : [];
|
|
52072
|
+
return {
|
|
52073
|
+
hasSamTransform: transforms.some((t) => t.startsWith("AWS::Serverless")),
|
|
52074
|
+
hasIncludeTransform: transforms.some((t) => t === "AWS::Include" || t.startsWith("AWS::Include"))
|
|
52075
|
+
};
|
|
52076
|
+
}
|
|
52077
|
+
|
|
52078
|
+
//#endregion
|
|
52079
|
+
//#region src/cli/commands/migrate/output-dir-guard.ts
|
|
52080
|
+
/**
|
|
52081
|
+
* Refuse to run `cdkd migrate` when the upstream `cdk migrate` would
|
|
52082
|
+
* write its output into an existing non-empty directory. `cdk migrate`
|
|
52083
|
+
* itself silently overwrites in that case; cdkd surfaces the collision
|
|
52084
|
+
* up-front with the exact recovery command so the user is not left
|
|
52085
|
+
* with a half-overwritten CDK app.
|
|
52086
|
+
*
|
|
52087
|
+
* Behavior matrix (`outputPath = <user-supplied dir>`, `stackName =
|
|
52088
|
+
* <CFn stack name>`; cdk writes to `<outputPath>/<stackName>`):
|
|
52089
|
+
* - target dir does not exist → OK
|
|
52090
|
+
* - target dir exists but is empty → OK (treated as fresh)
|
|
52091
|
+
* - target path is a file, not a directory → typed error
|
|
52092
|
+
* - target dir exists and contains entries → typed error with
|
|
52093
|
+
* the canonical
|
|
52094
|
+
* `rm -rf` recovery
|
|
52095
|
+
* command in the
|
|
52096
|
+
* message
|
|
52097
|
+
*
|
|
52098
|
+
* The parent `outputPath` is NOT checked — `cdk migrate` creates
|
|
52099
|
+
* `<outputPath>` itself if missing, so a non-existent parent is fine.
|
|
52100
|
+
*/
|
|
52101
|
+
function assertOutputDirAvailable(outputPath, stackName) {
|
|
52102
|
+
const targetDir = resolve(outputPath, stackName);
|
|
52103
|
+
if (!existsSync(targetDir)) return;
|
|
52104
|
+
if (!statSync(targetDir).isDirectory()) throw new LocalMigrateError(`Output path '${targetDir}' exists but is not a directory. Remove it (or pass --output-dir <NEW_PATH>) and retry:\n rm -f ${targetDir} && cdkd migrate --from-cfn-stack ${stackName}`);
|
|
52105
|
+
const entries = readdirSync(targetDir);
|
|
52106
|
+
if (entries.length === 0) return;
|
|
52107
|
+
throw new LocalMigrateError(`Output dir '${targetDir}' already exists and is non-empty (e.g. '${join(targetDir, entries[0])}'); remove it or pass --output-dir <NEW_PATH>:\n rm -rf ${targetDir} && cdkd migrate --from-cfn-stack ${stackName}`);
|
|
52108
|
+
}
|
|
52109
|
+
|
|
52110
|
+
//#endregion
|
|
52111
|
+
//#region src/cli/commands/migrate/cdk-migrate-spawn.ts
|
|
52112
|
+
/**
|
|
52113
|
+
* Spawn `cdk migrate --from-stack` as a subprocess and stream its
|
|
52114
|
+
* output through cdkd's logger.
|
|
52115
|
+
*
|
|
52116
|
+
* Why subprocess (not Node module import): the upstream `cdk` CLI is
|
|
52117
|
+
* a bundled CommonJS app with no stable public API; pinning a
|
|
52118
|
+
* runtime-injected `aws-cdk-lib` from cdkd is impractical. Subprocess
|
|
52119
|
+
* isolation also gives us a clean failure boundary — non-zero exit
|
|
52120
|
+
* surfaces as {@link LocalMigrateError} with the captured streams
|
|
52121
|
+
* folded into the error message.
|
|
52122
|
+
*
|
|
52123
|
+
* Streaming is preferred over a buffered `execFile` because `cdk
|
|
52124
|
+
* migrate` emits progress lines (downloading the resource schema,
|
|
52125
|
+
* synthesizing the L1 code for each resource) that users expect to
|
|
52126
|
+
* see in real time. Captured streams are still returned for caller
|
|
52127
|
+
* inspection.
|
|
52128
|
+
*/
|
|
52129
|
+
async function spawnCdkMigrate(opts) {
|
|
52130
|
+
const logger = getLogger();
|
|
52131
|
+
const cdkBin = opts.cdkBinPath ?? "cdk";
|
|
52132
|
+
const language = opts.language ?? "typescript";
|
|
52133
|
+
const outputDir = resolve(opts.outputPath, opts.stackName);
|
|
52134
|
+
const args = [
|
|
52135
|
+
"migrate",
|
|
52136
|
+
"--from-stack",
|
|
52137
|
+
"--stack-name",
|
|
52138
|
+
opts.fromStackName,
|
|
52139
|
+
"--output-path",
|
|
52140
|
+
opts.outputPath,
|
|
52141
|
+
"--language",
|
|
52142
|
+
language
|
|
52143
|
+
];
|
|
52144
|
+
if (opts.region) args.push("--region", opts.region);
|
|
52145
|
+
if (opts.account) args.push("--account", opts.account);
|
|
52146
|
+
if (opts.profile) args.push("--profile", opts.profile);
|
|
52147
|
+
for (const filter of opts.filters ?? []) args.push("--filter", filter);
|
|
52148
|
+
const env = {
|
|
52149
|
+
...process.env,
|
|
52150
|
+
...opts.extraEnv ?? {}
|
|
52151
|
+
};
|
|
52152
|
+
logger.info(`[cdk-migrate] ${cdkBin} ${args.join(" ")}`);
|
|
52153
|
+
return await new Promise((resolvePromise, rejectPromise) => {
|
|
52154
|
+
let stdout = "";
|
|
52155
|
+
let stderr = "";
|
|
52156
|
+
let child;
|
|
52157
|
+
try {
|
|
52158
|
+
child = spawn(cdkBin, args, {
|
|
52159
|
+
env,
|
|
52160
|
+
stdio: [
|
|
52161
|
+
"ignore",
|
|
52162
|
+
"pipe",
|
|
52163
|
+
"pipe"
|
|
52164
|
+
]
|
|
52165
|
+
});
|
|
52166
|
+
} catch (err) {
|
|
52167
|
+
rejectPromise(new LocalMigrateError(`Failed to spawn '${cdkBin} migrate': ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0));
|
|
52168
|
+
return;
|
|
52169
|
+
}
|
|
52170
|
+
child.stdout?.on("data", (chunk) => {
|
|
52171
|
+
const text = chunk.toString("utf-8");
|
|
52172
|
+
stdout += text;
|
|
52173
|
+
const trimmed = text.replace(/\n$/, "");
|
|
52174
|
+
if (trimmed) logger.info(`[cdk-migrate] ${trimmed}`);
|
|
52175
|
+
});
|
|
52176
|
+
child.stderr?.on("data", (chunk) => {
|
|
52177
|
+
const text = chunk.toString("utf-8");
|
|
52178
|
+
stderr += text;
|
|
52179
|
+
const trimmed = text.replace(/\n$/, "");
|
|
52180
|
+
if (trimmed) logger.warn(`[cdk-migrate] ${trimmed}`);
|
|
52181
|
+
});
|
|
52182
|
+
child.on("error", (err) => {
|
|
52183
|
+
rejectPromise(new LocalMigrateError(`'${cdkBin} migrate' subprocess error: ${err.message}`, err));
|
|
52184
|
+
});
|
|
52185
|
+
child.on("close", (code, signal) => {
|
|
52186
|
+
if (code === 0) {
|
|
52187
|
+
resolvePromise({
|
|
52188
|
+
outputDir,
|
|
52189
|
+
stdout,
|
|
52190
|
+
stderr
|
|
52191
|
+
});
|
|
52192
|
+
return;
|
|
52193
|
+
}
|
|
52194
|
+
rejectPromise(new LocalMigrateError(`'${cdkBin} migrate' ${signal !== null ? `killed by signal ${signal}` : code !== null ? `exited with code ${code}` : "process closed without a code or signal"}.\n--- stdout ---\n${stdout || "(empty)"}\n--- stderr ---\n${stderr || "(empty)"}\n`));
|
|
52195
|
+
});
|
|
52196
|
+
});
|
|
52197
|
+
}
|
|
52198
|
+
|
|
52199
|
+
//#endregion
|
|
52200
|
+
//#region src/cli/commands/migrate/synth-after-migrate.ts
|
|
52201
|
+
/**
|
|
52202
|
+
* Run `npm install` inside the generated CDK app directory so the
|
|
52203
|
+
* `cdk synth` step (which dynamically loads `aws-cdk-lib`) has its
|
|
52204
|
+
* dependencies in place.
|
|
52205
|
+
*
|
|
52206
|
+
* Skippable via `skipInstall: true` — CI users with a pre-populated
|
|
52207
|
+
* node_modules cache can save the ~30s `npm install` round-trip.
|
|
52208
|
+
*
|
|
52209
|
+
* Streams stdout / stderr through cdkd's logger so users see progress
|
|
52210
|
+
* for the often-slow npm step. Non-zero exit surfaces as a typed
|
|
52211
|
+
* {@link LocalMigrateError} with the captured streams folded into
|
|
52212
|
+
* the error message.
|
|
52213
|
+
*/
|
|
52214
|
+
async function installGeneratedAppDeps(outputDir, opts) {
|
|
52215
|
+
const logger = getLogger();
|
|
52216
|
+
if (opts.skipInstall) {
|
|
52217
|
+
logger.info(`[cdk-migrate] Skipping 'npm install' (--skip-install).`);
|
|
52218
|
+
return;
|
|
52219
|
+
}
|
|
52220
|
+
const absDir = resolve(outputDir);
|
|
52221
|
+
if (!existsSync(absDir)) throw new LocalMigrateError(`Generated app directory '${absDir}' does not exist; cannot run 'npm install'.`);
|
|
52222
|
+
logger.info(`[cdk-migrate] Running 'npm install' in ${absDir}...`);
|
|
52223
|
+
await runStreamingCommand("npm", ["install"], absDir, opts.extraEnv ? {
|
|
52224
|
+
...process.env,
|
|
52225
|
+
...opts.extraEnv
|
|
52226
|
+
} : void 0, "npm install");
|
|
52227
|
+
}
|
|
52228
|
+
/**
|
|
52229
|
+
* Run `cdk synth --quiet` inside the generated CDK app directory and
|
|
52230
|
+
* return both the assembly dir path and the parsed root-stack
|
|
52231
|
+
* template.
|
|
52232
|
+
*
|
|
52233
|
+
* `--quiet` suppresses the noisy template echo that `cdk synth`
|
|
52234
|
+
* defaults to on stdout — we read the template from disk
|
|
52235
|
+
* (`cdk.out/<StackName>.template.json`) instead.
|
|
52236
|
+
*
|
|
52237
|
+
* Skippable via `skipSynth: true` — useful when PR B's orchestrator
|
|
52238
|
+
* needs the codegen output without running synth (e.g. preview /
|
|
52239
|
+
* dry-run modes).
|
|
52240
|
+
*/
|
|
52241
|
+
async function synthGeneratedApp(outputDir, opts) {
|
|
52242
|
+
const logger = getLogger();
|
|
52243
|
+
const absDir = resolve(outputDir);
|
|
52244
|
+
const assemblyDir = join(absDir, "cdk.out");
|
|
52245
|
+
if (opts.skipSynth) {
|
|
52246
|
+
logger.info(`[cdk-migrate] Skipping 'cdk synth' (--skip-synth).`);
|
|
52247
|
+
return {
|
|
52248
|
+
assemblyDir,
|
|
52249
|
+
templateBody: null
|
|
52250
|
+
};
|
|
52251
|
+
}
|
|
52252
|
+
if (!existsSync(absDir)) throw new LocalMigrateError(`Generated app directory '${absDir}' does not exist; cannot run 'cdk synth'.`);
|
|
52253
|
+
const cdkBin = opts.cdkBinPath ?? "cdk";
|
|
52254
|
+
const env = {
|
|
52255
|
+
...process.env,
|
|
52256
|
+
...opts.extraEnv ?? {}
|
|
52257
|
+
};
|
|
52258
|
+
logger.info(`[cdk-migrate] Running '${cdkBin} synth --quiet' in ${absDir}...`);
|
|
52259
|
+
await runStreamingCommand(cdkBin, ["synth", "--quiet"], absDir, env, `${cdkBin} synth`);
|
|
52260
|
+
if (!existsSync(assemblyDir)) throw new LocalMigrateError(`'cdk synth' completed but produced no '${assemblyDir}' directory.`);
|
|
52261
|
+
const templates = readdirSync(assemblyDir).filter((f) => f.endsWith(".template.json"));
|
|
52262
|
+
if (templates.length === 0) return {
|
|
52263
|
+
assemblyDir,
|
|
52264
|
+
templateBody: null
|
|
52265
|
+
};
|
|
52266
|
+
const templatePath = join(assemblyDir, templates[0]);
|
|
52267
|
+
let templateBody;
|
|
52268
|
+
try {
|
|
52269
|
+
const raw = readFileSync(templatePath, "utf-8");
|
|
52270
|
+
templateBody = JSON.parse(raw);
|
|
52271
|
+
} catch (err) {
|
|
52272
|
+
throw new LocalMigrateError(`Failed to parse generated template '${templatePath}': ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0);
|
|
52273
|
+
}
|
|
52274
|
+
return {
|
|
52275
|
+
assemblyDir,
|
|
52276
|
+
templateBody
|
|
52277
|
+
};
|
|
52278
|
+
}
|
|
52279
|
+
/**
|
|
52280
|
+
* Helper — spawn a subprocess in the given working directory, stream
|
|
52281
|
+
* its output through cdkd's logger, and reject with a typed
|
|
52282
|
+
* {@link LocalMigrateError} on non-zero exit.
|
|
52283
|
+
*
|
|
52284
|
+
* Factored out so the `npm install` and `cdk synth` call sites use the
|
|
52285
|
+
* same shape; both expose progress to the user and surface failures
|
|
52286
|
+
* with the captured streams.
|
|
52287
|
+
*/
|
|
52288
|
+
async function runStreamingCommand(bin, args, cwd, env, label) {
|
|
52289
|
+
const logger = getLogger();
|
|
52290
|
+
return await new Promise((resolvePromise, rejectPromise) => {
|
|
52291
|
+
let stdout = "";
|
|
52292
|
+
let stderr = "";
|
|
52293
|
+
let child;
|
|
52294
|
+
try {
|
|
52295
|
+
child = spawn(bin, args, {
|
|
52296
|
+
cwd,
|
|
52297
|
+
env: env ?? process.env,
|
|
52298
|
+
stdio: [
|
|
52299
|
+
"ignore",
|
|
52300
|
+
"pipe",
|
|
52301
|
+
"pipe"
|
|
52302
|
+
]
|
|
52303
|
+
});
|
|
52304
|
+
} catch (err) {
|
|
52305
|
+
rejectPromise(new LocalMigrateError(`Failed to spawn ${label}: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0));
|
|
52306
|
+
return;
|
|
52307
|
+
}
|
|
52308
|
+
child.stdout?.on("data", (chunk) => {
|
|
52309
|
+
const text = chunk.toString("utf-8");
|
|
52310
|
+
stdout += text;
|
|
52311
|
+
const trimmed = text.replace(/\n$/, "");
|
|
52312
|
+
if (trimmed) logger.info(`[${label}] ${trimmed}`);
|
|
52313
|
+
});
|
|
52314
|
+
child.stderr?.on("data", (chunk) => {
|
|
52315
|
+
const text = chunk.toString("utf-8");
|
|
52316
|
+
stderr += text;
|
|
52317
|
+
const trimmed = text.replace(/\n$/, "");
|
|
52318
|
+
if (trimmed) logger.warn(`[${label}] ${trimmed}`);
|
|
52319
|
+
});
|
|
52320
|
+
child.on("error", (err) => {
|
|
52321
|
+
rejectPromise(new LocalMigrateError(`${label} subprocess error: ${err.message}`, err));
|
|
52322
|
+
});
|
|
52323
|
+
child.on("close", (code, signal) => {
|
|
52324
|
+
if (code === 0) {
|
|
52325
|
+
resolvePromise();
|
|
52326
|
+
return;
|
|
52327
|
+
}
|
|
52328
|
+
rejectPromise(new LocalMigrateError(`${label} ${signal !== null ? `killed by signal ${signal}` : code !== null ? `exited with code ${code}` : "process closed without a code or signal"}.\n--- stdout ---\n${stdout || "(empty)"}\n--- stderr ---\n${stderr || "(empty)"}\n`));
|
|
52329
|
+
});
|
|
52330
|
+
});
|
|
52331
|
+
}
|
|
52332
|
+
|
|
52333
|
+
//#endregion
|
|
52334
|
+
//#region src/cli/commands/migrate/index.ts
|
|
52335
|
+
/**
|
|
52336
|
+
* Build an extra-env bag for subprocesses (`npm install`, `cdk synth`)
|
|
52337
|
+
* so they inherit the AWS profile the caller selected via `--profile`.
|
|
52338
|
+
*
|
|
52339
|
+
* Without this, `cdk synth` falls back to the SDK default credential
|
|
52340
|
+
* chain (or no credentials at all) when the user runs cdkd under a
|
|
52341
|
+
* non-default profile — which silently produces a template synthesized
|
|
52342
|
+
* against the wrong account / region context.
|
|
52343
|
+
*/
|
|
52344
|
+
function buildAwsEnv(opts) {
|
|
52345
|
+
return opts.profile ? { AWS_PROFILE: opts.profile } : {};
|
|
52346
|
+
}
|
|
52347
|
+
/**
|
|
52348
|
+
* PR A library entry point — orchestrates the codegen + synth half of
|
|
52349
|
+
* `cdkd migrate --from-cfn-stack` without writing cdkd state, running
|
|
52350
|
+
* import, or touching the source CFn stack.
|
|
52351
|
+
*
|
|
52352
|
+
* Flow:
|
|
52353
|
+
* 1. `verifyCdkCliAvailable` — hard-fail if `cdk` is missing.
|
|
52354
|
+
* 2. `prefetchCfnStack` — read source stack state + resources +
|
|
52355
|
+
* transform info.
|
|
52356
|
+
* 3. `validatePrefetchResult` — reject CR / nested-stack / non-
|
|
52357
|
+
* terminal state; INFO log SAM / Include transforms.
|
|
52358
|
+
* 4. `assertOutputDirAvailable` — refuse pre-existing non-empty
|
|
52359
|
+
* output dir.
|
|
52360
|
+
* 5. `spawnCdkMigrate` — run `cdk migrate --from-stack` subprocess.
|
|
52361
|
+
* 6. `installGeneratedAppDeps` (gated by `skipInstall`).
|
|
52362
|
+
* 7. `synthGeneratedApp` (gated by `skipSynth`).
|
|
52363
|
+
* 8. Return every artifact PR B will consume.
|
|
52364
|
+
*
|
|
52365
|
+
* Per #465 Q4 (parent-session decision): PR A is library-only. The CLI
|
|
52366
|
+
* command (`cdkd migrate`), state writes, retire flow, and resource-
|
|
52367
|
+
* mapping algorithm all live in PR B.
|
|
52368
|
+
*/
|
|
52369
|
+
async function runMigrateLibrary(opts) {
|
|
52370
|
+
const logger = getLogger();
|
|
52371
|
+
const stackName = opts.fromCfnStack;
|
|
52372
|
+
const outputPath = opts.outputDir ?? resolve(process.cwd(), stackName);
|
|
52373
|
+
logger.info(`[migrate] Verifying upstream 'cdk' CLI...`);
|
|
52374
|
+
const cliCheck = await verifyCdkCliAvailable(opts.cdkBinPath);
|
|
52375
|
+
logger.info(`[migrate] Using cdk CLI v${cliCheck.version}`);
|
|
52376
|
+
if (cliCheck.warn) logger.warn(`[migrate] ${cliCheck.warn}`);
|
|
52377
|
+
logger.info(`[migrate] Pre-fetching CloudFormation stack '${stackName}'...`);
|
|
52378
|
+
const cfnClient = buildCfnClient(opts);
|
|
52379
|
+
let prefetch;
|
|
52380
|
+
try {
|
|
52381
|
+
prefetch = await prefetchCfnStack(stackName, cfnClient);
|
|
52382
|
+
} finally {
|
|
52383
|
+
cfnClient.destroy?.();
|
|
52384
|
+
}
|
|
52385
|
+
validatePrefetchResult(prefetch);
|
|
52386
|
+
if (prefetch.transformInfo.hasSamTransform) logger.info("[migrate] INFO: source CFn stack uses 'AWS::Serverless' transform; cdk migrate will expand it client-side and the generated CDK code will use plain Lambda + API Gateway L1 constructs (not SAM).");
|
|
52387
|
+
if (prefetch.transformInfo.hasIncludeTransform) logger.info("[migrate] INFO: source CFn stack uses 'AWS::Include' transform; cdk migrate will expand it client-side and the generated CDK code will inline the included template content.");
|
|
52388
|
+
assertOutputDirAvailable(outputPath, stackName);
|
|
52389
|
+
const spawnResult = await spawnCdkMigrate({
|
|
52390
|
+
stackName,
|
|
52391
|
+
fromStackName: stackName,
|
|
52392
|
+
outputPath,
|
|
52393
|
+
...opts.language && { language: opts.language },
|
|
52394
|
+
...opts.region && { region: opts.region },
|
|
52395
|
+
...opts.account && { account: opts.account },
|
|
52396
|
+
...opts.filters && { filters: opts.filters },
|
|
52397
|
+
...opts.profile && { profile: opts.profile },
|
|
52398
|
+
...opts.cdkBinPath && { cdkBinPath: opts.cdkBinPath }
|
|
52399
|
+
});
|
|
52400
|
+
await installGeneratedAppDeps(spawnResult.outputDir, {
|
|
52401
|
+
skipInstall: opts.skipInstall ?? false,
|
|
52402
|
+
extraEnv: buildAwsEnv(opts)
|
|
52403
|
+
});
|
|
52404
|
+
const synthResult = await synthGeneratedApp(spawnResult.outputDir, {
|
|
52405
|
+
skipSynth: opts.skipSynth ?? false,
|
|
52406
|
+
...opts.cdkBinPath && { cdkBinPath: opts.cdkBinPath },
|
|
52407
|
+
extraEnv: buildAwsEnv(opts)
|
|
52408
|
+
});
|
|
52409
|
+
if (prefetch.sourceCfnTemplate === null) throw new LocalMigrateError(`Could not read the source CloudFormation template for '${stackName}' (GetTemplate failed during pre-flight). Re-run after granting cloudformation:GetTemplate on the stack, or check the earlier warning for details.`);
|
|
52410
|
+
return {
|
|
52411
|
+
outputDir: spawnResult.outputDir,
|
|
52412
|
+
assemblyDir: synthResult.assemblyDir,
|
|
52413
|
+
templateBody: synthResult.templateBody,
|
|
52414
|
+
sourceCfnTemplate: prefetch.sourceCfnTemplate,
|
|
52415
|
+
sourceResources: prefetch.resources
|
|
52416
|
+
};
|
|
52417
|
+
}
|
|
52418
|
+
/**
|
|
52419
|
+
* Build a CloudFormation client using cdkd's shared {@link AwsClients}
|
|
52420
|
+
* factory so the credential chain matches every other cdkd command.
|
|
52421
|
+
*
|
|
52422
|
+
* Keeping the construction in one place makes the per-command CFn-
|
|
52423
|
+
* client recipe a single readable hook for future plumbing (e.g.
|
|
52424
|
+
* threading PR B's `--role-arn` STS-assume).
|
|
52425
|
+
*/
|
|
52426
|
+
function buildCfnClient(opts) {
|
|
52427
|
+
const config = {};
|
|
52428
|
+
if (opts.region) config.region = opts.region;
|
|
52429
|
+
if (opts.profile) config.profile = opts.profile;
|
|
52430
|
+
return new AwsClients(config).getCloudFormationClient();
|
|
52431
|
+
}
|
|
52432
|
+
|
|
52433
|
+
//#endregion
|
|
52434
|
+
//#region src/cli/commands/migrate/resource-mapper.ts
|
|
52435
|
+
/**
|
|
52436
|
+
* Run the 2-pass mapping algorithm against `(sourceCfnTemplate,
|
|
52437
|
+
* synthTemplate, sourceResources)` and return the resolved mapping plus
|
|
52438
|
+
* any unmatched entries.
|
|
52439
|
+
*
|
|
52440
|
+
* Pure-functional: no AWS calls, no I/O, no mutation of inputs. Safe to
|
|
52441
|
+
* call repeatedly in tests against shared fixtures.
|
|
52442
|
+
*
|
|
52443
|
+
* Throws when an override references a synth id that does not exist —
|
|
52444
|
+
* this is a user error in the hand-edited mapping JSON and surfacing
|
|
52445
|
+
* the available synth ids in the message is the path back to a working
|
|
52446
|
+
* config.
|
|
52447
|
+
*/
|
|
52448
|
+
function buildResourceMapping(opts) {
|
|
52449
|
+
const sourceEntries = extractSourceResources(opts.sourceCfnTemplate);
|
|
52450
|
+
const synthEntries = extractSynthResources(opts.synthTemplate);
|
|
52451
|
+
const physicalIdByLogicalId = /* @__PURE__ */ new Map();
|
|
52452
|
+
for (const r of opts.sourceResources) physicalIdByLogicalId.set(r.LogicalResourceId, r.PhysicalResourceId);
|
|
52453
|
+
const overrides = opts.overrides ?? {};
|
|
52454
|
+
const synthLogicalIds = new Set(synthEntries.map((s) => s.logicalId));
|
|
52455
|
+
for (const [srcId, synthId] of Object.entries(overrides)) if (!synthLogicalIds.has(synthId)) throw new Error(`Resource-mapping override targets synth logical id '${synthId}' (for source id '${srcId}'), but that id is not in the synthesized template. Available synth ids: ${[...synthLogicalIds].sort().join(", ")}`);
|
|
52456
|
+
const missingPhysicalIds = [];
|
|
52457
|
+
for (const src of sourceEntries) if (!physicalIdByLogicalId.has(src.logicalId)) missingPhysicalIds.push(` - ${src.logicalId} (${src.type})`);
|
|
52458
|
+
if (missingPhysicalIds.length > 0) throw new Error(`Source CFn resource(s) not returned by DescribeStackResources — cannot migrate:\n${missingPhysicalIds.join("\n")}\n\nThe stack may have been mid-operation; wait for it to settle (CREATE_COMPLETE / UPDATE_COMPLETE / etc.) and retry.`);
|
|
52459
|
+
const synthByLastPathSegment = indexSynthByLastPathSegment(synthEntries);
|
|
52460
|
+
const synthByLogicalId = new Map(synthEntries.map((s) => [s.logicalId, s]));
|
|
52461
|
+
const claimedSynthIds = /* @__PURE__ */ new Set();
|
|
52462
|
+
const pairs = [];
|
|
52463
|
+
const unmatched = [];
|
|
52464
|
+
const sourcesHandledByOverride = /* @__PURE__ */ new Set();
|
|
52465
|
+
for (const [srcId, synthId] of Object.entries(overrides)) {
|
|
52466
|
+
const src = sourceEntries.find((e) => e.logicalId === srcId);
|
|
52467
|
+
if (!src) {
|
|
52468
|
+
const availableSrc = sourceEntries.map((e) => e.logicalId).sort();
|
|
52469
|
+
throw new Error(`Resource-mapping override references source logical id '${srcId}', but no resource with that id exists in the source CloudFormation template. Available source ids: ${availableSrc.join(", ")}`);
|
|
52470
|
+
}
|
|
52471
|
+
const synth = synthByLogicalId.get(synthId);
|
|
52472
|
+
if (!synth) continue;
|
|
52473
|
+
const physicalId = physicalIdByLogicalId.get(src.logicalId);
|
|
52474
|
+
pairs.push({
|
|
52475
|
+
sourceLogicalId: src.logicalId,
|
|
52476
|
+
synthLogicalId: synth.logicalId,
|
|
52477
|
+
physicalId,
|
|
52478
|
+
resourceType: src.type
|
|
52479
|
+
});
|
|
52480
|
+
claimedSynthIds.add(synth.logicalId);
|
|
52481
|
+
sourcesHandledByOverride.add(src.logicalId);
|
|
52482
|
+
}
|
|
52483
|
+
const passOnePending = [];
|
|
52484
|
+
for (const src of sourceEntries) {
|
|
52485
|
+
if (sourcesHandledByOverride.has(src.logicalId)) continue;
|
|
52486
|
+
const candidatesForLastSegment = synthByLastPathSegment.get(src.logicalId) ?? [];
|
|
52487
|
+
const rawCandidateCount = candidatesForLastSegment.length;
|
|
52488
|
+
const availableCandidates = candidatesForLastSegment.filter((c) => !claimedSynthIds.has(c.logicalId));
|
|
52489
|
+
if (rawCandidateCount === 1 && availableCandidates.length === 1) {
|
|
52490
|
+
const synth = availableCandidates[0];
|
|
52491
|
+
pairs.push({
|
|
52492
|
+
sourceLogicalId: src.logicalId,
|
|
52493
|
+
synthLogicalId: synth.logicalId,
|
|
52494
|
+
physicalId: physicalIdByLogicalId.get(src.logicalId),
|
|
52495
|
+
resourceType: src.type
|
|
52496
|
+
});
|
|
52497
|
+
claimedSynthIds.add(synth.logicalId);
|
|
52498
|
+
} else passOnePending.push(src);
|
|
52499
|
+
}
|
|
52500
|
+
for (const src of passOnePending) {
|
|
52501
|
+
const candidatesSameType = synthEntries.filter((s) => s.type === src.type && !claimedSynthIds.has(s.logicalId));
|
|
52502
|
+
const propertyMatches = candidatesSameType.filter((s) => deepEqualIgnoreNoValue(src.properties, s.properties));
|
|
52503
|
+
if (propertyMatches.length === 1) {
|
|
52504
|
+
const synth = propertyMatches[0];
|
|
52505
|
+
pairs.push({
|
|
52506
|
+
sourceLogicalId: src.logicalId,
|
|
52507
|
+
synthLogicalId: synth.logicalId,
|
|
52508
|
+
physicalId: physicalIdByLogicalId.get(src.logicalId),
|
|
52509
|
+
resourceType: src.type
|
|
52510
|
+
});
|
|
52511
|
+
claimedSynthIds.add(synth.logicalId);
|
|
52512
|
+
} else {
|
|
52513
|
+
const isCollision = (synthByLastPathSegment.get(src.logicalId) ?? []).length >= 2;
|
|
52514
|
+
unmatched.push({
|
|
52515
|
+
sourceLogicalId: src.logicalId,
|
|
52516
|
+
resourceType: src.type,
|
|
52517
|
+
candidates: candidatesSameType.map((c) => c.logicalId).sort(),
|
|
52518
|
+
reason: isCollision ? "logical-id-collision" : "no-match"
|
|
52519
|
+
});
|
|
52520
|
+
}
|
|
52521
|
+
}
|
|
52522
|
+
const mapping = {};
|
|
52523
|
+
for (const p of pairs) mapping[p.sourceLogicalId] = p.synthLogicalId;
|
|
52524
|
+
return {
|
|
52525
|
+
mapping,
|
|
52526
|
+
pairs,
|
|
52527
|
+
unmatched
|
|
52528
|
+
};
|
|
52529
|
+
}
|
|
52530
|
+
/**
|
|
52531
|
+
* Walk a parsed CFn template's `Resources` and return one entry per
|
|
52532
|
+
* resource. `Type` defaults to `''` and `Properties` to `{}` when the
|
|
52533
|
+
* template omits them (CFn allows resources with only `DeletionPolicy`
|
|
52534
|
+
* + `Type`); the mapper's downstream deep-equal handles `{}` cleanly.
|
|
52535
|
+
*
|
|
52536
|
+
* Top-level keys other than `Resources` (Conditions / Parameters /
|
|
52537
|
+
* Rules / Outputs / Mappings / Metadata) are NOT walked — the mapping
|
|
52538
|
+
* is resource-to-resource only, and §5.5 of the design doc spells out
|
|
52539
|
+
* the four CDK-synth injections that live in those sibling keys.
|
|
52540
|
+
*/
|
|
52541
|
+
function extractSourceResources(template) {
|
|
52542
|
+
if (!template || typeof template !== "object") return [];
|
|
52543
|
+
const resources = template["Resources"];
|
|
52544
|
+
if (!resources || typeof resources !== "object") return [];
|
|
52545
|
+
const out = [];
|
|
52546
|
+
for (const [logicalId, raw] of Object.entries(resources)) {
|
|
52547
|
+
if (!raw || typeof raw !== "object") continue;
|
|
52548
|
+
const r = raw;
|
|
52549
|
+
const type = typeof r["Type"] === "string" ? r["Type"] : "";
|
|
52550
|
+
const props = r["Properties"] && typeof r["Properties"] === "object" ? r["Properties"] : {};
|
|
52551
|
+
out.push({
|
|
52552
|
+
logicalId,
|
|
52553
|
+
type,
|
|
52554
|
+
properties: props
|
|
52555
|
+
});
|
|
52556
|
+
}
|
|
52557
|
+
return out;
|
|
52558
|
+
}
|
|
52559
|
+
/**
|
|
52560
|
+
* Walk the synth template's `Resources` and return one entry per
|
|
52561
|
+
* resource, EXCLUDING `AWS::CDK::Metadata` (synth-only sentinel that has
|
|
52562
|
+
* no source counterpart). Captures `Metadata['aws:cdk:path']` so Pass 1
|
|
52563
|
+
* can match against the last `/`-separated segment.
|
|
52564
|
+
*
|
|
52565
|
+
* The four other CDK-synth injections (CDKMetadataAvailable Condition,
|
|
52566
|
+
* BootstrapVersion Parameter, CheckBootstrapVersion Rule) live outside
|
|
52567
|
+
* `Resources` and are structurally excluded by this function.
|
|
52568
|
+
*/
|
|
52569
|
+
function extractSynthResources(template) {
|
|
52570
|
+
if (!template || typeof template !== "object") return [];
|
|
52571
|
+
const resources = template["Resources"];
|
|
52572
|
+
if (!resources || typeof resources !== "object") return [];
|
|
52573
|
+
const out = [];
|
|
52574
|
+
for (const [logicalId, raw] of Object.entries(resources)) {
|
|
52575
|
+
if (!raw || typeof raw !== "object") continue;
|
|
52576
|
+
const r = raw;
|
|
52577
|
+
const type = typeof r["Type"] === "string" ? r["Type"] : "";
|
|
52578
|
+
if (type === "AWS::CDK::Metadata") continue;
|
|
52579
|
+
const props = r["Properties"] && typeof r["Properties"] === "object" ? r["Properties"] : {};
|
|
52580
|
+
const meta = r["Metadata"] && typeof r["Metadata"] === "object" ? r["Metadata"] : {};
|
|
52581
|
+
const path = typeof meta["aws:cdk:path"] === "string" ? meta["aws:cdk:path"] : "";
|
|
52582
|
+
out.push({
|
|
52583
|
+
logicalId,
|
|
52584
|
+
type,
|
|
52585
|
+
properties: props,
|
|
52586
|
+
awsCdkPath: path
|
|
52587
|
+
});
|
|
52588
|
+
}
|
|
52589
|
+
return out;
|
|
52590
|
+
}
|
|
52591
|
+
/**
|
|
52592
|
+
* Group synth entries by the LAST `/`-separated segment of their
|
|
52593
|
+
* `aws:cdk:path` metadata so Pass 1 can look up the source logical id
|
|
52594
|
+
* directly. Per [docs/design/465-cfn-migrate.md](../../../../docs/design/465-cfn-migrate.md) §5.5:
|
|
52595
|
+
* `cdk migrate` emits `<StackName>/<LogicalId>` as the metadata value,
|
|
52596
|
+
* so the last segment IS the source logical id in the typical case.
|
|
52597
|
+
*
|
|
52598
|
+
* Entries without an `aws:cdk:path` are silently skipped — they cannot
|
|
52599
|
+
* be addressed by source logical id and will only be reachable via
|
|
52600
|
+
* Pass 2's Properties match.
|
|
52601
|
+
*/
|
|
52602
|
+
function indexSynthByLastPathSegment(entries) {
|
|
52603
|
+
const out = /* @__PURE__ */ new Map();
|
|
52604
|
+
for (const e of entries) {
|
|
52605
|
+
if (!e.awsCdkPath) continue;
|
|
52606
|
+
const lastSegment = e.awsCdkPath.split("/").pop() ?? "";
|
|
52607
|
+
if (!lastSegment) continue;
|
|
52608
|
+
const bucket = out.get(lastSegment);
|
|
52609
|
+
if (bucket) bucket.push(e);
|
|
52610
|
+
else out.set(lastSegment, [e]);
|
|
52611
|
+
}
|
|
52612
|
+
return out;
|
|
52613
|
+
}
|
|
52614
|
+
/**
|
|
52615
|
+
* Recursive deep equality between two values, with two CFn-specific
|
|
52616
|
+
* accommodations:
|
|
52617
|
+
*
|
|
52618
|
+
* 1. Object key order is NOT significant — `JSON.stringify` would
|
|
52619
|
+
* diverge on the canonical-source-vs-synth-alphabetized case
|
|
52620
|
+
* (verified by PR A's empirical run, §5.5 point 3) so we walk the
|
|
52621
|
+
* keys explicitly.
|
|
52622
|
+
* 2. `AWS::NoValue` placeholder values are stripped before compare —
|
|
52623
|
+
* `cdk migrate`'s codegen sometimes emits the placeholder for a
|
|
52624
|
+
* property the source template simply omitted, and the structural
|
|
52625
|
+
* intent matches. Source side never has the placeholder; synth side
|
|
52626
|
+
* can on either side of a nested object.
|
|
52627
|
+
*
|
|
52628
|
+
* Arrays are compared positionally (order IS significant — Tags arrays
|
|
52629
|
+
* and similar carry ordering semantics).
|
|
52630
|
+
*/
|
|
52631
|
+
function deepEqualIgnoreNoValue(a, b) {
|
|
52632
|
+
if (a === b) return true;
|
|
52633
|
+
if (isAwsNoValue(a) !== isAwsNoValue(b)) return false;
|
|
52634
|
+
if (isAwsNoValue(a) && isAwsNoValue(b)) return true;
|
|
52635
|
+
if (typeof a !== typeof b) return false;
|
|
52636
|
+
if (a === null || b === null) return a === b;
|
|
52637
|
+
if (typeof a !== "object") return a === b;
|
|
52638
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
52639
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
52640
|
+
if (a.length !== b.length) return false;
|
|
52641
|
+
for (let i = 0; i < a.length; i++) if (!deepEqualIgnoreNoValue(a[i], b[i])) return false;
|
|
52642
|
+
return true;
|
|
52643
|
+
}
|
|
52644
|
+
const aObj = a;
|
|
52645
|
+
const bObj = b;
|
|
52646
|
+
const aKeys = Object.keys(aObj).filter((k) => !isAwsNoValue(aObj[k]) && aObj[k] !== void 0);
|
|
52647
|
+
const bKeys = Object.keys(bObj).filter((k) => !isAwsNoValue(bObj[k]) && bObj[k] !== void 0);
|
|
52648
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
52649
|
+
const aKeySet = new Set(aKeys);
|
|
52650
|
+
for (const k of bKeys) {
|
|
52651
|
+
if (!aKeySet.has(k)) return false;
|
|
52652
|
+
if (!deepEqualIgnoreNoValue(aObj[k], bObj[k])) return false;
|
|
52653
|
+
}
|
|
52654
|
+
return true;
|
|
52655
|
+
}
|
|
52656
|
+
/**
|
|
52657
|
+
* True when the value is the CFn `AWS::NoValue` placeholder
|
|
52658
|
+
* (`{Ref: 'AWS::NoValue'}`). `cdk migrate`'s codegen emits this for
|
|
52659
|
+
* conditional properties the source template omitted; the structural
|
|
52660
|
+
* intent matches and deep-equal must treat them as absent.
|
|
52661
|
+
*/
|
|
52662
|
+
function isAwsNoValue(v) {
|
|
52663
|
+
if (!v || typeof v !== "object") return false;
|
|
52664
|
+
const obj = v;
|
|
52665
|
+
const keys = Object.keys(obj);
|
|
52666
|
+
return keys.length === 1 && keys[0] === "Ref" && obj["Ref"] === "AWS::NoValue";
|
|
52667
|
+
}
|
|
52668
|
+
|
|
52669
|
+
//#endregion
|
|
52670
|
+
//#region src/cli/commands/migrate/resource-mapping-file.ts
|
|
52671
|
+
/** Conventional filename inside the migrate output dir. */
|
|
52672
|
+
const RESOURCE_MAPPING_FILENAME = "cdkd-resource-mapping.json";
|
|
52673
|
+
/**
|
|
52674
|
+
* Build the on-disk file shape from a {@link ResourceMappingResult} +
|
|
52675
|
+
* the source / output stack names, then write it to
|
|
52676
|
+
* `<outputDir>/cdkd-resource-mapping.json` (overwriting any prior file
|
|
52677
|
+
* on the same path — same idempotency contract as `cdk migrate`'s own
|
|
52678
|
+
* codegen, and the user expects a re-run to refresh stale data).
|
|
52679
|
+
*
|
|
52680
|
+
* Returns the absolute on-disk path so the orchestrator can name it in
|
|
52681
|
+
* the error message when the mapping is incomplete.
|
|
52682
|
+
*/
|
|
52683
|
+
function writeMappingFile(outputDir, args) {
|
|
52684
|
+
const path = join(outputDir, RESOURCE_MAPPING_FILENAME);
|
|
52685
|
+
const file = {
|
|
52686
|
+
version: 1,
|
|
52687
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
52688
|
+
sourceStack: args.sourceStack,
|
|
52689
|
+
outputStack: args.outputStack,
|
|
52690
|
+
mapping: args.result.mapping
|
|
52691
|
+
};
|
|
52692
|
+
if (args.result.unmatched.length > 0) file._unmatched = args.result.unmatched;
|
|
52693
|
+
writeFileSync(path, JSON.stringify(file, null, 2) + "\n", "utf-8");
|
|
52694
|
+
return path;
|
|
52695
|
+
}
|
|
52696
|
+
/**
|
|
52697
|
+
* Read a user-supplied resource-mapping file from disk and return the
|
|
52698
|
+
* `{<srcLogicalId>: <synthLogicalId>}` overrides for the mapper.
|
|
52699
|
+
*
|
|
52700
|
+
* Tolerates `_unmatched` on input (a user that re-runs against a
|
|
52701
|
+
* partial-failure file without editing should hit the same failure on
|
|
52702
|
+
* the resolution side — not a parse error here). Hard-errors on
|
|
52703
|
+
* structurally invalid input (missing `mapping`, non-object root,
|
|
52704
|
+
* non-string values, wrong `version`).
|
|
52705
|
+
*
|
|
52706
|
+
* Surfaces every shape error as `LocalMigrateError` (exit code 2) so
|
|
52707
|
+
* the CLI handler routes it through the normal error path.
|
|
52708
|
+
*/
|
|
52709
|
+
function readMappingFile(path) {
|
|
52710
|
+
if (!existsSync(path)) throw new LocalMigrateError(`Resource-mapping file not found: ${path}. Drop the --resource-mapping flag or supply a valid path.`);
|
|
52711
|
+
let raw;
|
|
52712
|
+
try {
|
|
52713
|
+
raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
52714
|
+
} catch (err) {
|
|
52715
|
+
throw new LocalMigrateError(`Resource-mapping file '${path}' is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
52716
|
+
}
|
|
52717
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new LocalMigrateError(`Resource-mapping file '${path}' must contain a JSON object at the top level.`);
|
|
52718
|
+
const obj = raw;
|
|
52719
|
+
if (obj["version"] !== 1) throw new LocalMigrateError(`Resource-mapping file '${path}' has unsupported version '${String(obj["version"])}'. Expected version 1.`);
|
|
52720
|
+
if (typeof obj["sourceStack"] !== "string" || obj["sourceStack"].length === 0) throw new LocalMigrateError(`Resource-mapping file '${path}' is missing the required 'sourceStack' field.`);
|
|
52721
|
+
if (typeof obj["outputStack"] !== "string" || obj["outputStack"].length === 0) throw new LocalMigrateError(`Resource-mapping file '${path}' is missing the required 'outputStack' field.`);
|
|
52722
|
+
if (typeof obj["generatedAt"] !== "string") throw new LocalMigrateError(`Resource-mapping file '${path}' is missing the required 'generatedAt' field.`);
|
|
52723
|
+
const mappingRaw = obj["mapping"];
|
|
52724
|
+
if (!mappingRaw || typeof mappingRaw !== "object" || Array.isArray(mappingRaw)) throw new LocalMigrateError(`Resource-mapping file '${path}' is missing the required 'mapping' object.`);
|
|
52725
|
+
const mapping = {};
|
|
52726
|
+
for (const [k, v] of Object.entries(mappingRaw)) {
|
|
52727
|
+
if (typeof v !== "string") throw new LocalMigrateError(`Resource-mapping file '${path}' has a non-string value for key '${k}'.`);
|
|
52728
|
+
mapping[k] = v;
|
|
52729
|
+
}
|
|
52730
|
+
const result = {
|
|
52731
|
+
version: 1,
|
|
52732
|
+
generatedAt: obj["generatedAt"],
|
|
52733
|
+
sourceStack: obj["sourceStack"],
|
|
52734
|
+
outputStack: obj["outputStack"],
|
|
52735
|
+
mapping
|
|
52736
|
+
};
|
|
52737
|
+
if (Array.isArray(obj["_unmatched"])) {
|
|
52738
|
+
const cleaned = [];
|
|
52739
|
+
for (const entry of obj["_unmatched"]) {
|
|
52740
|
+
if (!entry || typeof entry !== "object") continue;
|
|
52741
|
+
const e = entry;
|
|
52742
|
+
if (typeof e["sourceLogicalId"] !== "string" || typeof e["resourceType"] !== "string" || !Array.isArray(e["candidates"]) || e["reason"] !== "no-match" && e["reason"] !== "logical-id-collision") continue;
|
|
52743
|
+
cleaned.push({
|
|
52744
|
+
sourceLogicalId: e["sourceLogicalId"],
|
|
52745
|
+
resourceType: e["resourceType"],
|
|
52746
|
+
candidates: e["candidates"].filter((c) => typeof c === "string"),
|
|
52747
|
+
reason: e["reason"]
|
|
52748
|
+
});
|
|
52749
|
+
}
|
|
52750
|
+
if (cleaned.length > 0) result._unmatched = cleaned;
|
|
52751
|
+
}
|
|
52752
|
+
return result;
|
|
52753
|
+
}
|
|
52754
|
+
|
|
52755
|
+
//#endregion
|
|
52756
|
+
//#region src/cli/commands/migrate-command.ts
|
|
52757
|
+
/**
|
|
52758
|
+
* `cdkd migrate --from-cfn-stack <name>` end-to-end orchestrator.
|
|
52759
|
+
*
|
|
52760
|
+
* Closes the CLI half of [#465](https://github.com/go-to-k/cdkd/issues/465).
|
|
52761
|
+
* Reads the source CFn stack, runs upstream `cdk migrate` via the PR A
|
|
52762
|
+
* library, builds the source → synth logical-ID mapping with the 2-pass
|
|
52763
|
+
* algorithm, writes a `cdkd-resource-mapping.json` audit file, prompts
|
|
52764
|
+
* for confirmation, invokes `cdkd import` to write state under the
|
|
52765
|
+
* synth logical IDs, and (when `--retire-cfn-stack` is set) finally
|
|
52766
|
+
* retires the source CFn stack so management responsibility transfers
|
|
52767
|
+
* fully to cdkd.
|
|
52768
|
+
*
|
|
52769
|
+
* AWS resources are never modified — the migration is a metadata
|
|
52770
|
+
* transfer only. See [docs/design/465-cfn-migrate.md](../../../docs/design/465-cfn-migrate.md) §7
|
|
52771
|
+
* for the post-migration state matrix.
|
|
52772
|
+
*/
|
|
52773
|
+
async function migrateCommandAction(positionalStack, options) {
|
|
52774
|
+
const logger = getLogger();
|
|
52775
|
+
if (options.verbose) {
|
|
52776
|
+
logger.setLevel("debug");
|
|
52777
|
+
process.env["CDKD_NO_LIVE"] = "1";
|
|
52778
|
+
}
|
|
52779
|
+
const sourceCfnStackName = options.fromCfnStack ?? positionalStack;
|
|
52780
|
+
if (!sourceCfnStackName) throw new LocalMigrateError("Missing required argument: --from-cfn-stack <name> (or pass the stack name positionally).");
|
|
52781
|
+
if (options.retireCfnStack && options.skipSynth) throw new LocalMigrateError("--retire-cfn-stack is incompatible with --skip-synth: the post-state-write retirement (UpdateStack + DeleteStack) cannot run without a synthesized template + cdkd state.");
|
|
52782
|
+
if (options.retireCfnStack && options.dryRun) throw new LocalMigrateError("--retire-cfn-stack is incompatible with --dry-run: retirement issues real AWS calls (UpdateStack injects Retain policies, then DeleteStack).");
|
|
52783
|
+
if (options.retireCfnStack && options.filter && options.filter.length > 0) throw new LocalMigrateError("--retire-cfn-stack is incompatible with --filter: a partial migration that retires the whole source CFn stack would strand the un-migrated resources. Drop one of the flags or migrate the rest of the stack first.");
|
|
52784
|
+
await applyRoleArnIfSet({
|
|
52785
|
+
roleArn: options.roleArn,
|
|
52786
|
+
region: options.region
|
|
52787
|
+
});
|
|
52788
|
+
const region = options.region || process.env["AWS_REGION"] || "us-east-1";
|
|
52789
|
+
const outputDir = resolve(options.outputDir ?? sourceCfnStackName);
|
|
52790
|
+
logger.info(`[migrate] Source CFn stack: ${sourceCfnStackName}`);
|
|
52791
|
+
logger.info(`[migrate] Output directory: ${outputDir}`);
|
|
52792
|
+
const libResult = await runMigrateLibrary({
|
|
52793
|
+
fromCfnStack: sourceCfnStackName,
|
|
52794
|
+
outputDir,
|
|
52795
|
+
language: options.language ?? "typescript",
|
|
52796
|
+
...options.region && { region: options.region },
|
|
52797
|
+
...options.account && { account: options.account },
|
|
52798
|
+
...options.filter && options.filter.length > 0 && { filters: options.filter },
|
|
52799
|
+
...options.profile && { profile: options.profile },
|
|
52800
|
+
...options.cdkBin && { cdkBinPath: options.cdkBin },
|
|
52801
|
+
skipInstall: options.skipInstall ?? false,
|
|
52802
|
+
skipSynth: options.skipSynth ?? false
|
|
52803
|
+
});
|
|
52804
|
+
if (options.skipSynth || !libResult.templateBody) {
|
|
52805
|
+
logger.info(`[migrate] --skip-synth: generated CDK app at ${libResult.outputDir}. Re-run without --skip-synth to write cdkd state.`);
|
|
52806
|
+
return;
|
|
52807
|
+
}
|
|
52808
|
+
let overrides;
|
|
52809
|
+
if (options.resourceMapping) {
|
|
52810
|
+
const mappingPath = resolve(options.resourceMapping);
|
|
52811
|
+
logger.info(`[migrate] Loading user-supplied resource mapping from ${mappingPath}`);
|
|
52812
|
+
overrides = readMappingFile(mappingPath).mapping;
|
|
52813
|
+
}
|
|
52814
|
+
let mappingResult;
|
|
52815
|
+
try {
|
|
52816
|
+
mappingResult = buildResourceMapping({
|
|
52817
|
+
sourceCfnTemplate: libResult.sourceCfnTemplate,
|
|
52818
|
+
synthTemplate: libResult.templateBody,
|
|
52819
|
+
sourceResources: libResult.sourceResources,
|
|
52820
|
+
...overrides && { overrides }
|
|
52821
|
+
});
|
|
52822
|
+
} catch (err) {
|
|
52823
|
+
throw new LocalMigrateError(err instanceof Error ? err.message : String(err));
|
|
52824
|
+
}
|
|
52825
|
+
const outputStack = sourceCfnStackName;
|
|
52826
|
+
const mappingFilePath = writeMappingFile(libResult.outputDir, {
|
|
52827
|
+
sourceStack: sourceCfnStackName,
|
|
52828
|
+
outputStack,
|
|
52829
|
+
result: mappingResult
|
|
52830
|
+
});
|
|
52831
|
+
logger.info(`[migrate] Resource mapping written to ${mappingFilePath}`);
|
|
52832
|
+
if (mappingResult.unmatched.length > 0) {
|
|
52833
|
+
const lines = mappingResult.unmatched.map((u) => {
|
|
52834
|
+
const candidatesStr = u.candidates.length > 0 ? ` (synth candidates of same Type: ${u.candidates.join(", ")})` : " (no synth resource has the same Type)";
|
|
52835
|
+
return ` - ${u.sourceLogicalId} (${u.resourceType}) [${u.reason}]${candidatesStr}`;
|
|
52836
|
+
});
|
|
52837
|
+
throw new LocalMigrateError(`Could not auto-map ${mappingResult.unmatched.length} of ${mappingResult.unmatched.length + mappingResult.pairs.length} source resource(s) to the synthesized CDK template:\n${lines.join("\n")}\n\nEdit '${mappingFilePath}'\nto add the correct '<sourceLogicalId>: <synthLogicalId>' pairs under "mapping", then re-run:\n cdkd migrate --from-cfn-stack ${sourceCfnStackName} --resource-mapping ${mappingFilePath} --output-dir ${libResult.outputDir}\n(The --output-dir override re-uses the already-generated CDK code; drop it if you let the default location be used.)`);
|
|
52838
|
+
}
|
|
52839
|
+
printMappingTable(mappingResult, sourceCfnStackName, outputStack);
|
|
52840
|
+
if (options.dryRun) {
|
|
52841
|
+
logger.info(`[migrate] --dry-run: would import ${mappingResult.pairs.length} resource(s) into cdkd state under stack '${outputStack}' (region '${region}'). State NOT written.`);
|
|
52842
|
+
return;
|
|
52843
|
+
}
|
|
52844
|
+
if (!options.yes) {
|
|
52845
|
+
if (!await promptConfirm(`Import ${mappingResult.pairs.length} resource(s) into cdkd state for stack '${outputStack}' (${region})?`)) {
|
|
52846
|
+
logger.info("[migrate] Cancelled by user. No state written.");
|
|
52847
|
+
return;
|
|
52848
|
+
}
|
|
52849
|
+
}
|
|
52850
|
+
const importMapping = {};
|
|
52851
|
+
for (const p of mappingResult.pairs) importMapping[p.synthLogicalId] = p.physicalId;
|
|
52852
|
+
const resolvedStateBucket = await resolveStateBucketWithDefault(options.stateBucket, region);
|
|
52853
|
+
await runImport(outputStack, {
|
|
52854
|
+
app: libResult.assemblyDir,
|
|
52855
|
+
statePrefix: options.statePrefix ?? "cdkd",
|
|
52856
|
+
resourceMappingInline: JSON.stringify(importMapping),
|
|
52857
|
+
auto: false,
|
|
52858
|
+
dryRun: false,
|
|
52859
|
+
yes: true,
|
|
52860
|
+
force: false,
|
|
52861
|
+
verbose: options.verbose ?? false,
|
|
52862
|
+
stateBucket: resolvedStateBucket,
|
|
52863
|
+
...options.region && { region: options.region },
|
|
52864
|
+
...options.profile && { profile: options.profile },
|
|
52865
|
+
...options.roleArn && { roleArn: options.roleArn }
|
|
52866
|
+
});
|
|
52867
|
+
if (options.retireCfnStack) {
|
|
52868
|
+
logger.info(`[migrate] Retiring source CloudFormation stack '${sourceCfnStackName}'...`);
|
|
52869
|
+
const cfnConfig = {};
|
|
52870
|
+
if (options.region) cfnConfig.region = options.region;
|
|
52871
|
+
if (options.profile) cfnConfig.profile = options.profile;
|
|
52872
|
+
const awsClients = new AwsClients(cfnConfig);
|
|
52873
|
+
const cfnClient = awsClients.cloudFormation;
|
|
52874
|
+
try {
|
|
52875
|
+
await retireCloudFormationStack({
|
|
52876
|
+
cfnStackName: sourceCfnStackName,
|
|
52877
|
+
cfnClient,
|
|
52878
|
+
yes: options.yes ?? false,
|
|
52879
|
+
stateBucket: resolvedStateBucket,
|
|
52880
|
+
...options.profile && { s3ClientOpts: { profile: options.profile } }
|
|
52881
|
+
});
|
|
52882
|
+
} finally {
|
|
52883
|
+
awsClients.destroy();
|
|
52884
|
+
}
|
|
52885
|
+
}
|
|
52886
|
+
logger.info(`[migrate] Migrated ${mappingResult.pairs.length} resource(s) from CloudFormation stack '${sourceCfnStackName}' to cdkd state for stack '${outputStack}'.`);
|
|
52887
|
+
logger.info(`[migrate] Generated CDK app at ${libResult.outputDir}`);
|
|
52888
|
+
if (!options.retireCfnStack) logger.info(`[migrate] Source CloudFormation stack '${sourceCfnStackName}' is unchanged. Re-run with --retire-cfn-stack to retire it (or do so manually later).`);
|
|
52889
|
+
}
|
|
52890
|
+
/**
|
|
52891
|
+
* Print a tabular `(sourceLogicalId → synthLogicalId physicalId)`
|
|
52892
|
+
* summary so users can see what's about to be imported before the
|
|
52893
|
+
* confirmation prompt. Matches the visual shape of `cdkd import`'s
|
|
52894
|
+
* summary table for consistency.
|
|
52895
|
+
*/
|
|
52896
|
+
function printMappingTable(result, sourceStack, outputStack) {
|
|
52897
|
+
const logger = getLogger();
|
|
52898
|
+
logger.info("");
|
|
52899
|
+
logger.info(`[migrate] Resolved mapping (${sourceStack} → ${outputStack}):`);
|
|
52900
|
+
for (const p of result.pairs) logger.info(` ${p.sourceLogicalId} → ${p.synthLogicalId} [${p.resourceType}] ${p.physicalId}`);
|
|
52901
|
+
logger.info("");
|
|
52902
|
+
}
|
|
52903
|
+
/**
|
|
52904
|
+
* Minimal `(y/N)` confirmation prompt using `node:readline`. Mirrors
|
|
52905
|
+
* the shape of [src/cli/commands/import.ts](import.ts)'s `confirmPrompt`
|
|
52906
|
+
* so the user UX is consistent across the import + migrate surfaces.
|
|
52907
|
+
* Non-TTY callers (CI) should pass `--yes` to skip this entirely.
|
|
52908
|
+
*/
|
|
52909
|
+
async function promptConfirm(message) {
|
|
52910
|
+
if (!process.stdin.isTTY) throw new LocalMigrateError(`Non-interactive shell detected and --yes was not supplied. Re-run with --yes to confirm: "${message}"`);
|
|
52911
|
+
const rl = (await import("node:readline/promises")).createInterface({
|
|
52912
|
+
input: process.stdin,
|
|
52913
|
+
output: process.stdout
|
|
52914
|
+
});
|
|
52915
|
+
try {
|
|
52916
|
+
const v = (await rl.question(`${message} (y/N) `)).trim().toLowerCase();
|
|
52917
|
+
return v === "y" || v === "yes";
|
|
52918
|
+
} finally {
|
|
52919
|
+
rl.close();
|
|
52920
|
+
}
|
|
52921
|
+
}
|
|
52922
|
+
/**
|
|
52923
|
+
* Commander factory for `cdkd migrate`. Registered in `src/cli/index.ts`
|
|
52924
|
+
* alongside the existing top-level commands. Matches the design doc §3
|
|
52925
|
+
* flag set; the action handler routes through {@link migrateCommandAction}
|
|
52926
|
+
* via `withErrorHandling` so library callers see exceptions but CLI
|
|
52927
|
+
* callers get the standard exit-code-2 routing.
|
|
52928
|
+
*/
|
|
52929
|
+
function createMigrateCommand() {
|
|
52930
|
+
return new Command("migrate").description("Adopt a plain (non-CDK) CloudFormation stack into a cdkd-managed CDK app. Generates new CDK code via upstream `cdk migrate`, builds a logical-ID mapping between the source CFn template and the synth template, writes cdkd state, and (optionally) retires the source CFn stack. AWS resources are never modified.").argument("[stack]", "Source CFn stack name. Alias for --from-cfn-stack.").addOption(new Option("--from-cfn-stack <name>", "Source CloudFormation stack name to adopt. Required (or pass positionally).")).addOption(new Option("--output-dir <dir>", "Directory to write the generated CDK app to. Defaults to <cwd>/<CfnStackName>.")).addOption(new Option("--language <choice>", "Generated code language. v1: typescript only.").choices(["typescript"]).default("typescript")).addOption(new Option("--region <region>", "AWS region. Defaults to AWS_REGION env / profile.")).addOption(new Option("--account <id>", "AWS account ID. Auto-detected via STS when omitted.")).addOption(new Option("--retire-cfn-stack", "After cdkd state is written, inject DeletionPolicy=Retain on every resource in the source CFn stack and DeleteStack. AWS resources stay; the CFn stack record is gone. Off by default.").default(false)).addOption(new Option("--filter <key=value>", "Pass-through to `cdk migrate --filter` for resource subsetting. Repeatable.").argParser((value, previous) => [...previous ?? [], value]).default([])).addOption(new Option("--skip-install", "Skip `npm install` after codegen.").default(false)).addOption(new Option("--skip-synth", "Skip `cdk synth` (does NOT write cdkd state). Mutually exclusive with --retire-cfn-stack.").default(false)).addOption(new Option("--dry-run", "Print the import plan without writing state or retiring the CFn stack. Mutually exclusive with --retire-cfn-stack.").default(false)).addOption(new Option("-y, --yes", "Auto-confirm the import + retirement prompts.").default(false)).addOption(new Option("--cdk-bin <path>", "Override the `cdk` binary path.")).addOption(new Option("--resource-mapping <file>", `Path to a JSON file of {sourceLogicalId: synthLogicalId} overrides. Same shape as the auto-written ${RESOURCE_MAPPING_FILENAME}.`)).addOption(new Option("--state-bucket <name>", "cdkd state bucket. Defaults to cdkd-state-<accountId>.")).addOption(new Option("--state-prefix <prefix>", "cdkd state prefix inside the bucket.").default("cdkd")).addOption(new Option("--profile <name>", "AWS profile name.")).addOption(new Option("--role-arn <arn>", "IAM role to assume before any AWS call.")).addOption(new Option("--verbose", "Enable debug-level logging.").default(false)).action(withErrorHandling(migrateCommandAction));
|
|
52931
|
+
}
|
|
52932
|
+
|
|
51849
52933
|
//#endregion
|
|
51850
52934
|
//#region src/cli/index.ts
|
|
51851
52935
|
const SUBCOMMANDS = new Set([
|
|
@@ -51863,7 +52947,8 @@ const SUBCOMMANDS = new Set([
|
|
|
51863
52947
|
"publish-assets",
|
|
51864
52948
|
"force-unlock",
|
|
51865
52949
|
"state",
|
|
51866
|
-
"local"
|
|
52950
|
+
"local",
|
|
52951
|
+
"migrate"
|
|
51867
52952
|
]);
|
|
51868
52953
|
/**
|
|
51869
52954
|
* Reorder args so options before the subcommand are moved after it.
|
|
@@ -51887,7 +52972,7 @@ function reorderArgs(argv) {
|
|
|
51887
52972
|
*/
|
|
51888
52973
|
async function main() {
|
|
51889
52974
|
const program = new Command();
|
|
51890
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
52975
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.135.0");
|
|
51891
52976
|
program.addCommand(createBootstrapCommand());
|
|
51892
52977
|
program.addCommand(createSynthCommand());
|
|
51893
52978
|
program.addCommand(createListCommand());
|
|
@@ -51902,6 +52987,7 @@ async function main() {
|
|
|
51902
52987
|
program.addCommand(createStateCommand());
|
|
51903
52988
|
program.addCommand(createLocalCommand());
|
|
51904
52989
|
program.addCommand(createExportCommand());
|
|
52990
|
+
program.addCommand(createMigrateCommand());
|
|
51905
52991
|
const args = reorderArgs(process.argv);
|
|
51906
52992
|
await program.parseAsync(args);
|
|
51907
52993
|
}
|