@go-to-k/cdkd 0.135.0 → 0.136.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 +100 -190
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-CX1x5ug1.js → deploy-engine-DZYchTh6.js} +663 -3
- package/dist/deploy-engine-DZYchTh6.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-CX1x5ug1.js.map +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { a as runDockerStreaming, c as getLogger, d as getLiveRenderer, g as generateResourceNameWithFallback, m as applyDefaultNameForFallback, n as formatDockerLoginError, o as spawnStreaming, r as getDockerCmd, v as withStackName } from "./docker-cmd-EtWSTAje.js";
|
|
2
2
|
import { r as getAwsClients } from "./aws-clients-BF03Alpe.js";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { DeleteObjectCommand, GetBucketLocationCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectsV2Command, NoSuchKey, PutObjectCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
|
|
4
5
|
import { CloudControlClient, CreateResourceCommand, DeleteResourceCommand, GetResourceCommand, GetResourceRequestStatusCommand, ListResourcesCommand, UpdateResourceCommand } from "@aws-sdk/client-cloudcontrol";
|
|
5
6
|
import { AttachRolePolicyCommand, CreateRoleCommand, DeleteRoleCommand, DeleteRolePermissionsBoundaryCommand, DeleteRolePolicyCommand, DetachRolePolicyCommand, GetRoleCommand, GetRolePolicyCommand, IAMClient, ListAttachedRolePoliciesCommand, ListInstanceProfilesForRoleCommand, ListRolePoliciesCommand, ListRoleTagsCommand, ListRolesCommand, NoSuchEntityException, PutRolePermissionsBoundaryCommand, PutRolePolicyCommand, RemoveRoleFromInstanceProfileCommand, TagRoleCommand, UntagRoleCommand, UpdateAssumeRolePolicyCommand, UpdateRoleCommand } from "@aws-sdk/client-iam";
|
|
@@ -8,6 +9,7 @@ import { GetFunctionUrlConfigCommand, InvokeCommand, LambdaClient, waitUntilFunc
|
|
|
8
9
|
import { AssumeRoleCommand, GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
|
|
9
10
|
import { DescribeAvailabilityZonesCommand, DescribeImagesCommand, DescribeLaunchTemplatesCommand, DescribeRouteTablesCommand, DescribeSecurityGroupsCommand, DescribeSubnetsCommand, DescribeVpcsCommand, DescribeVpnGatewaysCommand, EC2Client } from "@aws-sdk/client-ec2";
|
|
10
11
|
import { DescribeTableCommand } from "@aws-sdk/client-dynamodb";
|
|
12
|
+
import { CloudFormationClient, CreateChangeSetCommand, DeleteStackCommand, DescribeChangeSetCommand, GetTemplateCommand, waitUntilChangeSetCreateComplete } from "@aws-sdk/client-cloudformation";
|
|
11
13
|
import { GetRestApiCommand } from "@aws-sdk/client-api-gateway";
|
|
12
14
|
import { GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";
|
|
13
15
|
import { GetParameterCommand, SSMClient } from "@aws-sdk/client-ssm";
|
|
@@ -388,6 +390,42 @@ var LocalMigrateError = class LocalMigrateError extends CdkdError {
|
|
|
388
390
|
}
|
|
389
391
|
};
|
|
390
392
|
/**
|
|
393
|
+
* CloudFormation macro / `Fn::Transform` expansion failure (#463).
|
|
394
|
+
*
|
|
395
|
+
* cdkd hands templates that declare `Transform: [...]` (or carry
|
|
396
|
+
* `Fn::Transform: {...}` snippets) to CloudFormation server-side via a
|
|
397
|
+
* transient `CreateChangeSet --change-set-type CREATE` against a
|
|
398
|
+
* `cdkd-macro-expand-<id>` stack name. This error wraps every failure
|
|
399
|
+
* mode of that round-trip:
|
|
400
|
+
*
|
|
401
|
+
* - `CreateChangeSet` rejection (bad template, missing macro IAM
|
|
402
|
+
* permission, custom macro not found in the account).
|
|
403
|
+
* - Changeset settles in `FAILED` (`StatusReason` from CFn is
|
|
404
|
+
* surfaced verbatim — typically a custom macro Lambda error).
|
|
405
|
+
* - Waiter timeout (the macro Lambda is stuck or oversized).
|
|
406
|
+
* - `GetTemplate --template-stage Processed` returns no body (would
|
|
407
|
+
* indicate a CFn-side regression — fail loud rather than silently
|
|
408
|
+
* proceed with the un-expanded template).
|
|
409
|
+
* - Multi-stage detection: the expanded template still contains
|
|
410
|
+
* macros, which cdkd v1 does not support (the design intentionally
|
|
411
|
+
* rejects this so a second round-trip is not silently triggered).
|
|
412
|
+
*
|
|
413
|
+
* The error surfaces at exit code 2 (partial-failure family) — the
|
|
414
|
+
* cleanup `finally` in the expander always runs `DeleteChangeSet` +
|
|
415
|
+
* `DeleteStack` regardless of this error firing, so a failed
|
|
416
|
+
* expansion never leaves a transient CFn stack behind in a routine
|
|
417
|
+
* case. The user can re-run `cdkd deploy` once the upstream cause is
|
|
418
|
+
* fixed.
|
|
419
|
+
*/
|
|
420
|
+
var MacroExpansionError = class MacroExpansionError extends CdkdError {
|
|
421
|
+
exitCode = 2;
|
|
422
|
+
constructor(message, cause) {
|
|
423
|
+
super(message, "MACRO_EXPANSION_ERROR", cause);
|
|
424
|
+
this.name = "MacroExpansionError";
|
|
425
|
+
Object.setPrototypeOf(this, MacroExpansionError.prototype);
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
/**
|
|
391
429
|
* Check if error is a cdkd error
|
|
392
430
|
*/
|
|
393
431
|
function isCdkdError(error) {
|
|
@@ -421,7 +459,8 @@ function handleError(error) {
|
|
|
421
459
|
const logger = getLogger();
|
|
422
460
|
if (!(error instanceof CdkdError && error.silent)) logger.error(formatError(error));
|
|
423
461
|
if (error instanceof Error && error.stack) logger.debug("Stack trace:", error.stack);
|
|
424
|
-
const
|
|
462
|
+
const customExitCode = error instanceof CdkdError ? error.exitCode : void 0;
|
|
463
|
+
const exitCode = typeof customExitCode === "number" ? customExitCode : 1;
|
|
425
464
|
process.exit(exitCode);
|
|
426
465
|
}
|
|
427
466
|
/**
|
|
@@ -1593,6 +1632,567 @@ var ContextProviderRegistry = class {
|
|
|
1593
1632
|
}
|
|
1594
1633
|
};
|
|
1595
1634
|
|
|
1635
|
+
//#endregion
|
|
1636
|
+
//#region src/synthesis/macro-detector.ts
|
|
1637
|
+
/**
|
|
1638
|
+
* CloudFormation macro detection (Issue #463 Phase 1).
|
|
1639
|
+
*
|
|
1640
|
+
* Pure-functional helpers that determine whether a synth template uses
|
|
1641
|
+
* any CloudFormation transform (top-level `Transform: [...]` or
|
|
1642
|
+
* snippet-level `Fn::Transform: {...}` blocks). cdkd's analyzer /
|
|
1643
|
+
* provisioner pipeline does NOT understand `Fn::Transform` — the
|
|
1644
|
+
* resolver has no handler and the DAG builder cannot extract refs
|
|
1645
|
+
* buried inside an unexpanded macro snippet — so a template that
|
|
1646
|
+
* triggers `containsMacro` must be handed to CloudFormation for
|
|
1647
|
+
* server-side expansion via {@link import('./macro-expander.js')}
|
|
1648
|
+
* before the rest of the pipeline can safely consume it.
|
|
1649
|
+
*
|
|
1650
|
+
* Design: [docs/design/463-cfn-macros.md](../../docs/design/463-cfn-macros.md).
|
|
1651
|
+
*/
|
|
1652
|
+
/**
|
|
1653
|
+
* Returns true when the given template uses any CloudFormation
|
|
1654
|
+
* transform. Tolerates malformed inputs (null / non-object / missing
|
|
1655
|
+
* `Resources`) by returning `false` so the rest of the synthesis
|
|
1656
|
+
* pipeline surfaces the malformed-template error rather than the
|
|
1657
|
+
* detector silently throwing.
|
|
1658
|
+
*
|
|
1659
|
+
* Detection rule:
|
|
1660
|
+
* - `template.Transform` is set (string OR array form).
|
|
1661
|
+
* - OR a recursive walk over `Resources` / `Outputs` / `Mappings` /
|
|
1662
|
+
* `Conditions` / `Rules` finds any `{Fn::Transform: {...}}` key.
|
|
1663
|
+
*
|
|
1664
|
+
* The walk does NOT descend into `Metadata` blocks: CloudFormation
|
|
1665
|
+
* does not expand transforms inside metadata (the field is preserved
|
|
1666
|
+
* verbatim to AWS), so a `Fn::Transform` literally appearing under
|
|
1667
|
+
* `Metadata` is not a real macro reference and we must not surface it
|
|
1668
|
+
* as such.
|
|
1669
|
+
*/
|
|
1670
|
+
function containsMacro(template) {
|
|
1671
|
+
if (!template || typeof template !== "object" || Array.isArray(template)) return false;
|
|
1672
|
+
const t = template;
|
|
1673
|
+
if (hasTopLevelTransform(t)) return true;
|
|
1674
|
+
for (const section of [
|
|
1675
|
+
"Resources",
|
|
1676
|
+
"Outputs",
|
|
1677
|
+
"Mappings",
|
|
1678
|
+
"Conditions",
|
|
1679
|
+
"Rules"
|
|
1680
|
+
]) {
|
|
1681
|
+
const sub = t[section];
|
|
1682
|
+
if (sub && typeof sub === "object" && hasFnTransformDeep(sub)) return true;
|
|
1683
|
+
}
|
|
1684
|
+
return false;
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Returns every transform name declared in `Transform` plus the names
|
|
1688
|
+
* referenced via `Fn::Transform`, deduplicated and in encounter order.
|
|
1689
|
+
* Used for telemetry / UX (e.g. logging which transforms are about to
|
|
1690
|
+
* be expanded). Malformed entries (non-string Transform names, missing
|
|
1691
|
+
* `Name` field on Fn::Transform) are skipped silently — they would be
|
|
1692
|
+
* surfaced as a clear error by CloudFormation at expansion time.
|
|
1693
|
+
*
|
|
1694
|
+
* The walk follows the same rules as {@link containsMacro}: it descends
|
|
1695
|
+
* into `Resources` / `Outputs` / `Mappings` / `Conditions` / `Rules`
|
|
1696
|
+
* but NOT into `Metadata`.
|
|
1697
|
+
*/
|
|
1698
|
+
function enumerateMacros(template) {
|
|
1699
|
+
if (!template || typeof template !== "object" || Array.isArray(template)) return [];
|
|
1700
|
+
const t = template;
|
|
1701
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1702
|
+
const result = [];
|
|
1703
|
+
const top = t["Transform"];
|
|
1704
|
+
if (typeof top === "string") pushName(top, seen, result);
|
|
1705
|
+
else if (Array.isArray(top)) {
|
|
1706
|
+
for (const entry of top) if (typeof entry === "string") pushName(entry, seen, result);
|
|
1707
|
+
else if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
1708
|
+
const name = entry["Name"];
|
|
1709
|
+
if (typeof name === "string") pushName(name, seen, result);
|
|
1710
|
+
}
|
|
1711
|
+
} else if (top && typeof top === "object" && !Array.isArray(top)) {
|
|
1712
|
+
const name = top["Name"];
|
|
1713
|
+
if (typeof name === "string") pushName(name, seen, result);
|
|
1714
|
+
}
|
|
1715
|
+
for (const section of [
|
|
1716
|
+
"Resources",
|
|
1717
|
+
"Outputs",
|
|
1718
|
+
"Mappings",
|
|
1719
|
+
"Conditions",
|
|
1720
|
+
"Rules"
|
|
1721
|
+
]) {
|
|
1722
|
+
const sub = t[section];
|
|
1723
|
+
if (sub && typeof sub === "object") collectFnTransformNames(sub, seen, result);
|
|
1724
|
+
}
|
|
1725
|
+
return result;
|
|
1726
|
+
}
|
|
1727
|
+
function pushName(name, seen, out) {
|
|
1728
|
+
if (seen.has(name)) return;
|
|
1729
|
+
seen.add(name);
|
|
1730
|
+
out.push(name);
|
|
1731
|
+
}
|
|
1732
|
+
function hasTopLevelTransform(t) {
|
|
1733
|
+
const top = t["Transform"];
|
|
1734
|
+
if (top === void 0 || top === null) return false;
|
|
1735
|
+
if (Array.isArray(top) && top.length === 0) return false;
|
|
1736
|
+
return true;
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Recursively walk the given value and return true if any nested
|
|
1740
|
+
* object carries a `Fn::Transform` key (with a non-null value — a
|
|
1741
|
+
* literal `Fn::Transform: null` is not a real macro reference).
|
|
1742
|
+
*
|
|
1743
|
+
* Does NOT descend into `Metadata` keys at any depth (CFn does not
|
|
1744
|
+
* expand transforms inside metadata blocks, so a `Fn::Transform`
|
|
1745
|
+
* literally appearing there must not trigger expansion). The
|
|
1746
|
+
* `Metadata` exclusion mirrors the same rule applied at the
|
|
1747
|
+
* top-level section walk in {@link containsMacro}.
|
|
1748
|
+
*/
|
|
1749
|
+
function hasFnTransformDeep(value) {
|
|
1750
|
+
if (!value || typeof value !== "object") return false;
|
|
1751
|
+
if (Array.isArray(value)) {
|
|
1752
|
+
for (const item of value) if (hasFnTransformDeep(item)) return true;
|
|
1753
|
+
return false;
|
|
1754
|
+
}
|
|
1755
|
+
const obj = value;
|
|
1756
|
+
if (obj["Fn::Transform"] !== void 0 && obj["Fn::Transform"] !== null) return true;
|
|
1757
|
+
for (const [key, sub] of Object.entries(obj)) {
|
|
1758
|
+
if (key === "Metadata") continue;
|
|
1759
|
+
if (hasFnTransformDeep(sub)) return true;
|
|
1760
|
+
}
|
|
1761
|
+
return false;
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Recursively walk and collect every `Fn::Transform.Name` value into
|
|
1765
|
+
* the provided dedup set / order-preserving array. Sibling to
|
|
1766
|
+
* {@link hasFnTransformDeep}; same `Metadata` exclusion.
|
|
1767
|
+
*/
|
|
1768
|
+
function collectFnTransformNames(value, seen, out) {
|
|
1769
|
+
if (!value || typeof value !== "object") return;
|
|
1770
|
+
if (Array.isArray(value)) {
|
|
1771
|
+
for (const item of value) collectFnTransformNames(item, seen, out);
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
const obj = value;
|
|
1775
|
+
const fnT = obj["Fn::Transform"];
|
|
1776
|
+
if (fnT && typeof fnT === "object" && !Array.isArray(fnT)) {
|
|
1777
|
+
const name = fnT["Name"];
|
|
1778
|
+
if (typeof name === "string") pushName(name, seen, out);
|
|
1779
|
+
}
|
|
1780
|
+
for (const [key, sub] of Object.entries(obj)) {
|
|
1781
|
+
if (key === "Metadata") continue;
|
|
1782
|
+
collectFnTransformNames(sub, seen, out);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
//#endregion
|
|
1787
|
+
//#region src/cli/upload-cfn-template.ts
|
|
1788
|
+
/**
|
|
1789
|
+
* CloudFormation `TemplateBody` hard limit (51,200 bytes). Templates larger
|
|
1790
|
+
* than this cannot be submitted inline and must be uploaded to S3 and
|
|
1791
|
+
* referenced via `TemplateURL` instead — see {@link uploadCfnTemplate}.
|
|
1792
|
+
*/
|
|
1793
|
+
const CFN_TEMPLATE_BODY_LIMIT = 51200;
|
|
1794
|
+
/**
|
|
1795
|
+
* CloudFormation `TemplateURL` hard limit (1 MB / 1,048,576 bytes).
|
|
1796
|
+
* Templates larger than this are structurally unsubmittable through any
|
|
1797
|
+
* CloudFormation API — no S3 indirection helps. The caller surfaces a
|
|
1798
|
+
* pre-flight error pointing the user at template-splitting (nested stacks)
|
|
1799
|
+
* or shrinking inline asset payloads (`lambda.Code.fromAsset`).
|
|
1800
|
+
*/
|
|
1801
|
+
const CFN_TEMPLATE_URL_LIMIT = 1048576;
|
|
1802
|
+
/**
|
|
1803
|
+
* Shared S3 key prefix for transient CFn templates uploaded by `cdkd import
|
|
1804
|
+
* --migrate-from-cloudformation` and `cdkd export`. Kept distinct from
|
|
1805
|
+
* cdkd's `cdkd/` state prefix so `state list` / `state info` never conflate
|
|
1806
|
+
* transient migration artifacts with persisted stack state. The prefix is
|
|
1807
|
+
* intentionally human-grep-able — leftovers (if cleanup fails) point
|
|
1808
|
+
* straight at the offending stack name.
|
|
1809
|
+
*
|
|
1810
|
+
* Re-used by both commands so operator-facing audit trails (CloudTrail
|
|
1811
|
+
* records of the migrate-tmp uploads) stay consistent across the two
|
|
1812
|
+
* flows.
|
|
1813
|
+
*/
|
|
1814
|
+
const MIGRATE_TMP_PREFIX = "cdkd-migrate-tmp";
|
|
1815
|
+
/**
|
|
1816
|
+
* Upload a CFn template body to the cdkd state bucket and return both a
|
|
1817
|
+
* virtual-hosted-style HTTPS URL CloudFormation can fetch via
|
|
1818
|
+
* `TemplateURL` and a `cleanup` callback that deletes the object (and
|
|
1819
|
+
* destroys the S3 client).
|
|
1820
|
+
*
|
|
1821
|
+
* The state bucket's actual region is resolved via `GetBucketLocation`
|
|
1822
|
+
* (cached per-process) so the upload client and the URL match the
|
|
1823
|
+
* bucket's region — the calling CLI's profile region is irrelevant here.
|
|
1824
|
+
*
|
|
1825
|
+
* Cleanup is the caller's responsibility: invoke `cleanup` in a `finally`
|
|
1826
|
+
* around the CFn call. CloudFormation copies the template into its own
|
|
1827
|
+
* internal storage during the synchronous `CreateChangeSet` /
|
|
1828
|
+
* `UpdateStack` API call, so the S3 object is no longer needed after that
|
|
1829
|
+
* call returns (success or failure).
|
|
1830
|
+
*
|
|
1831
|
+
* Shared between `cdkd import --migrate-from-cloudformation` (via
|
|
1832
|
+
* `retire-cfn-stack.ts`) and `cdkd export` (via `commands/export.ts`) so
|
|
1833
|
+
* the upload + cleanup contract is single-sourced.
|
|
1834
|
+
*/
|
|
1835
|
+
async function uploadCfnTemplate(args) {
|
|
1836
|
+
const { bucket, body, stackName, format, s3ClientOpts } = args;
|
|
1837
|
+
const region = await resolveBucketRegion(bucket, {
|
|
1838
|
+
...s3ClientOpts?.profile && { profile: s3ClientOpts.profile },
|
|
1839
|
+
...s3ClientOpts?.credentials && { credentials: s3ClientOpts.credentials }
|
|
1840
|
+
});
|
|
1841
|
+
const s3 = new S3Client({
|
|
1842
|
+
region,
|
|
1843
|
+
...s3ClientOpts?.profile && { profile: s3ClientOpts.profile },
|
|
1844
|
+
...s3ClientOpts?.credentials && { credentials: s3ClientOpts.credentials }
|
|
1845
|
+
});
|
|
1846
|
+
const ext = format === "yaml" ? "yaml" : "json";
|
|
1847
|
+
const contentType = format === "yaml" ? "application/x-yaml" : "application/json";
|
|
1848
|
+
const key = `${MIGRATE_TMP_PREFIX}/${stackName}/${Date.now()}.${ext}`;
|
|
1849
|
+
try {
|
|
1850
|
+
await s3.send(new PutObjectCommand({
|
|
1851
|
+
Bucket: bucket,
|
|
1852
|
+
Key: key,
|
|
1853
|
+
Body: body,
|
|
1854
|
+
ContentType: contentType
|
|
1855
|
+
}));
|
|
1856
|
+
} catch (err) {
|
|
1857
|
+
s3.destroy();
|
|
1858
|
+
throw err;
|
|
1859
|
+
}
|
|
1860
|
+
const url = `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
|
|
1861
|
+
const cleanup = async () => {
|
|
1862
|
+
try {
|
|
1863
|
+
await s3.send(new DeleteObjectCommand({
|
|
1864
|
+
Bucket: bucket,
|
|
1865
|
+
Key: key
|
|
1866
|
+
}));
|
|
1867
|
+
} finally {
|
|
1868
|
+
s3.destroy();
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
return {
|
|
1872
|
+
url,
|
|
1873
|
+
cleanup
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
/**
|
|
1877
|
+
* Threshold (in bytes) above which a single resource's serialized
|
|
1878
|
+
* `Properties` block is considered an "inline payload" worth surfacing as
|
|
1879
|
+
* a contributor to a template that exceeds the 1 MB CFn `TemplateURL`
|
|
1880
|
+
* ceiling. 4 KB matches the typical inline `Code.ZipFile` Lambda payload
|
|
1881
|
+
* that pushes a multi-resource CDK app over the wire-format limit.
|
|
1882
|
+
*/
|
|
1883
|
+
const LARGE_INLINE_RESOURCE_THRESHOLD = 4096;
|
|
1884
|
+
/**
|
|
1885
|
+
* Walk a CFn template and surface every resource whose serialized
|
|
1886
|
+
* `Properties` block exceeds {@link LARGE_INLINE_RESOURCE_THRESHOLD}.
|
|
1887
|
+
* Used to build the actionable "offending resources" list in the
|
|
1888
|
+
* pre-flight error when a template exceeds the 1 MB `TemplateURL`
|
|
1889
|
+
* ceiling — typical culprits are inline `Code.ZipFile` Lambdas, inline
|
|
1890
|
+
* StepFunctions definitions, or large `AWS::CloudFormation::Stack`
|
|
1891
|
+
* bodies.
|
|
1892
|
+
*
|
|
1893
|
+
* Returns entries sorted by `approxBytes` descending so the user sees
|
|
1894
|
+
* the biggest contributor first. A non-CFn-template input (no
|
|
1895
|
+
* `Resources` object) returns an empty array.
|
|
1896
|
+
*/
|
|
1897
|
+
function findLargeInlineResources(template, threshold = LARGE_INLINE_RESOURCE_THRESHOLD) {
|
|
1898
|
+
const result = [];
|
|
1899
|
+
const resources = template["Resources"];
|
|
1900
|
+
if (!resources || typeof resources !== "object" || Array.isArray(resources)) return result;
|
|
1901
|
+
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
1902
|
+
if (!resource || typeof resource !== "object" || Array.isArray(resource)) continue;
|
|
1903
|
+
const r = resource;
|
|
1904
|
+
const resourceType = typeof r["Type"] === "string" ? r["Type"] : "<unknown>";
|
|
1905
|
+
const properties = r["Properties"];
|
|
1906
|
+
if (properties === void 0 || properties === null) continue;
|
|
1907
|
+
let approxBytes;
|
|
1908
|
+
try {
|
|
1909
|
+
approxBytes = JSON.stringify(properties).length;
|
|
1910
|
+
} catch {
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
if (approxBytes >= threshold) result.push({
|
|
1914
|
+
logicalId,
|
|
1915
|
+
resourceType,
|
|
1916
|
+
approxBytes
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
result.sort((a, b) => b.approxBytes - a.approxBytes);
|
|
1920
|
+
return result;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
//#endregion
|
|
1924
|
+
//#region src/synthesis/macro-expander.ts
|
|
1925
|
+
/** 600 seconds = 10 minutes. SDK waiter's `maxWaitTime` is in seconds. */
|
|
1926
|
+
const WAITER_MAX_WAIT_SECONDS = 600;
|
|
1927
|
+
const PARAMETER_PLACEHOLDER = "cdkd-macro-expand-placeholder";
|
|
1928
|
+
/**
|
|
1929
|
+
* Capabilities sent on every `CreateChangeSet` call:
|
|
1930
|
+
*
|
|
1931
|
+
* - `CAPABILITY_AUTO_EXPAND` is the load-bearing one — required for CFn
|
|
1932
|
+
* to actually run macro / `Transform` expansion (without it, CFn
|
|
1933
|
+
* rejects the changeset on any template that declares a Transform).
|
|
1934
|
+
* - `CAPABILITY_NAMED_IAM` / `CAPABILITY_IAM` are defense-in-depth: SAM
|
|
1935
|
+
* transforms (`AWS::Serverless-2016-10-31`) emit Lambda execution
|
|
1936
|
+
* roles, and user-authored macros may emit arbitrary IAM resources
|
|
1937
|
+
* too. Sending them unconditionally avoids a second round-trip when
|
|
1938
|
+
* the expanded template carries IAM resources cdkd's deploy pipeline
|
|
1939
|
+
* would otherwise complain about.
|
|
1940
|
+
*/
|
|
1941
|
+
const CAPABILITIES = [
|
|
1942
|
+
"CAPABILITY_AUTO_EXPAND",
|
|
1943
|
+
"CAPABILITY_NAMED_IAM",
|
|
1944
|
+
"CAPABILITY_IAM"
|
|
1945
|
+
];
|
|
1946
|
+
/**
|
|
1947
|
+
* Per-Type placeholder values used when a template Parameter has no
|
|
1948
|
+
* `Default`. CFn validates Parameter `Type` BEFORE the macro Lambda
|
|
1949
|
+
* runs, so a single bare-string placeholder rejects `CreateChangeSet`
|
|
1950
|
+
* on `Number` / `List<*>` / `AWS::EC2::*::Id` typed parameters
|
|
1951
|
+
* (`Parameter '<X>' must be a number`, etc.). The actual values do NOT
|
|
1952
|
+
* leak into the Processed-stage template (CFn preserves
|
|
1953
|
+
* `Ref: <param>` intact through expansion — see {@link
|
|
1954
|
+
* EMPIRICAL_FINDINGS_VERIFIED_2026_05_23} Q4), so any type-valid value
|
|
1955
|
+
* is sufficient. AWS-published Parameter Types list:
|
|
1956
|
+
* <https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html#parameters-section-structure-properties>
|
|
1957
|
+
*/
|
|
1958
|
+
const PARAMETER_TYPE_PLACEHOLDERS = {
|
|
1959
|
+
String: PARAMETER_PLACEHOLDER,
|
|
1960
|
+
Number: "0",
|
|
1961
|
+
"List<Number>": "0",
|
|
1962
|
+
CommaDelimitedList: "",
|
|
1963
|
+
"List<String>": "",
|
|
1964
|
+
"AWS::EC2::AvailabilityZone::Name": "us-east-1a",
|
|
1965
|
+
"AWS::EC2::Image::Id": "ami-00000000",
|
|
1966
|
+
"AWS::EC2::Instance::Id": "i-00000000",
|
|
1967
|
+
"AWS::EC2::KeyPair::KeyName": "placeholder-key",
|
|
1968
|
+
"AWS::EC2::SecurityGroup::GroupName": "placeholder-sg",
|
|
1969
|
+
"AWS::EC2::SecurityGroup::Id": "sg-00000000",
|
|
1970
|
+
"AWS::EC2::Subnet::Id": "subnet-00000000",
|
|
1971
|
+
"AWS::EC2::Volume::Id": "vol-00000000",
|
|
1972
|
+
"AWS::EC2::VPC::Id": "vpc-00000000",
|
|
1973
|
+
"AWS::Route53::HostedZone::Id": "Z00000000000000000000",
|
|
1974
|
+
"AWS::SSM::Parameter::Name": "placeholder",
|
|
1975
|
+
"List<AWS::EC2::AvailabilityZone::Name>": "us-east-1a",
|
|
1976
|
+
"List<AWS::EC2::Image::Id>": "ami-00000000",
|
|
1977
|
+
"List<AWS::EC2::Instance::Id>": "i-00000000",
|
|
1978
|
+
"List<AWS::EC2::SecurityGroup::GroupName>": "placeholder-sg",
|
|
1979
|
+
"List<AWS::EC2::SecurityGroup::Id>": "sg-00000000",
|
|
1980
|
+
"List<AWS::EC2::Subnet::Id>": "subnet-00000000",
|
|
1981
|
+
"List<AWS::EC2::Volume::Id>": "vol-00000000",
|
|
1982
|
+
"List<AWS::EC2::VPC::Id>": "vpc-00000000",
|
|
1983
|
+
"List<AWS::Route53::HostedZone::Id>": "Z00000000000000000000"
|
|
1984
|
+
};
|
|
1985
|
+
/**
|
|
1986
|
+
* Expand CloudFormation macros / `Fn::Transform` blocks in a synth
|
|
1987
|
+
* template via a transient CloudFormation changeset round-trip.
|
|
1988
|
+
*
|
|
1989
|
+
* The flow (per design §3 Approach A):
|
|
1990
|
+
*
|
|
1991
|
+
* 1. Mint a unique transient stack name (`cdkd-macro-expand-<id>`).
|
|
1992
|
+
* 2. Build parameter values for every declared template Parameter
|
|
1993
|
+
* (template Default if present; synthetic placeholder otherwise).
|
|
1994
|
+
* CFn does NOT substitute these into the Processed-stage
|
|
1995
|
+
* template, but it requires them for the changeset to be valid.
|
|
1996
|
+
* 3. `CreateChangeSet --change-set-type CREATE` with
|
|
1997
|
+
* `Capabilities: ['CAPABILITY_AUTO_EXPAND','CAPABILITY_NAMED_IAM',
|
|
1998
|
+
* 'CAPABILITY_IAM']`. Use inline `TemplateBody` when <= 51,200
|
|
1999
|
+
* bytes; upload to the cdkd state bucket and pass `TemplateURL`
|
|
2000
|
+
* when between 51,200 bytes and 1 MB; refuse outright when > 1 MB
|
|
2001
|
+
* (CFn `TemplateURL` ceiling).
|
|
2002
|
+
* 4. Wait for `ChangeSetStatus: CREATE_COMPLETE`. On `FAILED`, surface
|
|
2003
|
+
* the `StatusReason` verbatim — typically the macro Lambda's error
|
|
2004
|
+
* message, or "no transforms found" for a template with an empty
|
|
2005
|
+
* `Transform` array.
|
|
2006
|
+
* 5. `GetTemplate --template-stage Processed` returns the
|
|
2007
|
+
* post-expansion template.
|
|
2008
|
+
* 6. Cleanup in `finally`: `DeleteChangeSet` + `DeleteStack`
|
|
2009
|
+
* (idempotent — both tolerate `*NotFound` errors). The transient
|
|
2010
|
+
* S3 upload (if any) is cleaned up too.
|
|
2011
|
+
* 7. Re-check `containsMacro(expanded)` and reject the multi-stage
|
|
2012
|
+
* case — cdkd v1 does not support templates whose expansion emits
|
|
2013
|
+
* ANOTHER macro reference. CFn handles single-step expansion
|
|
2014
|
+
* natively; second-round expansion is intentionally out of scope.
|
|
2015
|
+
*
|
|
2016
|
+
* Returns the expanded template as a parsed `CloudFormationTemplate`.
|
|
2017
|
+
* Throws {@link MacroExpansionError} on any failure mode. Cleanup
|
|
2018
|
+
* failures during the `finally` block log at WARN but do not mask the
|
|
2019
|
+
* outer success / error.
|
|
2020
|
+
*/
|
|
2021
|
+
async function expandMacros(template, opts) {
|
|
2022
|
+
const logger = getLogger().child("MacroExpander");
|
|
2023
|
+
if (!containsMacro(template)) return template;
|
|
2024
|
+
const macros = enumerateMacros(template);
|
|
2025
|
+
logger.debug(`Macro expansion: detected transforms [${macros.join(", ")}], starting CFn round-trip...`);
|
|
2026
|
+
const transientStackName = `cdkd-macro-expand-${randomUUID().slice(0, 16)}`;
|
|
2027
|
+
const changeSetName = `${transientStackName}-changeset`;
|
|
2028
|
+
const region = opts.region;
|
|
2029
|
+
const stateBucket = opts.stateBucket;
|
|
2030
|
+
const waiterMaxWaitSeconds = opts.waiterMaxWaitSeconds ?? WAITER_MAX_WAIT_SECONDS;
|
|
2031
|
+
let cfn;
|
|
2032
|
+
let ownsClient = false;
|
|
2033
|
+
let s3Cleanup;
|
|
2034
|
+
try {
|
|
2035
|
+
const serialized = JSON.stringify(template);
|
|
2036
|
+
const parameters = buildParameterValues(template, logger);
|
|
2037
|
+
ownsClient = opts.cfnClient === void 0;
|
|
2038
|
+
cfn = opts.cfnClient ?? new CloudFormationClient({ region });
|
|
2039
|
+
let templateInput;
|
|
2040
|
+
if (serialized.length > 1048576) throw new MacroExpansionError(`Template is ${serialized.length} bytes, which exceeds CloudFormation's ${CFN_TEMPLATE_URL_LIMIT}-byte TemplateURL ceiling for macro expansion. Shrink inline payloads (move inline lambda.Code.ZipFile to lambda.Code.fromAsset, etc.) or split the stack before retrying.`);
|
|
2041
|
+
if (serialized.length <= 51200) templateInput = { TemplateBody: serialized };
|
|
2042
|
+
else {
|
|
2043
|
+
if (!stateBucket) throw new MacroExpansionError(`Template is ${serialized.length} bytes (over ${CFN_TEMPLATE_BODY_LIMIT} inline limit) — cdkd needs a state bucket to upload the transient template for CloudFormation's TemplateURL parameter. Pass --state-bucket <name> or ensure STS GetCallerIdentity can resolve a default bucket (cdkd-state-<accountId>).`);
|
|
2044
|
+
logger.debug(`Macro expansion: template is ${serialized.length} bytes (over ${CFN_TEMPLATE_BODY_LIMIT} inline limit) — uploading to state bucket '${stateBucket}' for TemplateURL.`);
|
|
2045
|
+
const uploaded = await uploadCfnTemplate({
|
|
2046
|
+
bucket: stateBucket,
|
|
2047
|
+
body: serialized,
|
|
2048
|
+
stackName: transientStackName,
|
|
2049
|
+
format: "json",
|
|
2050
|
+
...opts.s3ClientOpts && { s3ClientOpts: opts.s3ClientOpts }
|
|
2051
|
+
});
|
|
2052
|
+
templateInput = { TemplateURL: uploaded.url };
|
|
2053
|
+
s3Cleanup = uploaded.cleanup;
|
|
2054
|
+
}
|
|
2055
|
+
try {
|
|
2056
|
+
await cfn.send(new CreateChangeSetCommand({
|
|
2057
|
+
StackName: transientStackName,
|
|
2058
|
+
ChangeSetName: changeSetName,
|
|
2059
|
+
ChangeSetType: "CREATE",
|
|
2060
|
+
...templateInput,
|
|
2061
|
+
Capabilities: CAPABILITIES,
|
|
2062
|
+
...parameters.length > 0 && { Parameters: parameters }
|
|
2063
|
+
}));
|
|
2064
|
+
} catch (err) {
|
|
2065
|
+
throw new MacroExpansionError(`CloudFormation rejected the macro-expansion changeset: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0);
|
|
2066
|
+
}
|
|
2067
|
+
let waiterFailed = false;
|
|
2068
|
+
let waiterError;
|
|
2069
|
+
try {
|
|
2070
|
+
await waitUntilChangeSetCreateComplete({
|
|
2071
|
+
client: cfn,
|
|
2072
|
+
maxWaitTime: waiterMaxWaitSeconds
|
|
2073
|
+
}, {
|
|
2074
|
+
StackName: transientStackName,
|
|
2075
|
+
ChangeSetName: changeSetName
|
|
2076
|
+
});
|
|
2077
|
+
} catch (waiterErr) {
|
|
2078
|
+
waiterFailed = true;
|
|
2079
|
+
waiterError = waiterErr;
|
|
2080
|
+
}
|
|
2081
|
+
if (waiterFailed) {
|
|
2082
|
+
const desc = await cfn.send(new DescribeChangeSetCommand({
|
|
2083
|
+
StackName: transientStackName,
|
|
2084
|
+
ChangeSetName: changeSetName
|
|
2085
|
+
})).catch(() => void 0);
|
|
2086
|
+
const reason = desc?.StatusReason ?? "unknown (DescribeChangeSet failed)";
|
|
2087
|
+
throw new MacroExpansionError(`CloudFormation macro expansion failed (status=${desc?.Status ?? "UNKNOWN"}): ${reason}`, waiterError instanceof Error ? waiterError : void 0);
|
|
2088
|
+
}
|
|
2089
|
+
const tpl = await cfn.send(new GetTemplateCommand({
|
|
2090
|
+
StackName: transientStackName,
|
|
2091
|
+
ChangeSetName: changeSetName,
|
|
2092
|
+
TemplateStage: "Processed"
|
|
2093
|
+
}));
|
|
2094
|
+
if (tpl.TemplateBody === void 0 || tpl.TemplateBody === null) throw new MacroExpansionError(`CloudFormation returned no Processed-stage template body for the macro-expansion changeset. This typically indicates a CFn-side regression — re-run, and if the failure persists open an issue with the transforms involved: [${macros.join(", ")}].`);
|
|
2095
|
+
const expanded = parseTemplateBody(tpl.TemplateBody);
|
|
2096
|
+
if (containsMacro(expanded)) throw new MacroExpansionError(`Macro expansion produced a template that still contains macros [${enumerateMacros(expanded).join(", ")}]. Multi-stage macros (a macro whose expansion emits another macro reference) are intentionally out of scope in cdkd v1 — see https://github.com/go-to-k/cdkd/issues/463. If you need this pattern, manually pre-expand the template and deploy the result.`);
|
|
2097
|
+
logger.debug(`Macro expansion: success — ${Object.keys(expanded.Resources ?? {}).length} resources after expansion.`);
|
|
2098
|
+
return expanded;
|
|
2099
|
+
} finally {
|
|
2100
|
+
if (cfn !== void 0) try {
|
|
2101
|
+
await cfn.send(new DeleteStackCommand({ StackName: transientStackName }));
|
|
2102
|
+
} catch (cleanupErr) {
|
|
2103
|
+
logger.warn(`Failed to delete transient macro-expand stack '${transientStackName}': ${formatErr(cleanupErr)}. Clean up manually via 'aws cloudformation delete-stack --stack-name ${transientStackName}'.`);
|
|
2104
|
+
}
|
|
2105
|
+
if (s3Cleanup) try {
|
|
2106
|
+
await s3Cleanup();
|
|
2107
|
+
} catch (cleanupErr) {
|
|
2108
|
+
logger.warn(`Failed to delete transient macro-expand template upload from state bucket '${stateBucket}' (key prefix 'cdkd-migrate-tmp/${transientStackName}/'): ${formatErr(cleanupErr)}. Sweep manually via 'aws s3 rm s3://${stateBucket}/cdkd-migrate-tmp/${transientStackName}/ --recursive'.`);
|
|
2109
|
+
}
|
|
2110
|
+
if (ownsClient && cfn !== void 0) cfn.destroy();
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Build the `Parameters` list passed to `CreateChangeSet`. CFn requires
|
|
2115
|
+
* a value for every declared parameter; we fall back to a Type-aware
|
|
2116
|
+
* synthetic placeholder when the template has no Default. Per the
|
|
2117
|
+
* empirical verification above, these values do NOT leak into the
|
|
2118
|
+
* Processed template (CFn keeps `Ref: <param>` intact for cdkd's own
|
|
2119
|
+
* resolver to substitute later with the real values).
|
|
2120
|
+
*/
|
|
2121
|
+
function buildParameterValues(template, logger) {
|
|
2122
|
+
if (!template || typeof template !== "object" || Array.isArray(template)) return [];
|
|
2123
|
+
const params = template["Parameters"];
|
|
2124
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) return [];
|
|
2125
|
+
const out = [];
|
|
2126
|
+
for (const [key, def] of Object.entries(params)) {
|
|
2127
|
+
if (!def || typeof def !== "object" || Array.isArray(def)) continue;
|
|
2128
|
+
const defObj = def;
|
|
2129
|
+
const defDefault = defObj["Default"];
|
|
2130
|
+
const defType = typeof defObj["Type"] === "string" ? defObj["Type"] : void 0;
|
|
2131
|
+
out.push({
|
|
2132
|
+
ParameterKey: key,
|
|
2133
|
+
ParameterValue: stringifyParamDefault(defDefault, defType, key, logger)
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
return out;
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Coerce a CFn Parameter `Default` (or build a synthetic placeholder
|
|
2140
|
+
* when the Default is absent) into the string `CreateChangeSet`
|
|
2141
|
+
* requires. Strings / numbers / booleans pass through verbatim;
|
|
2142
|
+
* object-shaped Defaults (rare — array-typed Defaults that haven't
|
|
2143
|
+
* been comma-joined) are JSON-stringified. When no Default is present,
|
|
2144
|
+
* routes through {@link PARAMETER_TYPE_PLACEHOLDERS} so a `Number` /
|
|
2145
|
+
* `List<Number>` / `AWS::EC2::*::Id` Parameter sees a value CFn's
|
|
2146
|
+
* pre-macro Type validator accepts. The actual value does NOT leak
|
|
2147
|
+
* into the Processed-stage template (CFn preserves `Ref: <param>`
|
|
2148
|
+
* intact through expansion — see empirical findings).
|
|
2149
|
+
*/
|
|
2150
|
+
function stringifyParamDefault(value, type, paramKey, logger) {
|
|
2151
|
+
if (value !== void 0 && value !== null) {
|
|
2152
|
+
if (typeof value === "string") return value;
|
|
2153
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
2154
|
+
try {
|
|
2155
|
+
return JSON.stringify(value);
|
|
2156
|
+
} catch {}
|
|
2157
|
+
}
|
|
2158
|
+
if (type !== void 0) {
|
|
2159
|
+
const known = PARAMETER_TYPE_PLACEHOLDERS[type];
|
|
2160
|
+
if (known !== void 0) return known;
|
|
2161
|
+
if (type.startsWith("AWS::SSM::Parameter::Value<")) {
|
|
2162
|
+
const inner = type.slice(27, -1);
|
|
2163
|
+
if (inner.startsWith("List<") || inner === "CommaDelimitedList") return "placeholder,placeholder";
|
|
2164
|
+
return "placeholder";
|
|
2165
|
+
}
|
|
2166
|
+
logger.warn(`Parameter '${paramKey}' has unrecognized CFn Type '${type}'; using a generic string placeholder for the transient macro-expansion changeset. If CFn rejects the changeset with a type error, file an issue with the offending Type.`);
|
|
2167
|
+
return PARAMETER_PLACEHOLDER;
|
|
2168
|
+
}
|
|
2169
|
+
return PARAMETER_PLACEHOLDER;
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* Parse the `TemplateBody` field returned by `GetTemplate`. The SDK
|
|
2173
|
+
* types it as `string | undefined`, but empirical observation shows
|
|
2174
|
+
* the wire shape may be either a string (for YAML or some pre-parsed
|
|
2175
|
+
* cases) or a parsed object (for JSON templates against
|
|
2176
|
+
* `--template-stage Processed`). Handle both.
|
|
2177
|
+
*/
|
|
2178
|
+
function parseTemplateBody(body) {
|
|
2179
|
+
let parsed;
|
|
2180
|
+
if (typeof body === "string") try {
|
|
2181
|
+
parsed = JSON.parse(body);
|
|
2182
|
+
} catch (err) {
|
|
2183
|
+
throw new MacroExpansionError(`CloudFormation returned a non-JSON Processed-stage template body. cdkd's macro-expansion path only supports JSON-shaped synth templates (CDK apps emit JSON by default). Cause: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0);
|
|
2184
|
+
}
|
|
2185
|
+
else if (body && typeof body === "object" && !Array.isArray(body)) parsed = body;
|
|
2186
|
+
else throw new MacroExpansionError(`CloudFormation returned an unexpected TemplateBody shape (${typeof body}). Expected a JSON string or parsed object.`);
|
|
2187
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new MacroExpansionError(`CloudFormation returned a malformed Processed-stage template body — expected a JSON object at the top level, got ${Array.isArray(parsed) ? "array" : typeof parsed}.`);
|
|
2188
|
+
const resources = parsed["Resources"];
|
|
2189
|
+
if (resources !== void 0 && (typeof resources !== "object" || resources === null || Array.isArray(resources))) throw new MacroExpansionError(`CloudFormation returned a malformed Processed-stage template body — 'Resources' must be an object map, got ${resources === null ? "null" : Array.isArray(resources) ? "array" : typeof resources}.`);
|
|
2190
|
+
return parsed;
|
|
2191
|
+
}
|
|
2192
|
+
function formatErr(err) {
|
|
2193
|
+
return err instanceof Error ? err.message : String(err);
|
|
2194
|
+
}
|
|
2195
|
+
|
|
1596
2196
|
//#endregion
|
|
1597
2197
|
//#region src/cli/config-loader.ts
|
|
1598
2198
|
/**
|
|
@@ -1903,6 +2503,19 @@ var Synthesizer = class {
|
|
|
1903
2503
|
this.logger.debug(`Using pre-synthesized cloud assembly at ${appPath}`);
|
|
1904
2504
|
const manifest = this.assemblyReader.readManifest(appPath);
|
|
1905
2505
|
const stacks = this.assemblyReader.getAllStacks(appPath, manifest);
|
|
2506
|
+
const presynthRegion = options.region || process.env["AWS_REGION"] || process.env["AWS_DEFAULT_REGION"];
|
|
2507
|
+
let presynthAccountId;
|
|
2508
|
+
try {
|
|
2509
|
+
const stsClient = new STSClient({ ...presynthRegion && { region: presynthRegion } });
|
|
2510
|
+
presynthAccountId = (await stsClient.send(new GetCallerIdentityCommand({}))).Account;
|
|
2511
|
+
stsClient.destroy();
|
|
2512
|
+
} catch {
|
|
2513
|
+
this.logger.debug("Could not resolve AWS account ID via STS (pre-synth branch)");
|
|
2514
|
+
}
|
|
2515
|
+
await this.expandMacrosForStacks(stacks, options, {
|
|
2516
|
+
region: presynthRegion,
|
|
2517
|
+
...presynthAccountId && { accountId: presynthAccountId }
|
|
2518
|
+
});
|
|
1906
2519
|
this.logger.debug(`Loaded ${stacks.length} stack(s) from pre-synthesized assembly`);
|
|
1907
2520
|
return {
|
|
1908
2521
|
manifest,
|
|
@@ -1955,6 +2568,10 @@ var Synthesizer = class {
|
|
|
1955
2568
|
const manifest = this.assemblyReader.readManifest(outputDir);
|
|
1956
2569
|
if (!manifest.missing || manifest.missing.length === 0) {
|
|
1957
2570
|
const stacks = this.assemblyReader.getAllStacks(outputDir, manifest);
|
|
2571
|
+
await this.expandMacrosForStacks(stacks, options, {
|
|
2572
|
+
region,
|
|
2573
|
+
...accountId && { accountId }
|
|
2574
|
+
});
|
|
1958
2575
|
this.logger.debug(`Synthesis complete: ${stacks.length} stack(s)`);
|
|
1959
2576
|
return {
|
|
1960
2577
|
manifest,
|
|
@@ -1978,6 +2595,49 @@ var Synthesizer = class {
|
|
|
1978
2595
|
async listStacks(options) {
|
|
1979
2596
|
return (await this.synthesize(options)).stacks.map((s) => s.stackName);
|
|
1980
2597
|
}
|
|
2598
|
+
/**
|
|
2599
|
+
* Per-stack macro-expansion pass (Issue #463). Mutates each stack's
|
|
2600
|
+
* `template` in place when {@link containsMacro} flags it. Runs
|
|
2601
|
+
* AFTER the context-provider loop has settled and BEFORE the
|
|
2602
|
+
* analyzer / provisioner pipeline consumes the templates, so every
|
|
2603
|
+
* downstream stage sees the post-expansion shape.
|
|
2604
|
+
*
|
|
2605
|
+
* Skipped silently when no stack carries a macro — pure no-op cost.
|
|
2606
|
+
* When a macro IS detected and the caller did NOT thread a
|
|
2607
|
+
* region into the synthesizer, falls back to resolving region from
|
|
2608
|
+
* the synthesized stack's environment (set by `cdk.Stack.region`).
|
|
2609
|
+
* Throws `SynthesisError` when no region can be resolved (the
|
|
2610
|
+
* upstream caller treats it as a synth failure) and propagates
|
|
2611
|
+
* `MacroExpansionError` (from {@link expandMacros}) on any CFn-side
|
|
2612
|
+
* failure during the round-trip.
|
|
2613
|
+
*/
|
|
2614
|
+
async expandMacrosForStacks(stacks, options, resolved) {
|
|
2615
|
+
const stacksWithMacros = stacks.filter((s) => containsMacro(s.template));
|
|
2616
|
+
if (stacksWithMacros.length === 0) return;
|
|
2617
|
+
const region = resolved?.region || options.region || process.env["AWS_REGION"] || process.env["AWS_DEFAULT_REGION"] || stacksWithMacros[0]?.region;
|
|
2618
|
+
if (!region) throw new SynthesisError(`Stack(s) [${stacksWithMacros.map((s) => s.stackName).join(", ")}] use CloudFormation macros (Transform / Fn::Transform) but cdkd could not resolve an AWS region for the expansion round-trip. Set AWS_REGION, pass --region <r>, or set env: { region: '<r>' } in your CDK Stack constructor.`);
|
|
2619
|
+
let stateBucket;
|
|
2620
|
+
if (options.stateBucket) stateBucket = options.stateBucket;
|
|
2621
|
+
else if (resolved?.accountId) stateBucket = `cdkd-state-${resolved.accountId}`;
|
|
2622
|
+
else {
|
|
2623
|
+
const oversize = stacksWithMacros.find((s) => JSON.stringify(s.template).length > 51200);
|
|
2624
|
+
if (oversize) throw new SynthesisError(`Stack '${oversize.stackName}' uses CloudFormation macros AND its serialized template exceeds the 51,200-byte inline TemplateBody limit, so cdkd must upload the template to S3 for the transient expansion changeset. cdkd could not resolve a state bucket: STS GetCallerIdentity failed AND --state-bucket was not provided. Pass --state-bucket <name> (cdkd uses the same bucket as cdkd deploy state storage; typically 'cdkd-state-<accountId>').`);
|
|
2625
|
+
stateBucket = void 0;
|
|
2626
|
+
}
|
|
2627
|
+
for (const stack of stacksWithMacros) {
|
|
2628
|
+
const macros = enumerateMacros(stack.template);
|
|
2629
|
+
this.logger.info(`[macros] Expanding CloudFormation macros for stack '${stack.stackName}' via CFn round-trip (transforms: ${macros.join(", ")}; may take 30-60s)...`);
|
|
2630
|
+
const before = Date.now();
|
|
2631
|
+
const expanded = await expandMacros(stack.template, {
|
|
2632
|
+
region,
|
|
2633
|
+
...stateBucket !== void 0 && { stateBucket },
|
|
2634
|
+
...options.macroExpandS3ClientOpts && { s3ClientOpts: options.macroExpandS3ClientOpts }
|
|
2635
|
+
});
|
|
2636
|
+
stack.template = expanded;
|
|
2637
|
+
const elapsedSec = Math.round((Date.now() - before) / 1e3);
|
|
2638
|
+
this.logger.info(`[macros] ... done in ${elapsedSec}s (${Object.keys(expanded.Resources ?? {}).length} resources after expansion).`);
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
1981
2641
|
};
|
|
1982
2642
|
/**
|
|
1983
2643
|
* Check if two sets contain the same elements
|
|
@@ -9238,5 +9898,5 @@ var DeployEngine = class {
|
|
|
9238
9898
|
};
|
|
9239
9899
|
|
|
9240
9900
|
//#endregion
|
|
9241
|
-
export {
|
|
9242
|
-
//# sourceMappingURL=deploy-engine-
|
|
9901
|
+
export { ConfigError as $, AssetPublisher as A, resolveStateBucketWithDefault as B, applyRoleArnIfSet as C, LockManager as D, TemplateParser as E, getDefaultStateBucketName as F, MIGRATE_TMP_PREFIX as G, warnDeprecatedNoPrefixCliFlag as H, getLegacyStateBucketName as I, AssemblyReader as J, findLargeInlineResources as K, resolveApp as L, WorkGraph as M, buildDockerImage as N, S3StateBackend as O, Synthesizer as P, CdkdError as Q, resolveCaptureObservedState as R, IntrinsicFunctionResolver as S, DagBuilder as T, CFN_TEMPLATE_BODY_LIMIT as U, resolveStateBucketWithDefaultAndSource as V, CFN_TEMPLATE_URL_LIMIT as W, resolveBucketRegion as X, clearBucketRegionCache as Y, AssetError as Z, normalizeAwsTagsToCfn as _, normalizeAwsError as _t, withRetry as a, MissingCdkCliError as at, CloudControlProvider as b, cyan as c, ResourceTimeoutError as ct, red as d, StackHasActiveImportsError as dt, DependencyError as et, yellow as f, StackTerminationProtectionError as ft, matchesCdkPath as g, isCdkdError as gt, CDK_PATH_TAG as h, formatError as ht, withResourceDeadline as i, LockError as it, stringifyValue as j, shouldRetainResource as k, gray as l, ResourceUpdateNotSupportedError as lt, collectInlinePolicyNamesManagedBySiblings as m, SynthesisError as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, LocalMigrateError as nt, IMPLICIT_DELETE_DEPENDENCIES as o, PartialFailureError as ot, IAMRoleProvider as p, StateError as pt, uploadCfnTemplate as q, DeployEngine as r, LocalStartServiceError as rt, bold as s, ProvisioningError as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, LocalInvokeBuildError as tt, green as u, RouteDiscoveryError as ut, resolveExplicitPhysicalId as v, withErrorHandling as vt, DiffCalculator as w, assertRegionMatch as x, ProviderRegistry as y, resolveSkipPrefix as z };
|
|
9902
|
+
//# sourceMappingURL=deploy-engine-DZYchTh6.js.map
|