@go-to-k/cdkd 0.28.2 → 0.30.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 +58 -526
- package/dist/cli.js +390 -13
- package/dist/cli.js.map +4 -4
- package/dist/go-to-k-cdkd-0.30.0.tgz +0 -0
- package/dist/index.js +103 -0
- package/dist/index.js.map +2 -2
- package/package.json +1 -1
- package/dist/go-to-k-cdkd-0.28.2.tgz +0 -0
package/dist/cli.js
CHANGED
|
@@ -1304,14 +1304,14 @@ async function resolveStateBucketWithDefaultAndSource(cliBucket, region) {
|
|
|
1304
1304
|
const logger = getLogger();
|
|
1305
1305
|
logger.debug("No state bucket specified, resolving default from account...");
|
|
1306
1306
|
const { GetCallerIdentityCommand: GetCallerIdentityCommand11 } = await import("@aws-sdk/client-sts");
|
|
1307
|
-
const { S3Client:
|
|
1307
|
+
const { S3Client: S3Client12 } = await import("@aws-sdk/client-s3");
|
|
1308
1308
|
const { getAwsClients: getAwsClients2 } = await Promise.resolve().then(() => (init_aws_clients(), aws_clients_exports));
|
|
1309
1309
|
const awsClients = getAwsClients2();
|
|
1310
1310
|
const identity = await awsClients.sts.send(new GetCallerIdentityCommand11({}));
|
|
1311
1311
|
const accountId = identity.Account;
|
|
1312
1312
|
const newName = getDefaultStateBucketName(accountId);
|
|
1313
1313
|
const legacyName = getLegacyStateBucketName(accountId, region);
|
|
1314
|
-
const probe = new
|
|
1314
|
+
const probe = new S3Client12({ region: "us-east-1" });
|
|
1315
1315
|
try {
|
|
1316
1316
|
const newExists = await bucketExists(probe, newName);
|
|
1317
1317
|
const legacyExists = await bucketExists(probe, legacyName);
|
|
@@ -5736,6 +5736,9 @@ var IntrinsicFunctionResolver = class {
|
|
|
5736
5736
|
if ("Fn::ImportValue" in obj) {
|
|
5737
5737
|
return await this.resolveImportValue(obj["Fn::ImportValue"], context);
|
|
5738
5738
|
}
|
|
5739
|
+
if ("Fn::GetStackOutput" in obj) {
|
|
5740
|
+
return await this.resolveGetStackOutput(obj["Fn::GetStackOutput"], context);
|
|
5741
|
+
}
|
|
5739
5742
|
if ("Fn::FindInMap" in obj) {
|
|
5740
5743
|
return await this.resolveFindInMap(
|
|
5741
5744
|
obj["Fn::FindInMap"],
|
|
@@ -6284,6 +6287,106 @@ var IntrinsicFunctionResolver = class {
|
|
|
6284
6287
|
`Fn::ImportValue: export '${exportName}' not found in any stack. Searched ${allStacks.length} state record(s). Make sure the exporting stack has been deployed and the Output has an Export.Name property.`
|
|
6285
6288
|
);
|
|
6286
6289
|
}
|
|
6290
|
+
/**
|
|
6291
|
+
* Resolve Fn::GetStackOutput (cross-stack / cross-region output reference)
|
|
6292
|
+
*
|
|
6293
|
+
* Shape: { "Fn::GetStackOutput": { "StackName": "...", "OutputName": "...",
|
|
6294
|
+
* "Region": "...", "RoleArn": "..." } }
|
|
6295
|
+
*
|
|
6296
|
+
* Unlike Fn::ImportValue, the producer stack is named explicitly and no
|
|
6297
|
+
* Export is required. cdkd reads the producer's `outputs` from the
|
|
6298
|
+
* region-scoped state record at
|
|
6299
|
+
* `s3://{bucket}/cdkd/{StackName}/{Region}/state.json`. When `Region` is
|
|
6300
|
+
* omitted, the consumer's deploy region is used.
|
|
6301
|
+
*
|
|
6302
|
+
* RoleArn (cross-account) is intentionally rejected — cdkd uses S3 state,
|
|
6303
|
+
* not CloudFormation DescribeStacks, so a cross-account reference would
|
|
6304
|
+
* require assuming the role and reading the producer's separate state
|
|
6305
|
+
* bucket. That path is not yet implemented; we surface a clear error
|
|
6306
|
+
* instead of silently downgrading.
|
|
6307
|
+
*/
|
|
6308
|
+
async resolveGetStackOutput(arg, context) {
|
|
6309
|
+
if (!arg || typeof arg !== "object" || Array.isArray(arg)) {
|
|
6310
|
+
throw new Error(
|
|
6311
|
+
`Fn::GetStackOutput: argument must be an object with StackName/OutputName/Region/RoleArn, got ${arg === null ? "null" : Array.isArray(arg) ? "array" : typeof arg}`
|
|
6312
|
+
);
|
|
6313
|
+
}
|
|
6314
|
+
const args = arg;
|
|
6315
|
+
if (!("StackName" in args)) {
|
|
6316
|
+
throw new Error("Fn::GetStackOutput: StackName is required");
|
|
6317
|
+
}
|
|
6318
|
+
if (!("OutputName" in args)) {
|
|
6319
|
+
throw new Error("Fn::GetStackOutput: OutputName is required");
|
|
6320
|
+
}
|
|
6321
|
+
const stackName = await this.resolveValue(args["StackName"], context);
|
|
6322
|
+
if (typeof stackName !== "string" || stackName === "") {
|
|
6323
|
+
throw new Error(
|
|
6324
|
+
`Fn::GetStackOutput: StackName must resolve to a non-empty string, got ${typeof stackName}`
|
|
6325
|
+
);
|
|
6326
|
+
}
|
|
6327
|
+
const outputName = await this.resolveValue(args["OutputName"], context);
|
|
6328
|
+
if (typeof outputName !== "string" || outputName === "") {
|
|
6329
|
+
throw new Error(
|
|
6330
|
+
`Fn::GetStackOutput: OutputName must resolve to a non-empty string, got ${typeof outputName}`
|
|
6331
|
+
);
|
|
6332
|
+
}
|
|
6333
|
+
let region = this.resolverRegion;
|
|
6334
|
+
if ("Region" in args && args["Region"] !== void 0 && args["Region"] !== null) {
|
|
6335
|
+
const resolvedRegion = await this.resolveValue(args["Region"], context);
|
|
6336
|
+
if (typeof resolvedRegion !== "string" || resolvedRegion === "") {
|
|
6337
|
+
throw new Error(
|
|
6338
|
+
`Fn::GetStackOutput: Region must resolve to a non-empty string, got ${typeof resolvedRegion}`
|
|
6339
|
+
);
|
|
6340
|
+
}
|
|
6341
|
+
region = resolvedRegion;
|
|
6342
|
+
}
|
|
6343
|
+
let roleArn;
|
|
6344
|
+
if ("RoleArn" in args && args["RoleArn"] !== void 0 && args["RoleArn"] !== null) {
|
|
6345
|
+
const resolvedRoleArn = await this.resolveValue(args["RoleArn"], context);
|
|
6346
|
+
if (typeof resolvedRoleArn !== "string" || resolvedRoleArn === "") {
|
|
6347
|
+
throw new Error(
|
|
6348
|
+
`Fn::GetStackOutput: RoleArn must resolve to a non-empty string, got ${typeof resolvedRoleArn}`
|
|
6349
|
+
);
|
|
6350
|
+
}
|
|
6351
|
+
roleArn = resolvedRoleArn;
|
|
6352
|
+
}
|
|
6353
|
+
if (roleArn) {
|
|
6354
|
+
throw new Error(
|
|
6355
|
+
`Fn::GetStackOutput: cross-account references via RoleArn are not yet supported by cdkd (StackName=${stackName}, Region=${region}, RoleArn=${roleArn}). cdkd reads outputs from S3 state instead of CloudFormation DescribeStacks, so cross-account requires assuming the role and reading the producer account's state bucket \u2014 not yet implemented.`
|
|
6356
|
+
);
|
|
6357
|
+
}
|
|
6358
|
+
if (!context.stateBackend) {
|
|
6359
|
+
throw new Error("Fn::GetStackOutput: state backend is required for cross-stack references");
|
|
6360
|
+
}
|
|
6361
|
+
if (context.stackName && context.stackName === stackName && region === this.resolverRegion) {
|
|
6362
|
+
throw new Error(
|
|
6363
|
+
`Fn::GetStackOutput: cannot reference own stack '${stackName}' in the same region '${region}'`
|
|
6364
|
+
);
|
|
6365
|
+
}
|
|
6366
|
+
this.logger.debug(
|
|
6367
|
+
`Resolving Fn::GetStackOutput: StackName=${stackName}, Region=${region}, OutputName=${outputName}`
|
|
6368
|
+
);
|
|
6369
|
+
const stateData = await context.stateBackend.getState(stackName, region);
|
|
6370
|
+
if (!stateData) {
|
|
6371
|
+
throw new Error(
|
|
6372
|
+
`Fn::GetStackOutput: stack '${stackName}' not found in region '${region}'. Make sure the producer stack has been deployed via cdkd.`
|
|
6373
|
+
);
|
|
6374
|
+
}
|
|
6375
|
+
const outputs = stateData.state.outputs ?? {};
|
|
6376
|
+
if (!(outputName in outputs)) {
|
|
6377
|
+
const available = Object.keys(outputs).join(", ") || "(none)";
|
|
6378
|
+
throw new Error(
|
|
6379
|
+
`Fn::GetStackOutput: output '${outputName}' not found in stack '${stackName}' (${region}). Available outputs: ${available}`
|
|
6380
|
+
);
|
|
6381
|
+
}
|
|
6382
|
+
const value = outputs[outputName];
|
|
6383
|
+
this.logger.info(
|
|
6384
|
+
`Resolved Fn::GetStackOutput: StackName=${stackName}, Region=${region}, OutputName=${outputName} -> ${JSON.stringify(
|
|
6385
|
+
value
|
|
6386
|
+
)}`
|
|
6387
|
+
);
|
|
6388
|
+
return value;
|
|
6389
|
+
}
|
|
6287
6390
|
/**
|
|
6288
6391
|
* Resolve Fn::FindInMap intrinsic function
|
|
6289
6392
|
*
|
|
@@ -35212,9 +35315,227 @@ function createStateCommand() {
|
|
|
35212
35315
|
|
|
35213
35316
|
// src/cli/commands/import.ts
|
|
35214
35317
|
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
35215
|
-
import * as
|
|
35318
|
+
import * as readline6 from "node:readline/promises";
|
|
35216
35319
|
import { Command as Command12 } from "commander";
|
|
35217
35320
|
init_aws_clients();
|
|
35321
|
+
|
|
35322
|
+
// src/cli/commands/retire-cfn-stack.ts
|
|
35323
|
+
import * as readline5 from "node:readline/promises";
|
|
35324
|
+
import {
|
|
35325
|
+
DescribeStacksCommand,
|
|
35326
|
+
DescribeStackResourcesCommand,
|
|
35327
|
+
GetTemplateCommand,
|
|
35328
|
+
UpdateStackCommand,
|
|
35329
|
+
DeleteStackCommand,
|
|
35330
|
+
waitUntilStackUpdateComplete,
|
|
35331
|
+
waitUntilStackDeleteComplete
|
|
35332
|
+
} from "@aws-sdk/client-cloudformation";
|
|
35333
|
+
import { S3Client as S3Client11, PutObjectCommand as PutObjectCommand5, DeleteObjectCommand as DeleteObjectCommand4 } from "@aws-sdk/client-s3";
|
|
35334
|
+
var STABLE_TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
35335
|
+
"CREATE_COMPLETE",
|
|
35336
|
+
"UPDATE_COMPLETE",
|
|
35337
|
+
"UPDATE_ROLLBACK_COMPLETE",
|
|
35338
|
+
"IMPORT_COMPLETE",
|
|
35339
|
+
"IMPORT_ROLLBACK_COMPLETE"
|
|
35340
|
+
]);
|
|
35341
|
+
var TEMPLATE_BODY_LIMIT = 51200;
|
|
35342
|
+
var TEMPLATE_URL_LIMIT = 1048576;
|
|
35343
|
+
var MIGRATE_TMP_PREFIX = "cdkd-migrate-tmp";
|
|
35344
|
+
async function retireCloudFormationStack(options) {
|
|
35345
|
+
const logger = getLogger();
|
|
35346
|
+
const { cfnStackName, cfnClient, yes, stateBucket, s3ClientOpts } = options;
|
|
35347
|
+
logger.info(`[1/4] Inspecting CloudFormation stack '${cfnStackName}'...`);
|
|
35348
|
+
const desc = await cfnClient.send(new DescribeStacksCommand({ StackName: cfnStackName }));
|
|
35349
|
+
const stack = desc.Stacks?.[0];
|
|
35350
|
+
if (!stack) {
|
|
35351
|
+
throw new Error(`CloudFormation stack '${cfnStackName}' not found.`);
|
|
35352
|
+
}
|
|
35353
|
+
const status = stack.StackStatus ?? "";
|
|
35354
|
+
if (!STABLE_TERMINAL_STATUSES.has(status)) {
|
|
35355
|
+
throw new Error(
|
|
35356
|
+
`CloudFormation stack '${cfnStackName}' is in status '${status}', which is not a stable terminal state. Wait for the stack to settle (or roll back) before retiring it.`
|
|
35357
|
+
);
|
|
35358
|
+
}
|
|
35359
|
+
const capabilities = stack.Capabilities ?? [];
|
|
35360
|
+
const tpl = await cfnClient.send(
|
|
35361
|
+
new GetTemplateCommand({ StackName: cfnStackName, TemplateStage: "Original" })
|
|
35362
|
+
);
|
|
35363
|
+
if (!tpl.TemplateBody) {
|
|
35364
|
+
throw new Error(`GetTemplate returned no body for '${cfnStackName}'.`);
|
|
35365
|
+
}
|
|
35366
|
+
const { body: newBody, modified } = injectRetainPolicies(tpl.TemplateBody, cfnStackName);
|
|
35367
|
+
if (!yes) {
|
|
35368
|
+
const ok = await confirmPrompt3(
|
|
35369
|
+
`Set DeletionPolicy=Retain and UpdateReplacePolicy=Retain on every resource in CloudFormation stack '${cfnStackName}', then delete the stack? AWS resources will NOT be deleted (cdkd state has been written).`
|
|
35370
|
+
);
|
|
35371
|
+
if (!ok) {
|
|
35372
|
+
logger.info("CloudFormation stack retirement cancelled. cdkd state is unaffected.");
|
|
35373
|
+
return { outcome: "cancelled" };
|
|
35374
|
+
}
|
|
35375
|
+
}
|
|
35376
|
+
if (!modified) {
|
|
35377
|
+
logger.info(`[2/4] Template already has Retain on every resource \u2014 skipping UpdateStack.`);
|
|
35378
|
+
} else {
|
|
35379
|
+
logger.info(`[2/4] Injected DeletionPolicy=Retain and UpdateReplacePolicy=Retain.`);
|
|
35380
|
+
if (newBody.length > TEMPLATE_URL_LIMIT) {
|
|
35381
|
+
throw new Error(
|
|
35382
|
+
`Modified template is ${newBody.length} bytes, exceeds the CloudFormation UpdateStack TemplateURL limit (${TEMPLATE_URL_LIMIT}). cdkd state has already been written; retire the stack manually with (1) shrink the template, then (2) UpdateStack with Retain policies, (3) DeleteStack \u2014 or split the stack and retry.`
|
|
35383
|
+
);
|
|
35384
|
+
}
|
|
35385
|
+
let updateInput;
|
|
35386
|
+
let s3Cleanup;
|
|
35387
|
+
if (newBody.length <= TEMPLATE_BODY_LIMIT) {
|
|
35388
|
+
updateInput = { TemplateBody: newBody };
|
|
35389
|
+
} else {
|
|
35390
|
+
logger.info(
|
|
35391
|
+
` Template is ${newBody.length} bytes (over ${TEMPLATE_BODY_LIMIT} inline limit) \u2014 uploading to state bucket '${stateBucket}'.`
|
|
35392
|
+
);
|
|
35393
|
+
const uploaded = await uploadTemplateForUpdateStack({
|
|
35394
|
+
bucket: stateBucket,
|
|
35395
|
+
body: newBody,
|
|
35396
|
+
cfnStackName,
|
|
35397
|
+
...s3ClientOpts && { s3ClientOpts }
|
|
35398
|
+
});
|
|
35399
|
+
updateInput = { TemplateURL: uploaded.url };
|
|
35400
|
+
s3Cleanup = uploaded.cleanup;
|
|
35401
|
+
}
|
|
35402
|
+
try {
|
|
35403
|
+
logger.info(`[3/4] Updating CloudFormation stack with Retain policies...`);
|
|
35404
|
+
let updateRan = false;
|
|
35405
|
+
try {
|
|
35406
|
+
await cfnClient.send(
|
|
35407
|
+
new UpdateStackCommand({
|
|
35408
|
+
StackName: cfnStackName,
|
|
35409
|
+
...updateInput,
|
|
35410
|
+
Capabilities: capabilities
|
|
35411
|
+
})
|
|
35412
|
+
);
|
|
35413
|
+
updateRan = true;
|
|
35414
|
+
} catch (err) {
|
|
35415
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
35416
|
+
if (/No updates are to be performed/i.test(msg)) {
|
|
35417
|
+
logger.info(` CloudFormation reports no updates needed \u2014 proceeding to delete.`);
|
|
35418
|
+
} else {
|
|
35419
|
+
throw err;
|
|
35420
|
+
}
|
|
35421
|
+
}
|
|
35422
|
+
if (updateRan) {
|
|
35423
|
+
await waitUntilStackUpdateComplete(
|
|
35424
|
+
{ client: cfnClient, maxWaitTime: 1800 },
|
|
35425
|
+
{ StackName: cfnStackName }
|
|
35426
|
+
);
|
|
35427
|
+
}
|
|
35428
|
+
} finally {
|
|
35429
|
+
if (s3Cleanup) {
|
|
35430
|
+
try {
|
|
35431
|
+
await s3Cleanup();
|
|
35432
|
+
} catch (cleanupErr) {
|
|
35433
|
+
const msg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
|
|
35434
|
+
logger.warn(
|
|
35435
|
+
`Failed to delete temporary template upload from '${stateBucket}'. Clean up manually under prefix '${MIGRATE_TMP_PREFIX}/'. Cause: ${msg}`
|
|
35436
|
+
);
|
|
35437
|
+
}
|
|
35438
|
+
}
|
|
35439
|
+
}
|
|
35440
|
+
}
|
|
35441
|
+
logger.info(`[4/4] Deleting CloudFormation stack '${cfnStackName}' (resources retained)...`);
|
|
35442
|
+
await cfnClient.send(new DeleteStackCommand({ StackName: cfnStackName }));
|
|
35443
|
+
await waitUntilStackDeleteComplete(
|
|
35444
|
+
{ client: cfnClient, maxWaitTime: 1800 },
|
|
35445
|
+
{ StackName: cfnStackName }
|
|
35446
|
+
);
|
|
35447
|
+
logger.info(
|
|
35448
|
+
`\u2713 CloudFormation stack '${cfnStackName}' retired. AWS resources are now solely managed by cdkd.`
|
|
35449
|
+
);
|
|
35450
|
+
return { outcome: modified ? "retired" : "no-template-change" };
|
|
35451
|
+
}
|
|
35452
|
+
async function uploadTemplateForUpdateStack(args) {
|
|
35453
|
+
const { bucket, body, cfnStackName, s3ClientOpts } = args;
|
|
35454
|
+
const region = await resolveBucketRegion(bucket, {
|
|
35455
|
+
...s3ClientOpts?.profile && { profile: s3ClientOpts.profile },
|
|
35456
|
+
...s3ClientOpts?.credentials && { credentials: s3ClientOpts.credentials }
|
|
35457
|
+
});
|
|
35458
|
+
const s3 = new S3Client11({
|
|
35459
|
+
region,
|
|
35460
|
+
...s3ClientOpts?.profile && { profile: s3ClientOpts.profile },
|
|
35461
|
+
...s3ClientOpts?.credentials && { credentials: s3ClientOpts.credentials }
|
|
35462
|
+
});
|
|
35463
|
+
const key = `${MIGRATE_TMP_PREFIX}/${cfnStackName}/${Date.now()}.json`;
|
|
35464
|
+
try {
|
|
35465
|
+
await s3.send(
|
|
35466
|
+
new PutObjectCommand5({
|
|
35467
|
+
Bucket: bucket,
|
|
35468
|
+
Key: key,
|
|
35469
|
+
Body: body,
|
|
35470
|
+
ContentType: "application/json"
|
|
35471
|
+
})
|
|
35472
|
+
);
|
|
35473
|
+
} catch (err) {
|
|
35474
|
+
s3.destroy();
|
|
35475
|
+
throw err;
|
|
35476
|
+
}
|
|
35477
|
+
const url = `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
|
|
35478
|
+
const cleanup = async () => {
|
|
35479
|
+
try {
|
|
35480
|
+
await s3.send(new DeleteObjectCommand4({ Bucket: bucket, Key: key }));
|
|
35481
|
+
} finally {
|
|
35482
|
+
s3.destroy();
|
|
35483
|
+
}
|
|
35484
|
+
};
|
|
35485
|
+
return { url, cleanup };
|
|
35486
|
+
}
|
|
35487
|
+
function injectRetainPolicies(templateBody, cfnStackName) {
|
|
35488
|
+
let parsed;
|
|
35489
|
+
try {
|
|
35490
|
+
parsed = JSON.parse(templateBody);
|
|
35491
|
+
} catch (err) {
|
|
35492
|
+
throw new Error(
|
|
35493
|
+
`Template for '${cfnStackName}' is not valid JSON. cdkd's --migrate-from-cloudformation flow only supports CDK-generated (JSON) templates. Cause: ${err instanceof Error ? err.message : String(err)}`
|
|
35494
|
+
);
|
|
35495
|
+
}
|
|
35496
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || !("Resources" in parsed) || typeof parsed.Resources !== "object" || parsed.Resources === null) {
|
|
35497
|
+
throw new Error(
|
|
35498
|
+
`Template for '${cfnStackName}' has no Resources section \u2014 refusing to retire.`
|
|
35499
|
+
);
|
|
35500
|
+
}
|
|
35501
|
+
let modified = false;
|
|
35502
|
+
const resources = parsed.Resources;
|
|
35503
|
+
for (const [, resource] of Object.entries(resources)) {
|
|
35504
|
+
if (!resource || typeof resource !== "object" || Array.isArray(resource))
|
|
35505
|
+
continue;
|
|
35506
|
+
const r = resource;
|
|
35507
|
+
if (r["DeletionPolicy"] !== "Retain") {
|
|
35508
|
+
r["DeletionPolicy"] = "Retain";
|
|
35509
|
+
modified = true;
|
|
35510
|
+
}
|
|
35511
|
+
if (r["UpdateReplacePolicy"] !== "Retain") {
|
|
35512
|
+
r["UpdateReplacePolicy"] = "Retain";
|
|
35513
|
+
modified = true;
|
|
35514
|
+
}
|
|
35515
|
+
}
|
|
35516
|
+
return { body: JSON.stringify(parsed, null, 2), modified };
|
|
35517
|
+
}
|
|
35518
|
+
async function getCloudFormationResourceMapping(cfnStackName, cfnClient) {
|
|
35519
|
+
const resp = await cfnClient.send(new DescribeStackResourcesCommand({ StackName: cfnStackName }));
|
|
35520
|
+
const map = /* @__PURE__ */ new Map();
|
|
35521
|
+
for (const r of resp.StackResources ?? []) {
|
|
35522
|
+
if (!r.LogicalResourceId || !r.PhysicalResourceId)
|
|
35523
|
+
continue;
|
|
35524
|
+
map.set(r.LogicalResourceId, r.PhysicalResourceId);
|
|
35525
|
+
}
|
|
35526
|
+
return map;
|
|
35527
|
+
}
|
|
35528
|
+
async function confirmPrompt3(prompt) {
|
|
35529
|
+
const rl = readline5.createInterface({ input: process.stdin, output: process.stdout });
|
|
35530
|
+
try {
|
|
35531
|
+
const ans = await rl.question(`${prompt} [y/N] `);
|
|
35532
|
+
return /^y(es)?$/i.test(ans.trim());
|
|
35533
|
+
} finally {
|
|
35534
|
+
rl.close();
|
|
35535
|
+
}
|
|
35536
|
+
}
|
|
35537
|
+
|
|
35538
|
+
// src/cli/commands/import.ts
|
|
35218
35539
|
async function importCommand(stackArg, options) {
|
|
35219
35540
|
const logger = getLogger();
|
|
35220
35541
|
if (options.verbose) {
|
|
@@ -35281,7 +35602,46 @@ async function importCommand(stackArg, options) {
|
|
|
35281
35602
|
if (overrides.size > 0) {
|
|
35282
35603
|
logger.debug(`User-supplied physical IDs: ${[...overrides.keys()].join(", ")}`);
|
|
35283
35604
|
}
|
|
35284
|
-
const
|
|
35605
|
+
const migrationCfnStackName = options.migrateFromCloudformation ? typeof options.migrateFromCloudformation === "string" && options.migrateFromCloudformation.length > 0 ? options.migrateFromCloudformation : stackInfo.stackName : void 0;
|
|
35606
|
+
if (options.migrateFromCloudformation && options.dryRun) {
|
|
35607
|
+
throw new Error(
|
|
35608
|
+
"--migrate-from-cloudformation is not compatible with --dry-run: the post-state-write retirement (UpdateStack + DeleteStack) issues real AWS calls. Use plain `cdkd import --dry-run` to preview the import in isolation."
|
|
35609
|
+
);
|
|
35610
|
+
}
|
|
35611
|
+
const template = stackInfo.template;
|
|
35612
|
+
const templateParser = new TemplateParser();
|
|
35613
|
+
const resources = collectImportableResources(template);
|
|
35614
|
+
const templateLogicalIds = new Set(resources.map((r) => r.logicalId));
|
|
35615
|
+
logger.info(`Found ${resources.length} resource(s) in template`);
|
|
35616
|
+
if (migrationCfnStackName) {
|
|
35617
|
+
logger.info(`Resolving physical IDs from CloudFormation stack '${migrationCfnStackName}'...`);
|
|
35618
|
+
const cfnMapping = await getCloudFormationResourceMapping(
|
|
35619
|
+
migrationCfnStackName,
|
|
35620
|
+
awsClients.cloudFormation
|
|
35621
|
+
);
|
|
35622
|
+
let derived = 0;
|
|
35623
|
+
let skippedNonImportable = 0;
|
|
35624
|
+
for (const [logicalId, physicalId] of cfnMapping) {
|
|
35625
|
+
if (!templateLogicalIds.has(logicalId)) {
|
|
35626
|
+
skippedNonImportable++;
|
|
35627
|
+
continue;
|
|
35628
|
+
}
|
|
35629
|
+
if (!overrides.has(logicalId)) {
|
|
35630
|
+
overrides.set(logicalId, physicalId);
|
|
35631
|
+
derived++;
|
|
35632
|
+
}
|
|
35633
|
+
}
|
|
35634
|
+
const overriddenByUser = cfnMapping.size - derived - skippedNonImportable;
|
|
35635
|
+
const detail = [];
|
|
35636
|
+
if (overriddenByUser > 0)
|
|
35637
|
+
detail.push(`${overriddenByUser} already overridden by --resource`);
|
|
35638
|
+
if (skippedNonImportable > 0)
|
|
35639
|
+
detail.push(`${skippedNonImportable} non-importable (e.g. CDKMetadata)`);
|
|
35640
|
+
logger.info(
|
|
35641
|
+
`Resolved ${derived} physical ID(s) from CloudFormation` + (detail.length > 0 ? ` (${detail.join(", ")})` : "")
|
|
35642
|
+
);
|
|
35643
|
+
}
|
|
35644
|
+
const selectiveMode = overrides.size > 0 && !options.auto && !options.migrateFromCloudformation;
|
|
35285
35645
|
if (selectiveMode) {
|
|
35286
35646
|
logger.info(
|
|
35287
35647
|
`Selective mode: only importing the ${overrides.size} resource(s) you listed (${[...overrides.keys()].join(", ")}). Pass --auto to also tag-import the rest.`
|
|
@@ -35315,11 +35675,6 @@ async function importCommand(stackArg, options) {
|
|
|
35315
35675
|
);
|
|
35316
35676
|
}
|
|
35317
35677
|
}
|
|
35318
|
-
const template = stackInfo.template;
|
|
35319
|
-
const templateParser = new TemplateParser();
|
|
35320
|
-
const resources = collectImportableResources(template);
|
|
35321
|
-
logger.info(`Found ${resources.length} resource(s) in template`);
|
|
35322
|
-
const templateLogicalIds = new Set(resources.map((r) => r.logicalId));
|
|
35323
35678
|
for (const overrideId of overrides.keys()) {
|
|
35324
35679
|
if (!templateLogicalIds.has(overrideId)) {
|
|
35325
35680
|
throw new Error(
|
|
@@ -35369,7 +35724,7 @@ async function importCommand(stackArg, options) {
|
|
|
35369
35724
|
const preservedCount = selectiveMode && existingState ? Object.keys(existingState.resources).filter((id) => !overrides.has(id)).length : 0;
|
|
35370
35725
|
const totalAfter = importedCount + preservedCount;
|
|
35371
35726
|
const breakdown = preservedCount > 0 ? ` (${importedCount} new/overwritten + ${preservedCount} preserved)` : "";
|
|
35372
|
-
const ok = await
|
|
35727
|
+
const ok = await confirmPrompt4(
|
|
35373
35728
|
`Write state for ${stackInfo.stackName} (${targetRegion}) with ${totalAfter} resource(s)${breakdown}?`
|
|
35374
35729
|
);
|
|
35375
35730
|
if (!ok) {
|
|
@@ -35398,6 +35753,25 @@ async function importCommand(stackArg, options) {
|
|
|
35398
35753
|
logger.info(
|
|
35399
35754
|
` ${importedRows.length} resource(s) imported. Run 'cdkd diff' to see how the imported state lines up with the template.`
|
|
35400
35755
|
);
|
|
35756
|
+
if (migrationCfnStackName) {
|
|
35757
|
+
const orphaned = resources.length - importedRows.length;
|
|
35758
|
+
if (orphaned > 0) {
|
|
35759
|
+
logger.warn(
|
|
35760
|
+
`--migrate-from-cloudformation: ${orphaned} of ${resources.length} template resource(s) were NOT imported into cdkd. After the CloudFormation stack is retired, those resources remain in AWS but are unmanaged by both CloudFormation and cdkd.`
|
|
35761
|
+
);
|
|
35762
|
+
}
|
|
35763
|
+
await retireCloudFormationStack({
|
|
35764
|
+
cfnStackName: migrationCfnStackName,
|
|
35765
|
+
cfnClient: awsClients.cloudFormation,
|
|
35766
|
+
yes: options.yes,
|
|
35767
|
+
// Reuse cdkd's state bucket as transient storage for the
|
|
35768
|
+
// Retain-injected template when it exceeds the 51,200-byte
|
|
35769
|
+
// inline UpdateStack limit. Forward `--profile` so the
|
|
35770
|
+
// upload identity matches the one that just wrote cdkd state.
|
|
35771
|
+
stateBucket,
|
|
35772
|
+
...options.profile && { s3ClientOpts: { profile: options.profile } }
|
|
35773
|
+
});
|
|
35774
|
+
}
|
|
35401
35775
|
} finally {
|
|
35402
35776
|
await lockManager.releaseLock(stackInfo.stackName, targetRegion).catch((err) => {
|
|
35403
35777
|
logger.warn(`Failed to release lock: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -35611,8 +35985,8 @@ function formatOutcome(outcome) {
|
|
|
35611
35985
|
return "\u2717";
|
|
35612
35986
|
}
|
|
35613
35987
|
}
|
|
35614
|
-
async function
|
|
35615
|
-
const rl =
|
|
35988
|
+
async function confirmPrompt4(prompt) {
|
|
35989
|
+
const rl = readline6.createInterface({ input: process.stdin, output: process.stdout });
|
|
35616
35990
|
try {
|
|
35617
35991
|
const ans = await rl.question(`${prompt} [y/N] `);
|
|
35618
35992
|
return /^y(es)?$/i.test(ans.trim());
|
|
@@ -35648,6 +36022,9 @@ function createImportCommand() {
|
|
|
35648
36022
|
"--force",
|
|
35649
36023
|
"Confirm a destructive write to existing state. Required for auto / whole-stack import when state already exists (rebuilds the entire resource map). Also required in selective mode if a listed override would overwrite a resource already in state. Not needed for a pure selective merge (adding new resources without touching unlisted entries).",
|
|
35650
36024
|
false
|
|
36025
|
+
).option(
|
|
36026
|
+
"--migrate-from-cloudformation [cfn-stack-name]",
|
|
36027
|
+
"After cdkd state is written, retire the named CloudFormation stack (deletes the CFn stack record; AWS resources are NOT deleted): inject DeletionPolicy=Retain and UpdateReplacePolicy=Retain on every resource via UpdateStack, then DeleteStack. cdkd takes over management. Pass without a value to use the cdkd stack name as the CFn stack name (the typical case for a CDK app that was previously deployed via `cdk deploy`); pass an explicit value when the CFn stack name differs."
|
|
35651
36028
|
).action(withErrorHandling(importCommand));
|
|
35652
36029
|
[...commonOptions, ...appOptions, ...stateOptions, ...contextOptions].forEach(
|
|
35653
36030
|
(o) => cmd.addOption(o)
|
|
@@ -35685,7 +36062,7 @@ function reorderArgs(argv) {
|
|
|
35685
36062
|
}
|
|
35686
36063
|
async function main() {
|
|
35687
36064
|
const program = new Command13();
|
|
35688
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
36065
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.30.0");
|
|
35689
36066
|
program.addCommand(createBootstrapCommand());
|
|
35690
36067
|
program.addCommand(createSynthCommand());
|
|
35691
36068
|
program.addCommand(createListCommand());
|