@go-to-k/cdkd 0.48.0 → 0.50.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 +59 -0
- package/dist/cli.js +231 -7
- package/dist/cli.js.map +2 -2
- package/dist/go-to-k-cdkd-0.50.0.tgz +0 -0
- package/package.json +1 -1
- package/dist/go-to-k-cdkd-0.48.0.tgz +0 -0
package/README.md
CHANGED
|
@@ -171,6 +171,57 @@ the full per-type table.
|
|
|
171
171
|
| CC API null value stripping | ✅ | Removes null values before API calls |
|
|
172
172
|
| Retry with HTTP status codes | ✅ | 429/503 + cause chain inspection |
|
|
173
173
|
|
|
174
|
+
### Drift detection
|
|
175
|
+
|
|
176
|
+
`cdkd drift <stack>` (state-driven; no synth) compares each resource
|
|
177
|
+
between the AWS-current snapshot returned by `provider.readCurrentState`
|
|
178
|
+
and the **deploy-time AWS snapshot** stored in
|
|
179
|
+
`ResourceState.observedProperties`. The observedProperties baseline is
|
|
180
|
+
populated automatically on every successful `cdkd deploy` /
|
|
181
|
+
`cdkd import`, so console-side changes to keys you did NOT template
|
|
182
|
+
(IAM policies attached out-of-band, S3 public-access-block toggled,
|
|
183
|
+
etc.) surface as drift instead of being silently ignored.
|
|
184
|
+
|
|
185
|
+
State schema `version: 3` (the layout that carries observedProperties)
|
|
186
|
+
is auto-migrated on the next write — no user action needed for new
|
|
187
|
+
deploys. **For stacks already deployed with an older binary**, the
|
|
188
|
+
upgrade story is:
|
|
189
|
+
|
|
190
|
+
1. `cdkd 0.46.x` (or earlier) deployed your stack — state.json is
|
|
191
|
+
`version: 2`, no observedProperties on any resource.
|
|
192
|
+
2. Upgrade cdkd to `0.47.0+`. The new binary reads v2 state cleanly,
|
|
193
|
+
and `cdkd drift` falls back to comparing against the user-templated
|
|
194
|
+
`properties` field (= the pre-v3 behavior) for any resource that
|
|
195
|
+
hasn't been refreshed yet.
|
|
196
|
+
3. **Populate observedProperties** for the existing stack — pick one:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
# Option A (recommended): explicit refresh, no redeploy.
|
|
200
|
+
cdkd state refresh-observed MyStack
|
|
201
|
+
|
|
202
|
+
# Option B: trigger an UPDATE on each affected resource via the
|
|
203
|
+
# next `cdkd deploy`. NO_CHANGE-skipped resources are NOT refreshed
|
|
204
|
+
# by deploy alone — only the ones whose template changed get a
|
|
205
|
+
# readCurrentState call. Use this only if you're already changing
|
|
206
|
+
# the template; for an upgrade-and-refresh-only flow prefer A.
|
|
207
|
+
cdkd deploy MyStack
|
|
208
|
+
```
|
|
209
|
+
4. Re-run `cdkd drift MyStack` — now observed-baseline drift detection
|
|
210
|
+
is fully enabled.
|
|
211
|
+
|
|
212
|
+
`cdkd state refresh-observed --all` does the same for every stack in
|
|
213
|
+
the state bucket; `--dry-run` prints the per-stack refresh count
|
|
214
|
+
without touching state. Resolve any drift the comparator finds with
|
|
215
|
+
`cdkd drift <stack> --accept` (state ← AWS) or `--revert` (AWS ← state).
|
|
216
|
+
|
|
217
|
+
`cdkd deploy --no-capture-observed-state` (or
|
|
218
|
+
`cdk.json context.cdkd.captureObservedState: false`) opts out of the
|
|
219
|
+
capture entirely if you care more about deploy speed than rich drift
|
|
220
|
+
detection — drift then falls back to comparing against `properties`,
|
|
221
|
+
the pre-v3 behavior. Bench measurements show roughly +0–4% deploy
|
|
222
|
+
time with the capture on (lambda integ within noise; bench-cdk-sample
|
|
223
|
+
+3.4% median).
|
|
224
|
+
|
|
174
225
|
## Prerequisites
|
|
175
226
|
|
|
176
227
|
- **Node.js** >= 20.0.0
|
|
@@ -300,6 +351,14 @@ cdkd drift MyStack --accept --yes
|
|
|
300
351
|
# Resolve drift: AWS ← state (push state values back into AWS via provider.update)
|
|
301
352
|
cdkd drift MyStack --revert --yes
|
|
302
353
|
|
|
354
|
+
# Refresh the deploy-time AWS snapshot used as drift baseline.
|
|
355
|
+
# Run this once after upgrading from a pre-v3 cdkd binary (= state schema
|
|
356
|
+
# `version: 2`) so console-side changes to keys you didn't template can
|
|
357
|
+
# be detected for resources that won't change in any near-future deploy.
|
|
358
|
+
# Same idempotent behavior on the same v3 state — see "Drift detection"
|
|
359
|
+
# below for the full upgrade story.
|
|
360
|
+
cdkd state refresh-observed MyStack
|
|
361
|
+
|
|
303
362
|
# Dry run (plan only, no changes)
|
|
304
363
|
cdkd deploy --dry-run
|
|
305
364
|
|
package/dist/cli.js
CHANGED
|
@@ -38666,10 +38666,11 @@ init_aws_clients();
|
|
|
38666
38666
|
function calculateResourceDrift(stateProperties, awsProperties, options) {
|
|
38667
38667
|
const drifts = [];
|
|
38668
38668
|
const ignore = options?.ignorePaths ?? [];
|
|
38669
|
+
const union = options?.unionWalkObjects ?? false;
|
|
38669
38670
|
for (const key of Object.keys(stateProperties)) {
|
|
38670
38671
|
if (isIgnoredPath(key, ignore))
|
|
38671
38672
|
continue;
|
|
38672
|
-
diffAt(key, stateProperties[key], awsProperties[key], drifts, ignore);
|
|
38673
|
+
diffAt(key, stateProperties[key], awsProperties[key], drifts, ignore, union);
|
|
38673
38674
|
}
|
|
38674
38675
|
return drifts;
|
|
38675
38676
|
}
|
|
@@ -38682,15 +38683,16 @@ function isIgnoredPath(path, ignorePaths) {
|
|
|
38682
38683
|
}
|
|
38683
38684
|
return false;
|
|
38684
38685
|
}
|
|
38685
|
-
function diffAt(path, stateValue, awsValue, out, ignorePaths) {
|
|
38686
|
+
function diffAt(path, stateValue, awsValue, out, ignorePaths, unionWalkObjects) {
|
|
38686
38687
|
if (deepEqual(stateValue, awsValue))
|
|
38687
38688
|
return;
|
|
38688
38689
|
if (isPlainObject(stateValue) && isPlainObject(awsValue) && !Array.isArray(stateValue) && !Array.isArray(awsValue)) {
|
|
38689
|
-
|
|
38690
|
+
const keys = unionWalkObjects ? /* @__PURE__ */ new Set([...Object.keys(stateValue), ...Object.keys(awsValue)]) : Object.keys(stateValue);
|
|
38691
|
+
for (const key of keys) {
|
|
38690
38692
|
const childPath = `${path}.${key}`;
|
|
38691
38693
|
if (isIgnoredPath(childPath, ignorePaths))
|
|
38692
38694
|
continue;
|
|
38693
|
-
diffAt(childPath, stateValue[key], awsValue[key], out, ignorePaths);
|
|
38695
|
+
diffAt(childPath, stateValue[key], awsValue[key], out, ignorePaths, unionWalkObjects);
|
|
38694
38696
|
}
|
|
38695
38697
|
return;
|
|
38696
38698
|
}
|
|
@@ -39028,6 +39030,14 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
|
|
|
39028
39030
|
resource.properties ?? {}
|
|
39029
39031
|
);
|
|
39030
39032
|
} else {
|
|
39033
|
+
if (resource.resourceType.startsWith("Custom::")) {
|
|
39034
|
+
outcomes.push({
|
|
39035
|
+
kind: "unsupported",
|
|
39036
|
+
logicalId,
|
|
39037
|
+
resourceType: resource.resourceType
|
|
39038
|
+
});
|
|
39039
|
+
continue;
|
|
39040
|
+
}
|
|
39031
39041
|
if (CC_API_FALLBACK_DENY_LIST[resource.resourceType]) {
|
|
39032
39042
|
outcomes.push({
|
|
39033
39043
|
kind: "unsupported",
|
|
@@ -39061,8 +39071,12 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
|
|
|
39061
39071
|
continue;
|
|
39062
39072
|
}
|
|
39063
39073
|
const ignorePaths = provider.getDriftUnknownPaths ? provider.getDriftUnknownPaths(resource.resourceType) : [];
|
|
39064
|
-
const
|
|
39065
|
-
const
|
|
39074
|
+
const useObserved = resource.observedProperties !== void 0;
|
|
39075
|
+
const baseline = useObserved ? resource.observedProperties : resource.properties ?? {};
|
|
39076
|
+
const changes = calculateResourceDrift(baseline, aws, {
|
|
39077
|
+
ignorePaths,
|
|
39078
|
+
unionWalkObjects: useObserved
|
|
39079
|
+
});
|
|
39066
39080
|
if (changes.length === 0) {
|
|
39067
39081
|
outcomes.push({ kind: "clean", logicalId, resourceType: resource.resourceType });
|
|
39068
39082
|
} else {
|
|
@@ -41846,6 +41860,215 @@ function createStateInfoCommand() {
|
|
|
41846
41860
|
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
41847
41861
|
return cmd;
|
|
41848
41862
|
}
|
|
41863
|
+
async function stateRefreshObservedCommand(stackArgs, options) {
|
|
41864
|
+
const logger = getLogger();
|
|
41865
|
+
if (options.verbose)
|
|
41866
|
+
logger.setLevel("debug");
|
|
41867
|
+
if (!options.all && stackArgs.length === 0) {
|
|
41868
|
+
throw new Error(
|
|
41869
|
+
"Stack name is required. Usage: cdkd state refresh-observed <stack> [<stack>...] | --all"
|
|
41870
|
+
);
|
|
41871
|
+
}
|
|
41872
|
+
const setup = await setupStateBackend(options);
|
|
41873
|
+
const providerRegistry = new ProviderRegistry();
|
|
41874
|
+
registerAllProviders(providerRegistry);
|
|
41875
|
+
providerRegistry.setCustomResourceResponseBucket(setup.bucket);
|
|
41876
|
+
try {
|
|
41877
|
+
const stateRefs = await setup.stateBackend.listStacks();
|
|
41878
|
+
let targets;
|
|
41879
|
+
if (options.all) {
|
|
41880
|
+
targets = options.stackRegion ? stateRefs.filter((r) => r.region === options.stackRegion) : stateRefs;
|
|
41881
|
+
if (targets.length === 0) {
|
|
41882
|
+
logger.info("No stacks found in state");
|
|
41883
|
+
return;
|
|
41884
|
+
}
|
|
41885
|
+
} else {
|
|
41886
|
+
targets = [];
|
|
41887
|
+
for (const stackName of stackArgs) {
|
|
41888
|
+
const matches = stateRefs.filter((r) => r.stackName === stackName);
|
|
41889
|
+
if (matches.length === 0) {
|
|
41890
|
+
throw new Error(
|
|
41891
|
+
`No state found for stack '${stackName}'. Run 'cdkd state list' to see available stacks.`
|
|
41892
|
+
);
|
|
41893
|
+
}
|
|
41894
|
+
if (options.stackRegion) {
|
|
41895
|
+
const ref = matches.find((r) => r.region === options.stackRegion);
|
|
41896
|
+
if (!ref) {
|
|
41897
|
+
const seen = matches.map((r) => r.region ?? "(legacy)").join(", ");
|
|
41898
|
+
throw new Error(
|
|
41899
|
+
`No state found for stack '${stackName}' in region '${options.stackRegion}'. Available regions: ${seen}.`
|
|
41900
|
+
);
|
|
41901
|
+
}
|
|
41902
|
+
targets.push(ref);
|
|
41903
|
+
} else if (matches.length === 1) {
|
|
41904
|
+
targets.push(matches[0]);
|
|
41905
|
+
} else {
|
|
41906
|
+
const regions = matches.map((r) => r.region ?? "(legacy)").join(", ");
|
|
41907
|
+
throw new Error(
|
|
41908
|
+
`Stack '${stackName}' has state in multiple regions: ${regions}. Re-run with --stack-region <region> to disambiguate.`
|
|
41909
|
+
);
|
|
41910
|
+
}
|
|
41911
|
+
}
|
|
41912
|
+
}
|
|
41913
|
+
if (!options.yes && !options.dryRun) {
|
|
41914
|
+
const targetList = targets.map(formatStackRef).join(", ");
|
|
41915
|
+
const ok = await confirmRefresh(
|
|
41916
|
+
`Refresh observedProperties for ${targets.length} stack(s) (${targetList})?`
|
|
41917
|
+
);
|
|
41918
|
+
if (!ok) {
|
|
41919
|
+
logger.info("Aborted.");
|
|
41920
|
+
return;
|
|
41921
|
+
}
|
|
41922
|
+
}
|
|
41923
|
+
let totalRefreshed = 0;
|
|
41924
|
+
let totalUnsupported = 0;
|
|
41925
|
+
let totalFailed = 0;
|
|
41926
|
+
for (const target of targets) {
|
|
41927
|
+
if (!target.region) {
|
|
41928
|
+
throw new Error(
|
|
41929
|
+
`Stack '${target.stackName}' has only a legacy state record without a region. Run 'cdkd deploy ${target.stackName}' (or any cdkd write) first to migrate it to the region-scoped layout, then re-run refresh-observed.`
|
|
41930
|
+
);
|
|
41931
|
+
}
|
|
41932
|
+
const counts = await refreshObservedForStack(
|
|
41933
|
+
target.stackName,
|
|
41934
|
+
target.region,
|
|
41935
|
+
setup.stateBackend,
|
|
41936
|
+
setup.lockManager,
|
|
41937
|
+
providerRegistry,
|
|
41938
|
+
{ dryRun: options.dryRun ?? false, logger }
|
|
41939
|
+
);
|
|
41940
|
+
totalRefreshed += counts.refreshed;
|
|
41941
|
+
totalUnsupported += counts.unsupported;
|
|
41942
|
+
totalFailed += counts.failed;
|
|
41943
|
+
}
|
|
41944
|
+
const summary = options.dryRun ? `Plan: ${totalRefreshed} resource(s) would be refreshed, ${totalUnsupported} unsupported, ${totalFailed} would fail (--dry-run, no state was written)` : `Done: ${totalRefreshed} resource(s) refreshed, ${totalUnsupported} unsupported, ${totalFailed} failed`;
|
|
41945
|
+
logger.info(`
|
|
41946
|
+
${summary}`);
|
|
41947
|
+
if (totalFailed > 0) {
|
|
41948
|
+
throw new PartialFailureError(
|
|
41949
|
+
`Refresh completed with ${totalFailed} per-resource readCurrentState failure(s). Affected resources keep their previous observedProperties (or no observedProperties at all). Re-run 'cdkd state refresh-observed' to retry.`
|
|
41950
|
+
);
|
|
41951
|
+
}
|
|
41952
|
+
} finally {
|
|
41953
|
+
setup.dispose();
|
|
41954
|
+
}
|
|
41955
|
+
}
|
|
41956
|
+
async function refreshObservedForStack(stackName, region, stateBackend, lockManager, providerRegistry, opts) {
|
|
41957
|
+
const { logger } = opts;
|
|
41958
|
+
const result = await stateBackend.getState(stackName, region);
|
|
41959
|
+
if (!result) {
|
|
41960
|
+
throw new Error(
|
|
41961
|
+
`No state found for stack '${stackName}' (${region}). Run 'cdkd state list' to see available stacks.`
|
|
41962
|
+
);
|
|
41963
|
+
}
|
|
41964
|
+
const { state, etag, migrationPending } = result;
|
|
41965
|
+
const entries = Object.entries(state.resources ?? {});
|
|
41966
|
+
if (entries.length === 0) {
|
|
41967
|
+
logger.info(`\u2713 ${stackName} (${region}): no resources in state, skipping`);
|
|
41968
|
+
return { refreshed: 0, unsupported: 0, failed: 0 };
|
|
41969
|
+
}
|
|
41970
|
+
if (opts.dryRun) {
|
|
41971
|
+
let wouldRefresh = 0;
|
|
41972
|
+
let wouldUnsupported = 0;
|
|
41973
|
+
for (const [, resource] of entries) {
|
|
41974
|
+
let provider;
|
|
41975
|
+
try {
|
|
41976
|
+
provider = providerRegistry.getProvider(resource.resourceType);
|
|
41977
|
+
} catch {
|
|
41978
|
+
wouldUnsupported++;
|
|
41979
|
+
continue;
|
|
41980
|
+
}
|
|
41981
|
+
if (provider.readCurrentState)
|
|
41982
|
+
wouldRefresh++;
|
|
41983
|
+
else
|
|
41984
|
+
wouldUnsupported++;
|
|
41985
|
+
}
|
|
41986
|
+
logger.info(
|
|
41987
|
+
`Plan ${stackName} (${region}): ${wouldRefresh} resource(s) would be refreshed, ${wouldUnsupported} unsupported`
|
|
41988
|
+
);
|
|
41989
|
+
return { refreshed: wouldRefresh, unsupported: wouldUnsupported, failed: 0 };
|
|
41990
|
+
}
|
|
41991
|
+
const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
|
|
41992
|
+
await lockManager.acquireLock(stackName, region, owner, "state-refresh-observed");
|
|
41993
|
+
try {
|
|
41994
|
+
let refreshed = 0;
|
|
41995
|
+
let unsupported = 0;
|
|
41996
|
+
let failed = 0;
|
|
41997
|
+
await withStackName(stackName, async () => {
|
|
41998
|
+
const tasks = entries.map(async ([logicalId, resource]) => {
|
|
41999
|
+
if (providerRegistry.shouldSkipResource(resource.resourceType)) {
|
|
42000
|
+
unsupported++;
|
|
42001
|
+
return;
|
|
42002
|
+
}
|
|
42003
|
+
let provider;
|
|
42004
|
+
try {
|
|
42005
|
+
provider = providerRegistry.getProvider(resource.resourceType);
|
|
42006
|
+
} catch {
|
|
42007
|
+
unsupported++;
|
|
42008
|
+
return;
|
|
42009
|
+
}
|
|
42010
|
+
if (!provider.readCurrentState) {
|
|
42011
|
+
unsupported++;
|
|
42012
|
+
return;
|
|
42013
|
+
}
|
|
42014
|
+
try {
|
|
42015
|
+
const observed = await provider.readCurrentState(
|
|
42016
|
+
resource.physicalId,
|
|
42017
|
+
logicalId,
|
|
42018
|
+
resource.resourceType,
|
|
42019
|
+
resource.properties ?? {}
|
|
42020
|
+
);
|
|
42021
|
+
if (observed === void 0) {
|
|
42022
|
+
unsupported++;
|
|
42023
|
+
return;
|
|
42024
|
+
}
|
|
42025
|
+
resource.observedProperties = observed;
|
|
42026
|
+
refreshed++;
|
|
42027
|
+
} catch (err) {
|
|
42028
|
+
failed++;
|
|
42029
|
+
logger.warn(
|
|
42030
|
+
` \u2717 ${stackName}/${logicalId} (${resource.resourceType}): readCurrentState failed \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
42031
|
+
);
|
|
42032
|
+
}
|
|
42033
|
+
});
|
|
42034
|
+
await Promise.all(tasks);
|
|
42035
|
+
});
|
|
42036
|
+
state.lastModified = Date.now();
|
|
42037
|
+
const saveOptions = {
|
|
42038
|
+
expectedEtag: etag
|
|
42039
|
+
};
|
|
42040
|
+
if (migrationPending)
|
|
42041
|
+
saveOptions.migrateLegacy = true;
|
|
42042
|
+
await stateBackend.saveState(stackName, region, state, saveOptions);
|
|
42043
|
+
logger.info(
|
|
42044
|
+
`\u2713 ${stackName} (${region}): ${refreshed} refreshed, ${unsupported} unsupported, ${failed} failed`
|
|
42045
|
+
);
|
|
42046
|
+
return { refreshed, unsupported, failed };
|
|
42047
|
+
} finally {
|
|
42048
|
+
await lockManager.releaseLock(stackName, region).catch((err) => {
|
|
42049
|
+
logger.warn(
|
|
42050
|
+
`Failed to release lock for ${stackName} (${region}): ` + (err instanceof Error ? err.message : String(err))
|
|
42051
|
+
);
|
|
42052
|
+
});
|
|
42053
|
+
}
|
|
42054
|
+
}
|
|
42055
|
+
async function confirmRefresh(prompt) {
|
|
42056
|
+
const rl = readline5.createInterface({ input: process.stdin, output: process.stdout });
|
|
42057
|
+
try {
|
|
42058
|
+
const ans = await rl.question(`${prompt} [y/N] `);
|
|
42059
|
+
return /^y(es)?$/i.test(ans.trim());
|
|
42060
|
+
} finally {
|
|
42061
|
+
rl.close();
|
|
42062
|
+
}
|
|
42063
|
+
}
|
|
42064
|
+
function createStateRefreshObservedCommand() {
|
|
42065
|
+
const cmd = new Command12("refresh-observed").description(
|
|
42066
|
+
"Refresh observedProperties for every resource in a stack by calling provider.readCurrentState \u2014 populates the drift baseline for stacks deployed before state schema v3, without redeploying."
|
|
42067
|
+
).argument("[stacks...]", "Stack name(s) to refresh (physical CloudFormation names)").option("--all", "Refresh every stack in the state bucket", false).option("--dry-run", "Print the per-stack refresh count without writing state", false).addOption(stackRegionOption2()).action(withErrorHandling(stateRefreshObservedCommand));
|
|
42068
|
+
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
42069
|
+
cmd.addOption(deprecatedRegionOption);
|
|
42070
|
+
return cmd;
|
|
42071
|
+
}
|
|
41849
42072
|
function createStateCommand() {
|
|
41850
42073
|
const cmd = new Command12("state").description("Manage cdkd state stored in S3");
|
|
41851
42074
|
cmd.addCommand(createStateInfoCommand());
|
|
@@ -41855,6 +42078,7 @@ function createStateCommand() {
|
|
|
41855
42078
|
cmd.addCommand(createStateOrphanCommand());
|
|
41856
42079
|
cmd.addCommand(createStateDestroyCommand());
|
|
41857
42080
|
cmd.addCommand(createStateMigrateCommand());
|
|
42081
|
+
cmd.addCommand(createStateRefreshObservedCommand());
|
|
41858
42082
|
return cmd;
|
|
41859
42083
|
}
|
|
41860
42084
|
|
|
@@ -42637,7 +42861,7 @@ function reorderArgs(argv) {
|
|
|
42637
42861
|
}
|
|
42638
42862
|
async function main() {
|
|
42639
42863
|
const program = new Command14();
|
|
42640
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
42864
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.50.0");
|
|
42641
42865
|
program.addCommand(createBootstrapCommand());
|
|
42642
42866
|
program.addCommand(createSynthCommand());
|
|
42643
42867
|
program.addCommand(createListCommand());
|