@go-to-k/cdkd 0.123.0 → 0.125.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 +671 -58
- package/dist/cli.js.map +1 -1
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -19,9 +19,9 @@ import { CloudFrontClient, CreateCloudFrontOriginAccessIdentityCommand, CreateDi
|
|
|
19
19
|
import { CloudWatchClient, DeleteAlarmsCommand, DescribeAlarmsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$4, PutMetricAlarmCommand, TagResourceCommand as TagResourceCommand$6, UntagResourceCommand as UntagResourceCommand$6 } from "@aws-sdk/client-cloudwatch";
|
|
20
20
|
import { CloudWatchLogsClient, CreateLogGroupCommand, DeleteDataProtectionPolicyCommand, DeleteIndexPolicyCommand, DeleteLogGroupCommand, DeleteRetentionPolicyCommand, DescribeIndexPoliciesCommand, DescribeLogGroupsCommand, GetDataProtectionPolicyCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$5, PutBearerTokenAuthenticationCommand, PutDataProtectionPolicyCommand, PutIndexPolicyCommand, PutLogGroupDeletionProtectionCommand, PutRetentionPolicyCommand, ResourceAlreadyExistsException, ResourceNotFoundException as ResourceNotFoundException$4, TagResourceCommand as TagResourceCommand$7, UntagResourceCommand as UntagResourceCommand$7 } from "@aws-sdk/client-cloudwatch-logs";
|
|
21
21
|
import { BedrockAgentCoreControlClient, CreateAgentRuntimeCommand, DeleteAgentRuntimeCommand, GetAgentRuntimeCommand, ResourceNotFoundException as ResourceNotFoundException$5, UpdateAgentRuntimeCommand } from "@aws-sdk/client-bedrock-agentcore-control";
|
|
22
|
-
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { cpSync, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
23
23
|
import * as path from "node:path";
|
|
24
|
-
import { dirname, isAbsolute, resolve } from "node:path";
|
|
24
|
+
import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
|
|
25
25
|
import { execFile, spawn } from "node:child_process";
|
|
26
26
|
import { tmpdir } from "node:os";
|
|
27
27
|
import { AssociateVPCWithHostedZoneCommand, ChangeResourceRecordSetsCommand, ChangeTagsForResourceCommand, CreateHostedZoneCommand, CreateQueryLoggingConfigCommand, DeleteHostedZoneCommand, DeleteQueryLoggingConfigCommand, DisassociateVPCFromHostedZoneCommand, GetHostedZoneCommand, ListHostedZonesByNameCommand, ListHostedZonesCommand, ListQueryLoggingConfigsCommand, ListResourceRecordSetsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$6, Route53Client, UpdateHostedZoneCommentCommand } from "@aws-sdk/client-route-53";
|
|
@@ -56,6 +56,10 @@ 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";
|
|
60
|
+
import { mkdir, mkdtemp } from "node:fs/promises";
|
|
61
|
+
import { Readable } from "node:stream";
|
|
62
|
+
import { pipeline } from "node:stream/promises";
|
|
59
63
|
import { createServer } from "node:net";
|
|
60
64
|
import { promisify } from "node:util";
|
|
61
65
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
@@ -34744,6 +34748,307 @@ function createStateCommand() {
|
|
|
34744
34748
|
return cmd;
|
|
34745
34749
|
}
|
|
34746
34750
|
|
|
34751
|
+
//#endregion
|
|
34752
|
+
//#region src/cli/yaml-cfn.ts
|
|
34753
|
+
/**
|
|
34754
|
+
* CloudFormation-aware YAML codec.
|
|
34755
|
+
*
|
|
34756
|
+
* Parses + serializes CFn templates while preserving every CFn shorthand
|
|
34757
|
+
* intrinsic tag (`!Ref`, `!GetAtt`, `!Sub`, `!Join`, ...) — generic YAML
|
|
34758
|
+
* libraries silently strip these tags on parse + re-emit, corrupting the
|
|
34759
|
+
* template. cdkd uses this codec on both directions of CFn migration:
|
|
34760
|
+
* `cdkd export` (cdkd → CFn IMPORT) and
|
|
34761
|
+
* `cdkd import --migrate-from-cloudformation` (CFn → cdkd state).
|
|
34762
|
+
*
|
|
34763
|
+
* Algorithm: each `!Foo` tag is registered as a YAML custom tag whose
|
|
34764
|
+
* resolver returns the long-form object shape `{Fn::Foo: <args>}` (or
|
|
34765
|
+
* `{Ref: <name>}` for the bare `!Ref`). On stringify, the same objects
|
|
34766
|
+
* are detected and emitted back to shorthand tag form. The internal
|
|
34767
|
+
* model cdkd carries between parse and stringify is the long-form object
|
|
34768
|
+
* shape — same shape JSON produces — so every downstream consumer
|
|
34769
|
+
* (`injectRetainPolicies`, `executeImportChangeSet`, etc.) reads one
|
|
34770
|
+
* representation.
|
|
34771
|
+
*
|
|
34772
|
+
* Each tag is registered three times (scalar / sequence / map) per the
|
|
34773
|
+
* `yaml` library's API contract — a single registration only matches one
|
|
34774
|
+
* collection shape. Nested intrinsics resolve recursively via
|
|
34775
|
+
* `node.toJSON()` (the yaml lib resolves child customTags before invoking
|
|
34776
|
+
* the parent resolver).
|
|
34777
|
+
*
|
|
34778
|
+
* Format detection sniffs the first non-whitespace byte: `{` or `[` →
|
|
34779
|
+
* JSON; anything else → YAML. Empty input rejected as JSON so the
|
|
34780
|
+
* caller's existing JSON-empty-input error path fires.
|
|
34781
|
+
*
|
|
34782
|
+
* Tag set supported (matches the AWS docs canonical list):
|
|
34783
|
+
* `!Ref`, `!GetAtt`, `!Sub`, `!Join`, `!Select`, `!Split`,
|
|
34784
|
+
* `!If`, `!Equals`, `!And`, `!Or`, `!Not`,
|
|
34785
|
+
* `!FindInMap`, `!Base64`, `!Cidr`, `!GetAZs`, `!ImportValue`,
|
|
34786
|
+
* `!Transform`, `!Condition` (used inside `Conditions:` references).
|
|
34787
|
+
*
|
|
34788
|
+
* `!GetAtt` accepts BOTH the scalar dot-delimited shape
|
|
34789
|
+
* (`!GetAtt LogicalId.Attribute.Path`) and the sequence shape
|
|
34790
|
+
* (`!GetAtt [LogicalId, Attribute]`); both round-trip back to the scalar
|
|
34791
|
+
* form when the attribute is a single string segment (the AWS-published
|
|
34792
|
+
* canonical shape), or to the sequence form when the attribute is itself
|
|
34793
|
+
* a sequence.
|
|
34794
|
+
*/
|
|
34795
|
+
/**
|
|
34796
|
+
* Tags whose long-form key uses the `Fn::` prefix. `!Ref`, `!Condition`,
|
|
34797
|
+
* and `!Transform` are special-cased separately because their long-form
|
|
34798
|
+
* key shape differs.
|
|
34799
|
+
*/
|
|
34800
|
+
const FN_TAGS = [
|
|
34801
|
+
"GetAtt",
|
|
34802
|
+
"Sub",
|
|
34803
|
+
"Join",
|
|
34804
|
+
"Select",
|
|
34805
|
+
"Split",
|
|
34806
|
+
"If",
|
|
34807
|
+
"Equals",
|
|
34808
|
+
"And",
|
|
34809
|
+
"Or",
|
|
34810
|
+
"Not",
|
|
34811
|
+
"FindInMap",
|
|
34812
|
+
"Base64",
|
|
34813
|
+
"Cidr",
|
|
34814
|
+
"GetAZs",
|
|
34815
|
+
"ImportValue",
|
|
34816
|
+
"Length",
|
|
34817
|
+
"ToJsonString",
|
|
34818
|
+
"ForEach"
|
|
34819
|
+
];
|
|
34820
|
+
function nodeJs(node) {
|
|
34821
|
+
if (node === null || node === void 0) return null;
|
|
34822
|
+
if (typeof node === "object" && node !== null && "toJSON" in node) return node.toJSON();
|
|
34823
|
+
return node;
|
|
34824
|
+
}
|
|
34825
|
+
/**
|
|
34826
|
+
* Build the set of custom YAML tags cdkd registers. Each tag may need
|
|
34827
|
+
* up to 3 entries (scalar / seq / map) so every shape an AWS-published
|
|
34828
|
+
* template carries is accepted; un-templated shapes for a given tag
|
|
34829
|
+
* fall through to the lib's default tag resolution (which the lib
|
|
34830
|
+
* surfaces as a warning — never an error — by design).
|
|
34831
|
+
*
|
|
34832
|
+
* `identify: () => false` on every entry: we never want the lib to
|
|
34833
|
+
* auto-tag a parsed result on stringify. We build the YAML output via
|
|
34834
|
+
* our own `jsToYamlNode` walk that emits the tag explicitly.
|
|
34835
|
+
*/
|
|
34836
|
+
function buildCustomTags() {
|
|
34837
|
+
const tags = [];
|
|
34838
|
+
tags.push({
|
|
34839
|
+
tag: "!Ref",
|
|
34840
|
+
resolve(value) {
|
|
34841
|
+
return { Ref: value };
|
|
34842
|
+
},
|
|
34843
|
+
identify: () => false
|
|
34844
|
+
});
|
|
34845
|
+
tags.push({
|
|
34846
|
+
tag: "!Condition",
|
|
34847
|
+
resolve(value) {
|
|
34848
|
+
return { Condition: value };
|
|
34849
|
+
},
|
|
34850
|
+
identify: () => false
|
|
34851
|
+
});
|
|
34852
|
+
tags.push({
|
|
34853
|
+
tag: "!Transform",
|
|
34854
|
+
collection: "map",
|
|
34855
|
+
resolve(node) {
|
|
34856
|
+
return { "Fn::Transform": nodeJs(node) };
|
|
34857
|
+
},
|
|
34858
|
+
identify: () => false
|
|
34859
|
+
});
|
|
34860
|
+
tags.push({
|
|
34861
|
+
tag: "!GetAtt",
|
|
34862
|
+
resolve(value) {
|
|
34863
|
+
const dot = value.indexOf(".");
|
|
34864
|
+
if (dot < 0) throw new Error(`!GetAtt requires '<LogicalId>.<Attribute>'; got '${value}'`);
|
|
34865
|
+
return { "Fn::GetAtt": [value.slice(0, dot), value.slice(dot + 1)] };
|
|
34866
|
+
},
|
|
34867
|
+
identify: () => false
|
|
34868
|
+
});
|
|
34869
|
+
tags.push({
|
|
34870
|
+
tag: "!GetAtt",
|
|
34871
|
+
collection: "seq",
|
|
34872
|
+
resolve(node) {
|
|
34873
|
+
return { "Fn::GetAtt": nodeJs(node) };
|
|
34874
|
+
},
|
|
34875
|
+
identify: () => false
|
|
34876
|
+
});
|
|
34877
|
+
for (const name of FN_TAGS) {
|
|
34878
|
+
if (name === "GetAtt") continue;
|
|
34879
|
+
const longKey = `Fn::${name}`;
|
|
34880
|
+
tags.push({
|
|
34881
|
+
tag: `!${name}`,
|
|
34882
|
+
resolve(value) {
|
|
34883
|
+
return { [longKey]: value };
|
|
34884
|
+
},
|
|
34885
|
+
identify: () => false
|
|
34886
|
+
});
|
|
34887
|
+
tags.push({
|
|
34888
|
+
tag: `!${name}`,
|
|
34889
|
+
collection: "seq",
|
|
34890
|
+
resolve(node) {
|
|
34891
|
+
return { [longKey]: nodeJs(node) };
|
|
34892
|
+
},
|
|
34893
|
+
identify: () => false
|
|
34894
|
+
});
|
|
34895
|
+
tags.push({
|
|
34896
|
+
tag: `!${name}`,
|
|
34897
|
+
collection: "map",
|
|
34898
|
+
resolve(node) {
|
|
34899
|
+
return { [longKey]: nodeJs(node) };
|
|
34900
|
+
},
|
|
34901
|
+
identify: () => false
|
|
34902
|
+
});
|
|
34903
|
+
}
|
|
34904
|
+
return tags;
|
|
34905
|
+
}
|
|
34906
|
+
const CUSTOM_TAGS = buildCustomTags();
|
|
34907
|
+
/**
|
|
34908
|
+
* Parse a CFn template string. JSON or YAML, auto-detected.
|
|
34909
|
+
*
|
|
34910
|
+
* @throws Error with a clear message when the input is empty, malformed,
|
|
34911
|
+
* or does not produce an object root.
|
|
34912
|
+
*/
|
|
34913
|
+
function parseCfnTemplate(text) {
|
|
34914
|
+
const format = detectTemplateFormat(text);
|
|
34915
|
+
const body = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
34916
|
+
let parsed;
|
|
34917
|
+
if (format === "json") try {
|
|
34918
|
+
parsed = JSON.parse(body);
|
|
34919
|
+
} catch (err) {
|
|
34920
|
+
throw new Error(`Template is not valid JSON. Cause: ${err instanceof Error ? err.message : String(err)}`);
|
|
34921
|
+
}
|
|
34922
|
+
else try {
|
|
34923
|
+
parsed = parse$1(body, { customTags: CUSTOM_TAGS });
|
|
34924
|
+
} catch (err) {
|
|
34925
|
+
throw new Error(`Template is not valid YAML. Cause: ${err instanceof Error ? err.message : String(err)}`);
|
|
34926
|
+
}
|
|
34927
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`Template root is not an object.`);
|
|
34928
|
+
return parsed;
|
|
34929
|
+
}
|
|
34930
|
+
/**
|
|
34931
|
+
* Serialize a CFn template back to JSON or YAML. The `format` parameter
|
|
34932
|
+
* controls the output shape; callers should pass the result of the
|
|
34933
|
+
* original `detectTemplateFormat` (or remembered from `parseCfnTemplate`)
|
|
34934
|
+
* so a YAML-authored template stays YAML on round-trip.
|
|
34935
|
+
*
|
|
34936
|
+
* YAML output emits CFn intrinsics back to shorthand tags (`!Ref`,
|
|
34937
|
+
* `!GetAtt`, etc.) by walking the JS object tree before passing it to
|
|
34938
|
+
* `yaml.stringify`. Long-form `{Fn::Foo: <args>}` objects are converted
|
|
34939
|
+
* to `Document` nodes carrying the appropriate `!Foo` tag.
|
|
34940
|
+
*
|
|
34941
|
+
* JSON output is two-space-indented to match cdkd's existing canonical
|
|
34942
|
+
* shape.
|
|
34943
|
+
*/
|
|
34944
|
+
function stringifyCfnTemplate(template, format) {
|
|
34945
|
+
if (format === "json") return JSON.stringify(template, null, 2);
|
|
34946
|
+
const doc = new Document();
|
|
34947
|
+
doc.contents = jsToYamlNode(template, doc);
|
|
34948
|
+
return stringify(doc, {
|
|
34949
|
+
aliasDuplicateObjects: false,
|
|
34950
|
+
lineWidth: 0
|
|
34951
|
+
});
|
|
34952
|
+
}
|
|
34953
|
+
/**
|
|
34954
|
+
* Convert a plain JS value into a YAML node, detecting CFn intrinsics and
|
|
34955
|
+
* emitting them with their shorthand tag. Recursive — every sub-tree is
|
|
34956
|
+
* inspected.
|
|
34957
|
+
*/
|
|
34958
|
+
function jsToYamlNode(value, doc) {
|
|
34959
|
+
if (value === null || value === void 0) return new Scalar(null);
|
|
34960
|
+
if (typeof value !== "object") return doc.createNode(value);
|
|
34961
|
+
if (Array.isArray(value)) {
|
|
34962
|
+
const seq = new YAMLSeq();
|
|
34963
|
+
for (const item of value) seq.items.push(jsToYamlNode(item, doc));
|
|
34964
|
+
return seq;
|
|
34965
|
+
}
|
|
34966
|
+
const keys = Object.keys(value);
|
|
34967
|
+
if (keys.length === 1) {
|
|
34968
|
+
const k = keys[0];
|
|
34969
|
+
const v = value[k];
|
|
34970
|
+
const tag = intrinsicShorthandFor(k);
|
|
34971
|
+
if (tag) return makeIntrinsicNode(tag, v, doc);
|
|
34972
|
+
}
|
|
34973
|
+
const map = new YAMLMap();
|
|
34974
|
+
for (const [k, v] of Object.entries(value)) map.items.push(new Pair(doc.createNode(k), jsToYamlNode(v, doc)));
|
|
34975
|
+
return map;
|
|
34976
|
+
}
|
|
34977
|
+
/**
|
|
34978
|
+
* Map a long-form intrinsic key (`Ref` / `Fn::Foo` / `Condition`) to its
|
|
34979
|
+
* shorthand tag (`!Ref` / `!Foo` / `!Condition`). Returns null when the
|
|
34980
|
+
* key is not a recognized intrinsic.
|
|
34981
|
+
*/
|
|
34982
|
+
function intrinsicShorthandFor(key) {
|
|
34983
|
+
if (key === "Ref") return "!Ref";
|
|
34984
|
+
if (key === "Condition") return "!Condition";
|
|
34985
|
+
if (!key.startsWith("Fn::")) return null;
|
|
34986
|
+
const name = key.slice(4);
|
|
34987
|
+
if (name === "Transform") return "!Transform";
|
|
34988
|
+
if (name === "GetAtt") return "!GetAtt";
|
|
34989
|
+
if (FN_TAGS.includes(name)) return `!${name}`;
|
|
34990
|
+
return null;
|
|
34991
|
+
}
|
|
34992
|
+
/**
|
|
34993
|
+
* Build a YAML node carrying a CFn shorthand tag. Tag-specific shapes:
|
|
34994
|
+
* - `!Ref` / `!Condition` → scalar (the referenced name).
|
|
34995
|
+
* - `!GetAtt` → scalar `LogicalId.Attribute` when the long-form arg
|
|
34996
|
+
* is `[LogicalId, Attribute]` of strings; sequence form otherwise.
|
|
34997
|
+
* - Everything else → whatever shape the long-form arg has
|
|
34998
|
+
* (scalar / sequence / map).
|
|
34999
|
+
*/
|
|
35000
|
+
function makeIntrinsicNode(tag, value, doc) {
|
|
35001
|
+
if (tag === "!Ref" || tag === "!Condition") {
|
|
35002
|
+
const s = new Scalar(typeof value === "string" ? value : String(value));
|
|
35003
|
+
s.tag = tag;
|
|
35004
|
+
return s;
|
|
35005
|
+
}
|
|
35006
|
+
if (tag === "!GetAtt") {
|
|
35007
|
+
if (Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && typeof value[1] === "string") {
|
|
35008
|
+
const s = new Scalar(`${value[0]}.${value[1]}`);
|
|
35009
|
+
s.tag = tag;
|
|
35010
|
+
return s;
|
|
35011
|
+
}
|
|
35012
|
+
const node = jsToYamlNode(value, doc);
|
|
35013
|
+
node.tag = tag;
|
|
35014
|
+
return node;
|
|
35015
|
+
}
|
|
35016
|
+
const node = jsToYamlNode(value, doc);
|
|
35017
|
+
node.tag = tag;
|
|
35018
|
+
return node;
|
|
35019
|
+
}
|
|
35020
|
+
/**
|
|
35021
|
+
* Sniff the input text and decide whether to parse it as JSON or YAML.
|
|
35022
|
+
*
|
|
35023
|
+
* Rule (matches AWS docs / CDK CLI): if the first non-whitespace byte is
|
|
35024
|
+
* `{` or `[`, parse as JSON; otherwise YAML. Empty input is treated as
|
|
35025
|
+
* JSON so the caller's existing JSON-empty-input error path fires.
|
|
35026
|
+
*
|
|
35027
|
+
* UTF-8 BOM (``) is stripped before the sniff — some editors /
|
|
35028
|
+
* scripts emit BOM-prefixed files, and the BOM is NOT whitespace under
|
|
35029
|
+
* `trimStart()`, so a BOM-prefixed JSON file would otherwise route to
|
|
35030
|
+
* the YAML parser and fail.
|
|
35031
|
+
*/
|
|
35032
|
+
function detectTemplateFormat(text) {
|
|
35033
|
+
const trimmed = (text.charCodeAt(0) === 65279 ? text.slice(1) : text).trimStart();
|
|
35034
|
+
if (trimmed.length === 0) return "json";
|
|
35035
|
+
const first = trimmed.charCodeAt(0);
|
|
35036
|
+
if (first === 123 || first === 91) return "json";
|
|
35037
|
+
return "yaml";
|
|
35038
|
+
}
|
|
35039
|
+
/**
|
|
35040
|
+
* Parse a CFn template + return both the parsed object AND the source
|
|
35041
|
+
* format, so the caller can later re-emit in the same shape. Convenience
|
|
35042
|
+
* wrapper around `parseCfnTemplate` + `detectTemplateFormat`.
|
|
35043
|
+
*/
|
|
35044
|
+
function parseCfnTemplateWithFormat(text) {
|
|
35045
|
+
const format = detectTemplateFormat(text);
|
|
35046
|
+
return {
|
|
35047
|
+
template: parseCfnTemplate(text),
|
|
35048
|
+
format
|
|
35049
|
+
};
|
|
35050
|
+
}
|
|
35051
|
+
|
|
34747
35052
|
//#endregion
|
|
34748
35053
|
//#region src/cli/commands/retire-cfn-stack.ts
|
|
34749
35054
|
/**
|
|
@@ -34823,7 +35128,7 @@ async function retireCloudFormationStack(options) {
|
|
|
34823
35128
|
TemplateStage: "Original"
|
|
34824
35129
|
}));
|
|
34825
35130
|
if (!tpl.TemplateBody) throw new Error(`GetTemplate returned no body for '${cfnStackName}'.`);
|
|
34826
|
-
const { body: newBody, modified } = injectRetainPolicies(tpl.TemplateBody, cfnStackName);
|
|
35131
|
+
const { body: newBody, modified, format } = injectRetainPolicies(tpl.TemplateBody, cfnStackName);
|
|
34827
35132
|
if (!yes) {
|
|
34828
35133
|
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).`)) {
|
|
34829
35134
|
logger.info("CloudFormation stack retirement cancelled. cdkd state is unaffected.");
|
|
@@ -34843,6 +35148,7 @@ async function retireCloudFormationStack(options) {
|
|
|
34843
35148
|
bucket: stateBucket,
|
|
34844
35149
|
body: newBody,
|
|
34845
35150
|
cfnStackName,
|
|
35151
|
+
format,
|
|
34846
35152
|
...s3ClientOpts && { s3ClientOpts }
|
|
34847
35153
|
});
|
|
34848
35154
|
updateInput = { TemplateURL: uploaded.url };
|
|
@@ -34904,7 +35210,7 @@ async function retireCloudFormationStack(options) {
|
|
|
34904
35210
|
* Exported for unit testing.
|
|
34905
35211
|
*/
|
|
34906
35212
|
async function uploadTemplateForUpdateStack(args) {
|
|
34907
|
-
const { bucket, body, cfnStackName, s3ClientOpts } = args;
|
|
35213
|
+
const { bucket, body, cfnStackName, format, s3ClientOpts } = args;
|
|
34908
35214
|
const region = await resolveBucketRegion(bucket, {
|
|
34909
35215
|
...s3ClientOpts?.profile && { profile: s3ClientOpts.profile },
|
|
34910
35216
|
...s3ClientOpts?.credentials && { credentials: s3ClientOpts.credentials }
|
|
@@ -34914,13 +35220,15 @@ async function uploadTemplateForUpdateStack(args) {
|
|
|
34914
35220
|
...s3ClientOpts?.profile && { profile: s3ClientOpts.profile },
|
|
34915
35221
|
...s3ClientOpts?.credentials && { credentials: s3ClientOpts.credentials }
|
|
34916
35222
|
});
|
|
34917
|
-
const
|
|
35223
|
+
const ext = format === "yaml" ? "yaml" : "json";
|
|
35224
|
+
const contentType = format === "yaml" ? "application/x-yaml" : "application/json";
|
|
35225
|
+
const key = `${MIGRATE_TMP_PREFIX}/${cfnStackName}/${Date.now()}.${ext}`;
|
|
34918
35226
|
try {
|
|
34919
35227
|
await s3.send(new PutObjectCommand({
|
|
34920
35228
|
Bucket: bucket,
|
|
34921
35229
|
Key: key,
|
|
34922
35230
|
Body: body,
|
|
34923
|
-
ContentType:
|
|
35231
|
+
ContentType: contentType
|
|
34924
35232
|
}));
|
|
34925
35233
|
} catch (err) {
|
|
34926
35234
|
s3.destroy();
|
|
@@ -34943,33 +35251,32 @@ async function uploadTemplateForUpdateStack(args) {
|
|
|
34943
35251
|
};
|
|
34944
35252
|
}
|
|
34945
35253
|
/**
|
|
34946
|
-
* Parse a CloudFormation template body (JSON), set
|
|
34947
|
-
* and `UpdateReplacePolicy: Retain` on every
|
|
34948
|
-
* have those exact values, and re-serialize
|
|
35254
|
+
* Parse a CloudFormation template body (JSON or YAML), set
|
|
35255
|
+
* `DeletionPolicy: Retain` and `UpdateReplacePolicy: Retain` on every
|
|
35256
|
+
* resource that doesn't already have those exact values, and re-serialize
|
|
35257
|
+
* in the SAME format as the input. Returns the resulting body, a
|
|
35258
|
+
* `modified` flag, and the detected source format so callers can stamp
|
|
35259
|
+
* the right content type / S3 key extension on follow-up uploads.
|
|
34949
35260
|
*
|
|
34950
|
-
*
|
|
34951
|
-
*
|
|
34952
|
-
*
|
|
34953
|
-
*
|
|
34954
|
-
* shorthand intrinsics (`!Ref`, `!Sub`, `!GetAtt`, …) which round-trip
|
|
34955
|
-
* incorrectly through generic YAML libraries — a generic YAML
|
|
34956
|
-
* unmarshal/remarshal silently strips the custom tags and corrupts the
|
|
34957
|
-
* template. Until a CFn-aware YAML codec is in scope, hand-written YAML
|
|
34958
|
-
* stacks are best retired with the manual 3-step procedure.
|
|
35261
|
+
* YAML templates are routed through cdkd's CFn-aware YAML codec
|
|
35262
|
+
* (`src/cli/yaml-cfn.ts`), which preserves every CFn shorthand intrinsic
|
|
35263
|
+
* (`!Ref`, `!GetAtt`, `!Sub`, etc.) on round-trip. JSON templates take
|
|
35264
|
+
* the canonical two-space-indented JSON path.
|
|
34959
35265
|
*
|
|
34960
35266
|
* Exported for unit testing (the AWS round-trips are mocked, but the
|
|
34961
35267
|
* mutation logic itself is pure and worth exercising directly).
|
|
34962
35268
|
*/
|
|
34963
35269
|
function injectRetainPolicies(templateBody, cfnStackName) {
|
|
35270
|
+
const format = detectTemplateFormat(templateBody);
|
|
34964
35271
|
let parsed;
|
|
34965
35272
|
try {
|
|
34966
|
-
parsed =
|
|
35273
|
+
parsed = parseCfnTemplate(templateBody);
|
|
34967
35274
|
} catch (err) {
|
|
34968
|
-
throw new Error(`Template for '${cfnStackName}' is not valid
|
|
35275
|
+
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)}`);
|
|
34969
35276
|
}
|
|
34970
|
-
if (!
|
|
35277
|
+
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.`);
|
|
34971
35278
|
let modified = false;
|
|
34972
|
-
const resources = parsed
|
|
35279
|
+
const resources = parsed["Resources"];
|
|
34973
35280
|
for (const [, resource] of Object.entries(resources)) {
|
|
34974
35281
|
if (!resource || typeof resource !== "object" || Array.isArray(resource)) continue;
|
|
34975
35282
|
const r = resource;
|
|
@@ -34983,8 +35290,9 @@ function injectRetainPolicies(templateBody, cfnStackName) {
|
|
|
34983
35290
|
}
|
|
34984
35291
|
}
|
|
34985
35292
|
return {
|
|
34986
|
-
body:
|
|
34987
|
-
modified
|
|
35293
|
+
body: stringifyCfnTemplate(parsed, format),
|
|
35294
|
+
modified,
|
|
35295
|
+
format
|
|
34988
35296
|
};
|
|
34989
35297
|
}
|
|
34990
35298
|
/**
|
|
@@ -36382,16 +36690,23 @@ function resolveAssetCodePath$1(stack, logicalId, resource) {
|
|
|
36382
36690
|
* bind mounts at the same target so cdkd cannot rely on overlay
|
|
36383
36691
|
* layering.
|
|
36384
36692
|
*
|
|
36385
|
-
* **
|
|
36693
|
+
* **Same-stack handling** (`{Ref: <Id>}` / `{Fn::GetAtt: [<Id>, 'Ref']}`):
|
|
36694
|
+
*
|
|
36695
|
+
* - Refs that don't point at an `AWS::Lambda::LayerVersion` resource
|
|
36696
|
+
* hard-error — almost always a typo'd logical ID.
|
|
36697
|
+
* - Refs to a `LayerVersion` whose `Metadata['aws:asset:path']` is
|
|
36698
|
+
* missing hard-error — the layer's content is `S3Bucket` / `S3Key`
|
|
36699
|
+
* from outside cdk.out and there's no local directory to bind-mount.
|
|
36386
36700
|
*
|
|
36387
|
-
*
|
|
36388
|
-
*
|
|
36389
|
-
*
|
|
36390
|
-
*
|
|
36391
|
-
*
|
|
36392
|
-
*
|
|
36393
|
-
*
|
|
36394
|
-
*
|
|
36701
|
+
* **Literal-ARN handling** (issue #448): entries shaped like the string
|
|
36702
|
+
* `arn:aws:lambda:<region>:<account>:layer:<name>:<version>` are parsed
|
|
36703
|
+
* into a `{kind: 'arn', ...}` resolved layer. The actual
|
|
36704
|
+
* `lambda:GetLayerVersion` + presigned-URL download + unzip happens
|
|
36705
|
+
* later in the CLI (`materializeLayerFromArn(...)`), which can optionally
|
|
36706
|
+
* `sts:AssumeRole` into the layer's account when the dev's default
|
|
36707
|
+
* credentials cannot read it. Covers AWS-published public layers (Lambda
|
|
36708
|
+
* Powertools, Datadog Extension, etc.) and cross-account / cross-region
|
|
36709
|
+
* shared layers.
|
|
36395
36710
|
*/
|
|
36396
36711
|
function resolveLambdaLayers(stack, logicalId, props) {
|
|
36397
36712
|
const layers = props["Layers"];
|
|
@@ -36402,13 +36717,24 @@ function resolveLambdaLayers(stack, logicalId, props) {
|
|
|
36402
36717
|
const out = [];
|
|
36403
36718
|
for (let i = 0; i < layers.length; i++) {
|
|
36404
36719
|
const entry = layers[i];
|
|
36720
|
+
if (typeof entry === "string") {
|
|
36721
|
+
const parsed = parseLayerVersionArn(entry);
|
|
36722
|
+
if (!parsed) throw new LocalInvokeResolutionError(`Lambda '${logicalId}' has a Layers entry [${i}] cdkd cannot resolve locally: literal string '${entry}'. Expected a same-stack Ref / Fn::GetAtt to an AWS::Lambda::LayerVersion OR a literal layer-version ARN of the form arn:aws:lambda:<region>:<account>:layer:<name>:<version>.`);
|
|
36723
|
+
out.push({
|
|
36724
|
+
kind: "arn",
|
|
36725
|
+
logicalId: parsed.arn,
|
|
36726
|
+
...parsed
|
|
36727
|
+
});
|
|
36728
|
+
continue;
|
|
36729
|
+
}
|
|
36405
36730
|
const layerLogicalId = pickLayerLogicalId(entry);
|
|
36406
|
-
if (!layerLogicalId) throw new LocalInvokeResolutionError(`Lambda '${logicalId}' has a Layers entry [${i}] cdkd cannot resolve locally: ${describeLayerEntry(entry)}.
|
|
36731
|
+
if (!layerLogicalId) throw new LocalInvokeResolutionError(`Lambda '${logicalId}' has a Layers entry [${i}] cdkd cannot resolve locally: ${describeLayerEntry(entry)}. Expected a same-stack Ref / Fn::GetAtt to an AWS::Lambda::LayerVersion OR a literal layer-version ARN of the form arn:aws:lambda:<region>:<account>:layer:<name>:<version>.`);
|
|
36407
36732
|
const layerResource = resources[layerLogicalId];
|
|
36408
36733
|
if (!layerResource) throw new LocalInvokeResolutionError(`Lambda '${logicalId}' Layers entry [${i}] references '${layerLogicalId}', but no resource with that logical ID exists in stack '${stack.stackName}'.`);
|
|
36409
36734
|
if (layerResource.Type !== "AWS::Lambda::LayerVersion") throw new LocalInvokeResolutionError(`Lambda '${logicalId}' Layers entry [${i}] references '${layerLogicalId}' (${layerResource.Type}), which is not an AWS::Lambda::LayerVersion.`);
|
|
36410
36735
|
const assetPath = resolveAssetCodePath$1(stack, layerLogicalId, layerResource);
|
|
36411
36736
|
out.push({
|
|
36737
|
+
kind: "asset",
|
|
36412
36738
|
logicalId: layerLogicalId,
|
|
36413
36739
|
assetPath
|
|
36414
36740
|
});
|
|
@@ -36416,6 +36742,29 @@ function resolveLambdaLayers(stack, logicalId, props) {
|
|
|
36416
36742
|
return out;
|
|
36417
36743
|
}
|
|
36418
36744
|
/**
|
|
36745
|
+
* Parse a Lambda layer-version ARN string into its segments.
|
|
36746
|
+
*
|
|
36747
|
+
* Returns `undefined` for anything that does not match the strict
|
|
36748
|
+
* `arn:aws:lambda:<region>:<account>:layer:<name>:<version>` shape so
|
|
36749
|
+
* the caller can produce a clearer error than a silent
|
|
36750
|
+
* misinterpretation of hand-edited templates. The partition segment
|
|
36751
|
+
* accepts `aws` / `aws-cn` / `aws-us-gov` so GovCloud / China-region
|
|
36752
|
+
* ARNs work without code changes.
|
|
36753
|
+
*
|
|
36754
|
+
* Exported for unit testing.
|
|
36755
|
+
*/
|
|
36756
|
+
function parseLayerVersionArn(input) {
|
|
36757
|
+
const m = /^arn:(aws|aws-cn|aws-us-gov):lambda:([a-z]{2}-(?:[a-z]+-){1,2}\d+):(\d{12}):layer:([A-Za-z0-9_-]+):(\d+)$/.exec(input);
|
|
36758
|
+
if (!m) return void 0;
|
|
36759
|
+
return {
|
|
36760
|
+
arn: input,
|
|
36761
|
+
region: m[2],
|
|
36762
|
+
accountId: m[3],
|
|
36763
|
+
name: m[4],
|
|
36764
|
+
version: m[5]
|
|
36765
|
+
};
|
|
36766
|
+
}
|
|
36767
|
+
/**
|
|
36419
36768
|
* Walk a single Layers-array entry and return the referenced layer's
|
|
36420
36769
|
* logical ID — or `undefined` for shapes we don't try to resolve in v1.
|
|
36421
36770
|
*
|
|
@@ -36484,6 +36833,195 @@ function notFoundError$1(target, stack, resources) {
|
|
|
36484
36833
|
return new LocalInvokeResolutionError(msg.trimEnd());
|
|
36485
36834
|
}
|
|
36486
36835
|
|
|
36836
|
+
//#endregion
|
|
36837
|
+
//#region src/local/layer-arn-materializer.ts
|
|
36838
|
+
var LayerMaterializationError = class LayerMaterializationError extends Error {
|
|
36839
|
+
constructor(message) {
|
|
36840
|
+
super(message);
|
|
36841
|
+
this.name = "LayerMaterializationError";
|
|
36842
|
+
Object.setPrototypeOf(this, LayerMaterializationError.prototype);
|
|
36843
|
+
}
|
|
36844
|
+
};
|
|
36845
|
+
async function materializeLayerFromArn(layer, options = {}) {
|
|
36846
|
+
const logger = getLogger();
|
|
36847
|
+
let credentials;
|
|
36848
|
+
if (options.roleArn) try {
|
|
36849
|
+
credentials = await assumeRoleForLayer(options.roleArn, layer.region, options);
|
|
36850
|
+
logger.debug(`Layer ${layer.arn}: assumed role ${options.roleArn} for GetLayerVersion`);
|
|
36851
|
+
} catch (err) {
|
|
36852
|
+
throw new LayerMaterializationError(`Layer ${layer.arn}: STS AssumeRole(${options.roleArn}) failed: ${errMsg(err)}. Check the role trust policy permits your principal and sts:AssumeRole is allowed.`);
|
|
36853
|
+
}
|
|
36854
|
+
let presignedUrl;
|
|
36855
|
+
try {
|
|
36856
|
+
presignedUrl = await fetchLayerContentUrl(layer, credentials, options);
|
|
36857
|
+
} catch (err) {
|
|
36858
|
+
const hint = looksLikeAccessDenied(err) ? " GetLayerVersion access denied; check the credentials / role can read the layer (grant lambda:GetLayerVersion on the layer ARN, or pass --layer-role-arn <arn> to assume a role in the layer account)." : "";
|
|
36859
|
+
throw new LayerMaterializationError(`Layer ${layer.arn}: GetLayerVersion failed in region ${layer.region}: ${errMsg(err)}.${hint}`);
|
|
36860
|
+
}
|
|
36861
|
+
let zipBytes;
|
|
36862
|
+
try {
|
|
36863
|
+
zipBytes = await downloadPresignedZip(presignedUrl, options);
|
|
36864
|
+
} catch (err) {
|
|
36865
|
+
throw new LayerMaterializationError(`Layer ${layer.arn}: failed to download layer ZIP from the presigned URL: ${errMsg(err)}.`);
|
|
36866
|
+
}
|
|
36867
|
+
const dir = await mkdtemp(join(tmpdir(), `cdkd-local-arn-layer-${layer.name}-${layer.version}-`));
|
|
36868
|
+
try {
|
|
36869
|
+
await unzipBufferToDirectory(zipBytes, dir);
|
|
36870
|
+
} catch (err) {
|
|
36871
|
+
try {
|
|
36872
|
+
rmSync(dir, {
|
|
36873
|
+
recursive: true,
|
|
36874
|
+
force: true
|
|
36875
|
+
});
|
|
36876
|
+
} catch {}
|
|
36877
|
+
throw new LayerMaterializationError(`Layer ${layer.arn}: failed to unzip layer contents into '${dir}': ${errMsg(err)}.`);
|
|
36878
|
+
}
|
|
36879
|
+
return dir;
|
|
36880
|
+
}
|
|
36881
|
+
async function fetchLayerContentUrl(layer, credentials, options) {
|
|
36882
|
+
const client = (options.lambdaClientFactory ?? await defaultLambdaClientFactory())(layer.region, credentials);
|
|
36883
|
+
try {
|
|
36884
|
+
const command = await buildGetLayerVersionCommand(`arn:aws:lambda:${layer.region}:${layer.accountId}:layer:${layer.name}`, Number(layer.version));
|
|
36885
|
+
const url = (await client.send(command))?.Content?.Location;
|
|
36886
|
+
if (!url || typeof url !== "string") throw new Error("GetLayerVersion response did not include Content.Location (presigned ZIP URL)");
|
|
36887
|
+
return url;
|
|
36888
|
+
} finally {
|
|
36889
|
+
client.destroy?.();
|
|
36890
|
+
}
|
|
36891
|
+
}
|
|
36892
|
+
async function assumeRoleForLayer(roleArn, region, options) {
|
|
36893
|
+
const client = (options.stsClientFactory ?? await defaultStsClientFactory())(region);
|
|
36894
|
+
try {
|
|
36895
|
+
const command = await buildAssumeRoleCommand(roleArn);
|
|
36896
|
+
const creds = (await client.send(command))?.Credentials;
|
|
36897
|
+
if (!creds?.AccessKeyId || !creds.SecretAccessKey) throw new Error("AssumeRole returned no Credentials");
|
|
36898
|
+
return {
|
|
36899
|
+
accessKeyId: creds.AccessKeyId,
|
|
36900
|
+
secretAccessKey: creds.SecretAccessKey,
|
|
36901
|
+
...creds.SessionToken !== void 0 && { sessionToken: creds.SessionToken }
|
|
36902
|
+
};
|
|
36903
|
+
} finally {
|
|
36904
|
+
client.destroy?.();
|
|
36905
|
+
}
|
|
36906
|
+
}
|
|
36907
|
+
async function defaultLambdaClientFactory() {
|
|
36908
|
+
const { LambdaClient } = await import("@aws-sdk/client-lambda");
|
|
36909
|
+
return (region, credentials) => new LambdaClient({
|
|
36910
|
+
region,
|
|
36911
|
+
...credentials && { credentials: {
|
|
36912
|
+
accessKeyId: credentials.accessKeyId,
|
|
36913
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
36914
|
+
...credentials.sessionToken !== void 0 && { sessionToken: credentials.sessionToken }
|
|
36915
|
+
} }
|
|
36916
|
+
});
|
|
36917
|
+
}
|
|
36918
|
+
async function defaultStsClientFactory() {
|
|
36919
|
+
const { STSClient } = await import("@aws-sdk/client-sts");
|
|
36920
|
+
return (region) => new STSClient({ region });
|
|
36921
|
+
}
|
|
36922
|
+
async function buildGetLayerVersionCommand(layerArn, versionNumber) {
|
|
36923
|
+
const { GetLayerVersionCommand } = await import("@aws-sdk/client-lambda");
|
|
36924
|
+
return new GetLayerVersionCommand({
|
|
36925
|
+
LayerName: layerArn,
|
|
36926
|
+
VersionNumber: versionNumber
|
|
36927
|
+
});
|
|
36928
|
+
}
|
|
36929
|
+
async function buildAssumeRoleCommand(roleArn) {
|
|
36930
|
+
const { AssumeRoleCommand } = await import("@aws-sdk/client-sts");
|
|
36931
|
+
return new AssumeRoleCommand({
|
|
36932
|
+
RoleArn: roleArn,
|
|
36933
|
+
RoleSessionName: `cdkd-local-layer-${Date.now()}`,
|
|
36934
|
+
DurationSeconds: 3600
|
|
36935
|
+
});
|
|
36936
|
+
}
|
|
36937
|
+
async function downloadPresignedZip(presignedUrl, options) {
|
|
36938
|
+
if (options.fetchZip) return options.fetchZip(presignedUrl);
|
|
36939
|
+
const response = await fetch(presignedUrl);
|
|
36940
|
+
if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText} from layer Content.Location URL`);
|
|
36941
|
+
const buf = await response.arrayBuffer();
|
|
36942
|
+
return new Uint8Array(buf);
|
|
36943
|
+
}
|
|
36944
|
+
/**
|
|
36945
|
+
* Minimal ZIP unzipper that handles the subset of the ZIP format Lambda
|
|
36946
|
+
* layer ZIPs ever use (DEFLATE compression method 8, STORE method 0).
|
|
36947
|
+
* Avoids bringing in a heavyweight dep for a 50-line task.
|
|
36948
|
+
*
|
|
36949
|
+
* Path-traversal guard: every entry's relative path is `normalize()`d
|
|
36950
|
+
* and rejected if the resulting absolute path escapes `destDir` (the
|
|
36951
|
+
* "Zip Slip" CVE class). Symlinks inside the ZIP are also rejected for
|
|
36952
|
+
* the same reason — they could point at arbitrary host paths.
|
|
36953
|
+
*/
|
|
36954
|
+
async function unzipBufferToDirectory(zipBytes, destDir) {
|
|
36955
|
+
const view = new DataView(zipBytes.buffer, zipBytes.byteOffset, zipBytes.byteLength);
|
|
36956
|
+
const eocdSig = 101010256;
|
|
36957
|
+
const minScan = Math.max(0, zipBytes.byteLength - 65535 - 22);
|
|
36958
|
+
let eocdOffset = -1;
|
|
36959
|
+
for (let i = zipBytes.byteLength - 22; i >= minScan; i--) if (view.getUint32(i, true) === eocdSig) {
|
|
36960
|
+
eocdOffset = i;
|
|
36961
|
+
break;
|
|
36962
|
+
}
|
|
36963
|
+
if (eocdOffset < 0) throw new Error("Not a ZIP file (no End of Central Directory record found)");
|
|
36964
|
+
const totalEntries = view.getUint16(eocdOffset + 10, true);
|
|
36965
|
+
const cdSize = view.getUint32(eocdOffset + 12, true);
|
|
36966
|
+
const cdOffset = view.getUint32(eocdOffset + 16, true);
|
|
36967
|
+
const destAbsolute = resolve(destDir);
|
|
36968
|
+
let cursor = cdOffset;
|
|
36969
|
+
const cdEnd = cdOffset + cdSize;
|
|
36970
|
+
let parsed = 0;
|
|
36971
|
+
while (cursor < cdEnd && parsed < totalEntries) {
|
|
36972
|
+
if (view.getUint32(cursor, true) !== 33639248) throw new Error(`Corrupt ZIP: missing Central Directory header at offset ${cursor}`);
|
|
36973
|
+
const compressionMethod = view.getUint16(cursor + 10, true);
|
|
36974
|
+
const compressedSize = view.getUint32(cursor + 20, true);
|
|
36975
|
+
const uncompressedSize = view.getUint32(cursor + 24, true);
|
|
36976
|
+
const fileNameLength = view.getUint16(cursor + 28, true);
|
|
36977
|
+
const extraFieldLength = view.getUint16(cursor + 30, true);
|
|
36978
|
+
const fileCommentLength = view.getUint16(cursor + 32, true);
|
|
36979
|
+
const externalAttrs = view.getUint32(cursor + 38, true);
|
|
36980
|
+
const localHeaderOffset = view.getUint32(cursor + 42, true);
|
|
36981
|
+
const fileName = new TextDecoder("utf-8").decode(zipBytes.subarray(cursor + 46, cursor + 46 + fileNameLength));
|
|
36982
|
+
cursor += 46 + fileNameLength + extraFieldLength + fileCommentLength;
|
|
36983
|
+
parsed++;
|
|
36984
|
+
const targetPath = resolve(destAbsolute, normalize(fileName));
|
|
36985
|
+
if (!targetPath.startsWith(destAbsolute + (destAbsolute.endsWith("/") ? "" : "/"))) throw new Error(`Refusing to extract entry '${fileName}' — path escapes the destination directory`);
|
|
36986
|
+
if ((externalAttrs >>> 16 & 61440) === 40960) throw new Error(`Refusing to extract symlink entry '${fileName}' from layer ZIP (security)`);
|
|
36987
|
+
if (fileName.endsWith("/")) {
|
|
36988
|
+
await mkdir(targetPath, { recursive: true });
|
|
36989
|
+
continue;
|
|
36990
|
+
}
|
|
36991
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
36992
|
+
if (view.getUint32(localHeaderOffset, true) !== 67324752) throw new Error(`Corrupt ZIP: missing Local File Header for '${fileName}'`);
|
|
36993
|
+
const lfhFileNameLength = view.getUint16(localHeaderOffset + 26, true);
|
|
36994
|
+
const lfhExtraFieldLength = view.getUint16(localHeaderOffset + 28, true);
|
|
36995
|
+
const dataOffset = localHeaderOffset + 30 + lfhFileNameLength + lfhExtraFieldLength;
|
|
36996
|
+
const compressedData = zipBytes.subarray(dataOffset, dataOffset + compressedSize);
|
|
36997
|
+
let payload;
|
|
36998
|
+
if (compressionMethod === 0) payload = compressedData;
|
|
36999
|
+
else if (compressionMethod === 8) payload = await inflateRaw(compressedData);
|
|
37000
|
+
else throw new Error(`Unsupported ZIP compression method ${compressionMethod} for entry '${fileName}' (only STORE and DEFLATE supported)`);
|
|
37001
|
+
if (payload.length !== uncompressedSize && compressionMethod !== 0) throw new Error(`ZIP entry '${fileName}': inflate produced ${payload.length} bytes, expected ${uncompressedSize}`);
|
|
37002
|
+
await pipeline(Readable.from(payload), createWriteStream(targetPath));
|
|
37003
|
+
}
|
|
37004
|
+
}
|
|
37005
|
+
async function inflateRaw(data) {
|
|
37006
|
+
const { inflateRaw: inflate } = await import("node:zlib");
|
|
37007
|
+
return new Promise((resolveP, rejectP) => {
|
|
37008
|
+
inflate(data, (err, out) => {
|
|
37009
|
+
if (err) rejectP(err);
|
|
37010
|
+
else resolveP(out);
|
|
37011
|
+
});
|
|
37012
|
+
});
|
|
37013
|
+
}
|
|
37014
|
+
function errMsg(err) {
|
|
37015
|
+
return err instanceof Error ? err.message : String(err);
|
|
37016
|
+
}
|
|
37017
|
+
function looksLikeAccessDenied(err) {
|
|
37018
|
+
if (!(err instanceof Error)) return false;
|
|
37019
|
+
const name = err.name ?? "";
|
|
37020
|
+
const code = err.Code ?? "";
|
|
37021
|
+
const message = err.message ?? "";
|
|
37022
|
+
return name === "AccessDeniedException" || code === "AccessDeniedException" || /access denied/i.test(message) || /not authorized/i.test(message);
|
|
37023
|
+
}
|
|
37024
|
+
|
|
36487
37025
|
//#endregion
|
|
36488
37026
|
//#region src/local/env-resolver.ts
|
|
36489
37027
|
/**
|
|
@@ -43362,7 +43900,8 @@ async function localStartApiCommand(target, options) {
|
|
|
43362
43900
|
stsRegion: options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"],
|
|
43363
43901
|
inlineTmpDirs,
|
|
43364
43902
|
layerTmpDirs,
|
|
43365
|
-
stateByStack
|
|
43903
|
+
stateByStack,
|
|
43904
|
+
...options.layerRoleArn !== void 0 && { layerRoleArn: options.layerRoleArn }
|
|
43366
43905
|
});
|
|
43367
43906
|
specs.set(logicalId, spec);
|
|
43368
43907
|
}
|
|
@@ -43672,10 +44211,10 @@ function warnIamRoutes(routesWithAuth) {
|
|
|
43672
44211
|
* missing, runtime not supported).
|
|
43673
44212
|
*/
|
|
43674
44213
|
async function buildContainerSpec(args) {
|
|
43675
|
-
const { logicalId, stacks, overrides, assumeRole, containerHost, debugPort, stsRegion, inlineTmpDirs, layerTmpDirs, stateByStack } = args;
|
|
44214
|
+
const { logicalId, stacks, overrides, assumeRole, containerHost, debugPort, stsRegion, inlineTmpDirs, layerTmpDirs, stateByStack, layerRoleArn } = args;
|
|
43676
44215
|
const lambda = resolveLambdaByLogicalId(logicalId, stacks);
|
|
43677
44216
|
const codeDir = lambda.codePath ?? materializeInlineCode$1(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime), inlineTmpDirs);
|
|
43678
|
-
const optDir = materializeLambdaLayers$1(lambda.layers, layerTmpDirs);
|
|
44217
|
+
const optDir = await materializeLambdaLayers$1(lambda.layers, layerTmpDirs, layerRoleArn);
|
|
43679
44218
|
let templateEnv = getTemplateEnv$1(lambda.resource);
|
|
43680
44219
|
const stateBundle = stateByStack.get(lambda.stack.stackName);
|
|
43681
44220
|
let stateAudit;
|
|
@@ -43734,22 +44273,46 @@ async function buildContainerSpec(args) {
|
|
|
43734
44273
|
*
|
|
43735
44274
|
* Three branches:
|
|
43736
44275
|
* - 0 layers → `undefined` (no `/opt` mount).
|
|
43737
|
-
* - 1 layer → bind-mount the layer's asset dir directly (no copy)
|
|
44276
|
+
* - 1 layer → bind-mount the layer's asset dir directly (no copy)
|
|
44277
|
+
* when the entry is a same-stack asset. Literal-ARN entries always
|
|
44278
|
+
* pre-materialize first.
|
|
43738
44279
|
* - 2+ layers → copy each into a fresh tmpdir IN ORDER (later
|
|
43739
44280
|
* layers overwrite earlier files via `cpSync({force: true})`),
|
|
43740
44281
|
* bind-mount the tmpdir at `/opt`. Records the tmpdir in
|
|
43741
44282
|
* `layerTmpDirs` so `shutdown(...)` removes it.
|
|
43742
44283
|
*
|
|
44284
|
+
* Issue #448: literal-ARN entries (`{kind: 'arn', ...}`) are downloaded
|
|
44285
|
+
* + unzipped via `lambda:GetLayerVersion` BEFORE the cpSync-merge
|
|
44286
|
+
* branches run. Every per-ARN tmpdir is also recorded in `layerTmpDirs`
|
|
44287
|
+
* so the same shutdown path cleans it up — even for the single-layer
|
|
44288
|
+
* fast path that bind-mounts the dir directly.
|
|
44289
|
+
*
|
|
43743
44290
|
* AWS Lambda's actual runtime extracts every layer ZIP into `/opt`
|
|
43744
44291
|
* in template order — the merge mirrors that. Docker rejects multiple
|
|
43745
44292
|
* `-v ...:/opt:ro` entries at the same target, so cdkd can't rely on
|
|
43746
44293
|
* overlay layering and must produce a single merged dir on the host.
|
|
43747
44294
|
*/
|
|
43748
|
-
function materializeLambdaLayers$1(layers, layerTmpDirs) {
|
|
44295
|
+
async function materializeLambdaLayers$1(layers, layerTmpDirs, layerRoleArn) {
|
|
43749
44296
|
if (layers.length === 0) return void 0;
|
|
43750
|
-
|
|
44297
|
+
const flat = [];
|
|
44298
|
+
for (const layer of layers) {
|
|
44299
|
+
if (layer.kind === "asset") {
|
|
44300
|
+
flat.push({
|
|
44301
|
+
logicalId: layer.logicalId,
|
|
44302
|
+
assetPath: layer.assetPath
|
|
44303
|
+
});
|
|
44304
|
+
continue;
|
|
44305
|
+
}
|
|
44306
|
+
const dir = await materializeLayerFromArn(layer, { ...layerRoleArn !== void 0 && { roleArn: layerRoleArn } });
|
|
44307
|
+
layerTmpDirs.add(dir);
|
|
44308
|
+
flat.push({
|
|
44309
|
+
logicalId: layer.arn,
|
|
44310
|
+
assetPath: dir
|
|
44311
|
+
});
|
|
44312
|
+
}
|
|
44313
|
+
if (flat.length === 1) return flat[0].assetPath;
|
|
43751
44314
|
const dir = mkdtempSync(path.join(tmpdir(), "cdkd-local-start-api-layers-"));
|
|
43752
|
-
for (const layer of
|
|
44315
|
+
for (const layer of flat) cpSync(layer.assetPath, dir, {
|
|
43753
44316
|
recursive: true,
|
|
43754
44317
|
force: true
|
|
43755
44318
|
});
|
|
@@ -44154,7 +44717,7 @@ function parseDebugPort(raw) {
|
|
|
44154
44717
|
* Builder for the `start-api` subcommand. Wired up by `local.ts`.
|
|
44155
44718
|
*/
|
|
44156
44719
|
function createLocalStartApiCommand() {
|
|
44157
|
-
const startApi = new Command("start-api").description("Run a long-running local HTTP server that maps API Gateway routes (REST v1, HTTP API, Function URL) to Lambda invocations against the AWS Lambda Runtime Interface Emulator (Docker required). Supports Lambda TOKEN/REQUEST authorizers, Cognito User Pool / HTTP v2 JWT authorizers, and REST v1 AWS_IAM (SigV4 signature verification only — IAM policy evaluation is NOT emulated; see docs/local-emulation.md). When JWKS is unreachable, JWT authorizers fall back to pass-through (every token accepted) with a warn line — local dev fallback. VPC-config Lambdas run locally and surface a warn line at startup; their containers do NOT get attached to the deployed VPC subnets, so calls to private RDS / ElastiCache will fail.").argument("[target]", "Optional API filter. Accepts the bare CDK logical id ('MyHttpApi'; single-stack apps only), stack-qualified logical id ('MyStack:MyHttpApi'), full CDK Construct path ('MyStack/MyHttpApi/Resource'), or an ancestor Construct path that prefix-matches ('MyStack/MyHttpApi'). When omitted, every discovered API gets its own server. Mirrors `cdkd local invoke` / `cdkd local run-task` target syntax.").addOption(new Option("--port <port>", "HTTP server port (default: auto-allocate)").default("0")).addOption(new Option("--host <host>", "Bind address").default("127.0.0.1")).addOption(new Option("--stack <name>", "Stack to start (single-stack apps auto-detect)")).addOption(new Option("--warm", "Pre-start one container per Lambda at server boot").default(false)).addOption(new Option("--per-lambda-concurrency <n>", "Pool size cap per Lambda (default 2, max 4)").default("2")).addOption(new Option("--no-pull", "Skip docker pull (cached image)")).addOption(new Option("--container-host <host>", "IP the host uses to bind/probe the RIE port (must be a numeric IP — `docker run -p <ip>:<port>:8080` rejects hostnames). Defaults to 127.0.0.1.").default("127.0.0.1")).addOption(new Option("--debug-port-base <port>", "Reserve a contiguous --debug-port range (one per Lambda)")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}, \"Parameters\": {...}})")).addOption(new Option("--assume-role <arn-or-pair>", "Assume the Lambda's execution role and forward STS-issued temp creds. Bare <arn> = global default; <LogicalId>=<arn> = per-Lambda override (repeatable). Per-Lambda > global > unset (developer creds passed through).").argParser((raw, prev) => parseAssumeRoleToken(raw, prev))).addOption(new Option("--watch", "Hot-reload: re-synth + re-discover routes when cdk.out/ or asset directories change. Off by default; the server keeps the previous version serving when synth fails mid-reload.").default(false)).addOption(new Option("--stage <name>", "Select an API Gateway Stage by its 'StageName'. Default: the first Stage attached to each API. Drives event.stageVariables for both REST v1 and HTTP API v2. NOTE: For HTTP API v2 routes, requestContext.stage is always '$default' regardless of this flag (AWS-side limitation — HTTP API only exposes one stage to the integration event); only event.stageVariables is affected for v2 routes. For REST v1 routes the selected StageName is also threaded into requestContext.stage.")).addOption(new Option("--api <id>", "DEPRECATED — use the positional <target> argument instead. Same accepted forms (bare logical id, stack-qualified, Construct path, ancestor prefix). Will be removed in a future major release.")).addOption(new Option("--from-state", "Read cdkd S3 state for every routed stack and substitute Ref / Fn::GetAtt / Fn::Sub / Fn::Join (and AWS pseudo parameters) in Lambda env vars with the deployed physical IDs / attributes. Off by default — pre-PR warn-and-drop semantics are preserved. Turn on for stacks already deployed via cdkd deploy. Mirrors `cdkd local invoke --from-state` / `cdkd local run-task --from-state`. Re-runs against fresh state on every hot-reload firing (--watch).").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).addOption(new Option("--allow-unverified-sigv4", "Opt-in: allow AWS_IAM SigV4 requests that cannot be cryptographically verified (foreign access-key-id, OR no local AWS credentials configured) to pass through with a placeholder principalId. DEFAULT off — fail-closed so unauthenticated bypass is impossible against `event.requestContext.identity.accessKey`-trusting handler code. Use only in dev loops where you understand the risk.").default(false)).action(withErrorHandling(localStartApiCommand));
|
|
44720
|
+
const startApi = new Command("start-api").description("Run a long-running local HTTP server that maps API Gateway routes (REST v1, HTTP API, Function URL) to Lambda invocations against the AWS Lambda Runtime Interface Emulator (Docker required). Supports Lambda TOKEN/REQUEST authorizers, Cognito User Pool / HTTP v2 JWT authorizers, and REST v1 AWS_IAM (SigV4 signature verification only — IAM policy evaluation is NOT emulated; see docs/local-emulation.md). When JWKS is unreachable, JWT authorizers fall back to pass-through (every token accepted) with a warn line — local dev fallback. VPC-config Lambdas run locally and surface a warn line at startup; their containers do NOT get attached to the deployed VPC subnets, so calls to private RDS / ElastiCache will fail.").argument("[target]", "Optional API filter. Accepts the bare CDK logical id ('MyHttpApi'; single-stack apps only), stack-qualified logical id ('MyStack:MyHttpApi'), full CDK Construct path ('MyStack/MyHttpApi/Resource'), or an ancestor Construct path that prefix-matches ('MyStack/MyHttpApi'). When omitted, every discovered API gets its own server. Mirrors `cdkd local invoke` / `cdkd local run-task` target syntax.").addOption(new Option("--port <port>", "HTTP server port (default: auto-allocate)").default("0")).addOption(new Option("--host <host>", "Bind address").default("127.0.0.1")).addOption(new Option("--stack <name>", "Stack to start (single-stack apps auto-detect)")).addOption(new Option("--warm", "Pre-start one container per Lambda at server boot").default(false)).addOption(new Option("--per-lambda-concurrency <n>", "Pool size cap per Lambda (default 2, max 4)").default("2")).addOption(new Option("--no-pull", "Skip docker pull (cached image)")).addOption(new Option("--container-host <host>", "IP the host uses to bind/probe the RIE port (must be a numeric IP — `docker run -p <ip>:<port>:8080` rejects hostnames). Defaults to 127.0.0.1.").default("127.0.0.1")).addOption(new Option("--debug-port-base <port>", "Reserve a contiguous --debug-port range (one per Lambda)")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}, \"Parameters\": {...}})")).addOption(new Option("--assume-role <arn-or-pair>", "Assume the Lambda's execution role and forward STS-issued temp creds. Bare <arn> = global default; <LogicalId>=<arn> = per-Lambda override (repeatable). Per-Lambda > global > unset (developer creds passed through).").argParser((raw, prev) => parseAssumeRoleToken(raw, prev))).addOption(new Option("--watch", "Hot-reload: re-synth + re-discover routes when cdk.out/ or asset directories change. Off by default; the server keeps the previous version serving when synth fails mid-reload.").default(false)).addOption(new Option("--stage <name>", "Select an API Gateway Stage by its 'StageName'. Default: the first Stage attached to each API. Drives event.stageVariables for both REST v1 and HTTP API v2. NOTE: For HTTP API v2 routes, requestContext.stage is always '$default' regardless of this flag (AWS-side limitation — HTTP API only exposes one stage to the integration event); only event.stageVariables is affected for v2 routes. For REST v1 routes the selected StageName is also threaded into requestContext.stage.")).addOption(new Option("--api <id>", "DEPRECATED — use the positional <target> argument instead. Same accepted forms (bare logical id, stack-qualified, Construct path, ancestor prefix). Will be removed in a future major release.")).addOption(new Option("--layer-role-arn <arn>", "Role to sts:AssumeRole before calling lambda:GetLayerVersion on every literal-ARN entry in Properties.Layers (issue #448). Use only when the dev credentials cannot read the layer — typically cross-account layers. AWS-published public layers (e.g. Lambda Powertools) are readable from every account and need no role.")).addOption(new Option("--from-state", "Read cdkd S3 state for every routed stack and substitute Ref / Fn::GetAtt / Fn::Sub / Fn::Join (and AWS pseudo parameters) in Lambda env vars with the deployed physical IDs / attributes. Off by default — pre-PR warn-and-drop semantics are preserved. Turn on for stacks already deployed via cdkd deploy. Mirrors `cdkd local invoke --from-state` / `cdkd local run-task --from-state`. Re-runs against fresh state on every hot-reload firing (--watch).").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).addOption(new Option("--allow-unverified-sigv4", "Opt-in: allow AWS_IAM SigV4 requests that cannot be cryptographically verified (foreign access-key-id, OR no local AWS credentials configured) to pass through with a placeholder principalId. DEFAULT off — fail-closed so unauthenticated bypass is impossible against `event.requestContext.identity.accessKey`-trusting handler code. Use only in dev loops where you understand the risk.").default(false)).action(withErrorHandling(localStartApiCommand));
|
|
44158
44721
|
[
|
|
44159
44722
|
...commonOptions,
|
|
44160
44723
|
...appOptions,
|
|
@@ -45257,6 +45820,14 @@ async function localInvokeCommand(target, options) {
|
|
|
45257
45820
|
} catch (err) {
|
|
45258
45821
|
getLogger().debug(`Failed to remove merged-layers tmpdir ${imagePlan.layersTmpDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
45259
45822
|
}
|
|
45823
|
+
if (imagePlan?.layerArnTmpDirs) for (const dir of imagePlan.layerArnTmpDirs) try {
|
|
45824
|
+
rmSync(dir, {
|
|
45825
|
+
recursive: true,
|
|
45826
|
+
force: true
|
|
45827
|
+
});
|
|
45828
|
+
} catch (err) {
|
|
45829
|
+
getLogger().debug(`Failed to remove ARN-layer tmpdir ${dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
45830
|
+
}
|
|
45260
45831
|
}, (err) => {
|
|
45261
45832
|
getLogger().debug(`cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
45262
45833
|
});
|
|
@@ -45442,7 +46013,7 @@ async function resolveZipImagePlan(lambda, options) {
|
|
|
45442
46013
|
}
|
|
45443
46014
|
const image = resolveRuntimeImage(lambda.runtime);
|
|
45444
46015
|
await pullImage(image, options.pull === false);
|
|
45445
|
-
const layerPlan =
|
|
46016
|
+
const layerPlan = await materializeLambdaLayersIncludingArns(lambda.layers, options);
|
|
45446
46017
|
const containerCodePath = resolveRuntimeCodeMountPath(lambda.runtime);
|
|
45447
46018
|
const tmpfs = resolveTmpfsForLambda(lambda);
|
|
45448
46019
|
return {
|
|
@@ -45456,10 +46027,48 @@ async function resolveZipImagePlan(lambda, options) {
|
|
|
45456
46027
|
cmd: [lambda.handler],
|
|
45457
46028
|
...inlineTmpDir !== void 0 && { inlineTmpDir },
|
|
45458
46029
|
...layerPlan.tmpDir !== void 0 && { layersTmpDir: layerPlan.tmpDir },
|
|
46030
|
+
...layerPlan.extraTmpDirs.length > 0 && { layerArnTmpDirs: layerPlan.extraTmpDirs },
|
|
45459
46031
|
...tmpfs !== void 0 && { tmpfs }
|
|
45460
46032
|
};
|
|
45461
46033
|
}
|
|
45462
46034
|
/**
|
|
46035
|
+
* Two-stage layer materialization (issue #448).
|
|
46036
|
+
*
|
|
46037
|
+
* - Stage 1: every `{kind: 'arn'}` entry is downloaded + unzipped
|
|
46038
|
+
* into its own tmpdir via `materializeLayerFromArn`. The per-ARN
|
|
46039
|
+
* tmpdirs are tracked in `extraTmpDirs` so the outer cleanup can
|
|
46040
|
+
* remove them.
|
|
46041
|
+
* - Stage 2: the resulting `{logicalId, assetPath}[]` list (in
|
|
46042
|
+
* template order — ARN entries surface their `arn` as the
|
|
46043
|
+
* `logicalId` for log lines) is handed to the existing
|
|
46044
|
+
* `materializeLambdaLayers` `cpSync`-merge path. AWS's "last layer
|
|
46045
|
+
* wins" file-collision semantic is preserved across both layer
|
|
46046
|
+
* kinds because the merge step is unchanged.
|
|
46047
|
+
*/
|
|
46048
|
+
async function materializeLambdaLayersIncludingArns(layers, options) {
|
|
46049
|
+
const extraTmpDirs = [];
|
|
46050
|
+
const flat = [];
|
|
46051
|
+
for (const layer of layers) {
|
|
46052
|
+
if (layer.kind === "asset") {
|
|
46053
|
+
flat.push({
|
|
46054
|
+
logicalId: layer.logicalId,
|
|
46055
|
+
assetPath: layer.assetPath
|
|
46056
|
+
});
|
|
46057
|
+
continue;
|
|
46058
|
+
}
|
|
46059
|
+
const dir = await materializeLayerFromArn(layer, { ...options.layerRoleArn !== void 0 && { roleArn: options.layerRoleArn } });
|
|
46060
|
+
extraTmpDirs.push(dir);
|
|
46061
|
+
flat.push({
|
|
46062
|
+
logicalId: layer.arn,
|
|
46063
|
+
assetPath: dir
|
|
46064
|
+
});
|
|
46065
|
+
}
|
|
46066
|
+
return {
|
|
46067
|
+
...materializeLambdaLayers(flat),
|
|
46068
|
+
extraTmpDirs
|
|
46069
|
+
};
|
|
46070
|
+
}
|
|
46071
|
+
/**
|
|
45463
46072
|
* Build the `--tmpfs /tmp:rw,size=<N>m` plan for a Lambda (issue #440).
|
|
45464
46073
|
*
|
|
45465
46074
|
* The shape is identical for ZIP and IMAGE Lambdas — `--tmpfs` overlays
|
|
@@ -45867,7 +46476,7 @@ function pickReferencedLogicalId(intrinsic) {
|
|
|
45867
46476
|
*/
|
|
45868
46477
|
function createLocalCommand() {
|
|
45869
46478
|
const local = new Command("local").description("Local execution of Lambda functions (RIE) and ECS task definitions (Docker required)");
|
|
45870
|
-
const invoke = new Command("invoke").description("Run a Lambda function locally in a Docker container (RIE-backed). Target accepts a CDK display path (MyStack/MyApi/Handler) or stack-qualified logical ID (MyStack:MyApiHandler1234ABCD). Single-stack apps may omit the stack prefix.").argument("<target>", "CDK display path or stack-qualified logical ID of the Lambda to invoke").addOption(new Option("-e, --event <file>", "JSON event payload file (default: {})")).addOption(new Option("--event-stdin", "Read event JSON from stdin").default(false)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}})")).addOption(new Option("--no-pull", "Skip docker pull (use cached image) — no-op for IMAGE local-build path; `docker build` does not pull base layers by default")).addOption(new Option("--no-build", "Skip docker build on the IMAGE local-build path (use the previously-built tag). Requires the deterministic tag to already be in the local registry; errors with an actionable message when missing. No-op for ZIP Lambdas and the IMAGE ECR-pull path. Compatible with --no-pull.")).addOption(new Option("--debug-port <port>", "Node --inspect-brk port (default: off)")).addOption(new Option("--container-host <host>", "Host to bind the RIE port to").default("127.0.0.1")).addOption(new Option("--assume-role [arn]", "Assume the Lambda's deployed execution role and forward STS-issued temp credentials to the container so the handler runs with the deployed function's narrow permissions (closes the \"developer admin / function narrow\" skew). Three forms: (1) `--assume-role <arn>` assumes the explicit ARN; (2) `--assume-role` (bare) auto-resolves the function's execution role ARN from cdkd state (requires --from-state); (3) `--no-assume-role` explicitly opts out (forces dev creds even with --from-state). Off by default — when omitted, the developer's shell credentials are forwarded unchanged (SAM-compatible default). STS failures degrade to a warn + dev-creds fallback.")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries (#455). Issues sts:AssumeRole via the default credential chain and uses the temporary credentials for ecr:GetAuthorizationToken + docker pull. Required when the caller does not have direct cross-account access to the target repository. Same-account / same-region pulls do not need this flag.")).addOption(new Option("--from-state", "Read cdkd S3 state for the target stack and substitute Ref / Fn::GetAtt / Fn::Sub in env vars with the deployed physical IDs / attributes. Off by default — keep PR 1 warn-and-drop semantics; turn on for stacks already deployed via cdkd deploy.").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).action(withErrorHandling(localInvokeCommand));
|
|
46479
|
+
const invoke = new Command("invoke").description("Run a Lambda function locally in a Docker container (RIE-backed). Target accepts a CDK display path (MyStack/MyApi/Handler) or stack-qualified logical ID (MyStack:MyApiHandler1234ABCD). Single-stack apps may omit the stack prefix.").argument("<target>", "CDK display path or stack-qualified logical ID of the Lambda to invoke").addOption(new Option("-e, --event <file>", "JSON event payload file (default: {})")).addOption(new Option("--event-stdin", "Read event JSON from stdin").default(false)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}})")).addOption(new Option("--no-pull", "Skip docker pull (use cached image) — no-op for IMAGE local-build path; `docker build` does not pull base layers by default")).addOption(new Option("--no-build", "Skip docker build on the IMAGE local-build path (use the previously-built tag). Requires the deterministic tag to already be in the local registry; errors with an actionable message when missing. No-op for ZIP Lambdas and the IMAGE ECR-pull path. Compatible with --no-pull.")).addOption(new Option("--debug-port <port>", "Node --inspect-brk port (default: off)")).addOption(new Option("--container-host <host>", "Host to bind the RIE port to").default("127.0.0.1")).addOption(new Option("--assume-role [arn]", "Assume the Lambda's deployed execution role and forward STS-issued temp credentials to the container so the handler runs with the deployed function's narrow permissions (closes the \"developer admin / function narrow\" skew). Three forms: (1) `--assume-role <arn>` assumes the explicit ARN; (2) `--assume-role` (bare) auto-resolves the function's execution role ARN from cdkd state (requires --from-state); (3) `--no-assume-role` explicitly opts out (forces dev creds even with --from-state). Off by default — when omitted, the developer's shell credentials are forwarded unchanged (SAM-compatible default). STS failures degrade to a warn + dev-creds fallback.")).addOption(new Option("--layer-role-arn <arn>", "Role to sts:AssumeRole before calling lambda:GetLayerVersion on every literal-ARN entry in Properties.Layers (issue #448). Use only when the dev credentials cannot read the layer — typically cross-account layers. AWS-published public layers (e.g. Lambda Powertools) are readable from every account and need no role.")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries (#455). Issues sts:AssumeRole via the default credential chain and uses the temporary credentials for ecr:GetAuthorizationToken + docker pull. Required when the caller does not have direct cross-account access to the target repository. Same-account / same-region pulls do not need this flag.")).addOption(new Option("--from-state", "Read cdkd S3 state for the target stack and substitute Ref / Fn::GetAtt / Fn::Sub in env vars with the deployed physical IDs / attributes. Off by default — keep PR 1 warn-and-drop semantics; turn on for stacks already deployed via cdkd deploy.").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).action(withErrorHandling(localInvokeCommand));
|
|
45871
46480
|
[
|
|
45872
46481
|
...commonOptions,
|
|
45873
46482
|
...appOptions,
|
|
@@ -46128,10 +46737,13 @@ async function exportCommand(stackArg, options) {
|
|
|
46128
46737
|
let template;
|
|
46129
46738
|
let resolvedStackName;
|
|
46130
46739
|
let synthedRegion;
|
|
46740
|
+
let templateFormat = "json";
|
|
46131
46741
|
let allSynthStacks = [];
|
|
46132
46742
|
if (options.template) {
|
|
46133
46743
|
if (!stackArg) throw new Error("--template requires a stack name as a positional argument to identify the cdkd state record.");
|
|
46134
|
-
|
|
46744
|
+
const parsed = parseTemplateFile(options.template);
|
|
46745
|
+
template = parsed.template;
|
|
46746
|
+
templateFormat = parsed.format;
|
|
46135
46747
|
resolvedStackName = stackArg;
|
|
46136
46748
|
} else {
|
|
46137
46749
|
const appCmd = options.app || resolveApp();
|
|
@@ -46228,7 +46840,7 @@ async function exportCommand(stackArg, options) {
|
|
|
46228
46840
|
const phase1Template = filterTemplateForImport(template, phase1Imports);
|
|
46229
46841
|
const injectedCount = injectDeletionPolicyForImport(phase1Template);
|
|
46230
46842
|
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).`);
|
|
46231
|
-
await executeImportChangeSet(awsClients.cloudFormation, cfnStackName, phase1Template, phase1Imports, cfnParameters);
|
|
46843
|
+
await executeImportChangeSet(awsClients.cloudFormation, cfnStackName, phase1Template, phase1Imports, cfnParameters, templateFormat);
|
|
46232
46844
|
logger.info(`✓ Phase 1: CloudFormation stack '${cfnStackName}' created via IMPORT. ${phase1Imports.length} resource(s) imported.`);
|
|
46233
46845
|
if (recreateBeforePhase2.length > 0) for (const entry of recreateBeforePhase2) {
|
|
46234
46846
|
const handler = PRE_DELETE_HANDLERS[entry.resourceType];
|
|
@@ -46245,7 +46857,7 @@ async function exportCommand(stackArg, options) {
|
|
|
46245
46857
|
const phase2Count = phase2Creates.length + recreateBeforePhase2.length;
|
|
46246
46858
|
if (phase2Count > 0) try {
|
|
46247
46859
|
const phase2Template = applyImportOverlayForPhase2(template, phase1Imports);
|
|
46248
|
-
await executeUpdateChangeSet(awsClients.cloudFormation, cfnStackName, phase2Template, cfnParameters);
|
|
46860
|
+
await executeUpdateChangeSet(awsClients.cloudFormation, cfnStackName, phase2Template, cfnParameters, templateFormat);
|
|
46249
46861
|
const parts = [];
|
|
46250
46862
|
if (phase2Creates.length > 0) parts.push(`${phase2Creates.length} non-importable resource(s) CREATEd`);
|
|
46251
46863
|
if (recreateBeforePhase2.length > 0) parts.push(`${recreateBeforePhase2.length} IMPORT-unsupported resource(s) re-CREATEd`);
|
|
@@ -46305,14 +46917,15 @@ function parseTemplateFile(path) {
|
|
|
46305
46917
|
} catch (err) {
|
|
46306
46918
|
throw new Error(`Failed to read template file '${path}': ` + (err instanceof Error ? err.message : String(err)));
|
|
46307
46919
|
}
|
|
46308
|
-
let parsed;
|
|
46309
46920
|
try {
|
|
46310
|
-
|
|
46921
|
+
const { template, format } = parseCfnTemplateWithFormat(raw);
|
|
46922
|
+
return {
|
|
46923
|
+
template,
|
|
46924
|
+
format
|
|
46925
|
+
};
|
|
46311
46926
|
} catch (err) {
|
|
46312
|
-
throw new Error(`Template file '${path}' is not valid
|
|
46927
|
+
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)));
|
|
46313
46928
|
}
|
|
46314
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`Template file '${path}' is not a JSON object.`);
|
|
46315
|
-
return parsed;
|
|
46316
46929
|
}
|
|
46317
46930
|
async function assertCfnStackAbsent(cfnClient, stackName) {
|
|
46318
46931
|
try {
|
|
@@ -46860,10 +47473,10 @@ function printPlan(plan, cfnStackName) {
|
|
|
46860
47473
|
}
|
|
46861
47474
|
logger.info("");
|
|
46862
47475
|
}
|
|
46863
|
-
async function executeImportChangeSet(cfnClient, stackName, template, plan, parameters) {
|
|
47476
|
+
async function executeImportChangeSet(cfnClient, stackName, template, plan, parameters, templateFormat = "json") {
|
|
46864
47477
|
const logger = getLogger();
|
|
46865
47478
|
const changeSetName = `cdkd-migrate-${Date.now()}`;
|
|
46866
|
-
const templateBody =
|
|
47479
|
+
const templateBody = stringifyCfnTemplate(template, templateFormat);
|
|
46867
47480
|
const resourcesToImport = plan.map((entry) => ({
|
|
46868
47481
|
ResourceType: entry.resourceType,
|
|
46869
47482
|
LogicalResourceId: entry.logicalId,
|
|
@@ -46974,10 +47587,10 @@ async function collectImportFailureSummary(cfnClient, stackName) {
|
|
|
46974
47587
|
* (cdkd state is intentionally NOT deleted between phases, so a phase-2
|
|
46975
47588
|
* failure leaves a recoverable state).
|
|
46976
47589
|
*/
|
|
46977
|
-
async function executeUpdateChangeSet(cfnClient, stackName, template, parameters) {
|
|
47590
|
+
async function executeUpdateChangeSet(cfnClient, stackName, template, parameters, templateFormat = "json") {
|
|
46978
47591
|
const logger = getLogger();
|
|
46979
47592
|
const changeSetName = `cdkd-phase2-${Date.now()}`;
|
|
46980
|
-
const templateBody =
|
|
47593
|
+
const templateBody = stringifyCfnTemplate(template, templateFormat);
|
|
46981
47594
|
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.`);
|
|
46982
47595
|
logger.info(`Creating UPDATE changeset '${changeSetName}' for phase 2 (${templateBody.length} bytes)...`);
|
|
46983
47596
|
try {
|
|
@@ -47103,7 +47716,7 @@ async function confirmPrompt(prompt) {
|
|
|
47103
47716
|
}
|
|
47104
47717
|
}
|
|
47105
47718
|
function createExportCommand() {
|
|
47106
|
-
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
|
|
47719
|
+
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));
|
|
47107
47720
|
[
|
|
47108
47721
|
...commonOptions,
|
|
47109
47722
|
...appOptions,
|
|
@@ -47155,7 +47768,7 @@ function reorderArgs(argv) {
|
|
|
47155
47768
|
*/
|
|
47156
47769
|
async function main() {
|
|
47157
47770
|
const program = new Command();
|
|
47158
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
47771
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.125.0");
|
|
47159
47772
|
program.addCommand(createBootstrapCommand());
|
|
47160
47773
|
program.addCommand(createSynthCommand());
|
|
47161
47774
|
program.addCommand(createListCommand());
|