@go-to-k/cdkd 0.48.0 → 0.49.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 +219 -1
- package/dist/cli.js.map +2 -2
- package/dist/go-to-k-cdkd-0.49.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
|
@@ -39028,6 +39028,14 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
|
|
|
39028
39028
|
resource.properties ?? {}
|
|
39029
39029
|
);
|
|
39030
39030
|
} else {
|
|
39031
|
+
if (resource.resourceType.startsWith("Custom::")) {
|
|
39032
|
+
outcomes.push({
|
|
39033
|
+
kind: "unsupported",
|
|
39034
|
+
logicalId,
|
|
39035
|
+
resourceType: resource.resourceType
|
|
39036
|
+
});
|
|
39037
|
+
continue;
|
|
39038
|
+
}
|
|
39031
39039
|
if (CC_API_FALLBACK_DENY_LIST[resource.resourceType]) {
|
|
39032
39040
|
outcomes.push({
|
|
39033
39041
|
kind: "unsupported",
|
|
@@ -41846,6 +41854,215 @@ function createStateInfoCommand() {
|
|
|
41846
41854
|
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
41847
41855
|
return cmd;
|
|
41848
41856
|
}
|
|
41857
|
+
async function stateRefreshObservedCommand(stackArgs, options) {
|
|
41858
|
+
const logger = getLogger();
|
|
41859
|
+
if (options.verbose)
|
|
41860
|
+
logger.setLevel("debug");
|
|
41861
|
+
if (!options.all && stackArgs.length === 0) {
|
|
41862
|
+
throw new Error(
|
|
41863
|
+
"Stack name is required. Usage: cdkd state refresh-observed <stack> [<stack>...] | --all"
|
|
41864
|
+
);
|
|
41865
|
+
}
|
|
41866
|
+
const setup = await setupStateBackend(options);
|
|
41867
|
+
const providerRegistry = new ProviderRegistry();
|
|
41868
|
+
registerAllProviders(providerRegistry);
|
|
41869
|
+
providerRegistry.setCustomResourceResponseBucket(setup.bucket);
|
|
41870
|
+
try {
|
|
41871
|
+
const stateRefs = await setup.stateBackend.listStacks();
|
|
41872
|
+
let targets;
|
|
41873
|
+
if (options.all) {
|
|
41874
|
+
targets = options.stackRegion ? stateRefs.filter((r) => r.region === options.stackRegion) : stateRefs;
|
|
41875
|
+
if (targets.length === 0) {
|
|
41876
|
+
logger.info("No stacks found in state");
|
|
41877
|
+
return;
|
|
41878
|
+
}
|
|
41879
|
+
} else {
|
|
41880
|
+
targets = [];
|
|
41881
|
+
for (const stackName of stackArgs) {
|
|
41882
|
+
const matches = stateRefs.filter((r) => r.stackName === stackName);
|
|
41883
|
+
if (matches.length === 0) {
|
|
41884
|
+
throw new Error(
|
|
41885
|
+
`No state found for stack '${stackName}'. Run 'cdkd state list' to see available stacks.`
|
|
41886
|
+
);
|
|
41887
|
+
}
|
|
41888
|
+
if (options.stackRegion) {
|
|
41889
|
+
const ref = matches.find((r) => r.region === options.stackRegion);
|
|
41890
|
+
if (!ref) {
|
|
41891
|
+
const seen = matches.map((r) => r.region ?? "(legacy)").join(", ");
|
|
41892
|
+
throw new Error(
|
|
41893
|
+
`No state found for stack '${stackName}' in region '${options.stackRegion}'. Available regions: ${seen}.`
|
|
41894
|
+
);
|
|
41895
|
+
}
|
|
41896
|
+
targets.push(ref);
|
|
41897
|
+
} else if (matches.length === 1) {
|
|
41898
|
+
targets.push(matches[0]);
|
|
41899
|
+
} else {
|
|
41900
|
+
const regions = matches.map((r) => r.region ?? "(legacy)").join(", ");
|
|
41901
|
+
throw new Error(
|
|
41902
|
+
`Stack '${stackName}' has state in multiple regions: ${regions}. Re-run with --stack-region <region> to disambiguate.`
|
|
41903
|
+
);
|
|
41904
|
+
}
|
|
41905
|
+
}
|
|
41906
|
+
}
|
|
41907
|
+
if (!options.yes && !options.dryRun) {
|
|
41908
|
+
const targetList = targets.map(formatStackRef).join(", ");
|
|
41909
|
+
const ok = await confirmRefresh(
|
|
41910
|
+
`Refresh observedProperties for ${targets.length} stack(s) (${targetList})?`
|
|
41911
|
+
);
|
|
41912
|
+
if (!ok) {
|
|
41913
|
+
logger.info("Aborted.");
|
|
41914
|
+
return;
|
|
41915
|
+
}
|
|
41916
|
+
}
|
|
41917
|
+
let totalRefreshed = 0;
|
|
41918
|
+
let totalUnsupported = 0;
|
|
41919
|
+
let totalFailed = 0;
|
|
41920
|
+
for (const target of targets) {
|
|
41921
|
+
if (!target.region) {
|
|
41922
|
+
throw new Error(
|
|
41923
|
+
`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.`
|
|
41924
|
+
);
|
|
41925
|
+
}
|
|
41926
|
+
const counts = await refreshObservedForStack(
|
|
41927
|
+
target.stackName,
|
|
41928
|
+
target.region,
|
|
41929
|
+
setup.stateBackend,
|
|
41930
|
+
setup.lockManager,
|
|
41931
|
+
providerRegistry,
|
|
41932
|
+
{ dryRun: options.dryRun ?? false, logger }
|
|
41933
|
+
);
|
|
41934
|
+
totalRefreshed += counts.refreshed;
|
|
41935
|
+
totalUnsupported += counts.unsupported;
|
|
41936
|
+
totalFailed += counts.failed;
|
|
41937
|
+
}
|
|
41938
|
+
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`;
|
|
41939
|
+
logger.info(`
|
|
41940
|
+
${summary}`);
|
|
41941
|
+
if (totalFailed > 0) {
|
|
41942
|
+
throw new PartialFailureError(
|
|
41943
|
+
`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.`
|
|
41944
|
+
);
|
|
41945
|
+
}
|
|
41946
|
+
} finally {
|
|
41947
|
+
setup.dispose();
|
|
41948
|
+
}
|
|
41949
|
+
}
|
|
41950
|
+
async function refreshObservedForStack(stackName, region, stateBackend, lockManager, providerRegistry, opts) {
|
|
41951
|
+
const { logger } = opts;
|
|
41952
|
+
const result = await stateBackend.getState(stackName, region);
|
|
41953
|
+
if (!result) {
|
|
41954
|
+
throw new Error(
|
|
41955
|
+
`No state found for stack '${stackName}' (${region}). Run 'cdkd state list' to see available stacks.`
|
|
41956
|
+
);
|
|
41957
|
+
}
|
|
41958
|
+
const { state, etag, migrationPending } = result;
|
|
41959
|
+
const entries = Object.entries(state.resources ?? {});
|
|
41960
|
+
if (entries.length === 0) {
|
|
41961
|
+
logger.info(`\u2713 ${stackName} (${region}): no resources in state, skipping`);
|
|
41962
|
+
return { refreshed: 0, unsupported: 0, failed: 0 };
|
|
41963
|
+
}
|
|
41964
|
+
if (opts.dryRun) {
|
|
41965
|
+
let wouldRefresh = 0;
|
|
41966
|
+
let wouldUnsupported = 0;
|
|
41967
|
+
for (const [, resource] of entries) {
|
|
41968
|
+
let provider;
|
|
41969
|
+
try {
|
|
41970
|
+
provider = providerRegistry.getProvider(resource.resourceType);
|
|
41971
|
+
} catch {
|
|
41972
|
+
wouldUnsupported++;
|
|
41973
|
+
continue;
|
|
41974
|
+
}
|
|
41975
|
+
if (provider.readCurrentState)
|
|
41976
|
+
wouldRefresh++;
|
|
41977
|
+
else
|
|
41978
|
+
wouldUnsupported++;
|
|
41979
|
+
}
|
|
41980
|
+
logger.info(
|
|
41981
|
+
`Plan ${stackName} (${region}): ${wouldRefresh} resource(s) would be refreshed, ${wouldUnsupported} unsupported`
|
|
41982
|
+
);
|
|
41983
|
+
return { refreshed: wouldRefresh, unsupported: wouldUnsupported, failed: 0 };
|
|
41984
|
+
}
|
|
41985
|
+
const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
|
|
41986
|
+
await lockManager.acquireLock(stackName, region, owner, "state-refresh-observed");
|
|
41987
|
+
try {
|
|
41988
|
+
let refreshed = 0;
|
|
41989
|
+
let unsupported = 0;
|
|
41990
|
+
let failed = 0;
|
|
41991
|
+
await withStackName(stackName, async () => {
|
|
41992
|
+
const tasks = entries.map(async ([logicalId, resource]) => {
|
|
41993
|
+
if (providerRegistry.shouldSkipResource(resource.resourceType)) {
|
|
41994
|
+
unsupported++;
|
|
41995
|
+
return;
|
|
41996
|
+
}
|
|
41997
|
+
let provider;
|
|
41998
|
+
try {
|
|
41999
|
+
provider = providerRegistry.getProvider(resource.resourceType);
|
|
42000
|
+
} catch {
|
|
42001
|
+
unsupported++;
|
|
42002
|
+
return;
|
|
42003
|
+
}
|
|
42004
|
+
if (!provider.readCurrentState) {
|
|
42005
|
+
unsupported++;
|
|
42006
|
+
return;
|
|
42007
|
+
}
|
|
42008
|
+
try {
|
|
42009
|
+
const observed = await provider.readCurrentState(
|
|
42010
|
+
resource.physicalId,
|
|
42011
|
+
logicalId,
|
|
42012
|
+
resource.resourceType,
|
|
42013
|
+
resource.properties ?? {}
|
|
42014
|
+
);
|
|
42015
|
+
if (observed === void 0) {
|
|
42016
|
+
unsupported++;
|
|
42017
|
+
return;
|
|
42018
|
+
}
|
|
42019
|
+
resource.observedProperties = observed;
|
|
42020
|
+
refreshed++;
|
|
42021
|
+
} catch (err) {
|
|
42022
|
+
failed++;
|
|
42023
|
+
logger.warn(
|
|
42024
|
+
` \u2717 ${stackName}/${logicalId} (${resource.resourceType}): readCurrentState failed \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
42025
|
+
);
|
|
42026
|
+
}
|
|
42027
|
+
});
|
|
42028
|
+
await Promise.all(tasks);
|
|
42029
|
+
});
|
|
42030
|
+
state.lastModified = Date.now();
|
|
42031
|
+
const saveOptions = {
|
|
42032
|
+
expectedEtag: etag
|
|
42033
|
+
};
|
|
42034
|
+
if (migrationPending)
|
|
42035
|
+
saveOptions.migrateLegacy = true;
|
|
42036
|
+
await stateBackend.saveState(stackName, region, state, saveOptions);
|
|
42037
|
+
logger.info(
|
|
42038
|
+
`\u2713 ${stackName} (${region}): ${refreshed} refreshed, ${unsupported} unsupported, ${failed} failed`
|
|
42039
|
+
);
|
|
42040
|
+
return { refreshed, unsupported, failed };
|
|
42041
|
+
} finally {
|
|
42042
|
+
await lockManager.releaseLock(stackName, region).catch((err) => {
|
|
42043
|
+
logger.warn(
|
|
42044
|
+
`Failed to release lock for ${stackName} (${region}): ` + (err instanceof Error ? err.message : String(err))
|
|
42045
|
+
);
|
|
42046
|
+
});
|
|
42047
|
+
}
|
|
42048
|
+
}
|
|
42049
|
+
async function confirmRefresh(prompt) {
|
|
42050
|
+
const rl = readline5.createInterface({ input: process.stdin, output: process.stdout });
|
|
42051
|
+
try {
|
|
42052
|
+
const ans = await rl.question(`${prompt} [y/N] `);
|
|
42053
|
+
return /^y(es)?$/i.test(ans.trim());
|
|
42054
|
+
} finally {
|
|
42055
|
+
rl.close();
|
|
42056
|
+
}
|
|
42057
|
+
}
|
|
42058
|
+
function createStateRefreshObservedCommand() {
|
|
42059
|
+
const cmd = new Command12("refresh-observed").description(
|
|
42060
|
+
"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."
|
|
42061
|
+
).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));
|
|
42062
|
+
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
42063
|
+
cmd.addOption(deprecatedRegionOption);
|
|
42064
|
+
return cmd;
|
|
42065
|
+
}
|
|
41849
42066
|
function createStateCommand() {
|
|
41850
42067
|
const cmd = new Command12("state").description("Manage cdkd state stored in S3");
|
|
41851
42068
|
cmd.addCommand(createStateInfoCommand());
|
|
@@ -41855,6 +42072,7 @@ function createStateCommand() {
|
|
|
41855
42072
|
cmd.addCommand(createStateOrphanCommand());
|
|
41856
42073
|
cmd.addCommand(createStateDestroyCommand());
|
|
41857
42074
|
cmd.addCommand(createStateMigrateCommand());
|
|
42075
|
+
cmd.addCommand(createStateRefreshObservedCommand());
|
|
41858
42076
|
return cmd;
|
|
41859
42077
|
}
|
|
41860
42078
|
|
|
@@ -42637,7 +42855,7 @@ function reorderArgs(argv) {
|
|
|
42637
42855
|
}
|
|
42638
42856
|
async function main() {
|
|
42639
42857
|
const program = new Command14();
|
|
42640
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
42858
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.49.0");
|
|
42641
42859
|
program.addCommand(createBootstrapCommand());
|
|
42642
42860
|
program.addCommand(createSynthCommand());
|
|
42643
42861
|
program.addCommand(createListCommand());
|