@go-to-k/cdkd 0.126.0 → 0.128.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +543 -185
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -34749,6 +34749,143 @@ function createStateCommand() {
|
|
|
34749
34749
|
return cmd;
|
|
34750
34750
|
}
|
|
34751
34751
|
|
|
34752
|
+
//#endregion
|
|
34753
|
+
//#region src/cli/upload-cfn-template.ts
|
|
34754
|
+
/**
|
|
34755
|
+
* CloudFormation `TemplateBody` hard limit (51,200 bytes). Templates larger
|
|
34756
|
+
* than this cannot be submitted inline and must be uploaded to S3 and
|
|
34757
|
+
* referenced via `TemplateURL` instead — see {@link uploadCfnTemplate}.
|
|
34758
|
+
*/
|
|
34759
|
+
const CFN_TEMPLATE_BODY_LIMIT = 51200;
|
|
34760
|
+
/**
|
|
34761
|
+
* CloudFormation `TemplateURL` hard limit (1 MB / 1,048,576 bytes).
|
|
34762
|
+
* Templates larger than this are structurally unsubmittable through any
|
|
34763
|
+
* CloudFormation API — no S3 indirection helps. The caller surfaces a
|
|
34764
|
+
* pre-flight error pointing the user at template-splitting (nested stacks)
|
|
34765
|
+
* or shrinking inline asset payloads (`lambda.Code.fromAsset`).
|
|
34766
|
+
*/
|
|
34767
|
+
const CFN_TEMPLATE_URL_LIMIT = 1048576;
|
|
34768
|
+
/**
|
|
34769
|
+
* Shared S3 key prefix for transient CFn templates uploaded by `cdkd import
|
|
34770
|
+
* --migrate-from-cloudformation` and `cdkd export`. Kept distinct from
|
|
34771
|
+
* cdkd's `cdkd/` state prefix so `state list` / `state info` never conflate
|
|
34772
|
+
* transient migration artifacts with persisted stack state. The prefix is
|
|
34773
|
+
* intentionally human-grep-able — leftovers (if cleanup fails) point
|
|
34774
|
+
* straight at the offending stack name.
|
|
34775
|
+
*
|
|
34776
|
+
* Re-used by both commands so operator-facing audit trails (CloudTrail
|
|
34777
|
+
* records of the migrate-tmp uploads) stay consistent across the two
|
|
34778
|
+
* flows.
|
|
34779
|
+
*/
|
|
34780
|
+
const MIGRATE_TMP_PREFIX = "cdkd-migrate-tmp";
|
|
34781
|
+
/**
|
|
34782
|
+
* Upload a CFn template body to the cdkd state bucket and return both a
|
|
34783
|
+
* virtual-hosted-style HTTPS URL CloudFormation can fetch via
|
|
34784
|
+
* `TemplateURL` and a `cleanup` callback that deletes the object (and
|
|
34785
|
+
* destroys the S3 client).
|
|
34786
|
+
*
|
|
34787
|
+
* The state bucket's actual region is resolved via `GetBucketLocation`
|
|
34788
|
+
* (cached per-process) so the upload client and the URL match the
|
|
34789
|
+
* bucket's region — the calling CLI's profile region is irrelevant here.
|
|
34790
|
+
*
|
|
34791
|
+
* Cleanup is the caller's responsibility: invoke `cleanup` in a `finally`
|
|
34792
|
+
* around the CFn call. CloudFormation copies the template into its own
|
|
34793
|
+
* internal storage during the synchronous `CreateChangeSet` /
|
|
34794
|
+
* `UpdateStack` API call, so the S3 object is no longer needed after that
|
|
34795
|
+
* call returns (success or failure).
|
|
34796
|
+
*
|
|
34797
|
+
* Shared between `cdkd import --migrate-from-cloudformation` (via
|
|
34798
|
+
* `retire-cfn-stack.ts`) and `cdkd export` (via `commands/export.ts`) so
|
|
34799
|
+
* the upload + cleanup contract is single-sourced.
|
|
34800
|
+
*/
|
|
34801
|
+
async function uploadCfnTemplate(args) {
|
|
34802
|
+
const { bucket, body, stackName, format, s3ClientOpts } = args;
|
|
34803
|
+
const region = await resolveBucketRegion(bucket, {
|
|
34804
|
+
...s3ClientOpts?.profile && { profile: s3ClientOpts.profile },
|
|
34805
|
+
...s3ClientOpts?.credentials && { credentials: s3ClientOpts.credentials }
|
|
34806
|
+
});
|
|
34807
|
+
const s3 = new S3Client({
|
|
34808
|
+
region,
|
|
34809
|
+
...s3ClientOpts?.profile && { profile: s3ClientOpts.profile },
|
|
34810
|
+
...s3ClientOpts?.credentials && { credentials: s3ClientOpts.credentials }
|
|
34811
|
+
});
|
|
34812
|
+
const ext = format === "yaml" ? "yaml" : "json";
|
|
34813
|
+
const contentType = format === "yaml" ? "application/x-yaml" : "application/json";
|
|
34814
|
+
const key = `${MIGRATE_TMP_PREFIX}/${stackName}/${Date.now()}.${ext}`;
|
|
34815
|
+
try {
|
|
34816
|
+
await s3.send(new PutObjectCommand({
|
|
34817
|
+
Bucket: bucket,
|
|
34818
|
+
Key: key,
|
|
34819
|
+
Body: body,
|
|
34820
|
+
ContentType: contentType
|
|
34821
|
+
}));
|
|
34822
|
+
} catch (err) {
|
|
34823
|
+
s3.destroy();
|
|
34824
|
+
throw err;
|
|
34825
|
+
}
|
|
34826
|
+
const url = `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
|
|
34827
|
+
const cleanup = async () => {
|
|
34828
|
+
try {
|
|
34829
|
+
await s3.send(new DeleteObjectCommand({
|
|
34830
|
+
Bucket: bucket,
|
|
34831
|
+
Key: key
|
|
34832
|
+
}));
|
|
34833
|
+
} finally {
|
|
34834
|
+
s3.destroy();
|
|
34835
|
+
}
|
|
34836
|
+
};
|
|
34837
|
+
return {
|
|
34838
|
+
url,
|
|
34839
|
+
cleanup
|
|
34840
|
+
};
|
|
34841
|
+
}
|
|
34842
|
+
/**
|
|
34843
|
+
* Threshold (in bytes) above which a single resource's serialized
|
|
34844
|
+
* `Properties` block is considered an "inline payload" worth surfacing as
|
|
34845
|
+
* a contributor to a template that exceeds the 1 MB CFn `TemplateURL`
|
|
34846
|
+
* ceiling. 4 KB matches the typical inline `Code.ZipFile` Lambda payload
|
|
34847
|
+
* that pushes a multi-resource CDK app over the wire-format limit.
|
|
34848
|
+
*/
|
|
34849
|
+
const LARGE_INLINE_RESOURCE_THRESHOLD = 4096;
|
|
34850
|
+
/**
|
|
34851
|
+
* Walk a CFn template and surface every resource whose serialized
|
|
34852
|
+
* `Properties` block exceeds {@link LARGE_INLINE_RESOURCE_THRESHOLD}.
|
|
34853
|
+
* Used to build the actionable "offending resources" list in the
|
|
34854
|
+
* pre-flight error when a template exceeds the 1 MB `TemplateURL`
|
|
34855
|
+
* ceiling — typical culprits are inline `Code.ZipFile` Lambdas, inline
|
|
34856
|
+
* StepFunctions definitions, or large `AWS::CloudFormation::Stack`
|
|
34857
|
+
* bodies.
|
|
34858
|
+
*
|
|
34859
|
+
* Returns entries sorted by `approxBytes` descending so the user sees
|
|
34860
|
+
* the biggest contributor first. A non-CFn-template input (no
|
|
34861
|
+
* `Resources` object) returns an empty array.
|
|
34862
|
+
*/
|
|
34863
|
+
function findLargeInlineResources(template, threshold = LARGE_INLINE_RESOURCE_THRESHOLD) {
|
|
34864
|
+
const result = [];
|
|
34865
|
+
const resources = template["Resources"];
|
|
34866
|
+
if (!resources || typeof resources !== "object" || Array.isArray(resources)) return result;
|
|
34867
|
+
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
34868
|
+
if (!resource || typeof resource !== "object" || Array.isArray(resource)) continue;
|
|
34869
|
+
const r = resource;
|
|
34870
|
+
const resourceType = typeof r["Type"] === "string" ? r["Type"] : "<unknown>";
|
|
34871
|
+
const properties = r["Properties"];
|
|
34872
|
+
if (properties === void 0 || properties === null) continue;
|
|
34873
|
+
let approxBytes;
|
|
34874
|
+
try {
|
|
34875
|
+
approxBytes = JSON.stringify(properties).length;
|
|
34876
|
+
} catch {
|
|
34877
|
+
continue;
|
|
34878
|
+
}
|
|
34879
|
+
if (approxBytes >= threshold) result.push({
|
|
34880
|
+
logicalId,
|
|
34881
|
+
resourceType,
|
|
34882
|
+
approxBytes
|
|
34883
|
+
});
|
|
34884
|
+
}
|
|
34885
|
+
result.sort((a, b) => b.approxBytes - a.approxBytes);
|
|
34886
|
+
return result;
|
|
34887
|
+
}
|
|
34888
|
+
|
|
34752
34889
|
//#endregion
|
|
34753
34890
|
//#region src/cli/yaml-cfn.ts
|
|
34754
34891
|
/**
|
|
@@ -35068,22 +35205,16 @@ const STABLE_TERMINAL_STATUSES = new Set([
|
|
|
35068
35205
|
* UpdateStack TemplateBody hard limit (51,200 bytes). Templates larger than
|
|
35069
35206
|
* this are uploaded to cdkd's state S3 bucket and submitted via `TemplateURL`
|
|
35070
35207
|
* instead — see {@link uploadTemplateForUpdateStack}.
|
|
35208
|
+
*
|
|
35209
|
+
* Re-exported from `upload-cfn-template.ts` as the shared source of truth.
|
|
35071
35210
|
*/
|
|
35072
|
-
const TEMPLATE_BODY_LIMIT =
|
|
35211
|
+
const TEMPLATE_BODY_LIMIT = CFN_TEMPLATE_BODY_LIMIT;
|
|
35073
35212
|
/**
|
|
35074
35213
|
* UpdateStack TemplateURL hard limit (1 MB / 1,048,576 bytes). Templates
|
|
35075
35214
|
* larger than this cannot be submitted at all and require manual
|
|
35076
35215
|
* intervention.
|
|
35077
35216
|
*/
|
|
35078
|
-
const TEMPLATE_URL_LIMIT =
|
|
35079
|
-
/**
|
|
35080
|
-
* S3 key prefix for the transient Retain-injected template uploaded by the
|
|
35081
|
-
* `--migrate-from-cloudformation` flow when the template exceeds the inline
|
|
35082
|
-
* 51,200-byte limit. Kept distinct from cdkd's `cdkd/` state prefix so
|
|
35083
|
-
* `state list` / `state info` never conflate transient migration artifacts
|
|
35084
|
-
* with persisted state.
|
|
35085
|
-
*/
|
|
35086
|
-
const MIGRATE_TMP_PREFIX = "cdkd-migrate-tmp";
|
|
35217
|
+
const TEMPLATE_URL_LIMIT = CFN_TEMPLATE_URL_LIMIT;
|
|
35087
35218
|
/**
|
|
35088
35219
|
* Retire a CloudFormation stack whose resources have just been adopted into
|
|
35089
35220
|
* cdkd state. The 4-step procedure is the one AWS recommends for handing
|
|
@@ -35211,45 +35342,13 @@ async function retireCloudFormationStack(options) {
|
|
|
35211
35342
|
* Exported for unit testing.
|
|
35212
35343
|
*/
|
|
35213
35344
|
async function uploadTemplateForUpdateStack(args) {
|
|
35214
|
-
|
|
35215
|
-
|
|
35216
|
-
|
|
35217
|
-
|
|
35345
|
+
return uploadCfnTemplate({
|
|
35346
|
+
bucket: args.bucket,
|
|
35347
|
+
body: args.body,
|
|
35348
|
+
stackName: args.cfnStackName,
|
|
35349
|
+
...args.format && { format: args.format },
|
|
35350
|
+
...args.s3ClientOpts && { s3ClientOpts: args.s3ClientOpts }
|
|
35218
35351
|
});
|
|
35219
|
-
const s3 = new S3Client({
|
|
35220
|
-
region,
|
|
35221
|
-
...s3ClientOpts?.profile && { profile: s3ClientOpts.profile },
|
|
35222
|
-
...s3ClientOpts?.credentials && { credentials: s3ClientOpts.credentials }
|
|
35223
|
-
});
|
|
35224
|
-
const ext = format === "yaml" ? "yaml" : "json";
|
|
35225
|
-
const contentType = format === "yaml" ? "application/x-yaml" : "application/json";
|
|
35226
|
-
const key = `${MIGRATE_TMP_PREFIX}/${cfnStackName}/${Date.now()}.${ext}`;
|
|
35227
|
-
try {
|
|
35228
|
-
await s3.send(new PutObjectCommand({
|
|
35229
|
-
Bucket: bucket,
|
|
35230
|
-
Key: key,
|
|
35231
|
-
Body: body,
|
|
35232
|
-
ContentType: contentType
|
|
35233
|
-
}));
|
|
35234
|
-
} catch (err) {
|
|
35235
|
-
s3.destroy();
|
|
35236
|
-
throw err;
|
|
35237
|
-
}
|
|
35238
|
-
const url = `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
|
|
35239
|
-
const cleanup = async () => {
|
|
35240
|
-
try {
|
|
35241
|
-
await s3.send(new DeleteObjectCommand({
|
|
35242
|
-
Bucket: bucket,
|
|
35243
|
-
Key: key
|
|
35244
|
-
}));
|
|
35245
|
-
} finally {
|
|
35246
|
-
s3.destroy();
|
|
35247
|
-
}
|
|
35248
|
-
};
|
|
35249
|
-
return {
|
|
35250
|
-
url,
|
|
35251
|
-
cleanup
|
|
35252
|
-
};
|
|
35253
35352
|
}
|
|
35254
35353
|
/**
|
|
35255
35354
|
* Parse a CloudFormation template body (JSON or YAML), set
|
|
@@ -36503,7 +36602,7 @@ function extractLambdaProperties(stack, logicalId, resource, resources) {
|
|
|
36503
36602
|
const timeoutSec = typeof props["Timeout"] === "number" ? props["Timeout"] : 3;
|
|
36504
36603
|
const ephemeralStorageMb = extractEphemeralStorageMb(props, logicalId);
|
|
36505
36604
|
const code = props["Code"] ?? {};
|
|
36506
|
-
const imageUri = extractImageUri(code["ImageUri"], logicalId, stack.stackName, resources);
|
|
36605
|
+
const imageUri = extractImageUri$1(code["ImageUri"], logicalId, stack.stackName, resources);
|
|
36507
36606
|
if (imageUri !== void 0) return extractImageLambdaProperties({
|
|
36508
36607
|
stack,
|
|
36509
36608
|
logicalId,
|
|
@@ -36600,7 +36699,7 @@ function extractEphemeralStorageMb(props, logicalId) {
|
|
|
36600
36699
|
* for genuinely unrecognized shapes so the caller's downstream ZIP-vs-
|
|
36601
36700
|
* IMAGE branching can route to its existing error path.
|
|
36602
36701
|
*/
|
|
36603
|
-
function extractImageUri(value, logicalId, stackName, resources) {
|
|
36702
|
+
function extractImageUri$1(value, logicalId, stackName, resources) {
|
|
36604
36703
|
if (typeof value === "string" && value.length > 0) return value;
|
|
36605
36704
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
36606
36705
|
const obj = value;
|
|
@@ -40248,31 +40347,56 @@ function createContainerPool(specs, options) {
|
|
|
40248
40347
|
/**
|
|
40249
40348
|
* Spin up one new container for the given Lambda spec. Returns a
|
|
40250
40349
|
* handle the caller can write into the entry's data structures.
|
|
40350
|
+
*
|
|
40351
|
+
* Branches on `spec.kind`:
|
|
40352
|
+
* - `'zip'`: bind-mount the function's local code dir at
|
|
40353
|
+
* `/var/task` (or `/var/runtime` for `provided.*` runtimes),
|
|
40354
|
+
* base image from `public.ecr.aws/lambda/<lang>:<v>`, CMD =
|
|
40355
|
+
* `[<Handler>]`.
|
|
40356
|
+
* - `'image'`: no code bind-mount (image already includes the
|
|
40357
|
+
* code), base image is the pre-built local tag, CMD =
|
|
40358
|
+
* `ImageConfig.Command` (may be empty), optional EntryPoint /
|
|
40359
|
+
* WorkingDirectory / --platform applied verbatim.
|
|
40251
40360
|
*/
|
|
40252
40361
|
async function startOne(spec) {
|
|
40253
|
-
const image = resolveRuntimeImage(spec.lambda.runtime);
|
|
40254
40362
|
const hostPort = await pickFreePort();
|
|
40255
40363
|
const name = `cdkd-local-${spec.lambda.logicalId}-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
40256
|
-
logger.debug(`Starting container ${name} for ${spec.lambda.logicalId} on ${spec.containerHost}:${hostPort}`);
|
|
40257
|
-
|
|
40258
|
-
|
|
40259
|
-
|
|
40260
|
-
|
|
40261
|
-
|
|
40262
|
-
const containerCodePath = resolveRuntimeCodeMountPath(spec.lambda.runtime);
|
|
40263
|
-
const containerId = await runDetached({
|
|
40264
|
-
image,
|
|
40265
|
-
mounts: [{
|
|
40266
|
-
hostPath: spec.codeDir,
|
|
40267
|
-
containerPath: containerCodePath,
|
|
40364
|
+
logger.debug(`Starting container ${name} for ${spec.lambda.logicalId} (kind=${spec.kind}) on ${spec.containerHost}:${hostPort}`);
|
|
40365
|
+
let containerId;
|
|
40366
|
+
if (spec.kind === "zip") {
|
|
40367
|
+
const optMount = spec.optDir ? [{
|
|
40368
|
+
hostPath: spec.optDir,
|
|
40369
|
+
containerPath: "/opt",
|
|
40268
40370
|
readOnly: true
|
|
40269
|
-
}]
|
|
40270
|
-
|
|
40371
|
+
}] : [];
|
|
40372
|
+
const containerCodePath = resolveRuntimeCodeMountPath(spec.lambda.runtime);
|
|
40373
|
+
containerId = await runDetached({
|
|
40374
|
+
image: resolveRuntimeImage(spec.lambda.runtime),
|
|
40375
|
+
mounts: [{
|
|
40376
|
+
hostPath: spec.codeDir,
|
|
40377
|
+
containerPath: containerCodePath,
|
|
40378
|
+
readOnly: true
|
|
40379
|
+
}],
|
|
40380
|
+
extraMounts: optMount,
|
|
40381
|
+
env: spec.env,
|
|
40382
|
+
cmd: [spec.lambda.handler],
|
|
40383
|
+
hostPort,
|
|
40384
|
+
host: spec.containerHost,
|
|
40385
|
+
name,
|
|
40386
|
+
...spec.debugPort !== void 0 && { debugPort: spec.debugPort },
|
|
40387
|
+
...spec.tmpfs !== void 0 && { tmpfs: spec.tmpfs }
|
|
40388
|
+
});
|
|
40389
|
+
} else containerId = await runDetached({
|
|
40390
|
+
image: spec.image,
|
|
40391
|
+
mounts: [],
|
|
40271
40392
|
env: spec.env,
|
|
40272
|
-
cmd:
|
|
40393
|
+
cmd: spec.command,
|
|
40273
40394
|
hostPort,
|
|
40274
40395
|
host: spec.containerHost,
|
|
40275
40396
|
name,
|
|
40397
|
+
platform: spec.platform,
|
|
40398
|
+
...spec.entryPoint !== void 0 && { entryPoint: spec.entryPoint },
|
|
40399
|
+
...spec.workingDir !== void 0 && { workingDir: spec.workingDir },
|
|
40276
40400
|
...spec.debugPort !== void 0 && { debugPort: spec.debugPort },
|
|
40277
40401
|
...spec.tmpfs !== void 0 && { tmpfs: spec.tmpfs }
|
|
40278
40402
|
});
|
|
@@ -44043,12 +44167,13 @@ async function localStartApiCommand(target, options) {
|
|
|
44043
44167
|
inlineTmpDirs,
|
|
44044
44168
|
layerTmpDirs,
|
|
44045
44169
|
stateByStack,
|
|
44170
|
+
skipPull: options.pull === false,
|
|
44046
44171
|
...options.layerRoleArn !== void 0 && { layerRoleArn: options.layerRoleArn }
|
|
44047
44172
|
});
|
|
44048
44173
|
specs.set(logicalId, spec);
|
|
44049
44174
|
}
|
|
44050
44175
|
const distinctImages = /* @__PURE__ */ new Set();
|
|
44051
|
-
for (const spec of specs.values()) distinctImages.add(resolveRuntimeImage(spec.lambda.runtime));
|
|
44176
|
+
for (const spec of specs.values()) if (spec.kind === "zip") distinctImages.add(resolveRuntimeImage(spec.lambda.runtime));
|
|
44052
44177
|
for (const image of distinctImages) await pullImage(image, options.pull === false);
|
|
44053
44178
|
return {
|
|
44054
44179
|
routes: routesWithAuth,
|
|
@@ -44077,13 +44202,23 @@ async function localStartApiCommand(target, options) {
|
|
|
44077
44202
|
/**
|
|
44078
44203
|
* Compute the watched-asset list from a spec map. Pure helper —
|
|
44079
44204
|
* keeps the side-effect (`lastAssetPaths.value = ...`) confined to
|
|
44080
|
-
* the post-swap call sites (initial boot + post-reload).
|
|
44081
|
-
* is either the unzipped asset directory or the
|
|
44082
|
-
* both are watch-worthy.
|
|
44205
|
+
* the post-swap call sites (initial boot + post-reload). For ZIP
|
|
44206
|
+
* Lambdas `codeDir` is either the unzipped asset directory or the
|
|
44207
|
+
* inline-code tmpdir; both are watch-worthy. IMAGE Lambdas
|
|
44208
|
+
* (`kind: 'image'`) don't have a host-side bind-mount source — the
|
|
44209
|
+
* code is baked into the docker image at build time. Their build
|
|
44210
|
+
* context (Dockerfile + source directory) is rebuilt on every
|
|
44211
|
+
* reload via `synthesizeAndBuild` → `buildContainerSpec` →
|
|
44212
|
+
* `resolveContainerImageForStartApi`, so a source edit DOES trigger
|
|
44213
|
+
* rebuild AND the deterministic `image` tag changes — but watching
|
|
44214
|
+
* the build-context dir explicitly here is deferred to a follow-up
|
|
44215
|
+
* (the watched-asset list is currently sourced from `cdk.out/`
|
|
44216
|
+
* which transitively covers most container-Lambda asset dirs since
|
|
44217
|
+
* `cdk synth` re-stages them on every synth call).
|
|
44083
44218
|
*/
|
|
44084
44219
|
const computeAssetPaths = (specs) => {
|
|
44085
44220
|
const assetPaths = /* @__PURE__ */ new Set();
|
|
44086
|
-
for (const spec of specs.values()) assetPaths.add(spec.codeDir);
|
|
44221
|
+
for (const spec of specs.values()) if (spec.kind === "zip") assetPaths.add(spec.codeDir);
|
|
44087
44222
|
return [...assetPaths];
|
|
44088
44223
|
};
|
|
44089
44224
|
const initialMaterial = await synthesizeAndBuild();
|
|
@@ -44356,10 +44491,19 @@ function warnIamRoutes(routesWithAuth) {
|
|
|
44356
44491
|
* missing, runtime not supported).
|
|
44357
44492
|
*/
|
|
44358
44493
|
async function buildContainerSpec(args) {
|
|
44359
|
-
const { logicalId, stacks, overrides, assumeRole, containerHost, debugPort, stsRegion, inlineTmpDirs, layerTmpDirs, stateByStack, layerRoleArn } = args;
|
|
44494
|
+
const { logicalId, stacks, overrides, assumeRole, containerHost, debugPort, stsRegion, inlineTmpDirs, layerTmpDirs, stateByStack, skipPull, layerRoleArn } = args;
|
|
44360
44495
|
const lambda = resolveLambdaByLogicalId(logicalId, stacks);
|
|
44361
|
-
|
|
44362
|
-
|
|
44496
|
+
let codeDir;
|
|
44497
|
+
let optDir;
|
|
44498
|
+
let imageRef;
|
|
44499
|
+
let platform;
|
|
44500
|
+
if (lambda.kind === "zip") {
|
|
44501
|
+
codeDir = lambda.codePath ?? materializeInlineCode$1(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime), inlineTmpDirs);
|
|
44502
|
+
optDir = await materializeLambdaLayers$1(lambda.layers, layerTmpDirs, layerRoleArn);
|
|
44503
|
+
} else {
|
|
44504
|
+
imageRef = (await resolveContainerImageForStartApi(lambda, skipPull)).imageRef;
|
|
44505
|
+
platform = architectureToPlatform(lambda.architecture);
|
|
44506
|
+
}
|
|
44363
44507
|
let templateEnv = getTemplateEnv$1(lambda.resource);
|
|
44364
44508
|
const stateBundle = stateByStack.get(lambda.stack.stackName);
|
|
44365
44509
|
let stateAudit;
|
|
@@ -44399,7 +44543,8 @@ async function buildContainerSpec(args) {
|
|
|
44399
44543
|
target: "/tmp",
|
|
44400
44544
|
sizeMb: lambda.ephemeralStorageMb
|
|
44401
44545
|
} : void 0;
|
|
44402
|
-
return {
|
|
44546
|
+
if (lambda.kind === "zip") return {
|
|
44547
|
+
kind: "zip",
|
|
44403
44548
|
lambda,
|
|
44404
44549
|
codeDir,
|
|
44405
44550
|
env: dockerEnv,
|
|
@@ -44408,6 +44553,60 @@ async function buildContainerSpec(args) {
|
|
|
44408
44553
|
...debugPort !== void 0 && { debugPort },
|
|
44409
44554
|
...tmpfs !== void 0 && { tmpfs }
|
|
44410
44555
|
};
|
|
44556
|
+
return {
|
|
44557
|
+
kind: "image",
|
|
44558
|
+
lambda,
|
|
44559
|
+
image: imageRef,
|
|
44560
|
+
platform,
|
|
44561
|
+
command: lambda.imageConfig.command ?? [],
|
|
44562
|
+
...lambda.imageConfig.entryPoint !== void 0 && lambda.imageConfig.entryPoint.length > 0 && { entryPoint: lambda.imageConfig.entryPoint },
|
|
44563
|
+
...lambda.imageConfig.workingDirectory !== void 0 && { workingDir: lambda.imageConfig.workingDirectory },
|
|
44564
|
+
env: dockerEnv,
|
|
44565
|
+
containerHost,
|
|
44566
|
+
...debugPort !== void 0 && { debugPort },
|
|
44567
|
+
...tmpfs !== void 0 && { tmpfs }
|
|
44568
|
+
};
|
|
44569
|
+
}
|
|
44570
|
+
/**
|
|
44571
|
+
* Resolve a container Lambda's local docker image — local build from
|
|
44572
|
+
* `cdk.out` asset manifest first, ECR-pull fallback when the asset
|
|
44573
|
+
* manifest has no matching entry. Mirrors `cdkd local invoke`'s
|
|
44574
|
+
* `resolveContainerImagePlan` shape; the start-api server doesn't
|
|
44575
|
+
* need the no-build flag (deterministic-tag cache reuse is automatic
|
|
44576
|
+
* across reloads because the per-Lambda tag is content-addressed).
|
|
44577
|
+
*
|
|
44578
|
+
* Same-account / same-region only on the ECR-pull path (matches the
|
|
44579
|
+
* `cdkd local invoke` PR 5 of #224 boundary). Cross-account /
|
|
44580
|
+
* cross-region ECR pull is the W2-1 deferred follow-up.
|
|
44581
|
+
*/
|
|
44582
|
+
async function resolveContainerImageForStartApi(lambda, skipPull) {
|
|
44583
|
+
const logger = getLogger();
|
|
44584
|
+
const localBuild = await resolveLocalBuildPlan$1(lambda);
|
|
44585
|
+
if (localBuild) return { imageRef: await buildContainerImage(localBuild.asset, localBuild.cdkOutDir, { architecture: lambda.architecture }) };
|
|
44586
|
+
if (!parseEcrUri(lambda.imageUri)) throw new Error(`Container Lambda '${lambda.logicalId}' has no matching asset in cdk.out, and Code.ImageUri '${lambda.imageUri}' is not an ECR URI cdkd can authenticate against. Re-synthesize the CDK app (so cdk.out includes the build context) or deploy the image to ECR first.`);
|
|
44587
|
+
logger.info(`No matching cdk.out asset for ${lambda.imageUri}; falling back to ECR pull (same-acct/region only)...`);
|
|
44588
|
+
return { imageRef: await pullEcrImage(lambda.imageUri, { skipPull }) };
|
|
44589
|
+
}
|
|
44590
|
+
/**
|
|
44591
|
+
* Look up the docker image asset that backs a container Lambda.
|
|
44592
|
+
* Returns `undefined` when the asset manifest has no matching entry —
|
|
44593
|
+
* the caller falls back to the ECR-pull path.
|
|
44594
|
+
*
|
|
44595
|
+
* Mirrors `local-invoke.ts:resolveLocalBuildPlan`; kept separate so
|
|
44596
|
+
* the two commands evolve their asset-lookup heuristics independently.
|
|
44597
|
+
*/
|
|
44598
|
+
async function resolveLocalBuildPlan$1(lambda) {
|
|
44599
|
+
const manifestPath = lambda.stack.assetManifestPath;
|
|
44600
|
+
if (!manifestPath) return void 0;
|
|
44601
|
+
const cdkOutDir = path.dirname(manifestPath);
|
|
44602
|
+
const manifest = await new AssetManifestLoader().loadManifest(cdkOutDir, lambda.stack.stackName);
|
|
44603
|
+
if (!manifest) return void 0;
|
|
44604
|
+
const entry = getDockerImageBySourceHash(manifest, lambda.imageUri);
|
|
44605
|
+
if (!entry) return void 0;
|
|
44606
|
+
return {
|
|
44607
|
+
asset: entry.asset,
|
|
44608
|
+
cdkOutDir
|
|
44609
|
+
};
|
|
44411
44610
|
}
|
|
44412
44611
|
/**
|
|
44413
44612
|
* Build the `/opt` bind-mount source for a Lambda's layers. Mirrors
|
|
@@ -44469,15 +44668,23 @@ function resolveLambdaByLogicalId(logicalId, stacks) {
|
|
|
44469
44668
|
const resource = stack.template.Resources?.[logicalId];
|
|
44470
44669
|
if (!resource || resource.Type !== "AWS::Lambda::Function") continue;
|
|
44471
44670
|
const props = resource.Properties ?? {};
|
|
44472
|
-
const runtime = typeof props["Runtime"] === "string" ? props["Runtime"] : "";
|
|
44473
|
-
const handler = typeof props["Handler"] === "string" ? props["Handler"] : "";
|
|
44474
44671
|
const memoryMb = typeof props["MemorySize"] === "number" ? props["MemorySize"] : 128;
|
|
44475
44672
|
const timeoutSec = typeof props["Timeout"] === "number" ? props["Timeout"] : 3;
|
|
44476
|
-
if (!runtime) throw new Error(`Lambda '${logicalId}' has no Runtime property. Container-image Lambdas (Code.ImageUri) are not supported in cdkd local start-api v1.`);
|
|
44477
|
-
if (!handler) throw new Error(`Lambda '${logicalId}' has no Handler property.`);
|
|
44478
44673
|
const code = props["Code"] ?? {};
|
|
44479
|
-
const imageUri = code["ImageUri"];
|
|
44480
|
-
if (
|
|
44674
|
+
const imageUri = extractImageUri(code["ImageUri"]);
|
|
44675
|
+
if (imageUri !== void 0) return resolveImageLambda({
|
|
44676
|
+
stack,
|
|
44677
|
+
logicalId,
|
|
44678
|
+
resource,
|
|
44679
|
+
props,
|
|
44680
|
+
memoryMb,
|
|
44681
|
+
timeoutSec,
|
|
44682
|
+
imageUri
|
|
44683
|
+
});
|
|
44684
|
+
const runtime = typeof props["Runtime"] === "string" ? props["Runtime"] : "";
|
|
44685
|
+
const handler = typeof props["Handler"] === "string" ? props["Handler"] : "";
|
|
44686
|
+
if (!runtime) throw new Error(`Lambda '${logicalId}' has no Runtime property and no Code.ImageUri. cdkd local start-api cannot tell if this is a ZIP or a container Lambda.`);
|
|
44687
|
+
if (!handler) throw new Error(`Lambda '${logicalId}' has no Handler property.`);
|
|
44481
44688
|
const inlineCode = typeof code["ZipFile"] === "string" ? code["ZipFile"] : void 0;
|
|
44482
44689
|
let codePath = null;
|
|
44483
44690
|
if (!inlineCode) codePath = resolveAssetCodePath(stack, logicalId, resource);
|
|
@@ -44501,6 +44708,66 @@ function resolveLambdaByLogicalId(logicalId, stacks) {
|
|
|
44501
44708
|
throw new Error(`No AWS::Lambda::Function resource named '${logicalId}' found in target stacks. This is likely a synthesis bug — the route-discovery phase resolved a route to this logical ID.`);
|
|
44502
44709
|
}
|
|
44503
44710
|
/**
|
|
44711
|
+
* Extract `Code.ImageUri` across the shapes CDK actually synthesizes.
|
|
44712
|
+
* Mirrors the simpler subset of `lambda-resolver.ts:extractImageUri`
|
|
44713
|
+
* scoped to the shapes `cdkd local start-api` consumes — flat string
|
|
44714
|
+
* and `Fn::Sub` (the canonical asset shape for
|
|
44715
|
+
* `lambda.DockerImageCode.fromImageAsset`). `Fn::Join` shapes for
|
|
44716
|
+
* `lambda.DockerImageCode.fromEcr` are deferred to a follow-up: the
|
|
44717
|
+
* start-api boot flow doesn't yet load cdkd state up front, and the
|
|
44718
|
+
* `Fn::Join` resolver needs it to recover same-stack ECR repository
|
|
44719
|
+
* URIs. When the user hits the unsupported shape, the downstream
|
|
44720
|
+
* resolveLocalBuildPlan / pullEcrImage path surfaces a clear error.
|
|
44721
|
+
*
|
|
44722
|
+
* Returns `undefined` when the field is absent or non-recognized,
|
|
44723
|
+
* which routes the caller to the ZIP branch (with its existing
|
|
44724
|
+
* "no Runtime / no Handler" validations).
|
|
44725
|
+
*/
|
|
44726
|
+
function extractImageUri(value) {
|
|
44727
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
44728
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
44729
|
+
const sub = value["Fn::Sub"];
|
|
44730
|
+
if (typeof sub === "string" && sub.length > 0) return sub;
|
|
44731
|
+
if (Array.isArray(sub) && typeof sub[0] === "string") return sub[0];
|
|
44732
|
+
}
|
|
44733
|
+
}
|
|
44734
|
+
/**
|
|
44735
|
+
* Build the IMAGE-variant `ResolvedStartApiLambda` from a Lambda
|
|
44736
|
+
* template entry with `Code.ImageUri`. Mirrors
|
|
44737
|
+
* `lambda-resolver.ts:extractImageLambdaProperties` but trimmed to the
|
|
44738
|
+
* fields `cdkd local start-api` actually consumes.
|
|
44739
|
+
*/
|
|
44740
|
+
function resolveImageLambda(args) {
|
|
44741
|
+
const { stack, logicalId, resource, props, memoryMb, timeoutSec, imageUri } = args;
|
|
44742
|
+
const rawImageConfig = props["ImageConfig"] ?? {};
|
|
44743
|
+
const imageConfig = {};
|
|
44744
|
+
if (Array.isArray(rawImageConfig["Command"])) imageConfig.command = rawImageConfig["Command"].filter((s) => typeof s === "string");
|
|
44745
|
+
if (Array.isArray(rawImageConfig["EntryPoint"])) imageConfig.entryPoint = rawImageConfig["EntryPoint"].filter((s) => typeof s === "string");
|
|
44746
|
+
if (typeof rawImageConfig["WorkingDirectory"] === "string") imageConfig.workingDirectory = rawImageConfig["WorkingDirectory"];
|
|
44747
|
+
const arches = props["Architectures"];
|
|
44748
|
+
let architecture = "x86_64";
|
|
44749
|
+
if (Array.isArray(arches) && arches.length > 0) {
|
|
44750
|
+
const first = arches[0];
|
|
44751
|
+
if (first === "arm64") architecture = "arm64";
|
|
44752
|
+
else if (first === "x86_64") architecture = "x86_64";
|
|
44753
|
+
else throw new Error(`Lambda '${logicalId}' has unsupported Architectures value '${String(first)}'. cdkd local start-api supports x86_64 and arm64.`);
|
|
44754
|
+
}
|
|
44755
|
+
const ephemeralStorageMb = extractEphemeralStorageMb(props, logicalId);
|
|
44756
|
+
return {
|
|
44757
|
+
kind: "image",
|
|
44758
|
+
stack,
|
|
44759
|
+
logicalId,
|
|
44760
|
+
resource,
|
|
44761
|
+
memoryMb,
|
|
44762
|
+
timeoutSec,
|
|
44763
|
+
imageUri,
|
|
44764
|
+
imageConfig,
|
|
44765
|
+
architecture,
|
|
44766
|
+
layers: [],
|
|
44767
|
+
...ephemeralStorageMb !== void 0 && { ephemeralStorageMb }
|
|
44768
|
+
};
|
|
44769
|
+
}
|
|
44770
|
+
/**
|
|
44504
44771
|
* Locate the Lambda's local code directory using the CDK-blessed
|
|
44505
44772
|
* `Metadata['aws:asset:path']` hint. Bind-mounted directly at
|
|
44506
44773
|
* `/var/task` (read-only) by the docker-runner.
|
|
@@ -47011,7 +47278,10 @@ async function exportCommand(stackArg, options) {
|
|
|
47011
47278
|
const phase1Template = filterTemplateForImport(template, phase1Imports);
|
|
47012
47279
|
const injectedCount = injectDeletionPolicyForImport(phase1Template);
|
|
47013
47280
|
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).`);
|
|
47014
|
-
await executeImportChangeSet(awsClients.cloudFormation, cfnStackName, phase1Template, phase1Imports, cfnParameters, templateFormat
|
|
47281
|
+
await executeImportChangeSet(awsClients.cloudFormation, cfnStackName, phase1Template, phase1Imports, cfnParameters, templateFormat, {
|
|
47282
|
+
stateBucket,
|
|
47283
|
+
...options.profile && { s3ClientOpts: { profile: options.profile } }
|
|
47284
|
+
});
|
|
47015
47285
|
logger.info(`✓ Phase 1: CloudFormation stack '${cfnStackName}' created via IMPORT. ${phase1Imports.length} resource(s) imported.`);
|
|
47016
47286
|
if (recreateBeforePhase2.length > 0) for (const entry of recreateBeforePhase2) {
|
|
47017
47287
|
const handler = PRE_DELETE_HANDLERS[entry.resourceType];
|
|
@@ -47028,7 +47298,10 @@ async function exportCommand(stackArg, options) {
|
|
|
47028
47298
|
const phase2Count = phase2Creates.length + recreateBeforePhase2.length;
|
|
47029
47299
|
if (phase2Count > 0) try {
|
|
47030
47300
|
const phase2Template = applyImportOverlayForPhase2(template, phase1Imports);
|
|
47031
|
-
await executeUpdateChangeSet(awsClients.cloudFormation, cfnStackName, phase2Template, cfnParameters, templateFormat
|
|
47301
|
+
await executeUpdateChangeSet(awsClients.cloudFormation, cfnStackName, phase2Template, cfnParameters, templateFormat, {
|
|
47302
|
+
stateBucket,
|
|
47303
|
+
...options.profile && { s3ClientOpts: { profile: options.profile } }
|
|
47304
|
+
});
|
|
47032
47305
|
const parts = [];
|
|
47033
47306
|
if (phase2Creates.length > 0) parts.push(`${phase2Creates.length} non-importable resource(s) CREATEd`);
|
|
47034
47307
|
if (recreateBeforePhase2.length > 0) parts.push(`${recreateBeforePhase2.length} IMPORT-unsupported resource(s) re-CREATEd`);
|
|
@@ -47644,7 +47917,84 @@ function printPlan(plan, cfnStackName) {
|
|
|
47644
47917
|
}
|
|
47645
47918
|
logger.info("");
|
|
47646
47919
|
}
|
|
47647
|
-
|
|
47920
|
+
/**
|
|
47921
|
+
* Decide whether to submit a CFn changeset's template inline via
|
|
47922
|
+
* `TemplateBody` or upload it to the cdkd state bucket and submit via
|
|
47923
|
+
* `TemplateURL`. Returns a discriminated outcome:
|
|
47924
|
+
*
|
|
47925
|
+
* - `kind: 'inline'` — payload <= 51,200 bytes; pass `TemplateBody`
|
|
47926
|
+
* directly. No S3 round-trip; the caller's `finally` cleanup is a
|
|
47927
|
+
* no-op.
|
|
47928
|
+
* - `kind: 'url'` — payload in (51,200, 1,048,576] bytes; helper
|
|
47929
|
+
* uploaded to `cdkd-migrate-tmp/<stackName>/<ts>.{json,yaml}`. The
|
|
47930
|
+
* caller MUST invoke the returned `cleanup` in a `finally` so the
|
|
47931
|
+
* transient object is deleted regardless of CFn success / failure.
|
|
47932
|
+
*
|
|
47933
|
+
* Payloads > 1,048,576 bytes throw pre-flight (the 1 MB ceiling applies
|
|
47934
|
+
* to every CFn API surface; no S3 indirection helps). The error names the
|
|
47935
|
+
* top inline-payload contributors so the user knows what to shrink.
|
|
47936
|
+
*
|
|
47937
|
+
* `phaseLabel` is interpolated into the pre-flight error so the user
|
|
47938
|
+
* sees which phase tripped the ceiling ("phase-1 IMPORT" vs "phase-2
|
|
47939
|
+
* UPDATE").
|
|
47940
|
+
*
|
|
47941
|
+
* When `uploadOpts` is undefined (e.g. unit tests that exercise only the
|
|
47942
|
+
* inline path), templates over the inline limit throw with a clear
|
|
47943
|
+
* "no upload bucket configured" error rather than silently failing on a
|
|
47944
|
+
* downstream CFn rejection.
|
|
47945
|
+
*
|
|
47946
|
+
* `templateFormat` (default `'json'`) drives the transient S3 object's
|
|
47947
|
+
* key suffix + Content-Type so YAML-authored templates stay YAML on the
|
|
47948
|
+
* wire (`.yaml` / `application/x-yaml`).
|
|
47949
|
+
*/
|
|
47950
|
+
async function selectChangeSetTemplateSource(template, templateBody, uploadOpts, stackName, phaseLabel, templateFormat = "json") {
|
|
47951
|
+
const logger = getLogger();
|
|
47952
|
+
if (templateBody.length <= 51200) return {
|
|
47953
|
+
kind: "inline",
|
|
47954
|
+
templateBody,
|
|
47955
|
+
cleanup: async () => void 0
|
|
47956
|
+
};
|
|
47957
|
+
if (templateBody.length > 1048576) {
|
|
47958
|
+
const offenders = findLargeInlineResources(template);
|
|
47959
|
+
let detail = "";
|
|
47960
|
+
if (offenders.length > 0) {
|
|
47961
|
+
detail = `\nLargest inline payloads (move these to lambda.Code.fromAsset or split into nested stacks):\n${offenders.slice(0, 10).map((o) => ` - ${o.logicalId} (${o.resourceType}): ~${o.approxBytes} bytes`).join("\n")}`;
|
|
47962
|
+
if (offenders.length > 10) detail += `\n (and ${offenders.length - 10} more above the 4096-byte threshold)`;
|
|
47963
|
+
}
|
|
47964
|
+
throw new Error(`${phaseLabel} template is ${templateBody.length} bytes, over the ${CFN_TEMPLATE_URL_LIMIT}-byte CloudFormation TemplateURL limit. Templates that large cannot be submitted to CloudFormation — shrink inline payloads (e.g. inline Lambda Code.ZipFile larger than ~4 KB → switch to lambda.Code.fromAsset) or split the stack into nested AWS::CloudFormation::Stack resources.${detail}`);
|
|
47965
|
+
}
|
|
47966
|
+
if (!uploadOpts) throw new Error(`${phaseLabel} template is ${templateBody.length} bytes, over the inline ${CFN_TEMPLATE_BODY_LIMIT}-byte TemplateBody limit, but no upload bucket was provided to selectChangeSetTemplateSource — pass uploadOpts to enable the TemplateURL upload path.`);
|
|
47967
|
+
logger.info(` Template is ${templateBody.length} bytes (over ${CFN_TEMPLATE_BODY_LIMIT} inline limit) — uploading to state bucket '${uploadOpts.stateBucket}'.`);
|
|
47968
|
+
const uploaded = await uploadCfnTemplate({
|
|
47969
|
+
bucket: uploadOpts.stateBucket,
|
|
47970
|
+
body: templateBody,
|
|
47971
|
+
stackName,
|
|
47972
|
+
format: templateFormat,
|
|
47973
|
+
...uploadOpts.s3ClientOpts && { s3ClientOpts: uploadOpts.s3ClientOpts }
|
|
47974
|
+
});
|
|
47975
|
+
return {
|
|
47976
|
+
kind: "url",
|
|
47977
|
+
templateUrl: uploaded.url,
|
|
47978
|
+
cleanup: uploaded.cleanup
|
|
47979
|
+
};
|
|
47980
|
+
}
|
|
47981
|
+
/**
|
|
47982
|
+
* Best-effort wrapper around the `cleanup` callback returned by
|
|
47983
|
+
* {@link selectChangeSetTemplateSource}. Mirrors the warn-on-failure
|
|
47984
|
+
* pattern from `retireCloudFormationStack` so a stranded `cdkd-migrate-tmp/`
|
|
47985
|
+
* object never blocks the calling command — it lives under an obviously
|
|
47986
|
+
* named prefix and can be reaped manually.
|
|
47987
|
+
*/
|
|
47988
|
+
async function runTemplateUploadCleanup(cleanup, bucket) {
|
|
47989
|
+
const logger = getLogger();
|
|
47990
|
+
try {
|
|
47991
|
+
await cleanup();
|
|
47992
|
+
} catch (cleanupErr) {
|
|
47993
|
+
const msg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
|
|
47994
|
+
logger.warn(`Failed to delete temporary template upload from '${bucket}'. Clean up manually under prefix 'cdkd-migrate-tmp/'. Cause: ${msg}`);
|
|
47995
|
+
}
|
|
47996
|
+
}
|
|
47997
|
+
async function executeImportChangeSet(cfnClient, stackName, template, plan, parameters, templateFormat = "json", uploadOpts) {
|
|
47648
47998
|
const logger = getLogger();
|
|
47649
47999
|
const changeSetName = `cdkd-migrate-${Date.now()}`;
|
|
47650
48000
|
const templateBody = stringifyCfnTemplate(template, templateFormat);
|
|
@@ -47654,67 +48004,71 @@ async function executeImportChangeSet(cfnClient, stackName, template, plan, para
|
|
|
47654
48004
|
ResourceIdentifier: entry.resourceIdentifier
|
|
47655
48005
|
}));
|
|
47656
48006
|
logger.info(`Creating IMPORT changeset '${changeSetName}' for stack '${stackName}' (${plan.length} resource(s), ${templateBody.length} bytes)...`);
|
|
47657
|
-
|
|
48007
|
+
const source = await selectChangeSetTemplateSource(template, templateBody, uploadOpts, stackName, "Filtered phase-1 IMPORT", templateFormat);
|
|
47658
48008
|
try {
|
|
47659
|
-
await cfnClient.send(new CreateChangeSetCommand({
|
|
47660
|
-
StackName: stackName,
|
|
47661
|
-
ChangeSetName: changeSetName,
|
|
47662
|
-
ChangeSetType: "IMPORT",
|
|
47663
|
-
TemplateBody: templateBody,
|
|
47664
|
-
ResourcesToImport: resourcesToImport,
|
|
47665
|
-
...parameters.length > 0 && { Parameters: parameters },
|
|
47666
|
-
Capabilities: [
|
|
47667
|
-
"CAPABILITY_IAM",
|
|
47668
|
-
"CAPABILITY_NAMED_IAM",
|
|
47669
|
-
"CAPABILITY_AUTO_EXPAND"
|
|
47670
|
-
]
|
|
47671
|
-
}));
|
|
47672
|
-
} catch (err) {
|
|
47673
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
47674
|
-
throw new Error(`Failed to create IMPORT changeset: ${msg}`);
|
|
47675
|
-
}
|
|
47676
|
-
try {
|
|
47677
|
-
await waitUntilChangeSetCreateComplete({
|
|
47678
|
-
client: cfnClient,
|
|
47679
|
-
maxWaitTime: 600
|
|
47680
|
-
}, {
|
|
47681
|
-
StackName: stackName,
|
|
47682
|
-
ChangeSetName: changeSetName
|
|
47683
|
-
});
|
|
47684
|
-
} catch (err) {
|
|
47685
48009
|
try {
|
|
47686
|
-
|
|
48010
|
+
await cfnClient.send(new CreateChangeSetCommand({
|
|
48011
|
+
StackName: stackName,
|
|
48012
|
+
ChangeSetName: changeSetName,
|
|
48013
|
+
ChangeSetType: "IMPORT",
|
|
48014
|
+
...source.kind === "inline" ? { TemplateBody: source.templateBody } : { TemplateURL: source.templateUrl },
|
|
48015
|
+
ResourcesToImport: resourcesToImport,
|
|
48016
|
+
...parameters.length > 0 && { Parameters: parameters },
|
|
48017
|
+
Capabilities: [
|
|
48018
|
+
"CAPABILITY_IAM",
|
|
48019
|
+
"CAPABILITY_NAMED_IAM",
|
|
48020
|
+
"CAPABILITY_AUTO_EXPAND"
|
|
48021
|
+
]
|
|
48022
|
+
}));
|
|
48023
|
+
} catch (err) {
|
|
48024
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
48025
|
+
throw new Error(`Failed to create IMPORT changeset: ${msg}`);
|
|
48026
|
+
}
|
|
48027
|
+
try {
|
|
48028
|
+
await waitUntilChangeSetCreateComplete({
|
|
48029
|
+
client: cfnClient,
|
|
48030
|
+
maxWaitTime: 600
|
|
48031
|
+
}, {
|
|
47687
48032
|
StackName: stackName,
|
|
47688
48033
|
ChangeSetName: changeSetName
|
|
47689
|
-
})
|
|
48034
|
+
});
|
|
48035
|
+
} catch (err) {
|
|
48036
|
+
try {
|
|
48037
|
+
const reason = (await cfnClient.send(new DescribeChangeSetCommand({
|
|
48038
|
+
StackName: stackName,
|
|
48039
|
+
ChangeSetName: changeSetName
|
|
48040
|
+
}))).StatusReason ?? "unknown";
|
|
48041
|
+
await cfnClient.send(new DeleteChangeSetCommand({
|
|
48042
|
+
StackName: stackName,
|
|
48043
|
+
ChangeSetName: changeSetName
|
|
48044
|
+
})).catch(() => {});
|
|
48045
|
+
throw new Error(`IMPORT changeset FAILED: ${reason}`);
|
|
48046
|
+
} catch (innerErr) {
|
|
48047
|
+
if (innerErr instanceof Error && innerErr.message.startsWith("IMPORT changeset FAILED")) throw innerErr;
|
|
48048
|
+
throw err;
|
|
48049
|
+
}
|
|
48050
|
+
}
|
|
48051
|
+
logger.info(`Executing IMPORT changeset...`);
|
|
48052
|
+
try {
|
|
48053
|
+
await cfnClient.send(new ExecuteChangeSetCommand({
|
|
48054
|
+
StackName: stackName,
|
|
48055
|
+
ChangeSetName: changeSetName
|
|
48056
|
+
}));
|
|
48057
|
+
await waitUntilStackImportComplete({
|
|
48058
|
+
client: cfnClient,
|
|
48059
|
+
maxWaitTime: 3600
|
|
48060
|
+
}, { StackName: stackName });
|
|
48061
|
+
} catch (err) {
|
|
48062
|
+
const failureSummary = await collectImportFailureSummary(cfnClient, stackName).catch(() => "");
|
|
47690
48063
|
await cfnClient.send(new DeleteChangeSetCommand({
|
|
47691
48064
|
StackName: stackName,
|
|
47692
48065
|
ChangeSetName: changeSetName
|
|
47693
48066
|
})).catch(() => {});
|
|
47694
|
-
throw new Error(`IMPORT changeset
|
|
47695
|
-
} catch (innerErr) {
|
|
47696
|
-
if (innerErr instanceof Error && innerErr.message.startsWith("IMPORT changeset FAILED")) throw innerErr;
|
|
48067
|
+
if (failureSummary) throw new Error(`IMPORT changeset failed:\n${failureSummary}`, { cause: err });
|
|
47697
48068
|
throw err;
|
|
47698
48069
|
}
|
|
47699
|
-
}
|
|
47700
|
-
|
|
47701
|
-
try {
|
|
47702
|
-
await cfnClient.send(new ExecuteChangeSetCommand({
|
|
47703
|
-
StackName: stackName,
|
|
47704
|
-
ChangeSetName: changeSetName
|
|
47705
|
-
}));
|
|
47706
|
-
await waitUntilStackImportComplete({
|
|
47707
|
-
client: cfnClient,
|
|
47708
|
-
maxWaitTime: 3600
|
|
47709
|
-
}, { StackName: stackName });
|
|
47710
|
-
} catch (err) {
|
|
47711
|
-
const failureSummary = await collectImportFailureSummary(cfnClient, stackName).catch(() => "");
|
|
47712
|
-
await cfnClient.send(new DeleteChangeSetCommand({
|
|
47713
|
-
StackName: stackName,
|
|
47714
|
-
ChangeSetName: changeSetName
|
|
47715
|
-
})).catch(() => {});
|
|
47716
|
-
if (failureSummary) throw new Error(`IMPORT changeset failed:\n${failureSummary}`, { cause: err });
|
|
47717
|
-
throw err;
|
|
48070
|
+
} finally {
|
|
48071
|
+
await runTemplateUploadCleanup(source.cleanup, uploadOpts?.stateBucket ?? "");
|
|
47718
48072
|
}
|
|
47719
48073
|
}
|
|
47720
48074
|
/**
|
|
@@ -47758,69 +48112,73 @@ async function collectImportFailureSummary(cfnClient, stackName) {
|
|
|
47758
48112
|
* (cdkd state is intentionally NOT deleted between phases, so a phase-2
|
|
47759
48113
|
* failure leaves a recoverable state).
|
|
47760
48114
|
*/
|
|
47761
|
-
async function executeUpdateChangeSet(cfnClient, stackName, template, parameters, templateFormat = "json") {
|
|
48115
|
+
async function executeUpdateChangeSet(cfnClient, stackName, template, parameters, templateFormat = "json", uploadOpts) {
|
|
47762
48116
|
const logger = getLogger();
|
|
47763
48117
|
const changeSetName = `cdkd-phase2-${Date.now()}`;
|
|
47764
48118
|
const templateBody = stringifyCfnTemplate(template, templateFormat);
|
|
47765
|
-
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.`);
|
|
47766
48119
|
logger.info(`Creating UPDATE changeset '${changeSetName}' for phase 2 (${templateBody.length} bytes)...`);
|
|
48120
|
+
const source = await selectChangeSetTemplateSource(template, templateBody, uploadOpts, stackName, "Phase-2 UPDATE", templateFormat);
|
|
47767
48121
|
try {
|
|
47768
|
-
await cfnClient.send(new CreateChangeSetCommand({
|
|
47769
|
-
StackName: stackName,
|
|
47770
|
-
ChangeSetName: changeSetName,
|
|
47771
|
-
ChangeSetType: "UPDATE",
|
|
47772
|
-
TemplateBody: templateBody,
|
|
47773
|
-
...parameters.length > 0 && { Parameters: parameters },
|
|
47774
|
-
Capabilities: [
|
|
47775
|
-
"CAPABILITY_IAM",
|
|
47776
|
-
"CAPABILITY_NAMED_IAM",
|
|
47777
|
-
"CAPABILITY_AUTO_EXPAND"
|
|
47778
|
-
]
|
|
47779
|
-
}));
|
|
47780
|
-
} catch (err) {
|
|
47781
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
47782
|
-
throw new Error(`Failed to create UPDATE changeset: ${msg}`);
|
|
47783
|
-
}
|
|
47784
|
-
try {
|
|
47785
|
-
await waitUntilChangeSetCreateComplete({
|
|
47786
|
-
client: cfnClient,
|
|
47787
|
-
maxWaitTime: 600
|
|
47788
|
-
}, {
|
|
47789
|
-
StackName: stackName,
|
|
47790
|
-
ChangeSetName: changeSetName
|
|
47791
|
-
});
|
|
47792
|
-
} catch (err) {
|
|
47793
48122
|
try {
|
|
47794
|
-
|
|
48123
|
+
await cfnClient.send(new CreateChangeSetCommand({
|
|
48124
|
+
StackName: stackName,
|
|
48125
|
+
ChangeSetName: changeSetName,
|
|
48126
|
+
ChangeSetType: "UPDATE",
|
|
48127
|
+
...source.kind === "inline" ? { TemplateBody: source.templateBody } : { TemplateURL: source.templateUrl },
|
|
48128
|
+
...parameters.length > 0 && { Parameters: parameters },
|
|
48129
|
+
Capabilities: [
|
|
48130
|
+
"CAPABILITY_IAM",
|
|
48131
|
+
"CAPABILITY_NAMED_IAM",
|
|
48132
|
+
"CAPABILITY_AUTO_EXPAND"
|
|
48133
|
+
]
|
|
48134
|
+
}));
|
|
48135
|
+
} catch (err) {
|
|
48136
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
48137
|
+
throw new Error(`Failed to create UPDATE changeset: ${msg}`);
|
|
48138
|
+
}
|
|
48139
|
+
try {
|
|
48140
|
+
await waitUntilChangeSetCreateComplete({
|
|
48141
|
+
client: cfnClient,
|
|
48142
|
+
maxWaitTime: 600
|
|
48143
|
+
}, {
|
|
47795
48144
|
StackName: stackName,
|
|
47796
48145
|
ChangeSetName: changeSetName
|
|
47797
|
-
})
|
|
48146
|
+
});
|
|
48147
|
+
} catch (err) {
|
|
48148
|
+
try {
|
|
48149
|
+
const reason = (await cfnClient.send(new DescribeChangeSetCommand({
|
|
48150
|
+
StackName: stackName,
|
|
48151
|
+
ChangeSetName: changeSetName
|
|
48152
|
+
}))).StatusReason ?? "unknown";
|
|
48153
|
+
await cfnClient.send(new DeleteChangeSetCommand({
|
|
48154
|
+
StackName: stackName,
|
|
48155
|
+
ChangeSetName: changeSetName
|
|
48156
|
+
})).catch(() => {});
|
|
48157
|
+
throw new Error(`UPDATE changeset FAILED: ${reason}`);
|
|
48158
|
+
} catch (innerErr) {
|
|
48159
|
+
if (innerErr instanceof Error && innerErr.message.startsWith("UPDATE changeset FAILED")) throw innerErr;
|
|
48160
|
+
throw err;
|
|
48161
|
+
}
|
|
48162
|
+
}
|
|
48163
|
+
logger.info(`Executing UPDATE changeset...`);
|
|
48164
|
+
try {
|
|
48165
|
+
await cfnClient.send(new ExecuteChangeSetCommand({
|
|
48166
|
+
StackName: stackName,
|
|
48167
|
+
ChangeSetName: changeSetName
|
|
48168
|
+
}));
|
|
48169
|
+
await waitUntilStackUpdateComplete({
|
|
48170
|
+
client: cfnClient,
|
|
48171
|
+
maxWaitTime: 3600
|
|
48172
|
+
}, { StackName: stackName });
|
|
48173
|
+
} catch (err) {
|
|
47798
48174
|
await cfnClient.send(new DeleteChangeSetCommand({
|
|
47799
48175
|
StackName: stackName,
|
|
47800
48176
|
ChangeSetName: changeSetName
|
|
47801
48177
|
})).catch(() => {});
|
|
47802
|
-
throw new Error(`UPDATE changeset FAILED: ${reason}`);
|
|
47803
|
-
} catch (innerErr) {
|
|
47804
|
-
if (innerErr instanceof Error && innerErr.message.startsWith("UPDATE changeset FAILED")) throw innerErr;
|
|
47805
48178
|
throw err;
|
|
47806
48179
|
}
|
|
47807
|
-
}
|
|
47808
|
-
|
|
47809
|
-
try {
|
|
47810
|
-
await cfnClient.send(new ExecuteChangeSetCommand({
|
|
47811
|
-
StackName: stackName,
|
|
47812
|
-
ChangeSetName: changeSetName
|
|
47813
|
-
}));
|
|
47814
|
-
await waitUntilStackUpdateComplete({
|
|
47815
|
-
client: cfnClient,
|
|
47816
|
-
maxWaitTime: 3600
|
|
47817
|
-
}, { StackName: stackName });
|
|
47818
|
-
} catch (err) {
|
|
47819
|
-
await cfnClient.send(new DeleteChangeSetCommand({
|
|
47820
|
-
StackName: stackName,
|
|
47821
|
-
ChangeSetName: changeSetName
|
|
47822
|
-
})).catch(() => {});
|
|
47823
|
-
throw err;
|
|
48180
|
+
} finally {
|
|
48181
|
+
await runTemplateUploadCleanup(source.cleanup, uploadOpts?.stateBucket ?? "");
|
|
47824
48182
|
}
|
|
47825
48183
|
}
|
|
47826
48184
|
/**
|
|
@@ -47939,7 +48297,7 @@ function reorderArgs(argv) {
|
|
|
47939
48297
|
*/
|
|
47940
48298
|
async function main() {
|
|
47941
48299
|
const program = new Command();
|
|
47942
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
48300
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.128.0");
|
|
47943
48301
|
program.addCommand(createBootstrapCommand());
|
|
47944
48302
|
program.addCommand(createSynthCommand());
|
|
47945
48303
|
program.addCommand(createListCommand());
|