@go-to-k/cdkd 0.122.0 → 0.124.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 +5 -2
- package/dist/cli.js +346 -94
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-B2RZT3ai.js → deploy-engine-DWpeb9wT.js} +339 -20
- package/dist/deploy-engine-DWpeb9wT.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -2
- package/dist/deploy-engine-B2RZT3ai.js.map +0 -1
package/README.md
CHANGED
|
@@ -552,11 +552,14 @@ adopt nor recreate them).
|
|
|
552
552
|
cdkd export MyStack # confirmation prompt; CFn stack name = cdkd stack name
|
|
553
553
|
cdkd export MyStack --cfn-stack-name MyStack-CFn
|
|
554
554
|
cdkd export MyStack --dry-run # print the import plan, do not call CFn
|
|
555
|
-
cdkd export MyStack --template path.json # skip synth, use a pre-rendered JSON
|
|
555
|
+
cdkd export MyStack --template path.json # skip synth, use a pre-rendered template (JSON or YAML — format auto-detected)
|
|
556
556
|
cdkd export MyStack --include-non-importable # 2-phase: IMPORT importable + CFn-CREATE Custom Resources
|
|
557
557
|
```
|
|
558
558
|
|
|
559
|
-
|
|
559
|
+
Accepts JSON and YAML templates. YAML round-trips through a CFn-aware codec
|
|
560
|
+
(`src/cli/yaml-cfn.ts`) that preserves every shorthand intrinsic (`!Ref` /
|
|
561
|
+
`!GetAtt` / `!Sub` / `!Join` / etc.), so a YAML-authored CFn stack stays YAML
|
|
562
|
+
on the phase-1 IMPORT and phase-2 UPDATE changesets.
|
|
560
563
|
|
|
561
564
|
## Drift detection
|
|
562
565
|
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-CuHRHcyW.js";
|
|
3
|
-
import {
|
|
3
|
+
import { A as AssetPublisher, B as getLegacyStateBucketName, C as applyRoleArnIfSet, Ct as generateResourceNameWithFallback, D as LockManager, E as TemplateParser, F as getDockerCmd, G as resolveStateBucketWithDefaultAndSource, H as resolveCaptureObservedState, I as runDockerForeground, K as warnDeprecatedNoPrefixCliFlag, L as runDockerStreaming, M as WorkGraph, N as buildDockerImage, O as S3StateBackend, P as formatDockerLoginError, R as Synthesizer, S as IntrinsicFunctionResolver, St as generateResourceName, T as DagBuilder, Tt as withStackName, U as resolveSkipPrefix, V as resolveApp, W as resolveStateBucketWithDefault, Y as resolveBucketRegion, Z as CdkdError, _ as normalizeAwsTagsToCfn, a as withRetry, at as ResourceUpdateNotSupportedError, b as CloudControlProvider, bt as PATTERN_B_NAME_PROPERTIES, c as cyan, ct as StackTerminationProtectionError, d as red, et as LocalInvokeBuildError, f as yellow, g as matchesCdkPath, gt as getLogger, h as CDK_PATH_TAG, i as withResourceDeadline, it as ResourceTimeoutError, j as stringifyValue, k as shouldRetainResource, l as gray, m as collectInlinePolicyNamesManagedBySiblings, mt as withErrorHandling, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as PartialFailureError, o as IMPLICIT_DELETE_DEPENDENCIES, ot as RouteDiscoveryError, p as IAMRoleProvider, pt as normalizeAwsError, q as AssemblyReader, r as DeployEngine, rt as ProvisioningError, s as bold, st as StackHasActiveImportsError, t as DEFAULT_RESOURCE_TIMEOUT_MS, u as green, v as resolveExplicitPhysicalId, vt as runStackBuffered, w as DiffCalculator, wt as withSkipPrefix, x as assertRegionMatch, xt as PATTERN_B_RESOURCE_TYPES, y as ProviderRegistry, yt as getLiveRenderer, z as getDefaultStateBucketName } from "./deploy-engine-DWpeb9wT.js";
|
|
4
4
|
import { createHash, createHmac, createPublicKey, createVerify, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
5
5
|
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";
|
|
6
6
|
import { AddRoleToInstanceProfileCommand, AddUserToGroupCommand, AttachGroupPolicyCommand, AttachUserPolicyCommand, CreateGroupCommand, CreateInstanceProfileCommand, CreateLoginProfileCommand, CreateUserCommand, DeleteAccessKeyCommand, DeleteGroupCommand, DeleteGroupPolicyCommand, DeleteInstanceProfileCommand, DeleteLoginProfileCommand, DeleteRolePolicyCommand, DeleteUserCommand, DeleteUserPermissionsBoundaryCommand, DeleteUserPolicyCommand, DetachGroupPolicyCommand, DetachUserPolicyCommand, GetGroupCommand, GetGroupPolicyCommand, GetInstanceProfileCommand, GetRolePolicyCommand, GetUserCommand, GetUserPolicyCommand, IAMClient, ListAccessKeysCommand, ListAttachedGroupPoliciesCommand, ListAttachedUserPoliciesCommand, ListGroupPoliciesCommand, ListGroupsForUserCommand, ListInstanceProfilesCommand, ListUserPoliciesCommand, ListUserTagsCommand, ListUsersCommand, NoSuchEntityException, PutGroupPolicyCommand, PutRolePolicyCommand, PutUserPermissionsBoundaryCommand, PutUserPolicyCommand, RemoveRoleFromInstanceProfileCommand, RemoveUserFromGroupCommand, TagUserCommand, UntagUserCommand, UpdateLoginProfileCommand } from "@aws-sdk/client-iam";
|
|
@@ -56,6 +56,7 @@ import { CreateVectorBucketCommand, DeleteIndexCommand, DeleteVectorBucketComman
|
|
|
56
56
|
import { CreateNamespaceCommand, CreateTableBucketCommand, CreateTableCommand as CreateTableCommand$2, DeleteNamespaceCommand as DeleteNamespaceCommand$1, DeleteTableBucketCommand, DeleteTableCommand as DeleteTableCommand$2, GetTableBucketCommand, GetTableCommand as GetTableCommand$1, ListNamespacesCommand as ListNamespacesCommand$1, ListTableBucketsCommand, ListTablesCommand as ListTablesCommand$1, ListTagsForResourceCommand as ListTagsForResourceCommand$19, NotFoundException as NotFoundException$5, S3TablesClient } from "@aws-sdk/client-s3tables";
|
|
57
57
|
import { AttachTrafficSourcesCommand, AutoScalingClient, CreateAutoScalingGroupCommand, DeleteAutoScalingGroupCommand, DeleteLifecycleHookCommand, DeleteNotificationConfigurationCommand, DescribeAutoScalingGroupsCommand, DescribeLifecycleHooksCommand, DescribeNotificationConfigurationsCommand, DescribeTrafficSourcesCommand, DetachTrafficSourcesCommand, DisableMetricsCollectionCommand, EnableMetricsCollectionCommand, PutLifecycleHookCommand, PutNotificationConfigurationCommand, UpdateAutoScalingGroupCommand } from "@aws-sdk/client-auto-scaling";
|
|
58
58
|
import * as readline from "node:readline/promises";
|
|
59
|
+
import { Document, Pair, Scalar, YAMLMap, YAMLSeq, parse as parse$1, stringify } from "yaml";
|
|
59
60
|
import { createServer } from "node:net";
|
|
60
61
|
import { promisify } from "node:util";
|
|
61
62
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
@@ -437,63 +438,6 @@ function effectiveAssumeRoleArn(logicalId, opt) {
|
|
|
437
438
|
*/
|
|
438
439
|
const destroyOptions = [new Option("-f, --force", "Do not ask for confirmation before destroying the stacks").default(false), new Option("--remove-protection", "Bypass deletion protection on protected resources by flipping the per-resource protection flag off in-place before delete. Covers stack-level terminationProtection (CDK property) and resource-level protection on AWS::Logs::LogGroup, AWS::RDS::DBInstance, AWS::RDS::DBCluster, AWS::DocDB::DBCluster, AWS::Neptune::DBCluster, AWS::Neptune::DBInstance, AWS::DynamoDB::Table, AWS::EC2::Instance, AWS::Cognito::UserPool, AWS::AutoScaling::AutoScalingGroup, and AWS::ElasticLoadBalancingV2::LoadBalancer.").default(false)];
|
|
439
440
|
|
|
440
|
-
//#endregion
|
|
441
|
-
//#region src/utils/role-arn.ts
|
|
442
|
-
/**
|
|
443
|
-
* Resolve the role-arn argument (CLI flag or `CDKD_ROLE_ARN` env var) and,
|
|
444
|
-
* when set, assume the role and write the resulting temporary credentials
|
|
445
|
-
* into `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN`
|
|
446
|
-
* for the rest of the process.
|
|
447
|
-
*
|
|
448
|
-
* **Why env vars, not threaded credentials.** cdkd constructs ~13
|
|
449
|
-
* independent `AwsClients` instances across deploy / destroy / state /
|
|
450
|
-
* import / etc. paths (each with its own region, sometimes — e.g. the
|
|
451
|
-
* state-bucket client lives in a different region from the provisioning
|
|
452
|
-
* clients). Threading a `credentials` object through every site is high
|
|
453
|
-
* churn for an opt-in flag. AWS SDK v3 reads the standard `AWS_*` env
|
|
454
|
-
* vars at the top of its default credentials chain, so writing into them
|
|
455
|
-
* once at the command's entry makes every later `new XxxClient()` pick
|
|
456
|
-
* up the assumed-role credentials automatically without touching the
|
|
457
|
-
* client construction sites.
|
|
458
|
-
*
|
|
459
|
-
* **Why cdkd needs admin-equivalent on the assumed role.** Unlike `cdk
|
|
460
|
-
* deploy`, cdkd does NOT route through CloudFormation. There is no
|
|
461
|
-
* cfn-exec-role to delegate to. Every IAM / EC2 / Lambda / etc. API
|
|
462
|
-
* call is issued from the cdkd process directly. The role you pass to
|
|
463
|
-
* `--role-arn` (or set in `CDKD_ROLE_ARN`) MUST therefore have
|
|
464
|
-
* admin-equivalent permissions on the resources being deployed; CDK
|
|
465
|
-
* CLI's `cdk-hnb659fds-deploy-role-*` is NOT sufficient — that role
|
|
466
|
-
* only carries CFn + asset-publish permissions.
|
|
467
|
-
*
|
|
468
|
-
* Default session duration is 1 hour. For longer-running deploys, the
|
|
469
|
-
* caller should re-issue the cdkd command (the in-flight credentials
|
|
470
|
-
* stay valid until expiry, but a re-run is the simplest recovery for
|
|
471
|
-
* the rare case where a deploy outlives them).
|
|
472
|
-
*/
|
|
473
|
-
async function applyRoleArnIfSet(opts) {
|
|
474
|
-
const roleArn = opts.roleArn || process.env["CDKD_ROLE_ARN"];
|
|
475
|
-
if (!roleArn) return;
|
|
476
|
-
const logger = getLogger().child("role-arn");
|
|
477
|
-
logger.debug(`Assuming role ${roleArn}...`);
|
|
478
|
-
const sts = new STSClient({ ...opts.region && { region: opts.region } });
|
|
479
|
-
try {
|
|
480
|
-
const response = await sts.send(new AssumeRoleCommand({
|
|
481
|
-
RoleArn: roleArn,
|
|
482
|
-
RoleSessionName: `cdkd-${Date.now()}`,
|
|
483
|
-
DurationSeconds: 3600
|
|
484
|
-
}));
|
|
485
|
-
if (!response.Credentials) throw new Error(`AssumeRole returned no credentials for role ${roleArn}`);
|
|
486
|
-
const { AccessKeyId, SecretAccessKey, SessionToken, Expiration } = response.Credentials;
|
|
487
|
-
if (!AccessKeyId || !SecretAccessKey || !SessionToken) throw new Error(`AssumeRole response missing credentials fields for role ${roleArn}`);
|
|
488
|
-
process.env["AWS_ACCESS_KEY_ID"] = AccessKeyId;
|
|
489
|
-
process.env["AWS_SECRET_ACCESS_KEY"] = SecretAccessKey;
|
|
490
|
-
process.env["AWS_SESSION_TOKEN"] = SessionToken;
|
|
491
|
-
logger.info(`Assumed role ${roleArn} (session expires ${Expiration?.toISOString() ?? "unknown"})`);
|
|
492
|
-
} finally {
|
|
493
|
-
sts.destroy();
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
441
|
//#endregion
|
|
498
442
|
//#region src/cli/commands/bootstrap.ts
|
|
499
443
|
/**
|
|
@@ -34801,6 +34745,307 @@ function createStateCommand() {
|
|
|
34801
34745
|
return cmd;
|
|
34802
34746
|
}
|
|
34803
34747
|
|
|
34748
|
+
//#endregion
|
|
34749
|
+
//#region src/cli/yaml-cfn.ts
|
|
34750
|
+
/**
|
|
34751
|
+
* CloudFormation-aware YAML codec.
|
|
34752
|
+
*
|
|
34753
|
+
* Parses + serializes CFn templates while preserving every CFn shorthand
|
|
34754
|
+
* intrinsic tag (`!Ref`, `!GetAtt`, `!Sub`, `!Join`, ...) — generic YAML
|
|
34755
|
+
* libraries silently strip these tags on parse + re-emit, corrupting the
|
|
34756
|
+
* template. cdkd uses this codec on both directions of CFn migration:
|
|
34757
|
+
* `cdkd export` (cdkd → CFn IMPORT) and
|
|
34758
|
+
* `cdkd import --migrate-from-cloudformation` (CFn → cdkd state).
|
|
34759
|
+
*
|
|
34760
|
+
* Algorithm: each `!Foo` tag is registered as a YAML custom tag whose
|
|
34761
|
+
* resolver returns the long-form object shape `{Fn::Foo: <args>}` (or
|
|
34762
|
+
* `{Ref: <name>}` for the bare `!Ref`). On stringify, the same objects
|
|
34763
|
+
* are detected and emitted back to shorthand tag form. The internal
|
|
34764
|
+
* model cdkd carries between parse and stringify is the long-form object
|
|
34765
|
+
* shape — same shape JSON produces — so every downstream consumer
|
|
34766
|
+
* (`injectRetainPolicies`, `executeImportChangeSet`, etc.) reads one
|
|
34767
|
+
* representation.
|
|
34768
|
+
*
|
|
34769
|
+
* Each tag is registered three times (scalar / sequence / map) per the
|
|
34770
|
+
* `yaml` library's API contract — a single registration only matches one
|
|
34771
|
+
* collection shape. Nested intrinsics resolve recursively via
|
|
34772
|
+
* `node.toJSON()` (the yaml lib resolves child customTags before invoking
|
|
34773
|
+
* the parent resolver).
|
|
34774
|
+
*
|
|
34775
|
+
* Format detection sniffs the first non-whitespace byte: `{` or `[` →
|
|
34776
|
+
* JSON; anything else → YAML. Empty input rejected as JSON so the
|
|
34777
|
+
* caller's existing JSON-empty-input error path fires.
|
|
34778
|
+
*
|
|
34779
|
+
* Tag set supported (matches the AWS docs canonical list):
|
|
34780
|
+
* `!Ref`, `!GetAtt`, `!Sub`, `!Join`, `!Select`, `!Split`,
|
|
34781
|
+
* `!If`, `!Equals`, `!And`, `!Or`, `!Not`,
|
|
34782
|
+
* `!FindInMap`, `!Base64`, `!Cidr`, `!GetAZs`, `!ImportValue`,
|
|
34783
|
+
* `!Transform`, `!Condition` (used inside `Conditions:` references).
|
|
34784
|
+
*
|
|
34785
|
+
* `!GetAtt` accepts BOTH the scalar dot-delimited shape
|
|
34786
|
+
* (`!GetAtt LogicalId.Attribute.Path`) and the sequence shape
|
|
34787
|
+
* (`!GetAtt [LogicalId, Attribute]`); both round-trip back to the scalar
|
|
34788
|
+
* form when the attribute is a single string segment (the AWS-published
|
|
34789
|
+
* canonical shape), or to the sequence form when the attribute is itself
|
|
34790
|
+
* a sequence.
|
|
34791
|
+
*/
|
|
34792
|
+
/**
|
|
34793
|
+
* Tags whose long-form key uses the `Fn::` prefix. `!Ref`, `!Condition`,
|
|
34794
|
+
* and `!Transform` are special-cased separately because their long-form
|
|
34795
|
+
* key shape differs.
|
|
34796
|
+
*/
|
|
34797
|
+
const FN_TAGS = [
|
|
34798
|
+
"GetAtt",
|
|
34799
|
+
"Sub",
|
|
34800
|
+
"Join",
|
|
34801
|
+
"Select",
|
|
34802
|
+
"Split",
|
|
34803
|
+
"If",
|
|
34804
|
+
"Equals",
|
|
34805
|
+
"And",
|
|
34806
|
+
"Or",
|
|
34807
|
+
"Not",
|
|
34808
|
+
"FindInMap",
|
|
34809
|
+
"Base64",
|
|
34810
|
+
"Cidr",
|
|
34811
|
+
"GetAZs",
|
|
34812
|
+
"ImportValue",
|
|
34813
|
+
"Length",
|
|
34814
|
+
"ToJsonString",
|
|
34815
|
+
"ForEach"
|
|
34816
|
+
];
|
|
34817
|
+
function nodeJs(node) {
|
|
34818
|
+
if (node === null || node === void 0) return null;
|
|
34819
|
+
if (typeof node === "object" && node !== null && "toJSON" in node) return node.toJSON();
|
|
34820
|
+
return node;
|
|
34821
|
+
}
|
|
34822
|
+
/**
|
|
34823
|
+
* Build the set of custom YAML tags cdkd registers. Each tag may need
|
|
34824
|
+
* up to 3 entries (scalar / seq / map) so every shape an AWS-published
|
|
34825
|
+
* template carries is accepted; un-templated shapes for a given tag
|
|
34826
|
+
* fall through to the lib's default tag resolution (which the lib
|
|
34827
|
+
* surfaces as a warning — never an error — by design).
|
|
34828
|
+
*
|
|
34829
|
+
* `identify: () => false` on every entry: we never want the lib to
|
|
34830
|
+
* auto-tag a parsed result on stringify. We build the YAML output via
|
|
34831
|
+
* our own `jsToYamlNode` walk that emits the tag explicitly.
|
|
34832
|
+
*/
|
|
34833
|
+
function buildCustomTags() {
|
|
34834
|
+
const tags = [];
|
|
34835
|
+
tags.push({
|
|
34836
|
+
tag: "!Ref",
|
|
34837
|
+
resolve(value) {
|
|
34838
|
+
return { Ref: value };
|
|
34839
|
+
},
|
|
34840
|
+
identify: () => false
|
|
34841
|
+
});
|
|
34842
|
+
tags.push({
|
|
34843
|
+
tag: "!Condition",
|
|
34844
|
+
resolve(value) {
|
|
34845
|
+
return { Condition: value };
|
|
34846
|
+
},
|
|
34847
|
+
identify: () => false
|
|
34848
|
+
});
|
|
34849
|
+
tags.push({
|
|
34850
|
+
tag: "!Transform",
|
|
34851
|
+
collection: "map",
|
|
34852
|
+
resolve(node) {
|
|
34853
|
+
return { "Fn::Transform": nodeJs(node) };
|
|
34854
|
+
},
|
|
34855
|
+
identify: () => false
|
|
34856
|
+
});
|
|
34857
|
+
tags.push({
|
|
34858
|
+
tag: "!GetAtt",
|
|
34859
|
+
resolve(value) {
|
|
34860
|
+
const dot = value.indexOf(".");
|
|
34861
|
+
if (dot < 0) throw new Error(`!GetAtt requires '<LogicalId>.<Attribute>'; got '${value}'`);
|
|
34862
|
+
return { "Fn::GetAtt": [value.slice(0, dot), value.slice(dot + 1)] };
|
|
34863
|
+
},
|
|
34864
|
+
identify: () => false
|
|
34865
|
+
});
|
|
34866
|
+
tags.push({
|
|
34867
|
+
tag: "!GetAtt",
|
|
34868
|
+
collection: "seq",
|
|
34869
|
+
resolve(node) {
|
|
34870
|
+
return { "Fn::GetAtt": nodeJs(node) };
|
|
34871
|
+
},
|
|
34872
|
+
identify: () => false
|
|
34873
|
+
});
|
|
34874
|
+
for (const name of FN_TAGS) {
|
|
34875
|
+
if (name === "GetAtt") continue;
|
|
34876
|
+
const longKey = `Fn::${name}`;
|
|
34877
|
+
tags.push({
|
|
34878
|
+
tag: `!${name}`,
|
|
34879
|
+
resolve(value) {
|
|
34880
|
+
return { [longKey]: value };
|
|
34881
|
+
},
|
|
34882
|
+
identify: () => false
|
|
34883
|
+
});
|
|
34884
|
+
tags.push({
|
|
34885
|
+
tag: `!${name}`,
|
|
34886
|
+
collection: "seq",
|
|
34887
|
+
resolve(node) {
|
|
34888
|
+
return { [longKey]: nodeJs(node) };
|
|
34889
|
+
},
|
|
34890
|
+
identify: () => false
|
|
34891
|
+
});
|
|
34892
|
+
tags.push({
|
|
34893
|
+
tag: `!${name}`,
|
|
34894
|
+
collection: "map",
|
|
34895
|
+
resolve(node) {
|
|
34896
|
+
return { [longKey]: nodeJs(node) };
|
|
34897
|
+
},
|
|
34898
|
+
identify: () => false
|
|
34899
|
+
});
|
|
34900
|
+
}
|
|
34901
|
+
return tags;
|
|
34902
|
+
}
|
|
34903
|
+
const CUSTOM_TAGS = buildCustomTags();
|
|
34904
|
+
/**
|
|
34905
|
+
* Parse a CFn template string. JSON or YAML, auto-detected.
|
|
34906
|
+
*
|
|
34907
|
+
* @throws Error with a clear message when the input is empty, malformed,
|
|
34908
|
+
* or does not produce an object root.
|
|
34909
|
+
*/
|
|
34910
|
+
function parseCfnTemplate(text) {
|
|
34911
|
+
const format = detectTemplateFormat(text);
|
|
34912
|
+
const body = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
34913
|
+
let parsed;
|
|
34914
|
+
if (format === "json") try {
|
|
34915
|
+
parsed = JSON.parse(body);
|
|
34916
|
+
} catch (err) {
|
|
34917
|
+
throw new Error(`Template is not valid JSON. Cause: ${err instanceof Error ? err.message : String(err)}`);
|
|
34918
|
+
}
|
|
34919
|
+
else try {
|
|
34920
|
+
parsed = parse$1(body, { customTags: CUSTOM_TAGS });
|
|
34921
|
+
} catch (err) {
|
|
34922
|
+
throw new Error(`Template is not valid YAML. Cause: ${err instanceof Error ? err.message : String(err)}`);
|
|
34923
|
+
}
|
|
34924
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`Template root is not an object.`);
|
|
34925
|
+
return parsed;
|
|
34926
|
+
}
|
|
34927
|
+
/**
|
|
34928
|
+
* Serialize a CFn template back to JSON or YAML. The `format` parameter
|
|
34929
|
+
* controls the output shape; callers should pass the result of the
|
|
34930
|
+
* original `detectTemplateFormat` (or remembered from `parseCfnTemplate`)
|
|
34931
|
+
* so a YAML-authored template stays YAML on round-trip.
|
|
34932
|
+
*
|
|
34933
|
+
* YAML output emits CFn intrinsics back to shorthand tags (`!Ref`,
|
|
34934
|
+
* `!GetAtt`, etc.) by walking the JS object tree before passing it to
|
|
34935
|
+
* `yaml.stringify`. Long-form `{Fn::Foo: <args>}` objects are converted
|
|
34936
|
+
* to `Document` nodes carrying the appropriate `!Foo` tag.
|
|
34937
|
+
*
|
|
34938
|
+
* JSON output is two-space-indented to match cdkd's existing canonical
|
|
34939
|
+
* shape.
|
|
34940
|
+
*/
|
|
34941
|
+
function stringifyCfnTemplate(template, format) {
|
|
34942
|
+
if (format === "json") return JSON.stringify(template, null, 2);
|
|
34943
|
+
const doc = new Document();
|
|
34944
|
+
doc.contents = jsToYamlNode(template, doc);
|
|
34945
|
+
return stringify(doc, {
|
|
34946
|
+
aliasDuplicateObjects: false,
|
|
34947
|
+
lineWidth: 0
|
|
34948
|
+
});
|
|
34949
|
+
}
|
|
34950
|
+
/**
|
|
34951
|
+
* Convert a plain JS value into a YAML node, detecting CFn intrinsics and
|
|
34952
|
+
* emitting them with their shorthand tag. Recursive — every sub-tree is
|
|
34953
|
+
* inspected.
|
|
34954
|
+
*/
|
|
34955
|
+
function jsToYamlNode(value, doc) {
|
|
34956
|
+
if (value === null || value === void 0) return new Scalar(null);
|
|
34957
|
+
if (typeof value !== "object") return doc.createNode(value);
|
|
34958
|
+
if (Array.isArray(value)) {
|
|
34959
|
+
const seq = new YAMLSeq();
|
|
34960
|
+
for (const item of value) seq.items.push(jsToYamlNode(item, doc));
|
|
34961
|
+
return seq;
|
|
34962
|
+
}
|
|
34963
|
+
const keys = Object.keys(value);
|
|
34964
|
+
if (keys.length === 1) {
|
|
34965
|
+
const k = keys[0];
|
|
34966
|
+
const v = value[k];
|
|
34967
|
+
const tag = intrinsicShorthandFor(k);
|
|
34968
|
+
if (tag) return makeIntrinsicNode(tag, v, doc);
|
|
34969
|
+
}
|
|
34970
|
+
const map = new YAMLMap();
|
|
34971
|
+
for (const [k, v] of Object.entries(value)) map.items.push(new Pair(doc.createNode(k), jsToYamlNode(v, doc)));
|
|
34972
|
+
return map;
|
|
34973
|
+
}
|
|
34974
|
+
/**
|
|
34975
|
+
* Map a long-form intrinsic key (`Ref` / `Fn::Foo` / `Condition`) to its
|
|
34976
|
+
* shorthand tag (`!Ref` / `!Foo` / `!Condition`). Returns null when the
|
|
34977
|
+
* key is not a recognized intrinsic.
|
|
34978
|
+
*/
|
|
34979
|
+
function intrinsicShorthandFor(key) {
|
|
34980
|
+
if (key === "Ref") return "!Ref";
|
|
34981
|
+
if (key === "Condition") return "!Condition";
|
|
34982
|
+
if (!key.startsWith("Fn::")) return null;
|
|
34983
|
+
const name = key.slice(4);
|
|
34984
|
+
if (name === "Transform") return "!Transform";
|
|
34985
|
+
if (name === "GetAtt") return "!GetAtt";
|
|
34986
|
+
if (FN_TAGS.includes(name)) return `!${name}`;
|
|
34987
|
+
return null;
|
|
34988
|
+
}
|
|
34989
|
+
/**
|
|
34990
|
+
* Build a YAML node carrying a CFn shorthand tag. Tag-specific shapes:
|
|
34991
|
+
* - `!Ref` / `!Condition` → scalar (the referenced name).
|
|
34992
|
+
* - `!GetAtt` → scalar `LogicalId.Attribute` when the long-form arg
|
|
34993
|
+
* is `[LogicalId, Attribute]` of strings; sequence form otherwise.
|
|
34994
|
+
* - Everything else → whatever shape the long-form arg has
|
|
34995
|
+
* (scalar / sequence / map).
|
|
34996
|
+
*/
|
|
34997
|
+
function makeIntrinsicNode(tag, value, doc) {
|
|
34998
|
+
if (tag === "!Ref" || tag === "!Condition") {
|
|
34999
|
+
const s = new Scalar(typeof value === "string" ? value : String(value));
|
|
35000
|
+
s.tag = tag;
|
|
35001
|
+
return s;
|
|
35002
|
+
}
|
|
35003
|
+
if (tag === "!GetAtt") {
|
|
35004
|
+
if (Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && typeof value[1] === "string") {
|
|
35005
|
+
const s = new Scalar(`${value[0]}.${value[1]}`);
|
|
35006
|
+
s.tag = tag;
|
|
35007
|
+
return s;
|
|
35008
|
+
}
|
|
35009
|
+
const node = jsToYamlNode(value, doc);
|
|
35010
|
+
node.tag = tag;
|
|
35011
|
+
return node;
|
|
35012
|
+
}
|
|
35013
|
+
const node = jsToYamlNode(value, doc);
|
|
35014
|
+
node.tag = tag;
|
|
35015
|
+
return node;
|
|
35016
|
+
}
|
|
35017
|
+
/**
|
|
35018
|
+
* Sniff the input text and decide whether to parse it as JSON or YAML.
|
|
35019
|
+
*
|
|
35020
|
+
* Rule (matches AWS docs / CDK CLI): if the first non-whitespace byte is
|
|
35021
|
+
* `{` or `[`, parse as JSON; otherwise YAML. Empty input is treated as
|
|
35022
|
+
* JSON so the caller's existing JSON-empty-input error path fires.
|
|
35023
|
+
*
|
|
35024
|
+
* UTF-8 BOM (``) is stripped before the sniff — some editors /
|
|
35025
|
+
* scripts emit BOM-prefixed files, and the BOM is NOT whitespace under
|
|
35026
|
+
* `trimStart()`, so a BOM-prefixed JSON file would otherwise route to
|
|
35027
|
+
* the YAML parser and fail.
|
|
35028
|
+
*/
|
|
35029
|
+
function detectTemplateFormat(text) {
|
|
35030
|
+
const trimmed = (text.charCodeAt(0) === 65279 ? text.slice(1) : text).trimStart();
|
|
35031
|
+
if (trimmed.length === 0) return "json";
|
|
35032
|
+
const first = trimmed.charCodeAt(0);
|
|
35033
|
+
if (first === 123 || first === 91) return "json";
|
|
35034
|
+
return "yaml";
|
|
35035
|
+
}
|
|
35036
|
+
/**
|
|
35037
|
+
* Parse a CFn template + return both the parsed object AND the source
|
|
35038
|
+
* format, so the caller can later re-emit in the same shape. Convenience
|
|
35039
|
+
* wrapper around `parseCfnTemplate` + `detectTemplateFormat`.
|
|
35040
|
+
*/
|
|
35041
|
+
function parseCfnTemplateWithFormat(text) {
|
|
35042
|
+
const format = detectTemplateFormat(text);
|
|
35043
|
+
return {
|
|
35044
|
+
template: parseCfnTemplate(text),
|
|
35045
|
+
format
|
|
35046
|
+
};
|
|
35047
|
+
}
|
|
35048
|
+
|
|
34804
35049
|
//#endregion
|
|
34805
35050
|
//#region src/cli/commands/retire-cfn-stack.ts
|
|
34806
35051
|
/**
|
|
@@ -34880,7 +35125,7 @@ async function retireCloudFormationStack(options) {
|
|
|
34880
35125
|
TemplateStage: "Original"
|
|
34881
35126
|
}));
|
|
34882
35127
|
if (!tpl.TemplateBody) throw new Error(`GetTemplate returned no body for '${cfnStackName}'.`);
|
|
34883
|
-
const { body: newBody, modified } = injectRetainPolicies(tpl.TemplateBody, cfnStackName);
|
|
35128
|
+
const { body: newBody, modified, format } = injectRetainPolicies(tpl.TemplateBody, cfnStackName);
|
|
34884
35129
|
if (!yes) {
|
|
34885
35130
|
if (!await confirmPrompt$2(`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).`)) {
|
|
34886
35131
|
logger.info("CloudFormation stack retirement cancelled. cdkd state is unaffected.");
|
|
@@ -34900,6 +35145,7 @@ async function retireCloudFormationStack(options) {
|
|
|
34900
35145
|
bucket: stateBucket,
|
|
34901
35146
|
body: newBody,
|
|
34902
35147
|
cfnStackName,
|
|
35148
|
+
format,
|
|
34903
35149
|
...s3ClientOpts && { s3ClientOpts }
|
|
34904
35150
|
});
|
|
34905
35151
|
updateInput = { TemplateURL: uploaded.url };
|
|
@@ -34961,7 +35207,7 @@ async function retireCloudFormationStack(options) {
|
|
|
34961
35207
|
* Exported for unit testing.
|
|
34962
35208
|
*/
|
|
34963
35209
|
async function uploadTemplateForUpdateStack(args) {
|
|
34964
|
-
const { bucket, body, cfnStackName, s3ClientOpts } = args;
|
|
35210
|
+
const { bucket, body, cfnStackName, format, s3ClientOpts } = args;
|
|
34965
35211
|
const region = await resolveBucketRegion(bucket, {
|
|
34966
35212
|
...s3ClientOpts?.profile && { profile: s3ClientOpts.profile },
|
|
34967
35213
|
...s3ClientOpts?.credentials && { credentials: s3ClientOpts.credentials }
|
|
@@ -34971,13 +35217,15 @@ async function uploadTemplateForUpdateStack(args) {
|
|
|
34971
35217
|
...s3ClientOpts?.profile && { profile: s3ClientOpts.profile },
|
|
34972
35218
|
...s3ClientOpts?.credentials && { credentials: s3ClientOpts.credentials }
|
|
34973
35219
|
});
|
|
34974
|
-
const
|
|
35220
|
+
const ext = format === "yaml" ? "yaml" : "json";
|
|
35221
|
+
const contentType = format === "yaml" ? "application/x-yaml" : "application/json";
|
|
35222
|
+
const key = `${MIGRATE_TMP_PREFIX}/${cfnStackName}/${Date.now()}.${ext}`;
|
|
34975
35223
|
try {
|
|
34976
35224
|
await s3.send(new PutObjectCommand({
|
|
34977
35225
|
Bucket: bucket,
|
|
34978
35226
|
Key: key,
|
|
34979
35227
|
Body: body,
|
|
34980
|
-
ContentType:
|
|
35228
|
+
ContentType: contentType
|
|
34981
35229
|
}));
|
|
34982
35230
|
} catch (err) {
|
|
34983
35231
|
s3.destroy();
|
|
@@ -35000,33 +35248,32 @@ async function uploadTemplateForUpdateStack(args) {
|
|
|
35000
35248
|
};
|
|
35001
35249
|
}
|
|
35002
35250
|
/**
|
|
35003
|
-
* Parse a CloudFormation template body (JSON), set
|
|
35004
|
-
* and `UpdateReplacePolicy: Retain` on every
|
|
35005
|
-
* have those exact values, and re-serialize
|
|
35251
|
+
* Parse a CloudFormation template body (JSON or YAML), set
|
|
35252
|
+
* `DeletionPolicy: Retain` and `UpdateReplacePolicy: Retain` on every
|
|
35253
|
+
* resource that doesn't already have those exact values, and re-serialize
|
|
35254
|
+
* in the SAME format as the input. Returns the resulting body, a
|
|
35255
|
+
* `modified` flag, and the detected source format so callers can stamp
|
|
35256
|
+
* the right content type / S3 key extension on follow-up uploads.
|
|
35006
35257
|
*
|
|
35007
|
-
*
|
|
35008
|
-
*
|
|
35009
|
-
*
|
|
35010
|
-
*
|
|
35011
|
-
* shorthand intrinsics (`!Ref`, `!Sub`, `!GetAtt`, …) which round-trip
|
|
35012
|
-
* incorrectly through generic YAML libraries — a generic YAML
|
|
35013
|
-
* unmarshal/remarshal silently strips the custom tags and corrupts the
|
|
35014
|
-
* template. Until a CFn-aware YAML codec is in scope, hand-written YAML
|
|
35015
|
-
* stacks are best retired with the manual 3-step procedure.
|
|
35258
|
+
* YAML templates are routed through cdkd's CFn-aware YAML codec
|
|
35259
|
+
* (`src/cli/yaml-cfn.ts`), which preserves every CFn shorthand intrinsic
|
|
35260
|
+
* (`!Ref`, `!GetAtt`, `!Sub`, etc.) on round-trip. JSON templates take
|
|
35261
|
+
* the canonical two-space-indented JSON path.
|
|
35016
35262
|
*
|
|
35017
35263
|
* Exported for unit testing (the AWS round-trips are mocked, but the
|
|
35018
35264
|
* mutation logic itself is pure and worth exercising directly).
|
|
35019
35265
|
*/
|
|
35020
35266
|
function injectRetainPolicies(templateBody, cfnStackName) {
|
|
35267
|
+
const format = detectTemplateFormat(templateBody);
|
|
35021
35268
|
let parsed;
|
|
35022
35269
|
try {
|
|
35023
|
-
parsed =
|
|
35270
|
+
parsed = parseCfnTemplate(templateBody);
|
|
35024
35271
|
} catch (err) {
|
|
35025
|
-
throw new Error(`Template for '${cfnStackName}' is not valid
|
|
35272
|
+
throw new Error(`Template for '${cfnStackName}' is not a valid CloudFormation template. cdkd's --migrate-from-cloudformation flow supports both JSON and YAML templates (YAML via a CFn-aware codec that preserves !Ref / !GetAtt / !Sub shorthand). Cause: ${err instanceof Error ? err.message : String(err)}`);
|
|
35026
35273
|
}
|
|
35027
|
-
if (!
|
|
35274
|
+
if (!("Resources" in parsed) || typeof parsed["Resources"] !== "object" || parsed["Resources"] === null || Array.isArray(parsed["Resources"])) throw new Error(`Template for '${cfnStackName}' has no Resources section — refusing to retire.`);
|
|
35028
35275
|
let modified = false;
|
|
35029
|
-
const resources = parsed
|
|
35276
|
+
const resources = parsed["Resources"];
|
|
35030
35277
|
for (const [, resource] of Object.entries(resources)) {
|
|
35031
35278
|
if (!resource || typeof resource !== "object" || Array.isArray(resource)) continue;
|
|
35032
35279
|
const r = resource;
|
|
@@ -35040,8 +35287,9 @@ function injectRetainPolicies(templateBody, cfnStackName) {
|
|
|
35040
35287
|
}
|
|
35041
35288
|
}
|
|
35042
35289
|
return {
|
|
35043
|
-
body:
|
|
35044
|
-
modified
|
|
35290
|
+
body: stringifyCfnTemplate(parsed, format),
|
|
35291
|
+
modified,
|
|
35292
|
+
format
|
|
35045
35293
|
};
|
|
35046
35294
|
}
|
|
35047
35295
|
/**
|
|
@@ -46185,10 +46433,13 @@ async function exportCommand(stackArg, options) {
|
|
|
46185
46433
|
let template;
|
|
46186
46434
|
let resolvedStackName;
|
|
46187
46435
|
let synthedRegion;
|
|
46436
|
+
let templateFormat = "json";
|
|
46188
46437
|
let allSynthStacks = [];
|
|
46189
46438
|
if (options.template) {
|
|
46190
46439
|
if (!stackArg) throw new Error("--template requires a stack name as a positional argument to identify the cdkd state record.");
|
|
46191
|
-
|
|
46440
|
+
const parsed = parseTemplateFile(options.template);
|
|
46441
|
+
template = parsed.template;
|
|
46442
|
+
templateFormat = parsed.format;
|
|
46192
46443
|
resolvedStackName = stackArg;
|
|
46193
46444
|
} else {
|
|
46194
46445
|
const appCmd = options.app || resolveApp();
|
|
@@ -46285,7 +46536,7 @@ async function exportCommand(stackArg, options) {
|
|
|
46285
46536
|
const phase1Template = filterTemplateForImport(template, phase1Imports);
|
|
46286
46537
|
const injectedCount = injectDeletionPolicyForImport(phase1Template);
|
|
46287
46538
|
if (injectedCount > 0) logger.info(`Injected DeletionPolicy: Delete on ${injectedCount} resource(s) without an explicit DeletionPolicy (required by CFn IMPORT — matches the CDK/CFn default for resources without RemovalPolicy).`);
|
|
46288
|
-
await executeImportChangeSet(awsClients.cloudFormation, cfnStackName, phase1Template, phase1Imports, cfnParameters);
|
|
46539
|
+
await executeImportChangeSet(awsClients.cloudFormation, cfnStackName, phase1Template, phase1Imports, cfnParameters, templateFormat);
|
|
46289
46540
|
logger.info(`✓ Phase 1: CloudFormation stack '${cfnStackName}' created via IMPORT. ${phase1Imports.length} resource(s) imported.`);
|
|
46290
46541
|
if (recreateBeforePhase2.length > 0) for (const entry of recreateBeforePhase2) {
|
|
46291
46542
|
const handler = PRE_DELETE_HANDLERS[entry.resourceType];
|
|
@@ -46302,7 +46553,7 @@ async function exportCommand(stackArg, options) {
|
|
|
46302
46553
|
const phase2Count = phase2Creates.length + recreateBeforePhase2.length;
|
|
46303
46554
|
if (phase2Count > 0) try {
|
|
46304
46555
|
const phase2Template = applyImportOverlayForPhase2(template, phase1Imports);
|
|
46305
|
-
await executeUpdateChangeSet(awsClients.cloudFormation, cfnStackName, phase2Template, cfnParameters);
|
|
46556
|
+
await executeUpdateChangeSet(awsClients.cloudFormation, cfnStackName, phase2Template, cfnParameters, templateFormat);
|
|
46306
46557
|
const parts = [];
|
|
46307
46558
|
if (phase2Creates.length > 0) parts.push(`${phase2Creates.length} non-importable resource(s) CREATEd`);
|
|
46308
46559
|
if (recreateBeforePhase2.length > 0) parts.push(`${recreateBeforePhase2.length} IMPORT-unsupported resource(s) re-CREATEd`);
|
|
@@ -46362,14 +46613,15 @@ function parseTemplateFile(path) {
|
|
|
46362
46613
|
} catch (err) {
|
|
46363
46614
|
throw new Error(`Failed to read template file '${path}': ` + (err instanceof Error ? err.message : String(err)));
|
|
46364
46615
|
}
|
|
46365
|
-
let parsed;
|
|
46366
46616
|
try {
|
|
46367
|
-
|
|
46617
|
+
const { template, format } = parseCfnTemplateWithFormat(raw);
|
|
46618
|
+
return {
|
|
46619
|
+
template,
|
|
46620
|
+
format
|
|
46621
|
+
};
|
|
46368
46622
|
} catch (err) {
|
|
46369
|
-
throw new Error(`Template file '${path}' is not valid
|
|
46623
|
+
throw new Error(`Template file '${path}' is not a valid CloudFormation template. cdkd export accepts JSON and YAML (YAML via a CFn-aware codec that preserves !Ref / !GetAtt / !Sub shorthand). Cause: ` + (err instanceof Error ? err.message : String(err)));
|
|
46370
46624
|
}
|
|
46371
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`Template file '${path}' is not a JSON object.`);
|
|
46372
|
-
return parsed;
|
|
46373
46625
|
}
|
|
46374
46626
|
async function assertCfnStackAbsent(cfnClient, stackName) {
|
|
46375
46627
|
try {
|
|
@@ -46917,10 +47169,10 @@ function printPlan(plan, cfnStackName) {
|
|
|
46917
47169
|
}
|
|
46918
47170
|
logger.info("");
|
|
46919
47171
|
}
|
|
46920
|
-
async function executeImportChangeSet(cfnClient, stackName, template, plan, parameters) {
|
|
47172
|
+
async function executeImportChangeSet(cfnClient, stackName, template, plan, parameters, templateFormat = "json") {
|
|
46921
47173
|
const logger = getLogger();
|
|
46922
47174
|
const changeSetName = `cdkd-migrate-${Date.now()}`;
|
|
46923
|
-
const templateBody =
|
|
47175
|
+
const templateBody = stringifyCfnTemplate(template, templateFormat);
|
|
46924
47176
|
const resourcesToImport = plan.map((entry) => ({
|
|
46925
47177
|
ResourceType: entry.resourceType,
|
|
46926
47178
|
LogicalResourceId: entry.logicalId,
|
|
@@ -47031,10 +47283,10 @@ async function collectImportFailureSummary(cfnClient, stackName) {
|
|
|
47031
47283
|
* (cdkd state is intentionally NOT deleted between phases, so a phase-2
|
|
47032
47284
|
* failure leaves a recoverable state).
|
|
47033
47285
|
*/
|
|
47034
|
-
async function executeUpdateChangeSet(cfnClient, stackName, template, parameters) {
|
|
47286
|
+
async function executeUpdateChangeSet(cfnClient, stackName, template, parameters, templateFormat = "json") {
|
|
47035
47287
|
const logger = getLogger();
|
|
47036
47288
|
const changeSetName = `cdkd-phase2-${Date.now()}`;
|
|
47037
|
-
const templateBody =
|
|
47289
|
+
const templateBody = stringifyCfnTemplate(template, templateFormat);
|
|
47038
47290
|
if (templateBody.length > 51200) throw new Error(`Full template is ${templateBody.length} bytes, over the 51,200-byte inline TemplateBody limit for phase-2 UPDATE. TemplateURL upload is not yet implemented.`);
|
|
47039
47291
|
logger.info(`Creating UPDATE changeset '${changeSetName}' for phase 2 (${templateBody.length} bytes)...`);
|
|
47040
47292
|
try {
|
|
@@ -47160,7 +47412,7 @@ async function confirmPrompt(prompt) {
|
|
|
47160
47412
|
}
|
|
47161
47413
|
}
|
|
47162
47414
|
function createExportCommand() {
|
|
47163
|
-
const cmd = new Command("export").description("Hand a cdkd-managed stack over to CloudFormation via CFn IMPORT (changeset). AWS resources are unchanged; cdkd state for the stack is deleted on success. Mirror of `cdkd import` (AWS → cdkd) in the reverse direction (cdkd → CFn). JSON templates
|
|
47415
|
+
const cmd = new Command("export").description("Hand a cdkd-managed stack over to CloudFormation via CFn IMPORT (changeset). AWS resources are unchanged; cdkd state for the stack is deleted on success. Mirror of `cdkd import` (AWS → cdkd) in the reverse direction (cdkd → CFn). Accepts JSON and YAML templates (YAML via a CFn-aware codec that preserves !Ref / !GetAtt / !Sub shorthand). Aborts if any resource is not CFn-importable.").argument("[stack]", "Stack name to export (auto-detected for single-stack apps)").option("--cfn-stack-name <name>", "Name of the destination CloudFormation stack. Defaults to the cdkd stack name.").option("--template <path>", "Path to a pre-rendered CloudFormation template (JSON or YAML — format auto-detected). Skips synth.").option("--stack-region <region>", "Region of the cdkd state record to operate on. Required when the same stack name has state in multiple regions.").option("--dry-run", "Print the import plan without creating a changeset.", false).option("--accept-transient-context", "Allow CLI -c key=value overrides at export time even though they are not persisted to cdk.json / cdk.context.json (default: refuse). When set, the user is responsible for passing the same -c flags to every future cdk deploy.", false).option("--include-non-importable", "Run a 2-phase migration when the stack contains non-importable resources (Custom::*). Phase 1 imports the importable resources; phase 2 CFn-CREATEs the non-importable ones, which re-invokes each Custom Resource's onCreate handler. Make sure onCreate is idempotent before enabling.", false).option("--parameter <key=value...>", "CFn template Parameter override, repeatable. Required when the synthesized template has Parameters without Default values; otherwise overrides the template's default value. Format: --parameter Key=Value.").option("--strict-cross-stack", "Refuse to export when sibling cdkd stacks in the same CDK app reference the exporting stack via Fn::GetStackOutput. Without the flag, cdkd warns but proceeds — the user is expected to migrate the consumer stacks in a follow-up.", false).option("--no-recreate-import-unsupported", "Block instead of auto-handling resource types AWS does NOT support in IMPORT changesets (currently only AWS::ApiGatewayV2::Stage, emitted by CDK HttpApi). Default behavior: cdkd skips these from phase 1, deletes the AWS-side resource between phases, and lets CFn re-CREATE in phase 2 (brief unavailability window). With this flag, the export aborts with a clear error instead.").action(withErrorHandling(exportCommand));
|
|
47164
47416
|
[
|
|
47165
47417
|
...commonOptions,
|
|
47166
47418
|
...appOptions,
|
|
@@ -47212,7 +47464,7 @@ function reorderArgs(argv) {
|
|
|
47212
47464
|
*/
|
|
47213
47465
|
async function main() {
|
|
47214
47466
|
const program = new Command();
|
|
47215
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
47467
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.124.0");
|
|
47216
47468
|
program.addCommand(createBootstrapCommand());
|
|
47217
47469
|
program.addCommand(createSynthCommand());
|
|
47218
47470
|
program.addCommand(createListCommand());
|