@go-to-k/cdkd 0.28.1 → 0.29.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 +82 -7
- package/dist/cli.js +334 -17
- package/dist/cli.js.map +4 -4
- package/dist/go-to-k-cdkd-0.29.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.1.tgz +0 -0
package/README.md
CHANGED
|
@@ -161,6 +161,7 @@ Reproduce with `./tests/benchmark/run-benchmark.sh all`. See [tests/benchmark/RE
|
|
|
161
161
|
| `Fn::Or` | ✅ Supported | Logical OR (2-10 conditions) |
|
|
162
162
|
| `Fn::Not` | ✅ Supported | Logical NOT |
|
|
163
163
|
| `Fn::ImportValue` | ✅ Supported | Cross-stack references via S3 state |
|
|
164
|
+
| `Fn::GetStackOutput` | ✅ Supported (same-account) | Cross-stack / cross-region output reference via S3 state. Cross-account `RoleArn` is rejected with a clear error (not yet implemented). |
|
|
164
165
|
| `Fn::FindInMap` | ✅ Supported | Mapping lookup |
|
|
165
166
|
| `Fn::GetAZs` | ✅ Supported | Availability Zone list |
|
|
166
167
|
| `Fn::Base64` | ✅ Supported | Base64 encoding |
|
|
@@ -283,6 +284,7 @@ Reproduce with `./tests/benchmark/run-benchmark.sh all`. See [tests/benchmark/RE
|
|
|
283
284
|
| CloudFormation Parameters | ✅ | Default values, type coercion |
|
|
284
285
|
| Conditions | ✅ | With logical operators |
|
|
285
286
|
| Cross-stack references | ✅ | Via `Fn::ImportValue` + S3 state |
|
|
287
|
+
| Cross-region references | ✅ (same-account) | Via `Fn::GetStackOutput` + S3 state. Cross-account `RoleArn` not yet implemented. |
|
|
286
288
|
| JSON Patch updates | ✅ | RFC 6902, minimal patches |
|
|
287
289
|
| Resource replacement detection | ✅ | 10+ resource types |
|
|
288
290
|
| Dynamic References | ✅ | `{{resolve:secretsmanager:...}}`, `{{resolve:ssm:...}}` |
|
|
@@ -457,8 +459,10 @@ cdkd state show MyStack --json # raw {state, lock} JSON
|
|
|
457
459
|
|
|
458
460
|
# Orphan one or more RESOURCES from cdkd's state (does NOT delete AWS resources).
|
|
459
461
|
# Per-resource, mirrors aws-cdk-cli's `cdk orphan --unstable=orphan`.
|
|
460
|
-
# Synth-driven — needs --app / cdk.json. Construct paths
|
|
461
|
-
#
|
|
462
|
+
# Synth-driven — needs --app / cdk.json. Construct paths use CDK's L2-style form
|
|
463
|
+
# (`<StackName>/<Path/To/Construct>`); the synthesized `/Resource` suffix is
|
|
464
|
+
# matched implicitly. Passing an L2 wrapper that contains multiple CFn resources
|
|
465
|
+
# orphans every child under it (matches upstream's prefix-match semantics).
|
|
462
466
|
cdkd orphan MyStack/MyTable # confirmation prompt (y/N)
|
|
463
467
|
cdkd orphan MyStack/MyTable --yes
|
|
464
468
|
cdkd orphan MyStack/MyTable MyStack/MyBucket # multiple resources, same stack
|
|
@@ -708,11 +712,12 @@ the rest by tag automatically.
|
|
|
708
712
|
|
|
709
713
|
### Common flags
|
|
710
714
|
|
|
711
|
-
| Flag
|
|
712
|
-
|
|
|
713
|
-
| `--dry-run` | Preview what would be imported. State is NOT written.
|
|
714
|
-
| `--yes`
|
|
715
|
-
| `--force`
|
|
715
|
+
| Flag | Purpose |
|
|
716
|
+
| --- | --- |
|
|
717
|
+
| `--dry-run` | Preview what would be imported. State is NOT written. |
|
|
718
|
+
| `--yes` | Skip the confirmation prompt before writing state (and the CloudFormation retirement prompt under `--migrate-from-cloudformation`). |
|
|
719
|
+
| `--force` | Confirm a destructive write to existing state — see below. |
|
|
720
|
+
| `--migrate-from-cloudformation [name]` | After cdkd state is written, retire the source CloudFormation stack: inject `DeletionPolicy: Retain` + `UpdateReplacePolicy: Retain` on every resource via `UpdateStack`, then `DeleteStack`. AWS resources are NOT deleted. See [Migrating from `cdk deploy` (CloudFormation) to cdkd](#migrating-from-cdk-deploy-cloudformation-to-cdkd) below. |
|
|
716
721
|
|
|
717
722
|
`--force` is only needed when the import would lose data:
|
|
718
723
|
|
|
@@ -725,6 +730,76 @@ the rest by tag automatically.
|
|
|
725
730
|
Unlisted state entries are preserved automatically.
|
|
726
731
|
- **No existing state (first-time import)**: not required.
|
|
727
732
|
|
|
733
|
+
### Migrating from `cdk deploy` (CloudFormation) to cdkd
|
|
734
|
+
|
|
735
|
+
If a stack was previously deployed via `cdk deploy` (and is therefore
|
|
736
|
+
managed by CloudFormation), `cdkd import --migrate-from-cloudformation` adopts
|
|
737
|
+
the resources into cdkd state AND retires the source CloudFormation
|
|
738
|
+
stack in one go:
|
|
739
|
+
|
|
740
|
+
```bash
|
|
741
|
+
cdkd import MyStack --migrate-from-cloudformation --yes
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
No `--resource <id>=<physical>` flags are needed — cdkd recovers each
|
|
745
|
+
resource's physical id directly from CloudFormation via
|
|
746
|
+
`DescribeStackResources`, so it works for both `cdk deploy`-managed and
|
|
747
|
+
`cdkd deploy`-managed stacks. (cdkd's tag-based auto-lookup can't help
|
|
748
|
+
here: upstream `cdk deploy` doesn't propagate the `aws:cdk:path` template
|
|
749
|
+
metadata as a real AWS tag, and AWS reserves the `aws:` tag prefix so
|
|
750
|
+
neither cdkd nor a CFn `UpdateStack` can add it on the way through.)
|
|
751
|
+
|
|
752
|
+
The flow:
|
|
753
|
+
|
|
754
|
+
1. `DescribeStackResources` — ask CloudFormation for every
|
|
755
|
+
`(LogicalResourceId, PhysicalResourceId)` pair in the source stack.
|
|
756
|
+
These are merged into the import overrides; user-supplied
|
|
757
|
+
`--resource <id>=<physical>` flags take precedence over CFn's view.
|
|
758
|
+
2. `cdkd import` runs and adopts every resource into cdkd state via
|
|
759
|
+
each provider's `import()` method, using the CFn-resolved physical
|
|
760
|
+
ids as direct lookups.
|
|
761
|
+
3. `cdkd` writes state.
|
|
762
|
+
4. `DescribeStacks` + `GetTemplate` + `UpdateStack` to inject
|
|
763
|
+
`DeletionPolicy: Retain` and `UpdateReplacePolicy: Retain` on every
|
|
764
|
+
resource — a metadata-only update.
|
|
765
|
+
5. `DeleteStack` — every resource is now `Retain`, so CloudFormation
|
|
766
|
+
walks the stack and skips every resource. The stack record disappears;
|
|
767
|
+
the underlying AWS resources are left intact and are now solely
|
|
768
|
+
managed by cdkd.
|
|
769
|
+
|
|
770
|
+
Steps 1–5 all run inside the same lock so a concurrent `cdkd deploy`
|
|
771
|
+
cannot race the in-flight migration.
|
|
772
|
+
|
|
773
|
+
By default the CloudFormation stack name is taken from the cdkd stack
|
|
774
|
+
name (the typical case — CDK uses the synthesized stack name as the CFn
|
|
775
|
+
stack name). Pass an explicit value when the names differ:
|
|
776
|
+
|
|
777
|
+
```bash
|
|
778
|
+
cdkd import MyStack --migrate-from-cloudformation LegacyCfnStackName --yes
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
Limitations:
|
|
782
|
+
|
|
783
|
+
- **JSON-only.** The Retain-policy injection in step 4 targets the CDK-
|
|
784
|
+
generated JSON template. Hand-written YAML CFn stacks fail with a
|
|
785
|
+
clear error; retire them manually.
|
|
786
|
+
- **51,200-byte template limit.** The modified template is submitted
|
|
787
|
+
inline via `TemplateBody`. Stacks whose modified template exceeds
|
|
788
|
+
this limit fail in step 4 with a clear error pointing to the manual
|
|
789
|
+
3-step procedure (S3-backed `TemplateURL` fallback is a planned
|
|
790
|
+
follow-up). cdkd state has already been written at that point, so
|
|
791
|
+
re-runs and manual cleanup are both supported.
|
|
792
|
+
- **Not compatible with `--dry-run`.** The post-state-write
|
|
793
|
+
`UpdateStack` + `DeleteStack` are real side-effects and cannot be
|
|
794
|
+
faithfully simulated. Use plain `cdkd import --dry-run` to preview
|
|
795
|
+
per-resource import outcomes.
|
|
796
|
+
- **Partial imports leave unmanaged resources.** If a resource cannot
|
|
797
|
+
be imported (no provider, AWS not-found, etc.), `DeleteStack` skips
|
|
798
|
+
it (Retain) and cdkd never wrote it into state — so the resource
|
|
799
|
+
exists in AWS but unmanaged by both CloudFormation and cdkd. cdkd
|
|
800
|
+
warns loudly when this happens; either re-import the missing
|
|
801
|
+
resources first or accept the orphaning intentionally.
|
|
802
|
+
|
|
728
803
|
### After import
|
|
729
804
|
|
|
730
805
|
Run `cdkd diff` to see how the imported state lines up with the
|
package/dist/cli.js
CHANGED
|
@@ -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
|
*
|
|
@@ -33391,12 +33494,25 @@ function readCdkPath(resource) {
|
|
|
33391
33494
|
function buildCdkPathIndex(template) {
|
|
33392
33495
|
const index = /* @__PURE__ */ new Map();
|
|
33393
33496
|
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
33497
|
+
if (resource.Type === "AWS::CDK::Metadata")
|
|
33498
|
+
continue;
|
|
33394
33499
|
const path = readCdkPath(resource);
|
|
33395
33500
|
if (path)
|
|
33396
33501
|
index.set(path, logicalId);
|
|
33397
33502
|
}
|
|
33398
33503
|
return index;
|
|
33399
33504
|
}
|
|
33505
|
+
function resolveCdkPathToLogicalIds(input, index) {
|
|
33506
|
+
const seen = /* @__PURE__ */ new Map();
|
|
33507
|
+
const prefix = `${input}/`;
|
|
33508
|
+
for (const [path, logicalId] of index) {
|
|
33509
|
+
if (path === input || path.startsWith(prefix)) {
|
|
33510
|
+
if (!seen.has(logicalId))
|
|
33511
|
+
seen.set(logicalId, path);
|
|
33512
|
+
}
|
|
33513
|
+
}
|
|
33514
|
+
return [...seen.entries()].map(([logicalId, cdkPath]) => ({ logicalId, cdkPath }));
|
|
33515
|
+
}
|
|
33400
33516
|
|
|
33401
33517
|
// src/analyzer/orphan-rewriter.ts
|
|
33402
33518
|
var AttributeFetcher = class {
|
|
@@ -33952,19 +34068,20 @@ function resolveConstructPaths(paths, stacks) {
|
|
|
33952
34068
|
`All construct paths must reference the same stack. Got '${stack.stackName}' and '${candidate.stackName}'. Run 'cdkd orphan' once per stack.`
|
|
33953
34069
|
);
|
|
33954
34070
|
}
|
|
33955
|
-
const cdkPath = p;
|
|
33956
34071
|
const index = buildCdkPathIndex(candidate.template);
|
|
33957
|
-
const
|
|
33958
|
-
if (
|
|
34072
|
+
const matches = resolveCdkPathToLogicalIds(p, index);
|
|
34073
|
+
if (matches.length === 0) {
|
|
33959
34074
|
const available = [...index.keys()].sort().join("\n ");
|
|
33960
34075
|
throw new Error(
|
|
33961
|
-
`Construct path '${
|
|
34076
|
+
`Construct path '${p}' not found in template for stack '${candidate.stackName}'.
|
|
33962
34077
|
Available paths:
|
|
33963
34078
|
${available}`
|
|
33964
34079
|
);
|
|
33965
34080
|
}
|
|
33966
|
-
|
|
33967
|
-
logicalIds.
|
|
34081
|
+
for (const { logicalId } of matches) {
|
|
34082
|
+
if (!logicalIds.includes(logicalId)) {
|
|
34083
|
+
logicalIds.push(logicalId);
|
|
34084
|
+
}
|
|
33968
34085
|
}
|
|
33969
34086
|
}
|
|
33970
34087
|
if (!stack) {
|
|
@@ -35198,9 +35315,159 @@ function createStateCommand() {
|
|
|
35198
35315
|
|
|
35199
35316
|
// src/cli/commands/import.ts
|
|
35200
35317
|
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
35201
|
-
import * as
|
|
35318
|
+
import * as readline6 from "node:readline/promises";
|
|
35202
35319
|
import { Command as Command12 } from "commander";
|
|
35203
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
|
+
var STABLE_TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
35334
|
+
"CREATE_COMPLETE",
|
|
35335
|
+
"UPDATE_COMPLETE",
|
|
35336
|
+
"UPDATE_ROLLBACK_COMPLETE",
|
|
35337
|
+
"IMPORT_COMPLETE",
|
|
35338
|
+
"IMPORT_ROLLBACK_COMPLETE"
|
|
35339
|
+
]);
|
|
35340
|
+
var TEMPLATE_BODY_LIMIT = 51200;
|
|
35341
|
+
async function retireCloudFormationStack(options) {
|
|
35342
|
+
const logger = getLogger();
|
|
35343
|
+
const { cfnStackName, cfnClient, yes } = options;
|
|
35344
|
+
logger.info(`[1/4] Inspecting CloudFormation stack '${cfnStackName}'...`);
|
|
35345
|
+
const desc = await cfnClient.send(new DescribeStacksCommand({ StackName: cfnStackName }));
|
|
35346
|
+
const stack = desc.Stacks?.[0];
|
|
35347
|
+
if (!stack) {
|
|
35348
|
+
throw new Error(`CloudFormation stack '${cfnStackName}' not found.`);
|
|
35349
|
+
}
|
|
35350
|
+
const status = stack.StackStatus ?? "";
|
|
35351
|
+
if (!STABLE_TERMINAL_STATUSES.has(status)) {
|
|
35352
|
+
throw new Error(
|
|
35353
|
+
`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.`
|
|
35354
|
+
);
|
|
35355
|
+
}
|
|
35356
|
+
const capabilities = stack.Capabilities ?? [];
|
|
35357
|
+
const tpl = await cfnClient.send(
|
|
35358
|
+
new GetTemplateCommand({ StackName: cfnStackName, TemplateStage: "Original" })
|
|
35359
|
+
);
|
|
35360
|
+
if (!tpl.TemplateBody) {
|
|
35361
|
+
throw new Error(`GetTemplate returned no body for '${cfnStackName}'.`);
|
|
35362
|
+
}
|
|
35363
|
+
const { body: newBody, modified } = injectRetainPolicies(tpl.TemplateBody, cfnStackName);
|
|
35364
|
+
if (!yes) {
|
|
35365
|
+
const ok = await confirmPrompt3(
|
|
35366
|
+
`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).`
|
|
35367
|
+
);
|
|
35368
|
+
if (!ok) {
|
|
35369
|
+
logger.info("CloudFormation stack retirement cancelled. cdkd state is unaffected.");
|
|
35370
|
+
return { outcome: "cancelled" };
|
|
35371
|
+
}
|
|
35372
|
+
}
|
|
35373
|
+
let updateRan = false;
|
|
35374
|
+
if (!modified) {
|
|
35375
|
+
logger.info(`[2/4] Template already has Retain on every resource \u2014 skipping UpdateStack.`);
|
|
35376
|
+
} else {
|
|
35377
|
+
logger.info(`[2/4] Injected DeletionPolicy=Retain and UpdateReplacePolicy=Retain.`);
|
|
35378
|
+
if (newBody.length > TEMPLATE_BODY_LIMIT) {
|
|
35379
|
+
throw new Error(
|
|
35380
|
+
`Modified template is ${newBody.length} bytes, exceeds the inline UpdateStack TemplateBody limit (${TEMPLATE_BODY_LIMIT}). cdkd state has already been written; retire the stack manually with: (1) edit the template to add DeletionPolicy: Retain and UpdateReplacePolicy: Retain to every resource, (2) UpdateStack with the modified template via S3 TemplateURL, (3) DeleteStack. Inline TemplateURL fallback is a planned follow-up.`
|
|
35381
|
+
);
|
|
35382
|
+
}
|
|
35383
|
+
logger.info(`[3/4] Updating CloudFormation stack with Retain policies...`);
|
|
35384
|
+
try {
|
|
35385
|
+
await cfnClient.send(
|
|
35386
|
+
new UpdateStackCommand({
|
|
35387
|
+
StackName: cfnStackName,
|
|
35388
|
+
TemplateBody: newBody,
|
|
35389
|
+
Capabilities: capabilities
|
|
35390
|
+
})
|
|
35391
|
+
);
|
|
35392
|
+
updateRan = true;
|
|
35393
|
+
} catch (err) {
|
|
35394
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
35395
|
+
if (/No updates are to be performed/i.test(msg)) {
|
|
35396
|
+
logger.info(` CloudFormation reports no updates needed \u2014 proceeding to delete.`);
|
|
35397
|
+
} else {
|
|
35398
|
+
throw err;
|
|
35399
|
+
}
|
|
35400
|
+
}
|
|
35401
|
+
if (updateRan) {
|
|
35402
|
+
await waitUntilStackUpdateComplete(
|
|
35403
|
+
{ client: cfnClient, maxWaitTime: 1800 },
|
|
35404
|
+
{ StackName: cfnStackName }
|
|
35405
|
+
);
|
|
35406
|
+
}
|
|
35407
|
+
}
|
|
35408
|
+
logger.info(`[4/4] Deleting CloudFormation stack '${cfnStackName}' (resources retained)...`);
|
|
35409
|
+
await cfnClient.send(new DeleteStackCommand({ StackName: cfnStackName }));
|
|
35410
|
+
await waitUntilStackDeleteComplete(
|
|
35411
|
+
{ client: cfnClient, maxWaitTime: 1800 },
|
|
35412
|
+
{ StackName: cfnStackName }
|
|
35413
|
+
);
|
|
35414
|
+
logger.info(
|
|
35415
|
+
`\u2713 CloudFormation stack '${cfnStackName}' retired. AWS resources are now solely managed by cdkd.`
|
|
35416
|
+
);
|
|
35417
|
+
return { outcome: modified ? "retired" : "no-template-change" };
|
|
35418
|
+
}
|
|
35419
|
+
function injectRetainPolicies(templateBody, cfnStackName) {
|
|
35420
|
+
let parsed;
|
|
35421
|
+
try {
|
|
35422
|
+
parsed = JSON.parse(templateBody);
|
|
35423
|
+
} catch (err) {
|
|
35424
|
+
throw new Error(
|
|
35425
|
+
`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)}`
|
|
35426
|
+
);
|
|
35427
|
+
}
|
|
35428
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || !("Resources" in parsed) || typeof parsed.Resources !== "object" || parsed.Resources === null) {
|
|
35429
|
+
throw new Error(
|
|
35430
|
+
`Template for '${cfnStackName}' has no Resources section \u2014 refusing to retire.`
|
|
35431
|
+
);
|
|
35432
|
+
}
|
|
35433
|
+
let modified = false;
|
|
35434
|
+
const resources = parsed.Resources;
|
|
35435
|
+
for (const [, resource] of Object.entries(resources)) {
|
|
35436
|
+
if (!resource || typeof resource !== "object" || Array.isArray(resource))
|
|
35437
|
+
continue;
|
|
35438
|
+
const r = resource;
|
|
35439
|
+
if (r["DeletionPolicy"] !== "Retain") {
|
|
35440
|
+
r["DeletionPolicy"] = "Retain";
|
|
35441
|
+
modified = true;
|
|
35442
|
+
}
|
|
35443
|
+
if (r["UpdateReplacePolicy"] !== "Retain") {
|
|
35444
|
+
r["UpdateReplacePolicy"] = "Retain";
|
|
35445
|
+
modified = true;
|
|
35446
|
+
}
|
|
35447
|
+
}
|
|
35448
|
+
return { body: JSON.stringify(parsed, null, 2), modified };
|
|
35449
|
+
}
|
|
35450
|
+
async function getCloudFormationResourceMapping(cfnStackName, cfnClient) {
|
|
35451
|
+
const resp = await cfnClient.send(new DescribeStackResourcesCommand({ StackName: cfnStackName }));
|
|
35452
|
+
const map = /* @__PURE__ */ new Map();
|
|
35453
|
+
for (const r of resp.StackResources ?? []) {
|
|
35454
|
+
if (!r.LogicalResourceId || !r.PhysicalResourceId)
|
|
35455
|
+
continue;
|
|
35456
|
+
map.set(r.LogicalResourceId, r.PhysicalResourceId);
|
|
35457
|
+
}
|
|
35458
|
+
return map;
|
|
35459
|
+
}
|
|
35460
|
+
async function confirmPrompt3(prompt) {
|
|
35461
|
+
const rl = readline5.createInterface({ input: process.stdin, output: process.stdout });
|
|
35462
|
+
try {
|
|
35463
|
+
const ans = await rl.question(`${prompt} [y/N] `);
|
|
35464
|
+
return /^y(es)?$/i.test(ans.trim());
|
|
35465
|
+
} finally {
|
|
35466
|
+
rl.close();
|
|
35467
|
+
}
|
|
35468
|
+
}
|
|
35469
|
+
|
|
35470
|
+
// src/cli/commands/import.ts
|
|
35204
35471
|
async function importCommand(stackArg, options) {
|
|
35205
35472
|
const logger = getLogger();
|
|
35206
35473
|
if (options.verbose) {
|
|
@@ -35267,7 +35534,46 @@ async function importCommand(stackArg, options) {
|
|
|
35267
35534
|
if (overrides.size > 0) {
|
|
35268
35535
|
logger.debug(`User-supplied physical IDs: ${[...overrides.keys()].join(", ")}`);
|
|
35269
35536
|
}
|
|
35270
|
-
const
|
|
35537
|
+
const migrationCfnStackName = options.migrateFromCloudformation ? typeof options.migrateFromCloudformation === "string" && options.migrateFromCloudformation.length > 0 ? options.migrateFromCloudformation : stackInfo.stackName : void 0;
|
|
35538
|
+
if (options.migrateFromCloudformation && options.dryRun) {
|
|
35539
|
+
throw new Error(
|
|
35540
|
+
"--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."
|
|
35541
|
+
);
|
|
35542
|
+
}
|
|
35543
|
+
const template = stackInfo.template;
|
|
35544
|
+
const templateParser = new TemplateParser();
|
|
35545
|
+
const resources = collectImportableResources(template);
|
|
35546
|
+
const templateLogicalIds = new Set(resources.map((r) => r.logicalId));
|
|
35547
|
+
logger.info(`Found ${resources.length} resource(s) in template`);
|
|
35548
|
+
if (migrationCfnStackName) {
|
|
35549
|
+
logger.info(`Resolving physical IDs from CloudFormation stack '${migrationCfnStackName}'...`);
|
|
35550
|
+
const cfnMapping = await getCloudFormationResourceMapping(
|
|
35551
|
+
migrationCfnStackName,
|
|
35552
|
+
awsClients.cloudFormation
|
|
35553
|
+
);
|
|
35554
|
+
let derived = 0;
|
|
35555
|
+
let skippedNonImportable = 0;
|
|
35556
|
+
for (const [logicalId, physicalId] of cfnMapping) {
|
|
35557
|
+
if (!templateLogicalIds.has(logicalId)) {
|
|
35558
|
+
skippedNonImportable++;
|
|
35559
|
+
continue;
|
|
35560
|
+
}
|
|
35561
|
+
if (!overrides.has(logicalId)) {
|
|
35562
|
+
overrides.set(logicalId, physicalId);
|
|
35563
|
+
derived++;
|
|
35564
|
+
}
|
|
35565
|
+
}
|
|
35566
|
+
const overriddenByUser = cfnMapping.size - derived - skippedNonImportable;
|
|
35567
|
+
const detail = [];
|
|
35568
|
+
if (overriddenByUser > 0)
|
|
35569
|
+
detail.push(`${overriddenByUser} already overridden by --resource`);
|
|
35570
|
+
if (skippedNonImportable > 0)
|
|
35571
|
+
detail.push(`${skippedNonImportable} non-importable (e.g. CDKMetadata)`);
|
|
35572
|
+
logger.info(
|
|
35573
|
+
`Resolved ${derived} physical ID(s) from CloudFormation` + (detail.length > 0 ? ` (${detail.join(", ")})` : "")
|
|
35574
|
+
);
|
|
35575
|
+
}
|
|
35576
|
+
const selectiveMode = overrides.size > 0 && !options.auto && !options.migrateFromCloudformation;
|
|
35271
35577
|
if (selectiveMode) {
|
|
35272
35578
|
logger.info(
|
|
35273
35579
|
`Selective mode: only importing the ${overrides.size} resource(s) you listed (${[...overrides.keys()].join(", ")}). Pass --auto to also tag-import the rest.`
|
|
@@ -35301,11 +35607,6 @@ async function importCommand(stackArg, options) {
|
|
|
35301
35607
|
);
|
|
35302
35608
|
}
|
|
35303
35609
|
}
|
|
35304
|
-
const template = stackInfo.template;
|
|
35305
|
-
const templateParser = new TemplateParser();
|
|
35306
|
-
const resources = collectImportableResources(template);
|
|
35307
|
-
logger.info(`Found ${resources.length} resource(s) in template`);
|
|
35308
|
-
const templateLogicalIds = new Set(resources.map((r) => r.logicalId));
|
|
35309
35610
|
for (const overrideId of overrides.keys()) {
|
|
35310
35611
|
if (!templateLogicalIds.has(overrideId)) {
|
|
35311
35612
|
throw new Error(
|
|
@@ -35355,7 +35656,7 @@ async function importCommand(stackArg, options) {
|
|
|
35355
35656
|
const preservedCount = selectiveMode && existingState ? Object.keys(existingState.resources).filter((id) => !overrides.has(id)).length : 0;
|
|
35356
35657
|
const totalAfter = importedCount + preservedCount;
|
|
35357
35658
|
const breakdown = preservedCount > 0 ? ` (${importedCount} new/overwritten + ${preservedCount} preserved)` : "";
|
|
35358
|
-
const ok = await
|
|
35659
|
+
const ok = await confirmPrompt4(
|
|
35359
35660
|
`Write state for ${stackInfo.stackName} (${targetRegion}) with ${totalAfter} resource(s)${breakdown}?`
|
|
35360
35661
|
);
|
|
35361
35662
|
if (!ok) {
|
|
@@ -35384,6 +35685,19 @@ async function importCommand(stackArg, options) {
|
|
|
35384
35685
|
logger.info(
|
|
35385
35686
|
` ${importedRows.length} resource(s) imported. Run 'cdkd diff' to see how the imported state lines up with the template.`
|
|
35386
35687
|
);
|
|
35688
|
+
if (migrationCfnStackName) {
|
|
35689
|
+
const orphaned = resources.length - importedRows.length;
|
|
35690
|
+
if (orphaned > 0) {
|
|
35691
|
+
logger.warn(
|
|
35692
|
+
`--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.`
|
|
35693
|
+
);
|
|
35694
|
+
}
|
|
35695
|
+
await retireCloudFormationStack({
|
|
35696
|
+
cfnStackName: migrationCfnStackName,
|
|
35697
|
+
cfnClient: awsClients.cloudFormation,
|
|
35698
|
+
yes: options.yes
|
|
35699
|
+
});
|
|
35700
|
+
}
|
|
35387
35701
|
} finally {
|
|
35388
35702
|
await lockManager.releaseLock(stackInfo.stackName, targetRegion).catch((err) => {
|
|
35389
35703
|
logger.warn(`Failed to release lock: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -35597,8 +35911,8 @@ function formatOutcome(outcome) {
|
|
|
35597
35911
|
return "\u2717";
|
|
35598
35912
|
}
|
|
35599
35913
|
}
|
|
35600
|
-
async function
|
|
35601
|
-
const rl =
|
|
35914
|
+
async function confirmPrompt4(prompt) {
|
|
35915
|
+
const rl = readline6.createInterface({ input: process.stdin, output: process.stdout });
|
|
35602
35916
|
try {
|
|
35603
35917
|
const ans = await rl.question(`${prompt} [y/N] `);
|
|
35604
35918
|
return /^y(es)?$/i.test(ans.trim());
|
|
@@ -35634,6 +35948,9 @@ function createImportCommand() {
|
|
|
35634
35948
|
"--force",
|
|
35635
35949
|
"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).",
|
|
35636
35950
|
false
|
|
35951
|
+
).option(
|
|
35952
|
+
"--migrate-from-cloudformation [cfn-stack-name]",
|
|
35953
|
+
"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."
|
|
35637
35954
|
).action(withErrorHandling(importCommand));
|
|
35638
35955
|
[...commonOptions, ...appOptions, ...stateOptions, ...contextOptions].forEach(
|
|
35639
35956
|
(o) => cmd.addOption(o)
|
|
@@ -35671,7 +35988,7 @@ function reorderArgs(argv) {
|
|
|
35671
35988
|
}
|
|
35672
35989
|
async function main() {
|
|
35673
35990
|
const program = new Command13();
|
|
35674
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
35991
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.29.0");
|
|
35675
35992
|
program.addCommand(createBootstrapCommand());
|
|
35676
35993
|
program.addCommand(createSynthCommand());
|
|
35677
35994
|
program.addCommand(createListCommand());
|