@go-to-k/cdkd 0.285.13 → 0.285.15
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/{asg-provider-DcFgnQQO.js → asg-provider-CKEf903K.js} +2 -2
- package/dist/{asg-provider-DcFgnQQO.js.map → asg-provider-CKEf903K.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-DhMm2M33.js → deploy-engine-DPh-XamZ.js} +632 -120
- package/dist/deploy-engine-DPh-XamZ.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-DHJ4G_ZZ.js → program-D8-mNNcf.js} +167 -207
- package/dist/{program-DHJ4G_ZZ.js.map → program-D8-mNNcf.js.map} +1 -1
- package/dist/{version-FLW4rE5A.js → version-Dcdouqd3.js} +2 -2
- package/dist/{version-FLW4rE5A.js.map → version-Dcdouqd3.js.map} +1 -1
- package/package.json +1 -7
- package/dist/deploy-engine-DhMm2M33.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { _ as withStackName, d as applyDefaultNameForFallback, f as generateResourceName, h as looksLikeCdkdGeneratedName, m as getCurrentStackName, n as getLogger, p as generateResourceNameWithFallback, r as isStdoutReservedForPayload, s as getLiveRenderer } from "./logger-Dw-3y48G.js";
|
|
2
2
|
import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
|
|
3
|
-
import { t as getCdkdVersion } from "./version-
|
|
3
|
+
import { t as getCdkdVersion } from "./version-Dcdouqd3.js";
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { CreateBucketCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketLocationCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchKey, PutBucketEncryptionCommand, PutBucketPolicyCommand, PutObjectCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
|
|
@@ -7927,6 +7927,140 @@ function parseRollbackJournal(bodyString, stackName) {
|
|
|
7927
7927
|
};
|
|
7928
7928
|
}
|
|
7929
7929
|
|
|
7930
|
+
//#endregion
|
|
7931
|
+
//#region src/state/state-prefix.ts
|
|
7932
|
+
/**
|
|
7933
|
+
* The default S3 key prefix for cdkd state.
|
|
7934
|
+
*
|
|
7935
|
+
* Homed in the STATE layer rather than in `src/cli/commands/state-file-keys.ts`
|
|
7936
|
+
* (which re-exports it, so its four existing importers are unchanged) because
|
|
7937
|
+
* `src/state/lock-contention-message.ts` needs it to decide whether a recovery
|
|
7938
|
+
* hint should spell `--state-prefix` at all, and a `src/state/**` module
|
|
7939
|
+
* importing from `src/cli/commands/**` inverts the layering — the CLI sits
|
|
7940
|
+
* ABOVE the state layer in the 7-layer architecture, not below it.
|
|
7941
|
+
*
|
|
7942
|
+
* Note this is only the DEFAULT. Other commands accept `--state-prefix`, so
|
|
7943
|
+
* whole-bucket listings deliberately do not scope to it.
|
|
7944
|
+
*/
|
|
7945
|
+
const DEFAULT_STATE_PREFIX = "cdkd";
|
|
7946
|
+
/**
|
|
7947
|
+
* The state-bucket prefix `CustomResourceProvider` PUTs its response
|
|
7948
|
+
* placeholders under, one object per invocation
|
|
7949
|
+
* (`custom-resource-responses/{requestId}.json`).
|
|
7950
|
+
*
|
|
7951
|
+
* Homed here for the same layering reason as {@link DEFAULT_STATE_PREFIX}: the
|
|
7952
|
+
* PRODUCER is `src/provisioning/providers/custom-resource-provider.ts` and the
|
|
7953
|
+
* COLLECTOR is `src/cli/commands/gc.ts`, so a copy in either would be a copy
|
|
7954
|
+
* the other could drift from — and the two spellings would then disagree about
|
|
7955
|
+
* which objects exist, which is the only way a sweeper can miss the family it
|
|
7956
|
+
* was written for (issue #2052). `src/cli/commands/state-file-keys.ts`
|
|
7957
|
+
* re-exports it so gc reads it alongside the other state-key constants.
|
|
7958
|
+
*
|
|
7959
|
+
* Note this is only the DEFAULT: `ProviderRegistry` can be configured with a
|
|
7960
|
+
* different `responsePrefix`, so a sweep scoped to this value is a sweep of the
|
|
7961
|
+
* default layout. gc has no access to a non-default one — nothing persists it —
|
|
7962
|
+
* which is stated at the sweep's own call site rather than implied here.
|
|
7963
|
+
*/
|
|
7964
|
+
const CUSTOM_RESOURCE_RESPONSE_PREFIX = "custom-resource-responses";
|
|
7965
|
+
|
|
7966
|
+
//#endregion
|
|
7967
|
+
//#region src/state/lock-contention-message.ts
|
|
7968
|
+
/**
|
|
7969
|
+
* The message every fail-fast lock-contention site raises.
|
|
7970
|
+
*
|
|
7971
|
+
* Issue #2161 made six commands throw on `acquireLock` returning `false`
|
|
7972
|
+
* instead of running under a foreign lock. Issue #2170 is the follow-up: the
|
|
7973
|
+
* message those sites raise asked the user to decide "is another process
|
|
7974
|
+
* active?" while printing none of the evidence that would answer it, and the
|
|
7975
|
+
* recovery command it suggested could resolve against the wrong AWS account.
|
|
7976
|
+
*
|
|
7977
|
+
* Both matter more than they look, because of WHICH locks reach this message.
|
|
7978
|
+
* `LockManager.acquireLock` reaps an EXPIRED foreign lock and retries, so a
|
|
7979
|
+
* `false` return means the lock is LIVE — in practice a running `cdkd deploy`.
|
|
7980
|
+
* The user who follows a bare `cdkd force-unlock <stack>` suggestion therefore
|
|
7981
|
+
* deletes a live owner's lock and reproduces issue #2161's harm by hand, which
|
|
7982
|
+
* is the outcome #2161 exists to prevent.
|
|
7983
|
+
*
|
|
7984
|
+
* Centralising the text here also settles the third finding: the nine sites
|
|
7985
|
+
* had drifted to three spellings (`for stack` / `for nested stack` /
|
|
7986
|
+
* `for nested-stack child`), so a user grepping CI logs for one of them found
|
|
7987
|
+
* two of three. `subject` now varies only the noun.
|
|
7988
|
+
*/
|
|
7989
|
+
/**
|
|
7990
|
+
* Stand-in for a value with nothing renderable left after sanitization. Named
|
|
7991
|
+
* rather than inlined so the message and the command-suppression branch cannot
|
|
7992
|
+
* disagree about what "unrenderable" looks like.
|
|
7993
|
+
*/
|
|
7994
|
+
const UNRENDERABLE = "<unrenderable>";
|
|
7995
|
+
/** Render `expiresIn` without implying more precision than a clock skew allows. */
|
|
7996
|
+
function formatRemaining(ms) {
|
|
7997
|
+
if (!Number.isFinite(ms)) return "at an unknown time";
|
|
7998
|
+
if (ms <= 0) return "already expired";
|
|
7999
|
+
const minutes = Math.round(ms / 6e4);
|
|
8000
|
+
if (minutes < 1) return "in under a minute";
|
|
8001
|
+
return `in ~${minutes}m`;
|
|
8002
|
+
}
|
|
8003
|
+
function shellQuote(value) {
|
|
8004
|
+
return /^[A-Za-z0-9._/@:+-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
8005
|
+
}
|
|
8006
|
+
/**
|
|
8007
|
+
* The `cdkd force-unlock ...` line, carrying every flag that decides which
|
|
8008
|
+
* lock object it resolves to.
|
|
8009
|
+
*/
|
|
8010
|
+
function buildForceUnlockCommand(stackName, region, recovery) {
|
|
8011
|
+
const safeStack = displaySafe(stackName, { asciiOnly: true });
|
|
8012
|
+
const stackIsExact = safeStack === stackName;
|
|
8013
|
+
const safeRegion = region === void 0 ? void 0 : displaySafe(region, { asciiOnly: true });
|
|
8014
|
+
if (!safeStack || safeRegion === "" || !stackIsExact || !(region === void 0 || safeRegion === region)) return "";
|
|
8015
|
+
const parts = [safeRegion === void 0 ? `cdkd force-unlock ${shellQuote(safeStack)}` : `cdkd force-unlock ${shellQuote(safeStack)} --stack-region ${shellQuote(safeRegion)}`];
|
|
8016
|
+
if (recovery?.profile) parts.push(`--profile ${shellQuote(recovery.profile)}`);
|
|
8017
|
+
if (recovery?.stateBucket) parts.push(`--state-bucket ${shellQuote(recovery.stateBucket)}`);
|
|
8018
|
+
if (recovery?.statePrefix && recovery.statePrefix !== "cdkd") parts.push(`--state-prefix ${shellQuote(recovery.statePrefix)}`);
|
|
8019
|
+
return parts.join(" ");
|
|
8020
|
+
}
|
|
8021
|
+
/**
|
|
8022
|
+
* The force-quit banner's recovery sentence.
|
|
8023
|
+
*
|
|
8024
|
+
* Exported so the two `destroy-runner.ts` banners do not each decide what to
|
|
8025
|
+
* say when {@link buildForceUnlockCommand} suppresses — a banner ending in a
|
|
8026
|
+
* bare `run: ` is the shape the review found, and two copies of the branch is
|
|
8027
|
+
* how the next one drifts. Returns a leading-space clause so the caller can
|
|
8028
|
+
* concatenate it unconditionally.
|
|
8029
|
+
*/
|
|
8030
|
+
function forceQuitRecoveryClause(stackName, region, recovery) {
|
|
8031
|
+
const command = buildForceUnlockCommand(stackName, region, recovery);
|
|
8032
|
+
return command ? ` If the next run reports a lock, run: ${command}` : " Inspect the lock object directly: the name or region recorded for this stack cannot be reproduced safely on a command line, so any command shown here would address a different lock.";
|
|
8033
|
+
}
|
|
8034
|
+
/**
|
|
8035
|
+
* Build the contention message, reading the holder's identity best-effort.
|
|
8036
|
+
*
|
|
8037
|
+
* The `getLockInfo` read is one GetObject and is deliberately NOT allowed to
|
|
8038
|
+
* fail the command: the caller is already on its way to throwing, and turning
|
|
8039
|
+
* a contention refusal into an S3 error would lose the reason. A failed or
|
|
8040
|
+
* absent read degrades to the evidence-free wording rather than to a crash.
|
|
8041
|
+
*/
|
|
8042
|
+
async function buildLockContentionMessage(args) {
|
|
8043
|
+
const { lockManager, stackName, region, subject = "stack", recovery, heldClause, suffix } = args;
|
|
8044
|
+
let held = heldClause ?? "another cdkd process holds it";
|
|
8045
|
+
let sawHolder = false;
|
|
8046
|
+
try {
|
|
8047
|
+
const info = await lockManager.getLockInfo(stackName, region);
|
|
8048
|
+
if (info) {
|
|
8049
|
+
const operation = info.operation ? `, operation: ${displaySafe(info.operation)}` : "";
|
|
8050
|
+
const expires = formatRemaining(info.expiresAt - Date.now());
|
|
8051
|
+
const owner = displaySafe(info.owner);
|
|
8052
|
+
const holder = owner ? `held by ${owner}${operation}` : `held by an unnamed holder`;
|
|
8053
|
+
held = `${heldClause ? `${heldClause} — ` : ""}${holder}, expires ${expires}`;
|
|
8054
|
+
if (owner) sawHolder = true;
|
|
8055
|
+
}
|
|
8056
|
+
} catch {}
|
|
8057
|
+
const advice = sawHolder ? `That process is still running — wait for it to finish. Only if you are certain it is gone` : `Wait for it to finish, or if you are certain no other process is active`;
|
|
8058
|
+
const recoveryCommand = buildForceUnlockCommand(stackName, region, recovery);
|
|
8059
|
+
const head = `Could not acquire lock for ${subject} '${displaySafe(stackName, { asciiOnly: true }) || "<unrenderable>"}' (${displaySafe(region, { asciiOnly: true }) || "<unrenderable>"}) — ${held}.` + (suffix ? ` ${suffix}` : "");
|
|
8060
|
+
if (!recoveryCommand) return `${head} ${advice}. No recovery command can be shown: the name or region recorded for this lock contains characters that cannot be reproduced safely on a command line, so any command shown here would address a different lock — inspect the lock object directly.`;
|
|
8061
|
+
return `${head} ${advice}, run: ${recoveryCommand}`;
|
|
8062
|
+
}
|
|
8063
|
+
|
|
7930
8064
|
//#endregion
|
|
7931
8065
|
//#region src/utils/bucket-region-client.ts
|
|
7932
8066
|
/**
|
|
@@ -8013,6 +8147,23 @@ const LEGACY_KEY_DEPTH = 2;
|
|
|
8013
8147
|
/** The `version: 2` region-prefixed key. */
|
|
8014
8148
|
const NEW_KEY_DEPTH = 3;
|
|
8015
8149
|
/**
|
|
8150
|
+
* Does a legacy record classified by {@link LegacyStateProbe} belong to an
|
|
8151
|
+
* operation targeting `region`?
|
|
8152
|
+
*
|
|
8153
|
+
* Free function, and shared by both consumers on purpose: the delete sweep
|
|
8154
|
+
* needs the probe's KIND as well as this verdict (to warn when a record may
|
|
8155
|
+
* have been left behind), and duplicating the mapping at that call site is
|
|
8156
|
+
* how the two would drift apart again.
|
|
8157
|
+
*/
|
|
8158
|
+
function legacyProbeBelongsTo(probe, region) {
|
|
8159
|
+
switch (probe.kind) {
|
|
8160
|
+
case "region": return probe.region === region;
|
|
8161
|
+
case "no-region": return true;
|
|
8162
|
+
case "absent":
|
|
8163
|
+
case "unreadable": return false;
|
|
8164
|
+
}
|
|
8165
|
+
}
|
|
8166
|
+
/**
|
|
8016
8167
|
* S3-based state backend using conditional writes for optimistic locking.
|
|
8017
8168
|
*
|
|
8018
8169
|
* State keys are region-scoped (`{prefix}/{stackName}/{region}/state.json`)
|
|
@@ -8178,7 +8329,7 @@ var S3StateBackend = class {
|
|
|
8178
8329
|
await this.ensureClientForBucket();
|
|
8179
8330
|
const newKey = this.getStateKey(stackName, region);
|
|
8180
8331
|
if (await this.headObject(newKey)) return true;
|
|
8181
|
-
return this.
|
|
8332
|
+
return this.legacyBelongsToRegion(stackName, region);
|
|
8182
8333
|
}
|
|
8183
8334
|
/**
|
|
8184
8335
|
* Get state for a stack, transparently falling back to the legacy key.
|
|
@@ -8310,7 +8461,12 @@ var S3StateBackend = class {
|
|
|
8310
8461
|
...await this.ownerParam(),
|
|
8311
8462
|
Key: this.getStateKey(stackName, region)
|
|
8312
8463
|
}));
|
|
8313
|
-
|
|
8464
|
+
const legacyProbe = await this.probeLegacyState(stackName);
|
|
8465
|
+
if (legacyProbe.kind === "unreadable") {
|
|
8466
|
+
const safeName = this.displayName(stackName);
|
|
8467
|
+
this.logger.warn(`Could not read the legacy state record for '${safeName}' while cleaning up (${legacyProbe.reason}). If one exists it was left in place. Re-run with --verbose for the details.`);
|
|
8468
|
+
}
|
|
8469
|
+
if (legacyProbeBelongsTo(legacyProbe, region)) {
|
|
8314
8470
|
await this.s3Client.send(new DeleteObjectCommand({
|
|
8315
8471
|
Bucket: this.config.bucket,
|
|
8316
8472
|
...await this.ownerParam(),
|
|
@@ -8330,6 +8486,48 @@ var S3StateBackend = class {
|
|
|
8330
8486
|
}
|
|
8331
8487
|
}
|
|
8332
8488
|
/**
|
|
8489
|
+
* Delete a legacy state file that names no region (issue #2537).
|
|
8490
|
+
*
|
|
8491
|
+
* `deleteState` cannot serve this case: it takes a region, keys the primary
|
|
8492
|
+
* delete off it, and sweeps the legacy key only when that key's own `region`
|
|
8493
|
+
* field matches. A `version: 1` blob whose body carries no `region` at all
|
|
8494
|
+
* has nothing to match, so it falls through every branch there — which is
|
|
8495
|
+
* how `cdkd state orphan` came to report a removal it never performed.
|
|
8496
|
+
*
|
|
8497
|
+
* Unconditional, and deliberately so — but NOT because the caller's ref
|
|
8498
|
+
* proves the body names no region. `listStacks` derives that ref from
|
|
8499
|
+
* `readLegacyRegion`, which also returns undefined on a swallowed 403 / 503
|
|
8500
|
+
* / unparseable body, so a record that DOES name a region can surface
|
|
8501
|
+
* region-less. The delete is still right there: the caller
|
|
8502
|
+
* (`cdkd state orphan` with no `--stack-region`) means "every record for
|
|
8503
|
+
* this name", and this key is one of them. A caller that means something
|
|
8504
|
+
* narrower must not use this method.
|
|
8505
|
+
*
|
|
8506
|
+
* No rollback-journal sweep: journal keys exist only in the region-scoped
|
|
8507
|
+
* layout (see `getRollbackJournalKey`), so a region-less record can have
|
|
8508
|
+
* none.
|
|
8509
|
+
*/
|
|
8510
|
+
async deleteLegacyState(stackName) {
|
|
8511
|
+
await this.ensureClientForBucket();
|
|
8512
|
+
const key = this.getLegacyStateKey(stackName);
|
|
8513
|
+
try {
|
|
8514
|
+
const safeStack = displaySafe(stackName, { asciiOnly: true }) || "<unrenderable>";
|
|
8515
|
+
this.logger.debug(`Deleting legacy state: ${safeStack} (${displaySafe(key, { asciiOnly: true }) || "<unrenderable>"})`);
|
|
8516
|
+
await this.s3Client.send(new DeleteObjectCommand({
|
|
8517
|
+
Bucket: this.config.bucket,
|
|
8518
|
+
...await this.ownerParam(),
|
|
8519
|
+
Key: key
|
|
8520
|
+
}));
|
|
8521
|
+
this.logger.debug(`Legacy state deleted: ${safeStack}`);
|
|
8522
|
+
} catch (error) {
|
|
8523
|
+
const normalized = normalizeAwsError(error, {
|
|
8524
|
+
bucket: this.config.bucket,
|
|
8525
|
+
operation: "DeleteObject"
|
|
8526
|
+
});
|
|
8527
|
+
throw new StateError(`Failed to delete legacy state for stack '${displaySafe(stackName, { asciiOnly: true }) || "<unrenderable>"}': ${normalized.message}`, normalized);
|
|
8528
|
+
}
|
|
8529
|
+
}
|
|
8530
|
+
/**
|
|
8333
8531
|
* List all stacks with state in the bucket.
|
|
8334
8532
|
*
|
|
8335
8533
|
* Returns one `{stackName, region}` pair per state file. Both layouts
|
|
@@ -8708,29 +8906,85 @@ var S3StateBackend = class {
|
|
|
8708
8906
|
}
|
|
8709
8907
|
}
|
|
8710
8908
|
/**
|
|
8711
|
-
* Read the legacy
|
|
8712
|
-
*
|
|
8713
|
-
* entries during `listStacks`.
|
|
8909
|
+
* Read the legacy key and classify what is there — {@link LegacyStateProbe}
|
|
8910
|
+
* says what each answer means and which consumer acts on it.
|
|
8714
8911
|
*/
|
|
8715
|
-
|
|
8912
|
+
/**
|
|
8913
|
+
* A stack name safe to put in a log line. Names reach this class from S3
|
|
8914
|
+
* key segments, which anyone able to write the bucket controls, so every
|
|
8915
|
+
* message that renders one goes through here (issue #2170's class).
|
|
8916
|
+
*/
|
|
8917
|
+
displayName(stackName) {
|
|
8918
|
+
return displaySafe(stackName, { asciiOnly: true }) || "<unrenderable>";
|
|
8919
|
+
}
|
|
8920
|
+
async probeLegacyState(stackName) {
|
|
8716
8921
|
try {
|
|
8717
8922
|
const response = await this.s3Client.send(new GetObjectCommand({
|
|
8718
8923
|
Bucket: this.config.bucket,
|
|
8719
8924
|
...await this.ownerParam(),
|
|
8720
8925
|
Key: this.getLegacyStateKey(stackName)
|
|
8721
8926
|
}));
|
|
8722
|
-
if (!response.Body)
|
|
8927
|
+
if (!response.Body) {
|
|
8928
|
+
this.logger.debug(`Legacy state probe for '${this.displayName(stackName)}': response carried no body`);
|
|
8929
|
+
return {
|
|
8930
|
+
kind: "unreadable",
|
|
8931
|
+
reason: "the response carried no body"
|
|
8932
|
+
};
|
|
8933
|
+
}
|
|
8723
8934
|
const bodyString = await response.Body.transformToString();
|
|
8724
|
-
const
|
|
8725
|
-
return
|
|
8935
|
+
const raw = JSON.parse(bodyString).region;
|
|
8936
|
+
if (!raw) return { kind: "no-region" };
|
|
8937
|
+
if (typeof raw !== "string") {
|
|
8938
|
+
this.logger.debug(`Legacy state probe for '${this.displayName(stackName)}': 'region' is ${typeof raw}, not a string`);
|
|
8939
|
+
return {
|
|
8940
|
+
kind: "unreadable",
|
|
8941
|
+
reason: `its 'region' field is ${typeof raw}, not a string`
|
|
8942
|
+
};
|
|
8943
|
+
}
|
|
8944
|
+
return {
|
|
8945
|
+
kind: "region",
|
|
8946
|
+
region: raw
|
|
8947
|
+
};
|
|
8726
8948
|
} catch (error) {
|
|
8727
|
-
if (isNoSuchKey(error)) return
|
|
8728
|
-
|
|
8729
|
-
|
|
8949
|
+
if (isNoSuchKey(error)) return { kind: "absent" };
|
|
8950
|
+
const { detail } = describeAwsFailure(error);
|
|
8951
|
+
this.logger.debug(`Could not read legacy state region for '${this.displayName(stackName)}': ${displaySafe(detail, { asciiOnly: true }) || "<unrenderable>"}`);
|
|
8952
|
+
return {
|
|
8953
|
+
kind: "unreadable",
|
|
8954
|
+
reason: displaySafe(error instanceof Error && error.name ? error.name : "an unknown error", { asciiOnly: true }) || "<unrenderable>"
|
|
8955
|
+
};
|
|
8730
8956
|
}
|
|
8731
8957
|
}
|
|
8732
|
-
|
|
8733
|
-
|
|
8958
|
+
/**
|
|
8959
|
+
* The region a legacy record should be listed under, or `undefined` when it
|
|
8960
|
+
* names none / could not be read. Preserves `listStacks`'s behaviour across
|
|
8961
|
+
* the {@link probeLegacyState} split.
|
|
8962
|
+
*/
|
|
8963
|
+
async readLegacyRegion(stackName) {
|
|
8964
|
+
const probe = await this.probeLegacyState(stackName);
|
|
8965
|
+
return probe.kind === "region" ? probe.region : void 0;
|
|
8966
|
+
}
|
|
8967
|
+
/**
|
|
8968
|
+
* Whether an operation targeting `region` owns the legacy record — the
|
|
8969
|
+
* DELETE-side counterpart of `tryGetLegacy`'s read gate, and issue #2550.
|
|
8970
|
+
*
|
|
8971
|
+
* The two must agree. `tryGetLegacy` accepts a body that names NO region
|
|
8972
|
+
* from any region (`if (state.region && state.region !== region)` is false
|
|
8973
|
+
* when the field is absent), so `cdkd destroy` reads such a record, deletes
|
|
8974
|
+
* the AWS resources, and finishes. The old equality test here answered
|
|
8975
|
+
* `undefined === 'us-east-1'` — false — so the record survived a successful
|
|
8976
|
+
* destroy, kept appearing in `cdkd state list`, and the next deploy of that
|
|
8977
|
+
* name planned updates against resources that were gone.
|
|
8978
|
+
*
|
|
8979
|
+
* `unreadable` is deliberately NOT treated as `no-region`: a 403, a 503 or a
|
|
8980
|
+
* malformed body says nothing about who owns the record, and a read that
|
|
8981
|
+
* failed must never authorise a delete. It collapsed into the same
|
|
8982
|
+
* `undefined` as the other two before, which is why the one-line fix — make
|
|
8983
|
+
* `undefined` match — was wrong: it would also have made `stateExists`
|
|
8984
|
+
* report state for a stack that has none, since `absent` reads the same way.
|
|
8985
|
+
*/
|
|
8986
|
+
async legacyBelongsToRegion(stackName, region) {
|
|
8987
|
+
return legacyProbeBelongsTo(await this.probeLegacyState(stackName), region);
|
|
8734
8988
|
}
|
|
8735
8989
|
/**
|
|
8736
8990
|
* Try to read the legacy `version: 1` state. Returns null when the legacy
|
|
@@ -9608,6 +9862,99 @@ var LockManager = class {
|
|
|
9608
9862
|
/** Fixed marker substituted for a secret value in log / error output. */
|
|
9609
9863
|
const SECRET_MASK = "***";
|
|
9610
9864
|
/**
|
|
9865
|
+
* The UNCOLLAPSED companion of a {@link RecordedSecretValues} map: for each map
|
|
9866
|
+
* instance, every `expression -> plaintext` pair the resolver recorded INTO IT,
|
|
9867
|
+
* keyed by EXPRESSION (issue [#2485](https://github.com/go-to-k/cdkd/issues/2485)).
|
|
9868
|
+
*
|
|
9869
|
+
* WHY IT EXISTS. The map is keyed by PLAINTEXT, so two expressions resolving to
|
|
9870
|
+
* one value keep ONE entry — whichever the resolver recorded last. A WHOLE-token
|
|
9871
|
+
* leaf is immune (the position pass copies its own source), but a leaf that
|
|
9872
|
+
* EMBEDS a token in a literal string is redacted by the value scan, which can
|
|
9873
|
+
* only write the map's surviving expression: the versioned sibling's, for a
|
|
9874
|
+
* template that spells the un-versioned one, and the next deploy diffs that
|
|
9875
|
+
* leaf forever. Recovering the losing expression needs evidence the map has
|
|
9876
|
+
* discarded, and it has to be PASS-LOCAL: `recordedSecretExpressions` is
|
|
9877
|
+
* process-wide and says only that an expression IS secret, never what it
|
|
9878
|
+
* resolved to in THIS resource — so it cannot tell "the source token lost the
|
|
9879
|
+
* map slot to its sibling" from "the source token was never resolved here"
|
|
9880
|
+
* (a previous generation's bag, where writing today's expression over the
|
|
9881
|
+
* framed value would record something that was never deployed).
|
|
9882
|
+
*
|
|
9883
|
+
* Keyed by the map INSTANCE, so the evidence is exactly as pass-local as the
|
|
9884
|
+
* map itself: a map the resolver populated (the deploy's `perResourceSecrets`
|
|
9885
|
+
* entry, and equally the map drift / scrub / import hand their own resolution)
|
|
9886
|
+
* carries the pairs of THAT resolution, while a map the resolver did not
|
|
9887
|
+
* populate — a derived needle map, a nested-stack inheritance copy, a
|
|
9888
|
+
* `new Map(secrets)` copy — starts with no entries here and takes the
|
|
9889
|
+
* pre-#2485 fall-through, the safe direction. A copy loses the evidence
|
|
9890
|
+
* deliberately: a copy is not the pass that resolved anything.
|
|
9891
|
+
*
|
|
9892
|
+
* `CONFLICTING_PLAINTEXT` marks an expression this map saw resolve to TWO
|
|
9893
|
+
* values (a region-pinned re-resolution of one spelling, say); it then vouches
|
|
9894
|
+
* for nothing, which is the same "answer nothing you cannot prove" rule
|
|
9895
|
+
* {@link plaintextIndexOf} applies to the collapsed map's reverse index.
|
|
9896
|
+
*/
|
|
9897
|
+
const resolvedPairsOf = /* @__PURE__ */ new WeakMap();
|
|
9898
|
+
/**
|
|
9899
|
+
* Record that `expression` resolved to `plaintext` in the pass that owns
|
|
9900
|
+
* `secrets` — the resolver's recording seam calls this beside its
|
|
9901
|
+
* `secrets.set(plaintext, expression)`, so the two never disagree about which
|
|
9902
|
+
* pass the evidence belongs to. Mask-only map entries (value `SECRET_MASK`)
|
|
9903
|
+
* never pass through that seam — they came from no `{{resolve:...}}` token —
|
|
9904
|
+
* so nothing here special-cases the mask string: a secret whose plaintext
|
|
9905
|
+
* happens to BE `***` is a secret like any other.
|
|
9906
|
+
*/
|
|
9907
|
+
function recordResolvedPair(secrets, expression, plaintext) {
|
|
9908
|
+
let pairs = resolvedPairsOf.get(secrets);
|
|
9909
|
+
if (pairs === void 0) {
|
|
9910
|
+
pairs = /* @__PURE__ */ new Map();
|
|
9911
|
+
resolvedPairsOf.set(secrets, pairs);
|
|
9912
|
+
}
|
|
9913
|
+
const previous = pairs.get(expression);
|
|
9914
|
+
if (previous === void 0) pairs.set(expression, plaintext);
|
|
9915
|
+
else if (previous !== plaintext) pairs.set(expression, CONFLICTING_PLAINTEXT);
|
|
9916
|
+
}
|
|
9917
|
+
/**
|
|
9918
|
+
* Carry the resolved pairs of `from` into `to`, for the one copy of a
|
|
9919
|
+
* resolver-populated map that POSITIONS anything: the deploy engine accumulates
|
|
9920
|
+
* each stack's output resolution into its `outputSecrets` bag entry by entry,
|
|
9921
|
+
* and without this the copy would keep the collapsed entries while dropping the
|
|
9922
|
+
* evidence — so a literal `Output` embedding one of two same-plaintext
|
|
9923
|
+
* references would fall back to the value scan and persist the sibling's
|
|
9924
|
+
* expression. The engine's other entry-by-entry copy — an `Export.Name`'s
|
|
9925
|
+
* secrets into the pass map — deliberately does NOT call this: a name never
|
|
9926
|
+
* positions a leaf, a value re-using the same token records its own pair at
|
|
9927
|
+
* the seam, and the only thing the merge could add is a CONFLICT (a
|
|
9928
|
+
* non-cacheable `{{resolve:ssm:X}}` whose value moved between the value pass
|
|
9929
|
+
* and the name's resolution), which would destroy positioning the value pass
|
|
9930
|
+
* had earned. A pair that conflicts across the two maps is marked conflicting
|
|
9931
|
+
* in `to`, the same rule {@link recordResolvedPair} applies within one map.
|
|
9932
|
+
*
|
|
9933
|
+
* Deliberately NOT a general "copy the map" helper: every other new map is a
|
|
9934
|
+
* different PASS, and starting it without evidence is the safe direction.
|
|
9935
|
+
*/
|
|
9936
|
+
function mergeResolvedPairs(from, to) {
|
|
9937
|
+
const pairs = resolvedPairsOf.get(from);
|
|
9938
|
+
if (pairs === void 0) return;
|
|
9939
|
+
for (const [expression, plaintext] of pairs) if (typeof plaintext === "string") recordResolvedPair(to, expression, plaintext);
|
|
9940
|
+
else {
|
|
9941
|
+
let target = resolvedPairsOf.get(to);
|
|
9942
|
+
if (target === void 0) {
|
|
9943
|
+
target = /* @__PURE__ */ new Map();
|
|
9944
|
+
resolvedPairsOf.set(to, target);
|
|
9945
|
+
}
|
|
9946
|
+
target.set(expression, CONFLICTING_PLAINTEXT);
|
|
9947
|
+
}
|
|
9948
|
+
}
|
|
9949
|
+
/**
|
|
9950
|
+
* The plaintext `expression` resolved to in the pass that owns `secrets`, or
|
|
9951
|
+
* `undefined` when that pass recorded nothing for it (or two different values).
|
|
9952
|
+
*/
|
|
9953
|
+
function resolvedPlaintextOf(secrets, expression) {
|
|
9954
|
+
const recorded = resolvedPairsOf.get(secrets)?.get(expression);
|
|
9955
|
+
return typeof recorded === "string" ? recorded : void 0;
|
|
9956
|
+
}
|
|
9957
|
+
/**
|
|
9611
9958
|
* Every `{{resolve:...}}` expression this process has PROVEN resolves to a
|
|
9612
9959
|
* secret, as a SET — uncollapsed by resolved value (issue #1910).
|
|
9613
9960
|
*
|
|
@@ -10618,8 +10965,9 @@ function isKnownSecretExpression(expression, secretExpressions) {
|
|
|
10618
10965
|
return isSecretExpressionByVerdictOrSpelling(expression) || secretExpressions.has(expression);
|
|
10619
10966
|
}
|
|
10620
10967
|
/**
|
|
10621
|
-
* The
|
|
10622
|
-
* `secretsmanager` by SPELLING, and anything this process
|
|
10968
|
+
* The arms of {@link isKnownSecretExpression} that need NO pass-local set:
|
|
10969
|
+
* `secretsmanager` / `ssm-secure` by SPELLING, and anything this process
|
|
10970
|
+
* PROVED secret.
|
|
10623
10971
|
*
|
|
10624
10972
|
* Split out so the resolver can ask the same question at the issue #2059
|
|
10625
10973
|
* recording seam, where no `secretExpressions` set is in hand. It must not
|
|
@@ -10644,7 +10992,7 @@ function isKnownSecretExpression(expression, secretExpressions) {
|
|
|
10644
10992
|
* change to a store this function only reads.
|
|
10645
10993
|
*/
|
|
10646
10994
|
function isSecretExpressionByVerdictOrSpelling(expression) {
|
|
10647
|
-
return expression.startsWith("{{resolve:secretsmanager:") || isRecordedSecretExpression(expression);
|
|
10995
|
+
return expression.startsWith("{{resolve:secretsmanager:") || expression.startsWith("{{resolve:ssm-secure:") || isRecordedSecretExpression(expression);
|
|
10648
10996
|
}
|
|
10649
10997
|
/**
|
|
10650
10998
|
* The character class a `{{resolve:...}}` reference's INNER text is built from,
|
|
@@ -11188,6 +11536,124 @@ function positionByIntrinsicSkeleton(bag, source, secrets, secretExpressions) {
|
|
|
11188
11536
|
return matched;
|
|
11189
11537
|
}
|
|
11190
11538
|
/**
|
|
11539
|
+
* The ONE-span frame shared by {@link positionByEmbeddedSpan} and
|
|
11540
|
+
* {@link learnMixedLeafNeedle}: a source holding exactly one `{{resolve:...}}`
|
|
11541
|
+
* token, and a bag that starts with the source's prefix and ends with its
|
|
11542
|
+
* suffix with something non-empty between them that is NOT itself a complete
|
|
11543
|
+
* token (an already-redacted record is a persisted answer, not a plaintext).
|
|
11544
|
+
* `undefined` for any other shape. One helper rather than two copies so the
|
|
11545
|
+
* two refusals cannot drift apart.
|
|
11546
|
+
*/
|
|
11547
|
+
function singleSpanFrame(bag, source) {
|
|
11548
|
+
const spans = dynamicReferenceSpans(source);
|
|
11549
|
+
if (spans.length !== 1) return void 0;
|
|
11550
|
+
const [span] = spans;
|
|
11551
|
+
const token = source.slice(span.start, span.end);
|
|
11552
|
+
const prefix = source.slice(0, span.start);
|
|
11553
|
+
const suffix = source.slice(span.end);
|
|
11554
|
+
if (bag.length <= prefix.length + suffix.length) return void 0;
|
|
11555
|
+
if (!bag.startsWith(prefix) || !bag.endsWith(suffix)) return void 0;
|
|
11556
|
+
const middle = bag.slice(prefix.length, bag.length - suffix.length);
|
|
11557
|
+
if (isSingleDynamicReferenceToken(middle)) return void 0;
|
|
11558
|
+
return {
|
|
11559
|
+
token,
|
|
11560
|
+
prefix,
|
|
11561
|
+
suffix,
|
|
11562
|
+
middle
|
|
11563
|
+
};
|
|
11564
|
+
}
|
|
11565
|
+
/**
|
|
11566
|
+
* Position a literal source leaf that EMBEDS exactly one `{{resolve:...}}`
|
|
11567
|
+
* token — `postgres://app-svc:{{resolve:ssm-secure:NAME}}@db/app` — by the
|
|
11568
|
+
* span the source states, writing `prefix + token + suffix` (issue
|
|
11569
|
+
* [#2485](https://github.com/go-to-k/cdkd/issues/2485)).
|
|
11570
|
+
*
|
|
11571
|
+
* WHY THE VALUE SCAN IS NOT ENOUGH HERE. The scan writes the map's surviving
|
|
11572
|
+
* expression for a plaintext, and the map keeps one expression per plaintext:
|
|
11573
|
+
* a whole-value `NAME:1` sibling that resolved LAST leaves `NAME:1` as the only
|
|
11574
|
+
* expression for the value, so the embedded leaf persists the versioned
|
|
11575
|
+
* spelling for a template that spells `NAME`, and the deploy diff — expression
|
|
11576
|
+
* against expression — reports that leaf on every run. The whole-token arm of
|
|
11577
|
+
* {@link redactByPath} is immune because it copies its own source; this arm
|
|
11578
|
+
* gives the one-span literal leaf the same immunity.
|
|
11579
|
+
*
|
|
11580
|
+
* THE EVIDENCE, and why the shape of the frame is not enough on its own: the
|
|
11581
|
+
* frame check (`bag` starts with the source's prefix and ends with its suffix,
|
|
11582
|
+
* with something between) is what {@link learnMixedLeafNeedle} already uses to
|
|
11583
|
+
* LEARN a needle, and it proves only that the bag has the source's shape. The
|
|
11584
|
+
* bag can also be a PREVIOUS generation's (`cdkd scrub`, a state-sourced walk)
|
|
11585
|
+
* with an earlier plaintext framed exactly like this, and writing today's
|
|
11586
|
+
* token over it would record an expression that was never deployed at that
|
|
11587
|
+
* position — the hazard `sourceIsSameGeneration` exists for on the whole-token
|
|
11588
|
+
* arm. So the middle must EQUAL what THIS pass recorded the source token
|
|
11589
|
+
* resolving to ({@link recordResolvedPair}, per map instance): that is evidence
|
|
11590
|
+
* of this resolution, not of shape, and it is absent by construction for every
|
|
11591
|
+
* bag this pass did not produce. It is also what keeps a PUBLIC `ssm` token
|
|
11592
|
+
* resolved (issue #1901) — the resolver records only secret verdicts — and what
|
|
11593
|
+
* keeps a mask-only `NoEcho` value out (never recorded).
|
|
11594
|
+
*
|
|
11595
|
+
* WHAT THIS EVIDENCE DOES NOT CLAIM, stated because a reviewer asked: it does
|
|
11596
|
+
* not prove the bag was produced FROM this source. A previous generation's bag
|
|
11597
|
+
* whose framed middle happens to EQUAL a plaintext this pass resolved the
|
|
11598
|
+
* source token to (`cdkd scrub` walking an old record against today's template,
|
|
11599
|
+
* or a failed deploy persisting an old bag) takes this arm and persists TODAY's
|
|
11600
|
+
* expression at that position. That is not a new claim: the value scan the
|
|
11601
|
+
* arm replaces rewrites that same plaintext onto one of THIS pass's expressions
|
|
11602
|
+
* regardless of generation — the map holds no other — so the class of answer
|
|
11603
|
+
* is unchanged and only the choice within it improves (the source's own
|
|
11604
|
+
* token rather than the map's survivor). The generation hazard this arm must
|
|
11605
|
+
* not create is the whole-token arm's: a middle that is ALREADY an expression
|
|
11606
|
+
* (a persisted answer from another generation), which the token refusal below
|
|
11607
|
+
* keeps out — and, by the same argument, any leaf the value scan would NOT
|
|
11608
|
+
* rewrite to exactly `prefix + survivor + suffix`: a middle shorter than the
|
|
11609
|
+
* scan's needle floor (an embedded 1-3 character secret stays the scan's
|
|
11610
|
+
* documented residual — issue #2516 tracks closing it with a bound that
|
|
11611
|
+
* proves the bag's generation, which this evidence does not), a whole leaf
|
|
11612
|
+
* that is itself another recorded plaintext, a needle starting in the prefix
|
|
11613
|
+
* and overlapping the middle. The
|
|
11614
|
+
* arm checks that equivalence against the scan's own answer rather than
|
|
11615
|
+
* re-deriving the scan's rules. Pinned by the cross-generation cases in
|
|
11616
|
+
* `secret-redaction-embedded-span.test.ts`.
|
|
11617
|
+
*
|
|
11618
|
+
* One shape reaches this arm that a reader may not expect: a WHOLE-token
|
|
11619
|
+
* source that FAILED the whole-token arm's `isKnownSecretExpression` gate (an
|
|
11620
|
+
* `ssm` token whose type came back unclassifiable and which lost the map slot
|
|
11621
|
+
* to a sibling). Its "frame" is empty, and if this pass recorded it resolving
|
|
11622
|
+
* to the bag it is written back as itself — an expression, and the leaf's own,
|
|
11623
|
+
* where the scan wrote the survivor. Stated so it is not mistaken for a leak.
|
|
11624
|
+
*
|
|
11625
|
+
* Everything else keeps the pre-#2485 fall-through: two or more spans (which
|
|
11626
|
+
* span produced which value is genuinely ambiguous when they share one), an
|
|
11627
|
+
* `Fn::Sub` / `Fn::Join` source (an object, not this arm at all — issue #2320's
|
|
11628
|
+
* placeholder primitive), a frame mismatch, a middle that is itself a complete
|
|
11629
|
+
* token (an already-redacted record, per the same refusal
|
|
11630
|
+
* {@link learnMixedLeafNeedle} makes), and a middle this pass cannot vouch for.
|
|
11631
|
+
*
|
|
11632
|
+
* The frame is copied from the SOURCE, not scanned. A needle occurring in the
|
|
11633
|
+
* literal frame would be a reference the template never had at that offset —
|
|
11634
|
+
* the fabricated-baseline direction {@link preferPositionDecisions} refuses —
|
|
11635
|
+
* and the whole-token arm returns its source unscanned for the same reason.
|
|
11636
|
+
*
|
|
11637
|
+
* RETURNS THE VALUE SCAN'S ANSWER ON EVERY REFUSAL, not `undefined`: the scan
|
|
11638
|
+
* is computed once, here, for `(bag, secrets)` — the arm's bound below compares
|
|
11639
|
+
* against it, and every fall-through IS it — so the compared value provably
|
|
11640
|
+
* comes from the same bag and map the arm positions. An earlier revision took
|
|
11641
|
+
* the scan as a parameter, which left the bound one wrong caller away from
|
|
11642
|
+
* comparing against a scan of some other bag with no type error.
|
|
11643
|
+
*/
|
|
11644
|
+
function positionByEmbeddedSpan(bag, source, secrets) {
|
|
11645
|
+
const scanned = redactSecretsForState(bag, secrets);
|
|
11646
|
+
const frame = singleSpanFrame(bag, source);
|
|
11647
|
+
if (frame === void 0) return scanned;
|
|
11648
|
+
const { token, prefix, suffix, middle } = frame;
|
|
11649
|
+
const recorded = resolvedPlaintextOf(secrets, token);
|
|
11650
|
+
if (recorded === void 0 || recorded !== middle) return scanned;
|
|
11651
|
+
const survivor = secrets.get(middle);
|
|
11652
|
+
if (survivor === void 0) return scanned;
|
|
11653
|
+
if (scanned !== prefix + survivor + suffix) return scanned;
|
|
11654
|
+
return prefix + token + suffix;
|
|
11655
|
+
}
|
|
11656
|
+
/**
|
|
11191
11657
|
* Keys tried, in order, when pairing two arrays whose ORDER cannot be trusted
|
|
11192
11658
|
* (issue #1915).
|
|
11193
11659
|
*
|
|
@@ -11387,7 +11853,7 @@ function redactByPath(bag, source, secrets, rules, secretExpressions) {
|
|
|
11387
11853
|
if (!rules.sourceIsSameGeneration && isSingleDynamicReferenceToken(bag)) return secrets.get(bag) ?? bag;
|
|
11388
11854
|
return source;
|
|
11389
11855
|
}
|
|
11390
|
-
return
|
|
11856
|
+
return positionByEmbeddedSpan(bag, source, secrets);
|
|
11391
11857
|
}
|
|
11392
11858
|
if (typeof bag === "string" && isPlainObject$2(source)) {
|
|
11393
11859
|
const certified = positionByCrossStackSource(bag, source, secrets);
|
|
@@ -11505,12 +11971,13 @@ function subtreeHasDynamicReference(value) {
|
|
|
11505
11971
|
* Widening it changes one answer, in the SAFE direction for BOTH readers.
|
|
11506
11972
|
*
|
|
11507
11973
|
* `drift.ts`'s `survivingDynamicReferences` is the reader that is easy to
|
|
11508
|
-
* forget, because it lives in another file — it feeds
|
|
11509
|
-
*
|
|
11510
|
-
*
|
|
11511
|
-
*
|
|
11512
|
-
*
|
|
11513
|
-
* `
|
|
11974
|
+
* forget, because it lives in another file — it feeds the survivor REPORT
|
|
11975
|
+
* (`onUnresolved`, and through it the `unresolvedToken` cause), so seeing MORE
|
|
11976
|
+
* tokens can only report more, never less. Do not shorten this to "the only
|
|
11977
|
+
* reader": that sentence is what a later editor uses to bound the blast
|
|
11978
|
+
* radius of touching the class, and getting it wrong points them away from
|
|
11979
|
+
* the report / `--json` / `--accept` path where an unreported survivor would
|
|
11980
|
+
* surface.
|
|
11514
11981
|
*
|
|
11515
11982
|
* The other reader is the DECLARED direction for issue #1901:
|
|
11516
11983
|
* {@link mixedLeafMayCarryPublicReference}, which asks whether a MIXED leaf
|
|
@@ -11916,9 +12383,9 @@ function unkeyedArrayPairsByAnchors(bag, source) {
|
|
|
11916
12383
|
* from one the pass decided IN FAVOUR of the value already there. Two shapes
|
|
11917
12384
|
* hit it, both fabricating a baseline `cdkd drift --revert` then pushes:
|
|
11918
12385
|
*
|
|
11919
|
-
* - the resolver's unsupported-service arm leaves
|
|
11920
|
-
*
|
|
11921
|
-
* leaf. The string arm returns `source` — a decision — and the equality made
|
|
12386
|
+
* - the resolver's unsupported-service arm leaves a `{{resolve:...}}` token it
|
|
12387
|
+
* has no arm for LITERAL (`ssm-secure:` was one until issue #2482), so AWS
|
|
12388
|
+
* echoes it back and the source leaf EQUALS the bag leaf. The string arm returns `source` — a decision — and the equality made
|
|
11922
12389
|
* it look like no decision at all. (A BARE such token takes the whole-token
|
|
11923
12390
|
* arm and one embedded in text takes the mixed-leaf arm; both decide, and
|
|
11924
12391
|
* both were misread.)
|
|
@@ -12102,7 +12569,8 @@ function learnWholeTokenNeedle(collector, bag, source) {
|
|
|
12102
12569
|
* would then contain a whole `{{resolve:...}}` token and a resolved readback
|
|
12103
12570
|
* cannot end with one. The shape it genuinely decides is a second reference
|
|
12104
12571
|
* that survives LITERALLY in the readback — the resolver's
|
|
12105
|
-
* unsupported-service arm (`ssm-secure:`
|
|
12572
|
+
* unsupported-service arm produces exactly that (`ssm-secure:` did until
|
|
12573
|
+
* issue #2482; a spelling with no arm still does) — where the
|
|
12106
12574
|
* extraction would in fact be right and is declined anyway. Measured: a
|
|
12107
12575
|
* both-resolved fixture leaves this line unfenced.
|
|
12108
12576
|
* - the source's literal PREFIX and SUFFIX must both be present at the ends of
|
|
@@ -12116,17 +12584,10 @@ function learnWholeTokenNeedle(collector, bag, source) {
|
|
|
12116
12584
|
* correctly, while an `indexOf` scan would cut it short.
|
|
12117
12585
|
*/
|
|
12118
12586
|
function learnMixedLeafNeedle(collector, bag, source) {
|
|
12119
|
-
const
|
|
12120
|
-
if (
|
|
12121
|
-
const
|
|
12122
|
-
const token = source.slice(span.start, span.end);
|
|
12587
|
+
const frame = singleSpanFrame(bag, source);
|
|
12588
|
+
if (frame === void 0) return;
|
|
12589
|
+
const { token, middle: plaintext } = frame;
|
|
12123
12590
|
if (!expressionMaySeedANeedle(token)) return;
|
|
12124
|
-
const prefix = source.slice(0, span.start);
|
|
12125
|
-
const suffix = source.slice(span.end);
|
|
12126
|
-
if (bag.length <= prefix.length + suffix.length) return;
|
|
12127
|
-
if (!bag.startsWith(prefix) || !bag.endsWith(suffix)) return;
|
|
12128
|
-
const plaintext = bag.slice(prefix.length, bag.length - suffix.length);
|
|
12129
|
-
if (isSingleDynamicReferenceToken(plaintext)) return;
|
|
12130
12591
|
learnNeedle(collector, plaintext, token);
|
|
12131
12592
|
}
|
|
12132
12593
|
/**
|
|
@@ -15382,17 +15843,22 @@ function s3BucketWebsiteUrl(bucketName, region) {
|
|
|
15382
15843
|
/**
|
|
15383
15844
|
* The `{{resolve:<service>:...}}` families whose value can be a SECRET, and
|
|
15384
15845
|
* therefore the only ones the region question below is asked about: every
|
|
15385
|
-
* `secretsmanager` reference by spelling,
|
|
15386
|
-
*
|
|
15387
|
-
*
|
|
15388
|
-
*
|
|
15389
|
-
*
|
|
15390
|
-
*
|
|
15391
|
-
*
|
|
15392
|
-
*
|
|
15393
|
-
*
|
|
15394
|
-
|
|
15395
|
-
|
|
15846
|
+
* `secretsmanager` reference by spelling, every `ssm-secure` one by spelling
|
|
15847
|
+
* (issue #2482 — it is resolved through the same `GetParameter` as `ssm`, so
|
|
15848
|
+
* the wrong region answers it in exactly the same way), and every `ssm` one,
|
|
15849
|
+
* which is secret exactly when its parameter is a `SecureString` (issue #1901).
|
|
15850
|
+
*
|
|
15851
|
+
* Every OTHER service is `local` because cdkd cannot resolve it at all — the
|
|
15852
|
+
* resolver's unsupported-service arm leaves such a token in place, so there
|
|
15853
|
+
* is no lookup for a region to get wrong. None of CloudFormation's three
|
|
15854
|
+
* services is in that position any more; the arm exists for a spelling that
|
|
15855
|
+
* is not a dynamic reference at all, or one AWS adds later.
|
|
15856
|
+
*/
|
|
15857
|
+
const REPLAY_SECRET_SERVICES = /* @__PURE__ */ new Set([
|
|
15858
|
+
"secretsmanager",
|
|
15859
|
+
"ssm",
|
|
15860
|
+
"ssm-secure"
|
|
15861
|
+
]);
|
|
15396
15862
|
/**
|
|
15397
15863
|
* Split a `{{resolve:secretsmanager:...}}` inner body into its SECRET_ID.
|
|
15398
15864
|
*
|
|
@@ -15428,7 +15894,7 @@ function secretsManagerSecretId(inner) {
|
|
|
15428
15894
|
* a different thing in the refusal message than the one that would be read.
|
|
15429
15895
|
*/
|
|
15430
15896
|
function ssmParameterName(inner) {
|
|
15431
|
-
return inner.substring(
|
|
15897
|
+
return inner.substring(inner.indexOf(":") + 1);
|
|
15432
15898
|
}
|
|
15433
15899
|
/**
|
|
15434
15900
|
* The region an ARN names, or `undefined` for anything that is not an ARN with
|
|
@@ -16155,7 +16621,7 @@ function normalizeAwsTagsToCfn(tags) {
|
|
|
16155
16621
|
* through `update()` and surface as a hard AWS rejection, so we sanitize
|
|
16156
16622
|
* the wire-layer payload while keeping the read-side placeholder
|
|
16157
16623
|
* intact. This is the Class 2 pattern from
|
|
16158
|
-
* `docs/provider-
|
|
16624
|
+
* `docs/provider-rules.md#readcurrentstate-for-drift-detection`.
|
|
16159
16625
|
*/
|
|
16160
16626
|
function sanitizeDescription(value) {
|
|
16161
16627
|
if (value === void 0 || value === null) return void 0;
|
|
@@ -20488,7 +20954,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
20488
20954
|
});
|
|
20489
20955
|
for (const { fullMatch, inner } of matches) {
|
|
20490
20956
|
const service = inner.split(":")[0];
|
|
20491
|
-
const isKnownSecret = service === "secretsmanager" || recordedSecretExpressions.has(fullMatch);
|
|
20957
|
+
const isKnownSecret = service === "secretsmanager" || service === "ssm-secure" || recordedSecretExpressions.has(fullMatch);
|
|
20492
20958
|
if (isKnownSecret && context?.skipDynamicReferences) continue;
|
|
20493
20959
|
const regionVerdict = classifyReplaySecretRegion(fullMatch, this.explicitRegion ?? this.resolverRegion, context?.producerRegions);
|
|
20494
20960
|
if (regionVerdict.kind === "ambiguous") throw markNonRetryable(new DynamicReferenceRegionAmbiguousError(`Refusing to resolve the secret reference ${fullMatch}: it names '${regionVerdict.secretName}' without a region, and this stack reads from ${regionVerdict.foreignProducerRegions.join(", ")} as well as its own region. cdkd cannot tell which one must answer, and resolving against the wrong one yields a different secret. Spell the reference as a full ARN to say which region owns it.`));
|
|
@@ -20499,7 +20965,10 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
20499
20965
|
}
|
|
20500
20966
|
const cached = this.cachedDynamicReferences.get(fullMatch);
|
|
20501
20967
|
if (cached) {
|
|
20502
|
-
if (cached.secret && cached.value)
|
|
20968
|
+
if (cached.secret && cached.value) {
|
|
20969
|
+
context?.recordedSecretValues?.set(cached.value, fullMatch);
|
|
20970
|
+
if (context?.recordedSecretValues) recordResolvedPair(context.recordedSecretValues, fullMatch, cached.value);
|
|
20971
|
+
}
|
|
20503
20972
|
result = result.replace(fullMatch, () => cached.value);
|
|
20504
20973
|
continue;
|
|
20505
20974
|
}
|
|
@@ -20531,6 +21000,11 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
20531
21000
|
if (!decrypt) continue;
|
|
20532
21001
|
}
|
|
20533
21002
|
resolved = param.value;
|
|
21003
|
+
} else if (service === "ssm-secure") {
|
|
21004
|
+
const param = await this.resolveSSMReference(parts, true, "ssm-secure");
|
|
21005
|
+
if (!param.secure) throw markNonRetryable(new IntrinsicResolutionRefusalError(`Refusing to resolve ${fullMatch}: the parameter is a ${param.type} parameter, and the ssm-secure spelling is defined for SecureString parameters only. Reference it as {{resolve:ssm:...}} if it is public configuration.`));
|
|
21006
|
+
isSecret = true;
|
|
21007
|
+
resolved = param.value;
|
|
20534
21008
|
} else {
|
|
20535
21009
|
this.logger.warn(`Unsupported dynamic reference service: ${service}`);
|
|
20536
21010
|
continue;
|
|
@@ -20541,7 +21015,8 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
20541
21015
|
});
|
|
20542
21016
|
if (isSecret && resolved) {
|
|
20543
21017
|
context?.recordedSecretValues?.set(resolved, fullMatch);
|
|
20544
|
-
if (
|
|
21018
|
+
if (context?.recordedSecretValues) recordResolvedPair(context.recordedSecretValues, fullMatch, resolved);
|
|
21019
|
+
if (service === "secretsmanager" || service === "ssm-secure") this.pinSecretVerdict(fullMatch, true);
|
|
20545
21020
|
}
|
|
20546
21021
|
result = result.replace(fullMatch, () => resolved);
|
|
20547
21022
|
}
|
|
@@ -20751,16 +21226,16 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
20751
21226
|
* discard the value when `secure` is set — it is ciphertext, not the resolved
|
|
20752
21227
|
* reference.
|
|
20753
21228
|
*/
|
|
20754
|
-
async resolveSSMReference(parts, decrypt = true) {
|
|
21229
|
+
async resolveSSMReference(parts, decrypt = true, service = "ssm") {
|
|
20755
21230
|
const parameterName = parts.slice(1).join(":");
|
|
20756
|
-
if (!parameterName) throw new Error(
|
|
20757
|
-
this.logger.debug(`Resolving dynamic reference:
|
|
21231
|
+
if (!parameterName) throw new Error(`Dynamic reference: ${service} PARAMETER_NAME is required`);
|
|
21232
|
+
this.logger.debug(`Resolving dynamic reference: ${service}:${parameterName}`);
|
|
20758
21233
|
const client = this.clientsForRegion(this.explicitRegion).ssm;
|
|
20759
21234
|
const command = new GetParameterCommand({
|
|
20760
21235
|
Name: parameterName,
|
|
20761
21236
|
WithDecryption: decrypt
|
|
20762
21237
|
});
|
|
20763
|
-
const response = await this.sendWithThrottleRetry(() => client.send(command),
|
|
21238
|
+
const response = await this.sendWithThrottleRetry(() => client.send(command), `${service}:${parameterName}`);
|
|
20764
21239
|
const paramValue = response.Parameter?.Value;
|
|
20765
21240
|
if (paramValue === void 0 || paramValue === null) throw new Error(`Dynamic reference: SSM parameter '${parameterName}' not found or has no value`);
|
|
20766
21241
|
const paramType = response.Parameter?.Type;
|
|
@@ -20768,7 +21243,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
20768
21243
|
if (secure && paramType !== "SecureString" && !this.warnedUnrecognizedSsmTypes.has(`${parameterName}\u0000${String(paramType)}`)) {
|
|
20769
21244
|
this.warnedUnrecognizedSsmTypes.add(`${parameterName}\u0000${String(paramType)}`);
|
|
20770
21245
|
const reported = paramType === void 0 ? "(absent)" : `'${String(paramType)}'`;
|
|
20771
|
-
this.logger.warn(`SSM parameter '${parameterName}' reported an unrecognized Type ${reported} — treating its value as a secret, so cdkd will persist the {{resolve
|
|
21246
|
+
this.logger.warn(`SSM parameter '${parameterName}' reported an unrecognized Type ${reported} — treating its value as a secret, so cdkd will persist the {{resolve:${service}:...}} expression rather than the resolved value. Declare the parameter as String / StringList if it is public config.`);
|
|
20772
21247
|
}
|
|
20773
21248
|
return {
|
|
20774
21249
|
value: paramValue,
|
|
@@ -21903,7 +22378,7 @@ var CloudControlProvider = class {
|
|
|
21903
22378
|
const indeterminateGuard = await this.confirmDeleteTargetIdentity(logicalId, resourceType, physicalId, context);
|
|
21904
22379
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
21905
22380
|
this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
|
|
21906
|
-
const { ASGProvider } = await import("./asg-provider-
|
|
22381
|
+
const { ASGProvider } = await import("./asg-provider-CKEf903K.js").then((n) => n.n);
|
|
21907
22382
|
return withIndeterminateGuard(await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context), indeterminateGuard);
|
|
21908
22383
|
}
|
|
21909
22384
|
const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
|
|
@@ -23088,42 +23563,6 @@ function interruptWatchListenerCount() {
|
|
|
23088
23563
|
return sharedSigintHandler === void 0 ? 0 : 1;
|
|
23089
23564
|
}
|
|
23090
23565
|
|
|
23091
|
-
//#endregion
|
|
23092
|
-
//#region src/state/state-prefix.ts
|
|
23093
|
-
/**
|
|
23094
|
-
* The default S3 key prefix for cdkd state.
|
|
23095
|
-
*
|
|
23096
|
-
* Homed in the STATE layer rather than in `src/cli/commands/state-file-keys.ts`
|
|
23097
|
-
* (which re-exports it, so its four existing importers are unchanged) because
|
|
23098
|
-
* `src/state/lock-contention-message.ts` needs it to decide whether a recovery
|
|
23099
|
-
* hint should spell `--state-prefix` at all, and a `src/state/**` module
|
|
23100
|
-
* importing from `src/cli/commands/**` inverts the layering — the CLI sits
|
|
23101
|
-
* ABOVE the state layer in the 7-layer architecture, not below it.
|
|
23102
|
-
*
|
|
23103
|
-
* Note this is only the DEFAULT. Other commands accept `--state-prefix`, so
|
|
23104
|
-
* whole-bucket listings deliberately do not scope to it.
|
|
23105
|
-
*/
|
|
23106
|
-
const DEFAULT_STATE_PREFIX = "cdkd";
|
|
23107
|
-
/**
|
|
23108
|
-
* The state-bucket prefix `CustomResourceProvider` PUTs its response
|
|
23109
|
-
* placeholders under, one object per invocation
|
|
23110
|
-
* (`custom-resource-responses/{requestId}.json`).
|
|
23111
|
-
*
|
|
23112
|
-
* Homed here for the same layering reason as {@link DEFAULT_STATE_PREFIX}: the
|
|
23113
|
-
* PRODUCER is `src/provisioning/providers/custom-resource-provider.ts` and the
|
|
23114
|
-
* COLLECTOR is `src/cli/commands/gc.ts`, so a copy in either would be a copy
|
|
23115
|
-
* the other could drift from — and the two spellings would then disagree about
|
|
23116
|
-
* which objects exist, which is the only way a sweeper can miss the family it
|
|
23117
|
-
* was written for (issue #2052). `src/cli/commands/state-file-keys.ts`
|
|
23118
|
-
* re-exports it so gc reads it alongside the other state-key constants.
|
|
23119
|
-
*
|
|
23120
|
-
* Note this is only the DEFAULT: `ProviderRegistry` can be configured with a
|
|
23121
|
-
* different `responsePrefix`, so a sweep scoped to this value is a sweep of the
|
|
23122
|
-
* default layout. gc has no access to a non-default one — nothing persists it —
|
|
23123
|
-
* which is stated at the sweep's own call site rather than implied here.
|
|
23124
|
-
*/
|
|
23125
|
-
const CUSTOM_RESOURCE_RESPONSE_PREFIX = "custom-resource-responses";
|
|
23126
|
-
|
|
23127
23566
|
//#endregion
|
|
23128
23567
|
//#region src/provisioning/masked-retry-logger.ts
|
|
23129
23568
|
/**
|
|
@@ -27724,7 +28163,7 @@ function isCustomResource(resourceType) {
|
|
|
27724
28163
|
* live value on removal. Route every optional mutable field of a
|
|
27725
28164
|
* merge-semantics API through this helper, with `clearValue` set to the
|
|
27726
28165
|
* property's CFn default or the SDK-documented clear sentinel — see
|
|
27727
|
-
* docs/provider-
|
|
28166
|
+
* docs/provider-rules.md#update-removal-semantics-clear-on-removal for the per-field checklist.
|
|
27728
28167
|
*
|
|
27729
28168
|
* Returns `newValue` when present, the `clearValue` when the field was
|
|
27730
28169
|
* present before and is now absent (removal), and `undefined` when it was
|
|
@@ -28741,9 +29180,16 @@ function formatResourceLine(op, logicalId, resourceType, verbOverride) {
|
|
|
28741
29180
|
*
|
|
28742
29181
|
* To avoid an accidental data-loss footgun, cdkd refuses to recreate
|
|
28743
29182
|
* any resource whose type is in {@link STATEFUL_TYPES} unless the user
|
|
28744
|
-
* ALSO passes `--force-stateful-recreation`.
|
|
28745
|
-
*
|
|
28746
|
-
*
|
|
29183
|
+
* ALSO passes `--force-stateful-recreation`. On the `--recreate-via-*`
|
|
29184
|
+
* and `--replace` opt-ins that is a two-flag protection, mirroring
|
|
29185
|
+
* `--remove-protection`'s pattern (see
|
|
29186
|
+
* `src/cli/commands/destroy-runner.ts`). It is NOT only that shape any
|
|
29187
|
+
* more: the guard also runs on replacement paths a plain `cdkd deploy`
|
|
29188
|
+
* reaches with no flag at all (a property-driven replacement, and the
|
|
29189
|
+
* update-failure fallback's Cloud Control trigger — issue [#2514]),
|
|
29190
|
+
* where `--force-stateful-recreation` is the ONLY flag involved. See
|
|
29191
|
+
* {@link isStatefulRecreateTargetForReplace} for the mid-deploy variant
|
|
29192
|
+
* those paths use.
|
|
28747
29193
|
*
|
|
28748
29194
|
* The list is hand-curated and intentionally **conservative**: every
|
|
28749
29195
|
* type here carries user data that the AWS service does NOT
|
|
@@ -28751,18 +29197,59 @@ function formatResourceLine(op, logicalId, resourceType, verbOverride) {
|
|
|
28751
29197
|
* AWS service treats as ephemeral (e.g. Lambda Function, IAM Role)
|
|
28752
29198
|
* are NOT in this list — recreate is cheap.
|
|
28753
29199
|
*
|
|
28754
|
-
* Two entries
|
|
28755
|
-
* the
|
|
29200
|
+
* Two entries carry a CONDITION instead of counting unconditionally.
|
|
29201
|
+
* The condition is what the guard evaluates; failing it is not a
|
|
29202
|
+
* finding that the resource holds no data (see the LogGroup note):
|
|
28756
29203
|
*
|
|
28757
29204
|
* - `AWS::S3::Bucket`: empty buckets are safe to recreate. The
|
|
28758
|
-
* deploy engine probes `s3:
|
|
29205
|
+
* deploy engine probes `s3:ListObjectVersions` at plan time and only
|
|
28759
29206
|
* refuses when the bucket has at least one object.
|
|
28760
|
-
* - `AWS::Logs::LogGroup`:
|
|
28761
|
-
*
|
|
28762
|
-
*
|
|
29207
|
+
* - `AWS::Logs::LogGroup`: the deploy engine refuses only when
|
|
29208
|
+
* `RetentionInDays > 0`. NOTE this is a KNOWN GAP, not a statement
|
|
29209
|
+
* that the rest are empty: an unset or zero retention is CloudWatch
|
|
29210
|
+
* Logs' "never expire", the most data-bearing setting there is, and
|
|
29211
|
+
* `LogsLogGroupProvider` writes `0` for precisely that. Issue
|
|
29212
|
+
* [#2558] tracks it; the predicate is left as-is here.
|
|
28763
29213
|
*
|
|
28764
29214
|
* Both conditional checks live in {@link isStatefulRecreateTarget};
|
|
28765
29215
|
* the bare {@link STATEFUL_TYPES} set is the type-only first-cut.
|
|
29216
|
+
*
|
|
29217
|
+
* **Lower bound, enforced by a test** (issue [#2514]'s review round):
|
|
29218
|
+
* every type in `final-snapshot.ts`'s `ATOMIC_FINAL_SNAPSHOT_TYPES` ∪
|
|
29219
|
+
* `PRE_DELETE_SNAPSHOT_TYPES` must appear here. That union is the
|
|
29220
|
+
* CloudFormation-documented `DeletionPolicy: Snapshot`-capable list, and
|
|
29221
|
+
* CloudFormation permits the attribute exactly where deleting the resource
|
|
29222
|
+
* destroys data worth capturing first — so membership there is an
|
|
29223
|
+
* AWS-authored statement that the type is data-bearing. The types that were
|
|
29224
|
+
* in that union and NOT here (`AWS::Redshift::Cluster`,
|
|
29225
|
+
* `AWS::ElastiCache::ReplicationGroup`, `AWS::ElastiCache::CacheCluster`)
|
|
29226
|
+
* meant cdkd took a final snapshot before a `cdkd destroy` of them
|
|
29227
|
+
* while replacing them mid-deploy with no consent flag at all.
|
|
29228
|
+
* `tests/unit/provisioning/stateful-types.test.ts` pins the subset
|
|
29229
|
+
* relation so a future addition to either snapshot set cannot land without
|
|
29230
|
+
* the guard entry.
|
|
29231
|
+
*
|
|
29232
|
+
* The relation is deliberately ONE-directional: most data-bearing types
|
|
29233
|
+
* (S3, DynamoDB, LogGroup, ECR, …) have no snapshot API at all and
|
|
29234
|
+
* CloudFormation rejects `DeletionPolicy: Snapshot` on them, so this set is
|
|
29235
|
+
* a strict superset and the reverse containment would be wrong.
|
|
29236
|
+
*
|
|
29237
|
+
* **Second lower bound, also enforced by a test**: every resource type
|
|
29238
|
+
* registered to a provider whose `delete()` consults
|
|
29239
|
+
* `DeleteContext.forceDataDelete` must appear here. That field is set ONLY
|
|
29240
|
+
* by the replacement / recreate delete sites under
|
|
29241
|
+
* `--force-stateful-recreation`, so a provider reading it has already
|
|
29242
|
+
* declared its delete destroys user data — which makes the guard list's
|
|
29243
|
+
* agreement checkable rather than hand-curated.
|
|
29244
|
+
* `AWS::S3Express::DirectoryBucket` was the one type on the wrong side.
|
|
29245
|
+
*
|
|
29246
|
+
* Neither fence can see a provider whose delete destroys data with NO opt-in
|
|
29247
|
+
* at all — an unconditional empty (`S3TablesProvider.deleteTableBucket`,
|
|
29248
|
+
* `S3VectorsProvider.deleteVectorBucket`) or a plainly destructive API call
|
|
29249
|
+
* (`KMSProvider`'s `ScheduleKeyDeletion`,
|
|
29250
|
+
* `CodeCommitRepositoryProvider`'s `DeleteRepository`). Those types are
|
|
29251
|
+
* hand-added with the reason at the entry, and a provider added that way
|
|
29252
|
+
* should be reviewed for whether it wants a `forceDataDelete` gate too.
|
|
28766
29253
|
*/
|
|
28767
29254
|
const STATEFUL_TYPES = /* @__PURE__ */ new Set([
|
|
28768
29255
|
"AWS::RDS::DBInstance",
|
|
@@ -28773,17 +29260,29 @@ const STATEFUL_TYPES = /* @__PURE__ */ new Set([
|
|
|
28773
29260
|
"AWS::Neptune::DBCluster",
|
|
28774
29261
|
"AWS::DynamoDB::Table",
|
|
28775
29262
|
"AWS::DynamoDB::GlobalTable",
|
|
29263
|
+
"AWS::Redshift::Cluster",
|
|
29264
|
+
"AWS::ElastiCache::CacheCluster",
|
|
29265
|
+
"AWS::ElastiCache::ReplicationGroup",
|
|
28776
29266
|
"AWS::EFS::FileSystem",
|
|
28777
29267
|
"AWS::FSx::FileSystem",
|
|
28778
29268
|
"AWS::S3::Bucket",
|
|
29269
|
+
"AWS::S3Express::DirectoryBucket",
|
|
29270
|
+
"AWS::S3Tables::TableBucket",
|
|
29271
|
+
"AWS::S3Tables::Table",
|
|
29272
|
+
"AWS::S3Tables::Namespace",
|
|
29273
|
+
"AWS::S3Vectors::VectorBucket",
|
|
28779
29274
|
"AWS::ECR::Repository",
|
|
28780
29275
|
"AWS::EC2::Volume",
|
|
29276
|
+
"AWS::EMR::Cluster",
|
|
28781
29277
|
"AWS::Kinesis::Stream",
|
|
28782
29278
|
"AWS::Elasticsearch::Domain",
|
|
28783
29279
|
"AWS::OpenSearchService::Domain",
|
|
28784
29280
|
"AWS::Cognito::UserPool",
|
|
28785
29281
|
"AWS::SecretsManager::Secret",
|
|
28786
29282
|
"AWS::SSM::Parameter",
|
|
29283
|
+
"AWS::KMS::Key",
|
|
29284
|
+
"AWS::KMS::ReplicaKey",
|
|
29285
|
+
"AWS::CodeCommit::Repository",
|
|
28787
29286
|
"AWS::Glue::Database",
|
|
28788
29287
|
"AWS::Glue::Table",
|
|
28789
29288
|
"AWS::Logs::LogGroup",
|
|
@@ -28804,7 +29303,7 @@ const STATEFUL_TYPES = /* @__PURE__ */ new Set([
|
|
|
28804
29303
|
const MULTI_REGION_RECREATE_BLOCKED_TYPES = /* @__PURE__ */ new Set(["AWS::DynamoDB::GlobalTable"]);
|
|
28805
29304
|
/**
|
|
28806
29305
|
* Cheap, synchronous read of the resource's recorded properties only.
|
|
28807
|
-
* For `AWS::S3::Bucket` this returns `null` — the live `
|
|
29306
|
+
* For `AWS::S3::Bucket` this returns `null` — the live `ListObjectVersions`
|
|
28808
29307
|
* probe to distinguish empty buckets (safe to recreate) from
|
|
28809
29308
|
* non-empty (data loss) lives in
|
|
28810
29309
|
* `src/deployment/recreate-targets.ts#probeStatefulRecreateTargetsAsync`
|
|
@@ -28827,17 +29326,26 @@ function isStatefulRecreateTargetSync(resourceType, recordedProperties) {
|
|
|
28827
29326
|
return "always";
|
|
28828
29327
|
}
|
|
28829
29328
|
/**
|
|
28830
|
-
* Conservative variant for the
|
|
29329
|
+
* Conservative variant for the deploy engine's mid-deploy guard sites, which
|
|
29330
|
+
* between them serve the paths below — only one of which is reached by a flag:
|
|
28831
29331
|
*
|
|
28832
|
-
*
|
|
28833
|
-
*
|
|
29332
|
+
* - property-driven replacement (an immutable / createOnly property changed
|
|
29333
|
+
* in the template) — fires on a plain `cdkd deploy`, no flag;
|
|
29334
|
+
* - the update-failure fallback's Cloud Control trigger (an
|
|
29335
|
+
* `UnsupportedActionException` / "does not support UPDATE" rejection) —
|
|
29336
|
+
* also no flag (issue [#2514]);
|
|
29337
|
+
* - the same fallback's `--replace` trigger (an SDK provider's typed
|
|
29338
|
+
* `ResourceUpdateNotSupportedError`).
|
|
29339
|
+
*
|
|
29340
|
+
* All of them catch the rejection or classify the diff while the deploy is
|
|
29341
|
+
* already in flight, so — unlike the `--recreate-via-*` pre-flight, which
|
|
28834
29342
|
* runs {@link probeStatefulRecreateTargetsAsync} (`s3:ListObjectVersions`) —
|
|
28835
29343
|
* there is no opportunity to probe an `AWS::S3::Bucket`'s object count. The
|
|
28836
29344
|
* sync check returns `null` for S3 (it defers to that async probe), which would
|
|
28837
29345
|
* let a NON-EMPTY bucket be DELETE + CREATEd (data loss) without
|
|
28838
29346
|
* `--force-stateful-recreation`. To stay fail-safe, treat a deferred S3 bucket
|
|
28839
29347
|
* as stateful here: the user must pass `--force-stateful-recreation` to replace
|
|
28840
|
-
* ANY S3 bucket
|
|
29348
|
+
* ANY S3 bucket on any of those paths, empty or not. Every other type matches
|
|
28841
29349
|
* {@link isStatefulRecreateTargetSync} exactly (the LogGroup retention check is
|
|
28842
29350
|
* fully resolvable from recorded properties, so no conservatism is needed there).
|
|
28843
29351
|
*/
|
|
@@ -30818,7 +31326,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
30818
31326
|
refuseMaskedReplayBaseline(resolvedPrevProps, op.logicalId);
|
|
30819
31327
|
recordNestedStackParameterExpressions(secrets, op.resourceType, resolvedPrevProps, prev.properties, STATE_DERIVED_RULES);
|
|
30820
31328
|
logger.info(` Rollback: Reversing replacement of ${op.logicalId} (${op.resourceType}) — re-creating the old resource and deleting the new one`);
|
|
30821
|
-
if (STATEFUL_TYPES.has(op.resourceType)) logger.warn(` ⚠ ${op.logicalId} (${op.resourceType}) is a stateful type — the old physical resource's data was destroyed by the replacement and
|
|
31329
|
+
if (STATEFUL_TYPES.has(op.resourceType)) logger.warn(` ⚠ ${op.logicalId} (${op.resourceType}) is a stateful type — the old physical resource's data was destroyed by the replacement and is NOT recovered by this rollback; the re-created resource starts empty.`);
|
|
30822
31330
|
const { provider: createProvider } = ctx.providerRegistry.getProviderFor({
|
|
30823
31331
|
resourceType: op.resourceType,
|
|
30824
31332
|
provisionedBy: prev.provisionedBy
|
|
@@ -33385,7 +33893,7 @@ var DeployEngine = class {
|
|
|
33385
33893
|
const statefulReason = isStatefulRecreateTargetForReplace(resourceType, currentProps);
|
|
33386
33894
|
if (statefulReason && this.options.forceStatefulRecreation !== true) {
|
|
33387
33895
|
const immutableProps = change.propertyChanges?.filter((pc) => pc.requiresReplacement).map((pc) => pc.path).join(", ");
|
|
33388
|
-
throw new CdkdError(`${logicalId} (${resourceType}) requires replacement (immutable property changed: ${immutableProps}) but it is a stateful resource — ${renderStatefulReason(statefulReason)}. Re-run with --force-stateful-recreation to confirm the data loss, or change the resource definition to avoid the immutable-property change.`, "STATEFUL_REPLACE_BLOCKED");
|
|
33896
|
+
throw markNonRetryable(new CdkdError(`${logicalId} (${resourceType}) requires replacement (immutable property changed: ${immutableProps}) but it is a stateful resource — ${renderStatefulReason(statefulReason)}. Re-run with --force-stateful-recreation to confirm the data loss, or change the resource definition to avoid the immutable-property change.`, "STATEFUL_REPLACE_BLOCKED"));
|
|
33389
33897
|
}
|
|
33390
33898
|
}
|
|
33391
33899
|
let replacementReason;
|
|
@@ -33528,9 +34036,10 @@ var DeployEngine = class {
|
|
|
33528
34036
|
const ccUnsupported = msg.includes("UnsupportedActionException") || msg.includes("does not support UPDATE");
|
|
33529
34037
|
const replaceOptIn = updateError instanceof ResourceUpdateNotSupportedError && this.options.replace === true;
|
|
33530
34038
|
if (ccUnsupported || replaceOptIn) {
|
|
33531
|
-
|
|
33532
|
-
|
|
33533
|
-
|
|
34039
|
+
const statefulReason = isStatefulRecreateTargetForReplace(resourceType, currentProps);
|
|
34040
|
+
if (statefulReason && this.options.forceStatefulRecreation !== true) {
|
|
34041
|
+
const retainNote = updateReplacePolicy === "Retain" ? " Note: UpdateReplacePolicy: Retain does NOT protect this path — the replacement deletes the old resource regardless." : "";
|
|
34042
|
+
throw markNonRetryable(new CdkdError((replaceOptIn ? `--replace would DELETE + CREATE the stateful resource ${logicalId} (${resourceType}) — ${renderStatefulReason(statefulReason)}. Re-run with --force-stateful-recreation to confirm the data loss, or change the resource definition to avoid the immutable-property change.` : `${logicalId} (${resourceType}) cannot be updated in place by the provisioning layer it routes through, so applying this change would DELETE + CREATE it — but it is a stateful resource: ${renderStatefulReason(statefulReason)}. Re-run with --force-stateful-recreation to confirm the data loss, or change the resource definition to avoid the update.`) + retainNote, "STATEFUL_REPLACE_BLOCKED", updateError instanceof Error ? updateError : void 0));
|
|
33534
34043
|
}
|
|
33535
34044
|
this.logger.info(`UPDATE not supported for ${logicalId} (${resourceType}), replacing (DELETE → CREATE)`);
|
|
33536
34045
|
const fallbackFinalSnapshotId = await this.prepareFinalSnapshotForDelete(logicalId, resourceType, currentResource, template?.Resources?.[logicalId]?.UpdateReplacePolicy ?? currentResource.updateReplacePolicy);
|
|
@@ -34007,7 +34516,10 @@ var DeployEngine = class {
|
|
|
34007
34516
|
}
|
|
34008
34517
|
outputsPassCompleted = true;
|
|
34009
34518
|
} finally {
|
|
34010
|
-
if (context.recordedSecretValues)
|
|
34519
|
+
if (context.recordedSecretValues) {
|
|
34520
|
+
for (const [value, expr] of context.recordedSecretValues) this.outputSecrets.set(value, expr);
|
|
34521
|
+
mergeResolvedPairs(context.recordedSecretValues, this.outputSecrets);
|
|
34522
|
+
}
|
|
34011
34523
|
if (!outputsPassCompleted) this.outputsSourceUsable = false;
|
|
34012
34524
|
}
|
|
34013
34525
|
for (const [outputKey, output] of Object.entries(template.Outputs)) {
|
|
@@ -34028,5 +34540,5 @@ var DeployEngine = class {
|
|
|
34028
34540
|
};
|
|
34029
34541
|
|
|
34030
34542
|
//#endregion
|
|
34031
|
-
export {
|
|
34032
|
-
//# sourceMappingURL=deploy-engine-
|
|
34543
|
+
export { endCommandInterruptScope as $, resolveCaptureObservedState as $n, isThrottlingError as $r, maskSecretsInText as $t, bold as A, getBootstrapMarkerKey as An, DeployCancelledError as Ar, classifyReplaySecretRegion as At, secretBearingStateKeyWarning as B, formatDockerLoginError as Bn, ProvisioningError as Br, describeTypeWithThrottleRetry as Bt, unsupportedFinalSnapshotError as C, rewriteTemplateAssetReferences as Cn, resetAwsClients as Cr, configBooleanRefusal as Ct, isStatefulRecreateTargetSync as D, BOOTSTRAP_MARKER_PREFIX as Dn, ConfigError as Dr, requireConfigArray as Dt, MULTI_REGION_RECREATE_BLOCKED_TYPES as E, AssetModeResolver as En, CdkdError as Er, replayWarn as Et, yellow as F, validateContainerRepoName as Fn, LocalStartServiceError as Fr, s3BucketRegionalDomainName as Ft, clearOnUpdateRemoval as G, AssetManifestLoader as Gn, StateError as Gr, STATE_SOURCED_READBACK_RULES as Gt, getCurrentResourceSecrets as H, partitionSensitiveEnv as Hn, ResourceUpdateNotSupportedError as Hr, DagBuilder as Ht, collectDeclaredOutputNames as I, buildDenyExternalAccessPolicy as In, LockError as Ir, s3BucketWebsiteUrl as It, findSilentDropProperties as J, synthesisStatusMessage as Jn, isCdkdError as Jr, createSecretMasker as Jt, ProviderRegistry as K, getDockerImageBySourceHash as Kn, SynthesisError as Kr, TEMPLATE_SOURCED_RULES as Kt, collectPublishedOutputNames as L, describeAwsFailure as Ln, MissingCdkCliError as Lr, applyRoleArnIfSet as Lt, gray as M, parseBootstrapMarker as Mn, IntrinsicResolutionRefusalError as Mr, s3BucketArn as Mt, green as N, readBootstrapMarkerBody as Nn, LocalInvokeBuildError as Nr, s3BucketDomainName as Nt, renderStatefulReason as O, assertAssetBucketRegion as On, CrossAccountSecretRefusalError as Or, requireConfigObject as Ot, red as P, validateAssetBucketName as Pn, LocalMigrateError as Pr, s3BucketDualStackDomainName as Pt, beginCommandInterruptScope as Q, resolveAutoAssetStorage as Qn, isRetryableTransientError as Qr, maskSecretsInError as Qt, exportAliasCollisionScrubWarning as R, buildDockerImage as Rn, NestedStackChildDirectDestroyError as Rr, DiffCalculator as Rt, refusesFinalSnapshot as S, loadPublishableAssetManifest as Sn, getAwsClients as Sr, coerceCfnBoolean as St, extractDeploymentEventError as T, stripControlChars as Tn, AssetError as Tr, readConfigString as Tt, IAMRoleProvider as U, runDockerForeground as Un, StackHasActiveImportsError as Ur, TemplateParser as Ut, stateKeySecretExposure as V, getDockerCmd as Vn, ResourceTimeoutError as Vr, withRetry as Vt, collectInlinePolicyNamesManagedBySiblings as W, runDockerStreaming as Wn, StackTerminationProtectionError as Wr, STATE_SOURCED_CROSS_GENERATION_RULES as Wt, maskDeep as X, getLegacyStateBucketName as Xn, withErrorHandling as Xr, errorCauseChain as Xt, createMaskedRetryLogger as Y, getDefaultStateBucketName as Yn, normalizeAwsError as Yr, dynamicReferenceTokens as Yt, maskerOrIdentity as Z, resolveApp as Zn, isMarkedNonRetryable as Zr, isSingleDynamicReferenceToken as Zt, PRE_DELETE_SNAPSHOT_TYPES as _, AssetPublisher as _n, AssemblyReader as _r, refStateLookupFromResource as _t, DeploymentEventsStore as a, S3StateBackend as an, warnDeprecatedNoPrefixCliFlag as ar, UNSPECIFIED_SKIP_REASON as at, createPreDeleteFinalSnapshot as b, buildAssetRedirectMap as bn, resolveBucketRegion as br, resolveExplicitPhysicalId as bt, replayFailedOperations as c, buildForceUnlockCommand as cn, MIGRATE_TMP_PREFIX as cr, disableInstanceApiTermination as ct, updatePartialReason as d, CUSTOM_RESOURCE_RESPONSE_PREFIX as dn, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as dr, carriesDynamicReference as dt, isTransientServerError as ei, recordMaskOnlyValue as en, resolveSkipPrefix as er, interruptWatchListenerCount as et, withResourceDeadline as f, DEFAULT_STATE_PREFIX as fn, displaySafe as fr, cfnRefValueFromPhysicalId as ft, ATOMIC_FINAL_SNAPSHOT_TYPES as g, shouldRetainResource as gn, derivePartitionAndUrlSuffix as gr, parameterTypeMayLoseSecretIdentity as gt, computeImplicitDeleteEdges as h, importableOutputs as hn, canonicalizeRegion as hr, isUnboundTemplateParameter as ht, DeploymentEventsReader as i, __exportAll as ii, LockManager as in, stateBucketExistenceConfirmed as ir, slowCcOperationTimeoutMs as it, cyan as j, isCrossRegionRedirect as jn, DynamicReferenceRegionAmbiguousError as jr, producerRegionsFromState as jt, formatResourceLine as k, ensureAssetStorage as kn, DependencyError as kr, requireConfigString as kt, replayRollback as l, buildLockContentionMessage as ln, findLargeInlineResources as lr, isTerminationProtectionPropagationError as lt, IMPLICIT_DELETE_DEPENDENCIES as m, importableOutputKeys as mn, PARTITION_TABLE as mr, getAccountInfo as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, markRedactedCause as ni, redactSecretsForState as nn, resolveStateBucketWithDefaultAndSource as nr, startInterruptWatch as nt, planFailedOps as o, rebuildClientForBucketRegion as on, CFN_TEMPLATE_BODY_LIMIT as or, deleteIndeterminateGuards as ot, maskingRetryLogger as p, exportNamesCarriedFrom as pn, expectedOwnerParam as pr, coerceParameterTypedValue as pt, findActionableSilentDrops as q, Synthesizer as qn, formatError as qr, carriesSecretMask as qt, DeployEngine as r, retryClassificationText as ri, scrubResourceRecord as rn, resolveUseCdkBootstrapAssets as rr, CloudControlProvider as rt, planRollback as s, UNRENDERABLE as sn, CFN_TEMPLATE_URL_LIMIT as sr, deleteSkipReason as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, markNonRetryable as ti, recoverMaskedOutput as tn, resolveStateBucketWithDefault as tr, isInterruptedWaitError as tt, updatePartialMessage as u, forceQuitRecoveryClause as un, uploadCfnTemplate as ur, IntrinsicFunctionResolver as ut, buildFinalSnapshotIdentifier as v, stringifyValue as vn, processStackMessages as vr, WAFv2WebACLProvider as vt, makeCanonicalizePropertiesFn as w, escapeRegExp$1 as wn, setAwsClients as wr, configStringRefusal as wt, isFinalSnapshotError as x, createAssetRedirectResolver as xn, AwsClients as xr, assertRegionMatch as xt, ccRoutedFinalSnapshotError as y, WorkGraph as yn, clearBucketRegionCache as yr, normalizeAwsTagsToCfn as yt, isExportAliasCollision as z, dockerSpawnEnvWithSensitive as zn, PartialFailureError as zr, INTRINSIC_KEYS as zt };
|
|
34544
|
+
//# sourceMappingURL=deploy-engine-DPh-XamZ.js.map
|