@go-to-k/cdkd 0.227.0 → 0.228.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/cli.js +339 -9
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-CBzCsd24.js → deploy-engine-BRZdKXDs.js} +65 -8
- package/dist/deploy-engine-BRZdKXDs.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-CBzCsd24.js.map +0 -1
package/README.md
CHANGED
|
@@ -588,12 +588,15 @@ stream to S3 — cdkd's local equivalent of CloudFormation's
|
|
|
588
588
|
cdkd events MyStack # list runs, newest first
|
|
589
589
|
cdkd events MyStack --run <runId> # one run's full event stream
|
|
590
590
|
cdkd events MyStack --format json # machine-readable (AI-agent hand-off)
|
|
591
|
+
cdkd events prune MyStack --all # purge event history (reclaim S3 space)
|
|
591
592
|
```
|
|
592
593
|
|
|
593
594
|
Events are persisted as JSONL under a `deployments/` key family separate
|
|
594
595
|
from `state.json` (no state schema bump), so a destroyed stack's failure
|
|
595
596
|
history stays readable. Recording is best-effort and never blocks the
|
|
596
|
-
run; events carry error + metadata only (never resource properties).
|
|
597
|
+
run; events carry error + metadata only (never resource properties). The
|
|
598
|
+
store self-bounds to the last 20 runs, and `cdkd events prune` purges old
|
|
599
|
+
history on demand (`--keep N` / `--older-than <dur>` / `--all`). See
|
|
597
600
|
**[docs/deployment-events.md](docs/deployment-events.md)** for the full
|
|
598
601
|
reference.
|
|
599
602
|
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { A as withErrorHandling, B as generateResourceNameWithFallback, C as StackHasActiveImportsError, F as getLiveRenderer, H as withStackName, I as PATTERN_B_NAME_PROPERTIES, L as PATTERN_B_RESOURCE_TYPES, M as getLogger, P as runStackBuffered, S as ResourceUpdateNotSupportedError, V as withSkipPrefix, _ as MissingCdkCliError, a as assertRegionMatch, b as ProvisioningError, f as LocalInvokeBuildError$1, i as resolveExplicitPhysicalId, k as normalizeAwsError, l as CdkdError, m as LocalStartServiceError, n as matchesCdkPath, o as disableInstanceApiTermination, p as LocalMigrateError, r as normalizeAwsTagsToCfn, s as isTerminationProtectionPropagationError, t as CDK_PATH_TAG, v as NestedStackChildDirectDestroyError, w as StackTerminationProtectionError, x as ResourceTimeoutError, y as PartialFailureError, z as generateResourceName } from "./import-helpers-wLipXr5g.js";
|
|
3
3
|
import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-pjPwZz1r.js";
|
|
4
|
-
import { $ as findLargeInlineResources, A as shouldRetainResource, B as getDockerImageBySourceHash, C as applyRoleArnIfSet, D as LockManager, E as TemplateParser, F as formatDockerLoginError, G as resolveCaptureObservedState, H as getDefaultStateBucketName, I as getDockerCmd, J as resolveStateBucketWithDefaultAndSource, K as resolveSkipPrefix, L as runDockerForeground, M as stringifyValue, N as WorkGraph, O as S3StateBackend, P as buildDockerImage, Q as MIGRATE_TMP_PREFIX, R as runDockerStreaming, S as IntrinsicFunctionResolver, T as DagBuilder, U as getLegacyStateBucketName, V as Synthesizer, W as resolveApp, X as CFN_TEMPLATE_BODY_LIMIT, Y as warnDeprecatedNoPrefixCliFlag, Z as CFN_TEMPLATE_URL_LIMIT, _ as IAMRoleProvider, a as withRetry, b as findActionableSilentDrops, c as computeImplicitDeleteEdges, d as bold, et as uploadCfnTemplate, f as cyan, g as yellow, h as red, i as withResourceDeadline, j as AssetPublisher, k as rebuildClientForBucketRegion, l as extractDeploymentEventError, m as green, n as DEFAULT_RESOURCE_WARN_AFTER_MS, o as isRetryableTransientError, p as gray, q as resolveStateBucketWithDefault, r as DeployEngine, rt as resolveBucketRegion, s as IMPLICIT_DELETE_DEPENDENCIES, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as AssemblyReader, u as formatResourceLine, v as collectInlinePolicyNamesManagedBySiblings, w as DiffCalculator, x as CloudControlProvider, y as ProviderRegistry, z as AssetManifestLoader } from "./deploy-engine-
|
|
4
|
+
import { $ as findLargeInlineResources, A as shouldRetainResource, B as getDockerImageBySourceHash, C as applyRoleArnIfSet, D as LockManager, E as TemplateParser, F as formatDockerLoginError, G as resolveCaptureObservedState, H as getDefaultStateBucketName, I as getDockerCmd, J as resolveStateBucketWithDefaultAndSource, K as resolveSkipPrefix, L as runDockerForeground, M as stringifyValue, N as WorkGraph, O as S3StateBackend, P as buildDockerImage, Q as MIGRATE_TMP_PREFIX, R as runDockerStreaming, S as IntrinsicFunctionResolver, T as DagBuilder, U as getLegacyStateBucketName, V as Synthesizer, W as resolveApp, X as CFN_TEMPLATE_BODY_LIMIT, Y as warnDeprecatedNoPrefixCliFlag, Z as CFN_TEMPLATE_URL_LIMIT, _ as IAMRoleProvider, a as withRetry, b as findActionableSilentDrops, c as computeImplicitDeleteEdges, d as bold, et as uploadCfnTemplate, f as cyan, g as yellow, h as red, i as withResourceDeadline, j as AssetPublisher, k as rebuildClientForBucketRegion, l as extractDeploymentEventError, m as green, n as DEFAULT_RESOURCE_WARN_AFTER_MS, o as isRetryableTransientError, p as gray, q as resolveStateBucketWithDefault, r as DeployEngine, rt as resolveBucketRegion, s as IMPLICIT_DELETE_DEPENDENCIES, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as AssemblyReader, u as formatResourceLine, v as collectInlinePolicyNamesManagedBySiblings, w as DiffCalculator, x as CloudControlProvider, y as ProviderRegistry, z as AssetManifestLoader } from "./deploy-engine-BRZdKXDs.js";
|
|
5
5
|
import { t as ASGProvider } from "./asg-provider-B_hrCxRx.js";
|
|
6
6
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
7
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
@@ -1543,6 +1543,10 @@ async function promptRecreateConfirm(input) {
|
|
|
1543
1543
|
* - **Separate keys from state.json** — no state schema bump; event
|
|
1544
1544
|
* files survive `cdkd destroy` (state deletion does not touch
|
|
1545
1545
|
* `deployments/`), preserving post-mortem context.
|
|
1546
|
+
* - **Bounded growth** (issue #885): `finalize()` prunes `{runId}.jsonl`
|
|
1547
|
+
* streams that fell out of the index window so the `deployments/`
|
|
1548
|
+
* prefix stays bounded; `cdkd events prune` (via
|
|
1549
|
+
* {@link DeploymentEventsReader.pruneRuns}) is the explicit purge.
|
|
1546
1550
|
*/
|
|
1547
1551
|
/** Max runs retained in `deployments/index.json` (newest first). */
|
|
1548
1552
|
const DEPLOYMENT_EVENTS_MAX_INDEX_RUNS = 20;
|
|
@@ -1552,7 +1556,7 @@ const FLUSH_INTERVAL_MS = 2e3;
|
|
|
1552
1556
|
const FLUSH_EVENT_THRESHOLD = 50;
|
|
1553
1557
|
/** Build-time cdkd version, with a dev fallback for non-built contexts. */
|
|
1554
1558
|
function getCdkdVersion() {
|
|
1555
|
-
return "0.
|
|
1559
|
+
return "0.228.0";
|
|
1556
1560
|
}
|
|
1557
1561
|
/**
|
|
1558
1562
|
* Generate a time-sortable unique run id, e.g.
|
|
@@ -1571,6 +1575,29 @@ function deploymentEventsKey(prefix, stackName, region, runId) {
|
|
|
1571
1575
|
function deploymentEventsIndexKey(prefix, stackName, region) {
|
|
1572
1576
|
return `${prefix}/${stackName}/${region}/deployments/index.json`;
|
|
1573
1577
|
}
|
|
1578
|
+
/** S3 key prefix under which a stack's `deployments/` artifacts live. */
|
|
1579
|
+
function deploymentsDirPrefix(prefix, stackName, region) {
|
|
1580
|
+
return `${prefix}/${stackName}/${region}/deployments/`;
|
|
1581
|
+
}
|
|
1582
|
+
/**
|
|
1583
|
+
* Parse the wall-clock time encoded in a run id's leading timestamp
|
|
1584
|
+
* (e.g. `20260613T012345678Z-1a2b3c4d`) back to epoch milliseconds.
|
|
1585
|
+
* Returns `null` for any id that does not start with the canonical
|
|
1586
|
+
* compact-ISO prefix — the age-based pruner treats an unparseable id as
|
|
1587
|
+
* "do not delete on age grounds", which is the safe direction.
|
|
1588
|
+
*/
|
|
1589
|
+
function runIdTimestampMs(runId) {
|
|
1590
|
+
const m = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(\d{3})Z/.exec(runId);
|
|
1591
|
+
if (!m) return null;
|
|
1592
|
+
const iso = `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}.${m[7]}Z`;
|
|
1593
|
+
const ms = Date.parse(iso);
|
|
1594
|
+
return Number.isNaN(ms) ? null : ms;
|
|
1595
|
+
}
|
|
1596
|
+
/** Extract the `{runId}` from a `.../{runId}.jsonl` event-stream key. */
|
|
1597
|
+
function runIdFromJsonlKey(key, dirPrefix) {
|
|
1598
|
+
if (!key.startsWith(dirPrefix) || !key.endsWith(".jsonl")) return null;
|
|
1599
|
+
return key.slice(dirPrefix.length, -6);
|
|
1600
|
+
}
|
|
1574
1601
|
/**
|
|
1575
1602
|
* Buffering JSONL writer for one deployment run. Implements the
|
|
1576
1603
|
* {@link DeploymentEventRecorder} seam the deploy engine / destroy
|
|
@@ -1634,7 +1661,8 @@ var DeploymentEventsStore = class {
|
|
|
1634
1661
|
if (this.events.length === 0) return;
|
|
1635
1662
|
await this.enqueueWrite(async () => {
|
|
1636
1663
|
await this.doFlush();
|
|
1637
|
-
await this.updateIndex(result);
|
|
1664
|
+
const keptRunIds = await this.updateIndex(result);
|
|
1665
|
+
await this.pruneSupersededRunFiles(keptRunIds);
|
|
1638
1666
|
});
|
|
1639
1667
|
}
|
|
1640
1668
|
/** Await any in-flight async flushes (used by tests). */
|
|
@@ -1671,6 +1699,10 @@ var DeploymentEventsStore = class {
|
|
|
1671
1699
|
* the last {@link DEPLOYMENT_EVENTS_MAX_INDEX_RUNS} runs. Read-modify-
|
|
1672
1700
|
* write WITHOUT optimistic locking — last-writer-wins (documented
|
|
1673
1701
|
* trade-off; the per-run `.jsonl` files are the source of truth).
|
|
1702
|
+
*
|
|
1703
|
+
* Returns the run ids retained in the index (newest-first), which the
|
|
1704
|
+
* caller feeds to {@link pruneSupersededRunFiles} so the `.jsonl` files
|
|
1705
|
+
* stay bounded to the same window as the index.
|
|
1674
1706
|
*/
|
|
1675
1707
|
async updateIndex(result) {
|
|
1676
1708
|
const key = deploymentEventsIndexKey(this.backend.prefix, this.stackName, this.region);
|
|
@@ -1701,6 +1733,35 @@ var DeploymentEventsStore = class {
|
|
|
1701
1733
|
lastModified: Date.now()
|
|
1702
1734
|
};
|
|
1703
1735
|
await this.backend.putRawObject(key, JSON.stringify(file, null, 2));
|
|
1736
|
+
return runs.map((r) => r.runId);
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Self-bounding prune (issue #885): delete `{runId}.jsonl` streams that
|
|
1740
|
+
* have fallen out of the index window so the `deployments/` prefix does
|
|
1741
|
+
* not grow without bound. Best-effort — runs inside the same write-chain
|
|
1742
|
+
* link as the flush + index write, so a failure here is caught + warned
|
|
1743
|
+
* once by {@link enqueueWrite} and never blocks the deploy / destroy.
|
|
1744
|
+
*
|
|
1745
|
+
* Concurrency-tolerant by construction: the cutoff is the OLDEST retained
|
|
1746
|
+
* run id, and only `.jsonl` files strictly below it are deleted. Run ids
|
|
1747
|
+
* are time-sortable, so a concurrent NEWER run's id sorts ABOVE every
|
|
1748
|
+
* retained id and is never touched. A concurrent run that STARTED earlier
|
|
1749
|
+
* but is still writing could in theory fall below the cutoff if 20+ newer
|
|
1750
|
+
* runs finalized while it ran — an extreme edge that self-heals anyway,
|
|
1751
|
+
* since the whole JSONL body is re-PUT on that run's next flush / finalize
|
|
1752
|
+
* (S3 has no append; each flush rewrites the full stream).
|
|
1753
|
+
*/
|
|
1754
|
+
async pruneSupersededRunFiles(keptRunIds) {
|
|
1755
|
+
if (keptRunIds.length < 20) return;
|
|
1756
|
+
const cutoff = keptRunIds.reduce((min, id) => id < min ? id : min, keptRunIds[0]);
|
|
1757
|
+
const dirPrefix = deploymentsDirPrefix(this.backend.prefix, this.stackName, this.region);
|
|
1758
|
+
const stale = (await this.backend.listRawKeys(dirPrefix)).filter((k) => {
|
|
1759
|
+
const runId = runIdFromJsonlKey(k, dirPrefix);
|
|
1760
|
+
return runId !== null && runId < cutoff;
|
|
1761
|
+
});
|
|
1762
|
+
if (stale.length === 0) return;
|
|
1763
|
+
await this.backend.deleteRawObjects(stale);
|
|
1764
|
+
this.logger.debug(`Pruned ${stale.length} superseded deployment-event stream(s) for ${this.stackName} (${this.region})`);
|
|
1704
1765
|
}
|
|
1705
1766
|
warnOnce(message) {
|
|
1706
1767
|
if (this.warnedOnce) {
|
|
@@ -1819,6 +1880,102 @@ var DeploymentEventsReader = class {
|
|
|
1819
1880
|
}
|
|
1820
1881
|
return events;
|
|
1821
1882
|
}
|
|
1883
|
+
/**
|
|
1884
|
+
* Prune a stack's recorded deployment-event runs (issue #885 — `cdkd
|
|
1885
|
+
* events prune`). Deletes `{runId}.jsonl` streams beyond a retention
|
|
1886
|
+
* window and rewrites (or removes) `index.json` to match.
|
|
1887
|
+
*
|
|
1888
|
+
* Retention semantics (see {@link DeploymentEventsPruneOptions}):
|
|
1889
|
+
* - `all` — delete every run + the index (full purge).
|
|
1890
|
+
* - `keep N` — retain the newest N runs, delete the rest.
|
|
1891
|
+
* - `olderThanMs`— delete runs whose run-id timestamp is older than the
|
|
1892
|
+
* cutoff; a run id with no parseable timestamp is kept.
|
|
1893
|
+
* - `keep` + `olderThanMs` — a run is deleted only when it is BOTH
|
|
1894
|
+
* beyond the newest-N window AND older than the cutoff
|
|
1895
|
+
* (the most conservative combination).
|
|
1896
|
+
* - none of the above — defaults to `keep DEPLOYMENT_EVENTS_MAX_INDEX_RUNS`
|
|
1897
|
+
* (matches the index window the writer self-bounds to).
|
|
1898
|
+
*
|
|
1899
|
+
* Unlike the writer's best-effort auto-prune, this is user-initiated and
|
|
1900
|
+
* surfaces errors to the caller (the command reports / exits non-zero).
|
|
1901
|
+
*/
|
|
1902
|
+
async pruneRuns(stackName, region, opts = {}) {
|
|
1903
|
+
const dirPrefix = deploymentsDirPrefix(this.backend.prefix, stackName, region);
|
|
1904
|
+
const runIdsDesc = (await this.backend.listRawKeys(dirPrefix)).map((k) => runIdFromJsonlKey(k, dirPrefix)).filter((id) => id !== null).sort().reverse();
|
|
1905
|
+
const indexKey = deploymentEventsIndexKey(this.backend.prefix, stackName, region);
|
|
1906
|
+
if (opts.all === true) {
|
|
1907
|
+
const toDelete = runIdsDesc.map((id) => deploymentEventsKey(this.backend.prefix, stackName, region, id));
|
|
1908
|
+
await this.backend.deleteRawObjects([...toDelete, indexKey]);
|
|
1909
|
+
return {
|
|
1910
|
+
deletedRunIds: runIdsDesc,
|
|
1911
|
+
remainingRunIds: [],
|
|
1912
|
+
indexDeleted: true
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
const noGuards = opts.keep === void 0 && opts.olderThanMs === void 0;
|
|
1916
|
+
const keep = opts.keep ?? (noGuards ? 20 : void 0);
|
|
1917
|
+
const protectedByCount = keep === void 0 ? /* @__PURE__ */ new Set() : new Set(runIdsDesc.slice(0, keep));
|
|
1918
|
+
let candidates = runIdsDesc.filter((id) => !protectedByCount.has(id));
|
|
1919
|
+
if (opts.olderThanMs !== void 0) {
|
|
1920
|
+
const cutoff = (opts.now ?? /* @__PURE__ */ new Date()).getTime() - opts.olderThanMs;
|
|
1921
|
+
candidates = candidates.filter((id) => {
|
|
1922
|
+
const t = runIdTimestampMs(id);
|
|
1923
|
+
return t !== null && t < cutoff;
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
if (candidates.length === 0) return {
|
|
1927
|
+
deletedRunIds: [],
|
|
1928
|
+
remainingRunIds: runIdsDesc,
|
|
1929
|
+
indexDeleted: false
|
|
1930
|
+
};
|
|
1931
|
+
const deletedSet = new Set(candidates);
|
|
1932
|
+
const deleteKeys = candidates.map((id) => deploymentEventsKey(this.backend.prefix, stackName, region, id));
|
|
1933
|
+
const remainingRunIds = runIdsDesc.filter((id) => !deletedSet.has(id));
|
|
1934
|
+
const indexDeleted = await this.rewriteIndexAfterPrune(indexKey, stackName, region, deletedSet, remainingRunIds.length === 0);
|
|
1935
|
+
await this.backend.deleteRawObjects(deleteKeys);
|
|
1936
|
+
return {
|
|
1937
|
+
deletedRunIds: candidates,
|
|
1938
|
+
remainingRunIds,
|
|
1939
|
+
indexDeleted
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* Drop the pruned run ids from `index.json`, or delete the index entirely
|
|
1944
|
+
* when no `.jsonl` streams remain. A corrupt / unreadable index is left
|
|
1945
|
+
* untouched (the `.jsonl` files are the source of truth; `cdkd events`
|
|
1946
|
+
* falls back to key enumeration). Returns whether the index was deleted.
|
|
1947
|
+
*/
|
|
1948
|
+
async rewriteIndexAfterPrune(indexKey, stackName, region, deletedRunIds, noRunsRemain) {
|
|
1949
|
+
if (noRunsRemain) {
|
|
1950
|
+
await this.backend.deleteRawObjects([indexKey]);
|
|
1951
|
+
return true;
|
|
1952
|
+
}
|
|
1953
|
+
let raw;
|
|
1954
|
+
try {
|
|
1955
|
+
raw = await this.backend.getRawObject(indexKey);
|
|
1956
|
+
} catch {
|
|
1957
|
+
return false;
|
|
1958
|
+
}
|
|
1959
|
+
if (raw === null) return false;
|
|
1960
|
+
let parsed;
|
|
1961
|
+
try {
|
|
1962
|
+
parsed = JSON.parse(raw);
|
|
1963
|
+
} catch {
|
|
1964
|
+
return false;
|
|
1965
|
+
}
|
|
1966
|
+
if (!Array.isArray(parsed.runs)) return false;
|
|
1967
|
+
const remaining = parsed.runs.filter((r) => !deletedRunIds.has(r.runId));
|
|
1968
|
+
if (remaining.length === parsed.runs.length) return false;
|
|
1969
|
+
const file = {
|
|
1970
|
+
indexVersion: 1,
|
|
1971
|
+
stackName,
|
|
1972
|
+
region,
|
|
1973
|
+
runs: remaining,
|
|
1974
|
+
lastModified: Date.now()
|
|
1975
|
+
};
|
|
1976
|
+
await this.backend.putRawObject(indexKey, JSON.stringify(file, null, 2));
|
|
1977
|
+
return false;
|
|
1978
|
+
}
|
|
1822
1979
|
};
|
|
1823
1980
|
|
|
1824
1981
|
//#endregion
|
|
@@ -9757,6 +9914,7 @@ var DynamoDBTableProvider = class {
|
|
|
9757
9914
|
this.logger.debug(`Updated WarmThroughput on DynamoDB table ${physicalId}`);
|
|
9758
9915
|
}
|
|
9759
9916
|
}
|
|
9917
|
+
if (JSON.stringify(properties["GlobalSecondaryIndexes"]) !== JSON.stringify(previousProperties["GlobalSecondaryIndexes"])) await this.applyGsiUpdates(physicalId, resourceType, logicalId, previousProperties["GlobalSecondaryIndexes"], properties["GlobalSecondaryIndexes"], properties["AttributeDefinitions"]);
|
|
9760
9918
|
if (JSON.stringify(properties["PointInTimeRecoverySpecification"]) !== JSON.stringify(previousProperties["PointInTimeRecoverySpecification"])) await this.applyPointInTimeRecovery(physicalId, properties["PointInTimeRecoverySpecification"], previousProperties["PointInTimeRecoverySpecification"]);
|
|
9761
9919
|
if (JSON.stringify(properties["TimeToLiveSpecification"]) !== JSON.stringify(previousProperties["TimeToLiveSpecification"])) await this.applyTimeToLive(physicalId, properties["TimeToLiveSpecification"], previousProperties["TimeToLiveSpecification"]);
|
|
9762
9920
|
if (JSON.stringify(properties["ResourcePolicy"]) !== JSON.stringify(previousProperties["ResourcePolicy"])) {
|
|
@@ -10082,6 +10240,79 @@ var DynamoDBTableProvider = class {
|
|
|
10082
10240
|
throw new Error(`Table ${tableName} did not reach ACTIVE status within ${maxAttempts} seconds after UpdateTable`);
|
|
10083
10241
|
}
|
|
10084
10242
|
/**
|
|
10243
|
+
* Poll DescribeTable until the table is ACTIVE AND every Global Secondary
|
|
10244
|
+
* Index is ACTIVE (not CREATING / UPDATING / DELETING / BACKFILLING). Used
|
|
10245
|
+
* between GSI mutations because a freshly-created index keeps backfilling
|
|
10246
|
+
* after the table itself returns to ACTIVE, and AWS rejects the next GSI op
|
|
10247
|
+
* until the prior index settles.
|
|
10248
|
+
*/
|
|
10249
|
+
async waitForTableAndIndexesActive(tableName, maxAttempts = 1800) {
|
|
10250
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
10251
|
+
const table = (await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table;
|
|
10252
|
+
const tableActive = table?.TableStatus === "ACTIVE";
|
|
10253
|
+
const indexesActive = (table?.GlobalSecondaryIndexes ?? []).every((gsi) => gsi.IndexStatus === "ACTIVE");
|
|
10254
|
+
if (tableActive && indexesActive) return;
|
|
10255
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
10256
|
+
}
|
|
10257
|
+
throw new Error(`Table ${tableName} and its global secondary indexes did not reach ACTIVE within ${maxAttempts} seconds after UpdateTable`);
|
|
10258
|
+
}
|
|
10259
|
+
/**
|
|
10260
|
+
* Apply Global Secondary Index add / remove / per-index throughput changes
|
|
10261
|
+
* via UpdateTable's `GlobalSecondaryIndexUpdates`.
|
|
10262
|
+
*
|
|
10263
|
+
* AWS constraints mirrored here:
|
|
10264
|
+
* - At most ONE GSI Create or Delete per UpdateTable call; a second
|
|
10265
|
+
* mutation while the table / an index is still building is rejected. Each
|
|
10266
|
+
* op therefore runs in its own UpdateTable and waits for ACTIVE before the
|
|
10267
|
+
* next (creating GSIs also go through a BACKFILLING phase that ACTIVE
|
|
10268
|
+
* covers).
|
|
10269
|
+
* - A GSI `Create` must carry the AttributeDefinitions for the new index's
|
|
10270
|
+
* key attributes — the full desired AttributeDefinitions array is passed
|
|
10271
|
+
* so every newly-referenced attribute is defined.
|
|
10272
|
+
* - On an existing same-name index, only a PROVISIONED `ProvisionedThroughput`
|
|
10273
|
+
* (RCU/WCU) change is mutable in place and is issued as an `Update`. A
|
|
10274
|
+
* `KeySchema` / `Projection` change is immutable in place (AWS models it as
|
|
10275
|
+
* a delete + re-create); since the diff keys GSIs by name it would not emit
|
|
10276
|
+
* a remove-then-add pair, so this method throws on such a change rather than
|
|
10277
|
+
* silently dropping it.
|
|
10278
|
+
*/
|
|
10279
|
+
async applyGsiUpdates(physicalId, resourceType, logicalId, previousGsis, desiredGsis, desiredAttributeDefinitions) {
|
|
10280
|
+
const prev = previousGsis ?? [];
|
|
10281
|
+
const desired = desiredGsis ?? [];
|
|
10282
|
+
const prevByName = new Map(prev.filter((g) => g.IndexName).map((g) => [g.IndexName, g]));
|
|
10283
|
+
const desiredByName = new Map(desired.filter((g) => g.IndexName).map((g) => [g.IndexName, g]));
|
|
10284
|
+
const ops = [];
|
|
10285
|
+
for (const name of prevByName.keys()) if (!desiredByName.has(name)) ops.push({ Delete: { IndexName: name } });
|
|
10286
|
+
for (const [name, gsi] of desiredByName) if (!prevByName.has(name)) {
|
|
10287
|
+
if (!gsi.KeySchema) throw new ProvisioningError(`GlobalSecondaryIndex ${name} on DynamoDB table ${logicalId} is missing KeySchema`, resourceType, logicalId, physicalId);
|
|
10288
|
+
ops.push({ Create: {
|
|
10289
|
+
IndexName: name,
|
|
10290
|
+
KeySchema: gsi.KeySchema,
|
|
10291
|
+
Projection: gsi.Projection,
|
|
10292
|
+
...gsi.ProvisionedThroughput ? { ProvisionedThroughput: gsi.ProvisionedThroughput } : {},
|
|
10293
|
+
...gsi.OnDemandThroughput ? { OnDemandThroughput: gsi.OnDemandThroughput } : {}
|
|
10294
|
+
} });
|
|
10295
|
+
} else {
|
|
10296
|
+
const before = prevByName.get(name);
|
|
10297
|
+
if (JSON.stringify(before.KeySchema) !== JSON.stringify(gsi.KeySchema) || JSON.stringify(before.Projection) !== JSON.stringify(gsi.Projection)) throw new ProvisioningError(`GlobalSecondaryIndex ${name} on DynamoDB table ${logicalId} changed its KeySchema or Projection, which DynamoDB cannot modify in place. Rename the index (so it is dropped and re-created) or replace the table.`, resourceType, logicalId, physicalId);
|
|
10298
|
+
if (gsi.ProvisionedThroughput && JSON.stringify(before.ProvisionedThroughput) !== JSON.stringify(gsi.ProvisionedThroughput)) ops.push({ Update: {
|
|
10299
|
+
IndexName: name,
|
|
10300
|
+
ProvisionedThroughput: gsi.ProvisionedThroughput
|
|
10301
|
+
} });
|
|
10302
|
+
}
|
|
10303
|
+
for (const op of ops) {
|
|
10304
|
+
const input = {
|
|
10305
|
+
TableName: physicalId,
|
|
10306
|
+
GlobalSecondaryIndexUpdates: [op]
|
|
10307
|
+
};
|
|
10308
|
+
if (op.Create && desiredAttributeDefinitions) input.AttributeDefinitions = desiredAttributeDefinitions;
|
|
10309
|
+
await this.dynamoDBClient.send(new UpdateTableCommand(input));
|
|
10310
|
+
await this.waitForTableAndIndexesActive(physicalId);
|
|
10311
|
+
const verb = op.Create ? "created" : op.Delete ? "deleted" : "updated";
|
|
10312
|
+
this.logger.debug(`${verb} GSI ${op.Create?.IndexName ?? op.Delete?.IndexName ?? op.Update?.IndexName} on DynamoDB table ${physicalId}`);
|
|
10313
|
+
}
|
|
10314
|
+
}
|
|
10315
|
+
/**
|
|
10085
10316
|
* Resolve a single `Fn::GetAtt` attribute for an existing DynamoDB table.
|
|
10086
10317
|
*
|
|
10087
10318
|
* CloudFormation's `AWS::DynamoDB::Table` exposes `Arn`, `StreamArn`
|
|
@@ -37276,7 +37507,7 @@ async function retireCloudFormationStack(options) {
|
|
|
37276
37507
|
}
|
|
37277
37508
|
} else ({body: newBody, modified, format} = injectRetainPolicies(tpl.TemplateBody, cfnStackName));
|
|
37278
37509
|
if (!yes) {
|
|
37279
|
-
if (!await confirmPrompt$
|
|
37510
|
+
if (!await confirmPrompt$6(`Set DeletionPolicy=Retain and UpdateReplacePolicy=Retain on every resource in CloudFormation stack '${cfnStackName}', then delete the stack? AWS resources will NOT be deleted (cdkd state has been written).`)) {
|
|
37280
37511
|
logger.info("CloudFormation stack retirement cancelled. cdkd state is unaffected.");
|
|
37281
37512
|
for (const cleanup of nestedCleanups) try {
|
|
37282
37513
|
await cleanup();
|
|
@@ -37653,7 +37884,7 @@ async function walkCfnStackTree(stackName, physicalId, cfnClient) {
|
|
|
37653
37884
|
nested
|
|
37654
37885
|
};
|
|
37655
37886
|
}
|
|
37656
|
-
async function confirmPrompt$
|
|
37887
|
+
async function confirmPrompt$6(prompt) {
|
|
37657
37888
|
const rl = readline$1.createInterface({
|
|
37658
37889
|
input: process.stdin,
|
|
37659
37890
|
output: process.stdout
|
|
@@ -38822,7 +39053,7 @@ async function runAccept(reports, stateBackend, stateConfig, awsClients, options
|
|
|
38822
39053
|
return;
|
|
38823
39054
|
}
|
|
38824
39055
|
if (!options.yes) {
|
|
38825
|
-
if (!await confirmPrompt$
|
|
39056
|
+
if (!await confirmPrompt$5(`Update cdkd state with the AWS-current values shown above?`)) {
|
|
38826
39057
|
logger.info("Aborted.");
|
|
38827
39058
|
return;
|
|
38828
39059
|
}
|
|
@@ -38948,7 +39179,7 @@ async function runRevert(reports, providerRegistry, stateConfig, awsClients, opt
|
|
|
38948
39179
|
return;
|
|
38949
39180
|
}
|
|
38950
39181
|
if (!options.yes) {
|
|
38951
|
-
if (!await confirmPrompt$
|
|
39182
|
+
if (!await confirmPrompt$5(`Push cdkd state values back into AWS for the resources shown above?`)) {
|
|
38952
39183
|
logger.info("Aborted.");
|
|
38953
39184
|
return;
|
|
38954
39185
|
}
|
|
@@ -39056,7 +39287,7 @@ async function runWithConcurrency(tasks, concurrency) {
|
|
|
39056
39287
|
})());
|
|
39057
39288
|
await Promise.all(workers);
|
|
39058
39289
|
}
|
|
39059
|
-
async function confirmPrompt$
|
|
39290
|
+
async function confirmPrompt$5(prompt) {
|
|
39060
39291
|
const rl = readline$1.createInterface({
|
|
39061
39292
|
input: process.stdin,
|
|
39062
39293
|
output: process.stdout
|
|
@@ -39518,6 +39749,86 @@ async function resolveEventsRegion(reader, stackName, explicitRegion) {
|
|
|
39518
39749
|
if (regions.length > 1) throw new CdkdError(`Stack '${stackName}' has deployment-event history in multiple regions: ${regions.join(", ")}. Re-run with '--stack-region <region>' to disambiguate.`, "EVENTS_REGION_AMBIGUOUS");
|
|
39519
39750
|
return regions[0];
|
|
39520
39751
|
}
|
|
39752
|
+
/**
|
|
39753
|
+
* `cdkd events prune <stack>` — reclaim S3 space by deleting old per-run
|
|
39754
|
+
* `{runId}.jsonl` event streams (issue #885). `cdkd destroy` deliberately
|
|
39755
|
+
* keeps event history as post-mortem context, so this is the explicit way
|
|
39756
|
+
* to purge it; the deploy/destroy writer also self-bounds to the last
|
|
39757
|
+
* {@link DEPLOYMENT_EVENTS_MAX_INDEX_RUNS} runs automatically.
|
|
39758
|
+
*
|
|
39759
|
+
* Retention selection:
|
|
39760
|
+
* - `--all` purge every run + the index.
|
|
39761
|
+
* - `--keep <N>` retain the newest N runs.
|
|
39762
|
+
* - `--older-than <dur>` delete runs older than the duration.
|
|
39763
|
+
* - both keep+older-than: delete runs that are BOTH beyond newest-N AND
|
|
39764
|
+
* older than the cutoff.
|
|
39765
|
+
* - none of the above: default to keeping the newest
|
|
39766
|
+
* {@link DEPLOYMENT_EVENTS_MAX_INDEX_RUNS}.
|
|
39767
|
+
*/
|
|
39768
|
+
async function eventsPruneCommand(stackName, options) {
|
|
39769
|
+
const logger = getLogger();
|
|
39770
|
+
if (options.verbose) logger.setLevel("debug");
|
|
39771
|
+
warnIfDeprecatedRegion(options);
|
|
39772
|
+
if (options.all === true && (options.keep !== void 0 || options.olderThan !== void 0)) throw new CdkdError("'--all' purges every run and cannot be combined with '--keep' / '--older-than'.", "EVENTS_PRUNE_BAD_FLAGS");
|
|
39773
|
+
const olderThanMs = options.olderThan !== void 0 ? parseDuration(options.olderThan) : void 0;
|
|
39774
|
+
const awsClients = new AwsClients({
|
|
39775
|
+
...options.region && { region: options.region },
|
|
39776
|
+
...options.profile && { profile: options.profile }
|
|
39777
|
+
});
|
|
39778
|
+
setAwsClients(awsClients);
|
|
39779
|
+
try {
|
|
39780
|
+
const region = options.region || process.env["AWS_REGION"] || "us-east-1";
|
|
39781
|
+
const bucket = await resolveStateBucketWithDefault(options.stateBucket, region);
|
|
39782
|
+
const prefix = options.statePrefix ?? "cdkd";
|
|
39783
|
+
const stateBackend = new S3StateBackend(awsClients.s3, {
|
|
39784
|
+
bucket,
|
|
39785
|
+
prefix
|
|
39786
|
+
}, {
|
|
39787
|
+
region,
|
|
39788
|
+
...options.profile && { profile: options.profile }
|
|
39789
|
+
});
|
|
39790
|
+
await stateBackend.verifyBucketExists();
|
|
39791
|
+
const reader = new DeploymentEventsReader(stateBackend);
|
|
39792
|
+
const targetRegion = await resolveEventsRegion(reader, stackName, options.stackRegion);
|
|
39793
|
+
const totalRuns = (await reader.listRuns(stackName, targetRegion)).length;
|
|
39794
|
+
const scope = options.all ? `ALL ${totalRuns} run(s)` : options.keep !== void 0 && olderThanMs !== void 0 ? `runs beyond the newest ${options.keep} AND older than ${options.olderThan}` : options.keep !== void 0 ? `runs beyond the newest ${options.keep}` : olderThanMs !== void 0 ? `runs older than ${options.olderThan}` : `runs beyond the newest ${20}`;
|
|
39795
|
+
if (options.yes !== true) {
|
|
39796
|
+
if (!process.stdin.isTTY) {
|
|
39797
|
+
logger.info(gray(`Refusing to prune deployment-event history for ${stackName} (${targetRegion}) without confirmation on a non-interactive terminal. Re-run with --yes to proceed.`));
|
|
39798
|
+
return;
|
|
39799
|
+
}
|
|
39800
|
+
if (!await confirmPrompt$4(`Prune deployment-event history for ${cyan(stackName)} ${gray(`(${targetRegion})`)}: ${scope}?`)) {
|
|
39801
|
+
logger.info(gray("Aborted; nothing was deleted."));
|
|
39802
|
+
return;
|
|
39803
|
+
}
|
|
39804
|
+
}
|
|
39805
|
+
const result = await reader.pruneRuns(stackName, targetRegion, {
|
|
39806
|
+
...options.all === true && { all: true },
|
|
39807
|
+
...options.keep !== void 0 && { keep: options.keep },
|
|
39808
|
+
...olderThanMs !== void 0 && { olderThanMs }
|
|
39809
|
+
});
|
|
39810
|
+
if (result.deletedRunIds.length === 0) {
|
|
39811
|
+
logger.info(gray(result.indexDeleted ? `Removed the empty deployment-event index for ${stackName} (${targetRegion}); no run streams to delete.` : `No runs matched the prune criteria for ${stackName} (${targetRegion}).`));
|
|
39812
|
+
return;
|
|
39813
|
+
}
|
|
39814
|
+
logger.info(`${green("Pruned")} ${result.deletedRunIds.length} deployment-event run(s) for ${cyan(stackName)} ${gray(`(${targetRegion})`)}; ${result.remainingRunIds.length} retained` + (result.indexDeleted ? gray(" (index removed)") : "") + ".");
|
|
39815
|
+
} finally {
|
|
39816
|
+
awsClients.destroy();
|
|
39817
|
+
}
|
|
39818
|
+
}
|
|
39819
|
+
/** Minimal `(y/N)` confirmation prompt. */
|
|
39820
|
+
async function confirmPrompt$4(prompt) {
|
|
39821
|
+
const rl = readline$1.createInterface({
|
|
39822
|
+
input: process.stdin,
|
|
39823
|
+
output: process.stdout
|
|
39824
|
+
});
|
|
39825
|
+
try {
|
|
39826
|
+
const ans = await rl.question(`${prompt} [y/N] `);
|
|
39827
|
+
return /^y(es)?$/i.test(ans.trim());
|
|
39828
|
+
} finally {
|
|
39829
|
+
rl.close();
|
|
39830
|
+
}
|
|
39831
|
+
}
|
|
39521
39832
|
/** Human-readable run listing (newest first). */
|
|
39522
39833
|
function printRunList(stackName, region, runs) {
|
|
39523
39834
|
const logger = getLogger();
|
|
@@ -39578,8 +39889,27 @@ function createEventsCommand() {
|
|
|
39578
39889
|
}));
|
|
39579
39890
|
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
39580
39891
|
cmd.addOption(deprecatedRegionOption);
|
|
39892
|
+
cmd.addCommand(createEventsPruneCommand());
|
|
39581
39893
|
return cmd;
|
|
39582
39894
|
}
|
|
39895
|
+
/**
|
|
39896
|
+
* Create the `cdkd events prune <stack>` subcommand (issue #885).
|
|
39897
|
+
*
|
|
39898
|
+
* Only the prune-SPECIFIC options (`--keep` / `--older-than` / `--all`) are
|
|
39899
|
+
* declared here. The shared option blocks (`commonOptions` + `stateOptions`
|
|
39900
|
+
* + the deprecated region option) and `--stack-region` are inherited from
|
|
39901
|
+
* the parent `events` command — declaring the same flag on BOTH parent and
|
|
39902
|
+
* child makes Commander route a post-subcommand flag (`events prune X --yes`)
|
|
39903
|
+
* to the PARENT's storage, leaving the child's value at its default. The
|
|
39904
|
+
* action therefore reads the merged view via `command.optsWithGlobals()`.
|
|
39905
|
+
*/
|
|
39906
|
+
function createEventsPruneCommand() {
|
|
39907
|
+
return new Command("prune").description("Delete old per-run deployment-event streams to reclaim S3 space").argument("<stack>", "Stack name (physical CloudFormation name)").addOption(new Option("--keep <N>", "Retain only the newest N runs").argParser((v) => {
|
|
39908
|
+
const n = parseInt(v, 10);
|
|
39909
|
+
if (!Number.isInteger(n) || n < 0) throw new Error(`Invalid --keep value "${v}": expected a non-negative integer.`);
|
|
39910
|
+
return n;
|
|
39911
|
+
})).option("--older-than <duration>", "Delete runs older than this duration (e.g. 24h, 90m)").option("--all", "Delete every recorded run and the index (full purge)", false).action(withErrorHandling((stack, _options, command) => eventsPruneCommand(stack, command.optsWithGlobals())));
|
|
39912
|
+
}
|
|
39583
39913
|
|
|
39584
39914
|
//#endregion
|
|
39585
39915
|
//#region src/cli/cdk-path.ts
|
|
@@ -54622,7 +54952,7 @@ function reorderArgs(argv) {
|
|
|
54622
54952
|
async function main() {
|
|
54623
54953
|
installPipeCloseHandler();
|
|
54624
54954
|
const program = new Command();
|
|
54625
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
54955
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.228.0");
|
|
54626
54956
|
program.addCommand(createBootstrapCommand());
|
|
54627
54957
|
program.addCommand(createSynthCommand());
|
|
54628
54958
|
program.addCommand(createListCommand());
|