@go-to-k/cdkd 0.126.0 → 0.127.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 +342 -152
- 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
|
|
@@ -47011,7 +47110,10 @@ async function exportCommand(stackArg, options) {
|
|
|
47011
47110
|
const phase1Template = filterTemplateForImport(template, phase1Imports);
|
|
47012
47111
|
const injectedCount = injectDeletionPolicyForImport(phase1Template);
|
|
47013
47112
|
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
|
|
47113
|
+
await executeImportChangeSet(awsClients.cloudFormation, cfnStackName, phase1Template, phase1Imports, cfnParameters, templateFormat, {
|
|
47114
|
+
stateBucket,
|
|
47115
|
+
...options.profile && { s3ClientOpts: { profile: options.profile } }
|
|
47116
|
+
});
|
|
47015
47117
|
logger.info(`✓ Phase 1: CloudFormation stack '${cfnStackName}' created via IMPORT. ${phase1Imports.length} resource(s) imported.`);
|
|
47016
47118
|
if (recreateBeforePhase2.length > 0) for (const entry of recreateBeforePhase2) {
|
|
47017
47119
|
const handler = PRE_DELETE_HANDLERS[entry.resourceType];
|
|
@@ -47028,7 +47130,10 @@ async function exportCommand(stackArg, options) {
|
|
|
47028
47130
|
const phase2Count = phase2Creates.length + recreateBeforePhase2.length;
|
|
47029
47131
|
if (phase2Count > 0) try {
|
|
47030
47132
|
const phase2Template = applyImportOverlayForPhase2(template, phase1Imports);
|
|
47031
|
-
await executeUpdateChangeSet(awsClients.cloudFormation, cfnStackName, phase2Template, cfnParameters, templateFormat
|
|
47133
|
+
await executeUpdateChangeSet(awsClients.cloudFormation, cfnStackName, phase2Template, cfnParameters, templateFormat, {
|
|
47134
|
+
stateBucket,
|
|
47135
|
+
...options.profile && { s3ClientOpts: { profile: options.profile } }
|
|
47136
|
+
});
|
|
47032
47137
|
const parts = [];
|
|
47033
47138
|
if (phase2Creates.length > 0) parts.push(`${phase2Creates.length} non-importable resource(s) CREATEd`);
|
|
47034
47139
|
if (recreateBeforePhase2.length > 0) parts.push(`${recreateBeforePhase2.length} IMPORT-unsupported resource(s) re-CREATEd`);
|
|
@@ -47644,7 +47749,84 @@ function printPlan(plan, cfnStackName) {
|
|
|
47644
47749
|
}
|
|
47645
47750
|
logger.info("");
|
|
47646
47751
|
}
|
|
47647
|
-
|
|
47752
|
+
/**
|
|
47753
|
+
* Decide whether to submit a CFn changeset's template inline via
|
|
47754
|
+
* `TemplateBody` or upload it to the cdkd state bucket and submit via
|
|
47755
|
+
* `TemplateURL`. Returns a discriminated outcome:
|
|
47756
|
+
*
|
|
47757
|
+
* - `kind: 'inline'` — payload <= 51,200 bytes; pass `TemplateBody`
|
|
47758
|
+
* directly. No S3 round-trip; the caller's `finally` cleanup is a
|
|
47759
|
+
* no-op.
|
|
47760
|
+
* - `kind: 'url'` — payload in (51,200, 1,048,576] bytes; helper
|
|
47761
|
+
* uploaded to `cdkd-migrate-tmp/<stackName>/<ts>.{json,yaml}`. The
|
|
47762
|
+
* caller MUST invoke the returned `cleanup` in a `finally` so the
|
|
47763
|
+
* transient object is deleted regardless of CFn success / failure.
|
|
47764
|
+
*
|
|
47765
|
+
* Payloads > 1,048,576 bytes throw pre-flight (the 1 MB ceiling applies
|
|
47766
|
+
* to every CFn API surface; no S3 indirection helps). The error names the
|
|
47767
|
+
* top inline-payload contributors so the user knows what to shrink.
|
|
47768
|
+
*
|
|
47769
|
+
* `phaseLabel` is interpolated into the pre-flight error so the user
|
|
47770
|
+
* sees which phase tripped the ceiling ("phase-1 IMPORT" vs "phase-2
|
|
47771
|
+
* UPDATE").
|
|
47772
|
+
*
|
|
47773
|
+
* When `uploadOpts` is undefined (e.g. unit tests that exercise only the
|
|
47774
|
+
* inline path), templates over the inline limit throw with a clear
|
|
47775
|
+
* "no upload bucket configured" error rather than silently failing on a
|
|
47776
|
+
* downstream CFn rejection.
|
|
47777
|
+
*
|
|
47778
|
+
* `templateFormat` (default `'json'`) drives the transient S3 object's
|
|
47779
|
+
* key suffix + Content-Type so YAML-authored templates stay YAML on the
|
|
47780
|
+
* wire (`.yaml` / `application/x-yaml`).
|
|
47781
|
+
*/
|
|
47782
|
+
async function selectChangeSetTemplateSource(template, templateBody, uploadOpts, stackName, phaseLabel, templateFormat = "json") {
|
|
47783
|
+
const logger = getLogger();
|
|
47784
|
+
if (templateBody.length <= 51200) return {
|
|
47785
|
+
kind: "inline",
|
|
47786
|
+
templateBody,
|
|
47787
|
+
cleanup: async () => void 0
|
|
47788
|
+
};
|
|
47789
|
+
if (templateBody.length > 1048576) {
|
|
47790
|
+
const offenders = findLargeInlineResources(template);
|
|
47791
|
+
let detail = "";
|
|
47792
|
+
if (offenders.length > 0) {
|
|
47793
|
+
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")}`;
|
|
47794
|
+
if (offenders.length > 10) detail += `\n (and ${offenders.length - 10} more above the 4096-byte threshold)`;
|
|
47795
|
+
}
|
|
47796
|
+
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}`);
|
|
47797
|
+
}
|
|
47798
|
+
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.`);
|
|
47799
|
+
logger.info(` Template is ${templateBody.length} bytes (over ${CFN_TEMPLATE_BODY_LIMIT} inline limit) — uploading to state bucket '${uploadOpts.stateBucket}'.`);
|
|
47800
|
+
const uploaded = await uploadCfnTemplate({
|
|
47801
|
+
bucket: uploadOpts.stateBucket,
|
|
47802
|
+
body: templateBody,
|
|
47803
|
+
stackName,
|
|
47804
|
+
format: templateFormat,
|
|
47805
|
+
...uploadOpts.s3ClientOpts && { s3ClientOpts: uploadOpts.s3ClientOpts }
|
|
47806
|
+
});
|
|
47807
|
+
return {
|
|
47808
|
+
kind: "url",
|
|
47809
|
+
templateUrl: uploaded.url,
|
|
47810
|
+
cleanup: uploaded.cleanup
|
|
47811
|
+
};
|
|
47812
|
+
}
|
|
47813
|
+
/**
|
|
47814
|
+
* Best-effort wrapper around the `cleanup` callback returned by
|
|
47815
|
+
* {@link selectChangeSetTemplateSource}. Mirrors the warn-on-failure
|
|
47816
|
+
* pattern from `retireCloudFormationStack` so a stranded `cdkd-migrate-tmp/`
|
|
47817
|
+
* object never blocks the calling command — it lives under an obviously
|
|
47818
|
+
* named prefix and can be reaped manually.
|
|
47819
|
+
*/
|
|
47820
|
+
async function runTemplateUploadCleanup(cleanup, bucket) {
|
|
47821
|
+
const logger = getLogger();
|
|
47822
|
+
try {
|
|
47823
|
+
await cleanup();
|
|
47824
|
+
} catch (cleanupErr) {
|
|
47825
|
+
const msg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
|
|
47826
|
+
logger.warn(`Failed to delete temporary template upload from '${bucket}'. Clean up manually under prefix 'cdkd-migrate-tmp/'. Cause: ${msg}`);
|
|
47827
|
+
}
|
|
47828
|
+
}
|
|
47829
|
+
async function executeImportChangeSet(cfnClient, stackName, template, plan, parameters, templateFormat = "json", uploadOpts) {
|
|
47648
47830
|
const logger = getLogger();
|
|
47649
47831
|
const changeSetName = `cdkd-migrate-${Date.now()}`;
|
|
47650
47832
|
const templateBody = stringifyCfnTemplate(template, templateFormat);
|
|
@@ -47654,67 +47836,71 @@ async function executeImportChangeSet(cfnClient, stackName, template, plan, para
|
|
|
47654
47836
|
ResourceIdentifier: entry.resourceIdentifier
|
|
47655
47837
|
}));
|
|
47656
47838
|
logger.info(`Creating IMPORT changeset '${changeSetName}' for stack '${stackName}' (${plan.length} resource(s), ${templateBody.length} bytes)...`);
|
|
47657
|
-
|
|
47658
|
-
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
|
-
}
|
|
47839
|
+
const source = await selectChangeSetTemplateSource(template, templateBody, uploadOpts, stackName, "Filtered phase-1 IMPORT", templateFormat);
|
|
47676
47840
|
try {
|
|
47677
|
-
await waitUntilChangeSetCreateComplete({
|
|
47678
|
-
client: cfnClient,
|
|
47679
|
-
maxWaitTime: 600
|
|
47680
|
-
}, {
|
|
47681
|
-
StackName: stackName,
|
|
47682
|
-
ChangeSetName: changeSetName
|
|
47683
|
-
});
|
|
47684
|
-
} catch (err) {
|
|
47685
47841
|
try {
|
|
47686
|
-
|
|
47842
|
+
await cfnClient.send(new CreateChangeSetCommand({
|
|
47843
|
+
StackName: stackName,
|
|
47844
|
+
ChangeSetName: changeSetName,
|
|
47845
|
+
ChangeSetType: "IMPORT",
|
|
47846
|
+
...source.kind === "inline" ? { TemplateBody: source.templateBody } : { TemplateURL: source.templateUrl },
|
|
47847
|
+
ResourcesToImport: resourcesToImport,
|
|
47848
|
+
...parameters.length > 0 && { Parameters: parameters },
|
|
47849
|
+
Capabilities: [
|
|
47850
|
+
"CAPABILITY_IAM",
|
|
47851
|
+
"CAPABILITY_NAMED_IAM",
|
|
47852
|
+
"CAPABILITY_AUTO_EXPAND"
|
|
47853
|
+
]
|
|
47854
|
+
}));
|
|
47855
|
+
} catch (err) {
|
|
47856
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
47857
|
+
throw new Error(`Failed to create IMPORT changeset: ${msg}`);
|
|
47858
|
+
}
|
|
47859
|
+
try {
|
|
47860
|
+
await waitUntilChangeSetCreateComplete({
|
|
47861
|
+
client: cfnClient,
|
|
47862
|
+
maxWaitTime: 600
|
|
47863
|
+
}, {
|
|
47864
|
+
StackName: stackName,
|
|
47865
|
+
ChangeSetName: changeSetName
|
|
47866
|
+
});
|
|
47867
|
+
} catch (err) {
|
|
47868
|
+
try {
|
|
47869
|
+
const reason = (await cfnClient.send(new DescribeChangeSetCommand({
|
|
47870
|
+
StackName: stackName,
|
|
47871
|
+
ChangeSetName: changeSetName
|
|
47872
|
+
}))).StatusReason ?? "unknown";
|
|
47873
|
+
await cfnClient.send(new DeleteChangeSetCommand({
|
|
47874
|
+
StackName: stackName,
|
|
47875
|
+
ChangeSetName: changeSetName
|
|
47876
|
+
})).catch(() => {});
|
|
47877
|
+
throw new Error(`IMPORT changeset FAILED: ${reason}`);
|
|
47878
|
+
} catch (innerErr) {
|
|
47879
|
+
if (innerErr instanceof Error && innerErr.message.startsWith("IMPORT changeset FAILED")) throw innerErr;
|
|
47880
|
+
throw err;
|
|
47881
|
+
}
|
|
47882
|
+
}
|
|
47883
|
+
logger.info(`Executing IMPORT changeset...`);
|
|
47884
|
+
try {
|
|
47885
|
+
await cfnClient.send(new ExecuteChangeSetCommand({
|
|
47687
47886
|
StackName: stackName,
|
|
47688
47887
|
ChangeSetName: changeSetName
|
|
47689
|
-
}))
|
|
47888
|
+
}));
|
|
47889
|
+
await waitUntilStackImportComplete({
|
|
47890
|
+
client: cfnClient,
|
|
47891
|
+
maxWaitTime: 3600
|
|
47892
|
+
}, { StackName: stackName });
|
|
47893
|
+
} catch (err) {
|
|
47894
|
+
const failureSummary = await collectImportFailureSummary(cfnClient, stackName).catch(() => "");
|
|
47690
47895
|
await cfnClient.send(new DeleteChangeSetCommand({
|
|
47691
47896
|
StackName: stackName,
|
|
47692
47897
|
ChangeSetName: changeSetName
|
|
47693
47898
|
})).catch(() => {});
|
|
47694
|
-
throw new Error(`IMPORT changeset
|
|
47695
|
-
} catch (innerErr) {
|
|
47696
|
-
if (innerErr instanceof Error && innerErr.message.startsWith("IMPORT changeset FAILED")) throw innerErr;
|
|
47899
|
+
if (failureSummary) throw new Error(`IMPORT changeset failed:\n${failureSummary}`, { cause: err });
|
|
47697
47900
|
throw err;
|
|
47698
47901
|
}
|
|
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;
|
|
47902
|
+
} finally {
|
|
47903
|
+
await runTemplateUploadCleanup(source.cleanup, uploadOpts?.stateBucket ?? "");
|
|
47718
47904
|
}
|
|
47719
47905
|
}
|
|
47720
47906
|
/**
|
|
@@ -47758,69 +47944,73 @@ async function collectImportFailureSummary(cfnClient, stackName) {
|
|
|
47758
47944
|
* (cdkd state is intentionally NOT deleted between phases, so a phase-2
|
|
47759
47945
|
* failure leaves a recoverable state).
|
|
47760
47946
|
*/
|
|
47761
|
-
async function executeUpdateChangeSet(cfnClient, stackName, template, parameters, templateFormat = "json") {
|
|
47947
|
+
async function executeUpdateChangeSet(cfnClient, stackName, template, parameters, templateFormat = "json", uploadOpts) {
|
|
47762
47948
|
const logger = getLogger();
|
|
47763
47949
|
const changeSetName = `cdkd-phase2-${Date.now()}`;
|
|
47764
47950
|
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
47951
|
logger.info(`Creating UPDATE changeset '${changeSetName}' for phase 2 (${templateBody.length} bytes)...`);
|
|
47952
|
+
const source = await selectChangeSetTemplateSource(template, templateBody, uploadOpts, stackName, "Phase-2 UPDATE", templateFormat);
|
|
47767
47953
|
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
47954
|
try {
|
|
47794
|
-
|
|
47955
|
+
await cfnClient.send(new CreateChangeSetCommand({
|
|
47956
|
+
StackName: stackName,
|
|
47957
|
+
ChangeSetName: changeSetName,
|
|
47958
|
+
ChangeSetType: "UPDATE",
|
|
47959
|
+
...source.kind === "inline" ? { TemplateBody: source.templateBody } : { TemplateURL: source.templateUrl },
|
|
47960
|
+
...parameters.length > 0 && { Parameters: parameters },
|
|
47961
|
+
Capabilities: [
|
|
47962
|
+
"CAPABILITY_IAM",
|
|
47963
|
+
"CAPABILITY_NAMED_IAM",
|
|
47964
|
+
"CAPABILITY_AUTO_EXPAND"
|
|
47965
|
+
]
|
|
47966
|
+
}));
|
|
47967
|
+
} catch (err) {
|
|
47968
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
47969
|
+
throw new Error(`Failed to create UPDATE changeset: ${msg}`);
|
|
47970
|
+
}
|
|
47971
|
+
try {
|
|
47972
|
+
await waitUntilChangeSetCreateComplete({
|
|
47973
|
+
client: cfnClient,
|
|
47974
|
+
maxWaitTime: 600
|
|
47975
|
+
}, {
|
|
47976
|
+
StackName: stackName,
|
|
47977
|
+
ChangeSetName: changeSetName
|
|
47978
|
+
});
|
|
47979
|
+
} catch (err) {
|
|
47980
|
+
try {
|
|
47981
|
+
const reason = (await cfnClient.send(new DescribeChangeSetCommand({
|
|
47982
|
+
StackName: stackName,
|
|
47983
|
+
ChangeSetName: changeSetName
|
|
47984
|
+
}))).StatusReason ?? "unknown";
|
|
47985
|
+
await cfnClient.send(new DeleteChangeSetCommand({
|
|
47986
|
+
StackName: stackName,
|
|
47987
|
+
ChangeSetName: changeSetName
|
|
47988
|
+
})).catch(() => {});
|
|
47989
|
+
throw new Error(`UPDATE changeset FAILED: ${reason}`);
|
|
47990
|
+
} catch (innerErr) {
|
|
47991
|
+
if (innerErr instanceof Error && innerErr.message.startsWith("UPDATE changeset FAILED")) throw innerErr;
|
|
47992
|
+
throw err;
|
|
47993
|
+
}
|
|
47994
|
+
}
|
|
47995
|
+
logger.info(`Executing UPDATE changeset...`);
|
|
47996
|
+
try {
|
|
47997
|
+
await cfnClient.send(new ExecuteChangeSetCommand({
|
|
47795
47998
|
StackName: stackName,
|
|
47796
47999
|
ChangeSetName: changeSetName
|
|
47797
|
-
}))
|
|
48000
|
+
}));
|
|
48001
|
+
await waitUntilStackUpdateComplete({
|
|
48002
|
+
client: cfnClient,
|
|
48003
|
+
maxWaitTime: 3600
|
|
48004
|
+
}, { StackName: stackName });
|
|
48005
|
+
} catch (err) {
|
|
47798
48006
|
await cfnClient.send(new DeleteChangeSetCommand({
|
|
47799
48007
|
StackName: stackName,
|
|
47800
48008
|
ChangeSetName: changeSetName
|
|
47801
48009
|
})).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
48010
|
throw err;
|
|
47806
48011
|
}
|
|
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;
|
|
48012
|
+
} finally {
|
|
48013
|
+
await runTemplateUploadCleanup(source.cleanup, uploadOpts?.stateBucket ?? "");
|
|
47824
48014
|
}
|
|
47825
48015
|
}
|
|
47826
48016
|
/**
|
|
@@ -47939,7 +48129,7 @@ function reorderArgs(argv) {
|
|
|
47939
48129
|
*/
|
|
47940
48130
|
async function main() {
|
|
47941
48131
|
const program = new Command();
|
|
47942
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
48132
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.127.0");
|
|
47943
48133
|
program.addCommand(createBootstrapCommand());
|
|
47944
48134
|
program.addCommand(createSynthCommand());
|
|
47945
48135
|
program.addCommand(createListCommand());
|