@go-to-k/cdkd 0.284.6 → 0.284.8
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-B9L-88MC.js → asg-provider-jYyUCsx_.js} +2 -2
- package/dist/{asg-provider-B9L-88MC.js.map → asg-provider-jYyUCsx_.js.map} +1 -1
- package/dist/cli.js +443 -79
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-Ce7kNQp2.js → deploy-engine-36yWG1OT.js} +223 -6
- package/dist/deploy-engine-36yWG1OT.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-Ce7kNQp2.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as IntrinsicFunctionResolver, $n as StackHasActiveImportsError, $t as validateContainerRepoName, A as formatResourceLine, An as AssemblyReader, At as describeTypeWithThrottleRetry, B as isExportAliasCollision, Bt as WorkGraph, C as refusesFinalSnapshot, Cn as MIGRATE_TMP_PREFIX, Ct as s3BucketDomainName, D as MULTI_REGION_RECREATE_BLOCKED_TYPES, Dn as PARTITION_TABLE, Dt as applyRoleArnIfSet, E as extractDeploymentEventError, En as expectedOwnerParam, Et as s3BucketWebsiteUrl, F as red, Fn as getAwsClients, Ft as S3StateBackend, G as clearOnUpdateRemoval, Gn as LocalStartServiceError, Gt as escapeRegExp, H as stateKeySecretExposure, Hn as DeployCancelledError, Ht as createAssetRedirectResolver, I as yellow, In as resetAwsClients, It as rebuildClientForBucketRegion, J as findSilentDropProperties, Jn as NestedStackChildDirectDestroyError, Jt as BOOTSTRAP_MARKER_PREFIX, K as ProviderRegistry, Kt as stripControlChars, L as collectDeclaredOutputNames, Ln as setAwsClients, Lt as shouldRetainResource, M as cyan, Mt as DagBuilder, N as gray, Nn as resolveBucketRegion, Nt as TemplateParser, O as isStatefulRecreateTargetSync, On as canonicalizeRegion, Ot as DiffCalculator, P as green, Pn as AwsClients, Pt as LockManager, Q as isTerminationProtectionPropagationError, Qn as ResourceUpdateNotSupportedError, Qt as validateAssetBucketName, R as collectPublishedOutputNames, Rt as AssetPublisher, S as isFinalSnapshotError, Sn as CFN_TEMPLATE_URL_LIMIT, St as s3BucketArn, T as makeCanonicalizePropertiesFn, Tn as uploadCfnTemplate, Tt as s3BucketRegionalDomainName, U as IAMRoleProvider, Un as LocalInvokeBuildError$1, Ut as loadPublishableAssetManifest, V as secretBearingStateKeyWarning, Vt as buildAssetRedirectMap, W as collectInlinePolicyNamesManagedBySiblings, Wn as LocalMigrateError, Wt as rewriteTemplateAssetReferences, X as slowCcOperationTimeoutMs, Xn as ProvisioningError, Xt as getBootstrapMarkerKey, Y as CloudControlProvider, Yn as PartialFailureError, Yt as ensureAssetStorage, Z as disableInstanceApiTermination, Zn as ResourceTimeoutError, Zt as parseBootstrapMarker, _ as ATOMIC_FINAL_SNAPSHOT_TYPES, _n as resolveStateBucketWithDefaultAndSource, _t as TEMPLATE_SOURCED_RULES, a as DeploymentEventsStore, an as runDockerStreaming, ar as normalizeAwsError, at as resolveExplicitPhysicalId, b as ccRoutedFinalSnapshotError, bn as warnDeprecatedNoPrefixCliFlag, bt as redactSecretsForState, c as replayFailedOperations, cn as Synthesizer, cr as isRetryableTransientError, ct as configBooleanRefusal, d as updatePartialReason, dn as getLegacyStateBucketName, dt as replayWarn, en as buildDenyExternalAccessPolicy, er as StackTerminationProtectionError, et as cfnRefValueFromPhysicalId, f as UNSPECIFIED_SKIP_REASON, fn as resolveApp, ft as requireConfigArray, g as computeImplicitDeleteEdges, gn as resolveStateBucketWithDefault, gt as STATE_SOURCED_READBACK_RULES, h as IMPLICIT_DELETE_DEPENDENCIES, hn as resolveSkipPrefix, ht as STATE_SOURCED_CROSS_GENERATION_RULES, i as DeploymentEventsReader, in as runDockerForeground, it as normalizeAwsTagsToCfn, j as bold, jn as processStackMessages, jt as withRetry, k as renderStatefulReason, kn as derivePartitionAndUrlSuffix, kt as INTRINSIC_KEYS, l as replayRollback, ln as synthesisStatusMessage, lr as isThrottlingError, lt as configStringRefusal, m as withResourceDeadline, mn as resolveCaptureObservedState, mt as requireConfigString, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as formatDockerLoginError, nt as refStateLookupFromResource, o as planFailedOps, on as AssetManifestLoader, or as withErrorHandling, ot as assertRegionMatch, p as deleteSkipReason, pn as resolveAutoAssetStorage, pt as requireConfigObject, q as findActionableSilentDrops, qn as MissingCdkCliError, qt as AssetModeResolver, r as DeployEngine, rn as getDockerCmd, rt as WAFv2WebACLProvider, s as planRollback, sn as getDockerImageBySourceHash, sr as isMarkedNonRetryable, st as coerceCfnBoolean, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as buildDockerImage, tt as getAccountInfo, u as updatePartialMessage, un as getDefaultStateBucketName, ur as markNonRetryable, ut as readConfigString, v as PRE_DELETE_SNAPSHOT_TYPES, vn as resolveUseCdkBootstrapAssets, vt as createSecretMasker, w as unsupportedFinalSnapshotError, wn as findLargeInlineResources, wt as s3BucketDualStackDomainName, x as createPreDeleteFinalSnapshot, xn as CFN_TEMPLATE_BODY_LIMIT, xt as scrubResourceRecord, y as buildFinalSnapshotIdentifier, yn as stateBucketExistenceConfirmed, yt as maskSecretsInText, z as exportAliasCollisionScrubWarning, zn as CdkdError, zt as stringifyValue } from "./deploy-engine-
|
|
2
|
+
import { $ as IntrinsicFunctionResolver, $n as StackHasActiveImportsError, $t as validateContainerRepoName, A as formatResourceLine, An as AssemblyReader, At as describeTypeWithThrottleRetry, B as isExportAliasCollision, Bt as WorkGraph, C as refusesFinalSnapshot, Cn as MIGRATE_TMP_PREFIX, Ct as s3BucketDomainName, D as MULTI_REGION_RECREATE_BLOCKED_TYPES, Dn as PARTITION_TABLE, Dt as applyRoleArnIfSet, E as extractDeploymentEventError, En as expectedOwnerParam, Et as s3BucketWebsiteUrl, F as red, Fn as getAwsClients, Ft as S3StateBackend, G as clearOnUpdateRemoval, Gn as LocalStartServiceError, Gt as escapeRegExp, H as stateKeySecretExposure, Hn as DeployCancelledError, Ht as createAssetRedirectResolver, I as yellow, In as resetAwsClients, It as rebuildClientForBucketRegion, J as findSilentDropProperties, Jn as NestedStackChildDirectDestroyError, Jt as BOOTSTRAP_MARKER_PREFIX, K as ProviderRegistry, Kt as stripControlChars, L as collectDeclaredOutputNames, Ln as setAwsClients, Lt as shouldRetainResource, M as cyan, Mt as DagBuilder, N as gray, Nn as resolveBucketRegion, Nt as TemplateParser, O as isStatefulRecreateTargetSync, On as canonicalizeRegion, Ot as DiffCalculator, P as green, Pn as AwsClients, Pt as LockManager, Q as isTerminationProtectionPropagationError, Qn as ResourceUpdateNotSupportedError, Qt as validateAssetBucketName, R as collectPublishedOutputNames, Rt as AssetPublisher, S as isFinalSnapshotError, Sn as CFN_TEMPLATE_URL_LIMIT, St as s3BucketArn, T as makeCanonicalizePropertiesFn, Tn as uploadCfnTemplate, Tt as s3BucketRegionalDomainName, U as IAMRoleProvider, Un as LocalInvokeBuildError$1, Ut as loadPublishableAssetManifest, V as secretBearingStateKeyWarning, Vt as buildAssetRedirectMap, W as collectInlinePolicyNamesManagedBySiblings, Wn as LocalMigrateError, Wt as rewriteTemplateAssetReferences, X as slowCcOperationTimeoutMs, Xn as ProvisioningError, Xt as getBootstrapMarkerKey, Y as CloudControlProvider, Yn as PartialFailureError, Yt as ensureAssetStorage, Z as disableInstanceApiTermination, Zn as ResourceTimeoutError, Zt as parseBootstrapMarker, _ as ATOMIC_FINAL_SNAPSHOT_TYPES, _n as resolveStateBucketWithDefaultAndSource, _t as TEMPLATE_SOURCED_RULES, a as DeploymentEventsStore, an as runDockerStreaming, ar as normalizeAwsError, at as resolveExplicitPhysicalId, b as ccRoutedFinalSnapshotError, bn as warnDeprecatedNoPrefixCliFlag, bt as redactSecretsForState, c as replayFailedOperations, cn as Synthesizer, cr as isRetryableTransientError, ct as configBooleanRefusal, d as updatePartialReason, dn as getLegacyStateBucketName, dt as replayWarn, en as buildDenyExternalAccessPolicy, er as StackTerminationProtectionError, et as cfnRefValueFromPhysicalId, f as UNSPECIFIED_SKIP_REASON, fn as resolveApp, ft as requireConfigArray, g as computeImplicitDeleteEdges, gn as resolveStateBucketWithDefault, gt as STATE_SOURCED_READBACK_RULES, h as IMPLICIT_DELETE_DEPENDENCIES, hn as resolveSkipPrefix, ht as STATE_SOURCED_CROSS_GENERATION_RULES, i as DeploymentEventsReader, in as runDockerForeground, it as normalizeAwsTagsToCfn, j as bold, jn as processStackMessages, jt as withRetry, k as renderStatefulReason, kn as derivePartitionAndUrlSuffix, kt as INTRINSIC_KEYS, l as replayRollback, ln as synthesisStatusMessage, lr as isThrottlingError, lt as configStringRefusal, m as withResourceDeadline, mn as resolveCaptureObservedState, mt as requireConfigString, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as formatDockerLoginError, nt as refStateLookupFromResource, o as planFailedOps, on as AssetManifestLoader, or as withErrorHandling, ot as assertRegionMatch, p as deleteSkipReason, pn as resolveAutoAssetStorage, pt as requireConfigObject, q as findActionableSilentDrops, qn as MissingCdkCliError, qt as AssetModeResolver, r as DeployEngine, rn as getDockerCmd, rt as WAFv2WebACLProvider, s as planRollback, sn as getDockerImageBySourceHash, sr as isMarkedNonRetryable, st as coerceCfnBoolean, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as buildDockerImage, tt as getAccountInfo, u as updatePartialMessage, un as getDefaultStateBucketName, ur as markNonRetryable, ut as readConfigString, v as PRE_DELETE_SNAPSHOT_TYPES, vn as resolveUseCdkBootstrapAssets, vt as createSecretMasker, w as unsupportedFinalSnapshotError, wn as findLargeInlineResources, wt as s3BucketDualStackDomainName, x as createPreDeleteFinalSnapshot, xn as CFN_TEMPLATE_BODY_LIMIT, xt as scrubResourceRecord, y as buildFinalSnapshotIdentifier, yn as stateBucketExistenceConfirmed, yt as maskSecretsInText, z as exportAliasCollisionScrubWarning, zn as CdkdError, zt as stringifyValue } from "./deploy-engine-36yWG1OT.js";
|
|
3
3
|
import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, h as withStackName, i as runStackBuffered, m as withSkipPrefix, n as getLogger, o as PATTERN_B_NAME_OPTIONS, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-zRrlbaQt.js";
|
|
4
|
-
import { t as ASGProvider } from "./asg-provider-
|
|
4
|
+
import { t as ASGProvider } from "./asg-provider-jYyUCsx_.js";
|
|
5
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
6
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
7
7
|
import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketEncryptionCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketOwnershipControlsCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
|
|
@@ -995,12 +995,66 @@ async function deleteContainerRepo(containerRepo, region, profile, logger) {
|
|
|
995
995
|
ecrClient.destroy();
|
|
996
996
|
}
|
|
997
997
|
}
|
|
998
|
-
/**
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
998
|
+
/** The region spelling a bootstrap marker key carries, e.g. `Us-East-1`. */
|
|
999
|
+
function markerRegionOfKey(markerKey) {
|
|
1000
|
+
return markerKey.slice(BOOTSTRAP_MARKER_PREFIX.length, -5);
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Partition the state bucket's bootstrap markers, relative to the ONE this run
|
|
1004
|
+
* resolved, into the two kinds that must not be silently destroyed with the
|
|
1005
|
+
* bucket:
|
|
1006
|
+
*
|
|
1007
|
+
* - `otherRegions` — markers for a DIFFERENT region, still opted in.
|
|
1008
|
+
* - `sameRegionKeys` — markers for THIS region under a different SPELLING of
|
|
1009
|
+
* its name. The teardown deletes exactly one key, so every other spelling
|
|
1010
|
+
* survives, naming asset storage this run did not touch.
|
|
1011
|
+
*
|
|
1012
|
+
* Both come from ONE listing, and the second kind is why the listing rather
|
|
1013
|
+
* than a per-key probe is the source: a probe is only as deep as the keys it
|
|
1014
|
+
* guesses (`us-east-1` / `US-EAST-1`), while a region can hold a marker under
|
|
1015
|
+
* any spelling (`Us-East-1`), and the listing sees all of them.
|
|
1016
|
+
*
|
|
1017
|
+
* `region` arrives CANONICAL (issue #1995) while the key segments are whatever
|
|
1018
|
+
* `cdkd bootstrap` actually wrote, which is not folded (issue #1820) — so the
|
|
1019
|
+
* two sides are compared through {@link canonicalizeRegion} rather than
|
|
1020
|
+
* directly. A raw compare re-introduces this command's own bug in a different
|
|
1021
|
+
* flag: with a marker at `cdkd-bootstrap/US-EAST-1.json`,
|
|
1022
|
+
* `--destroy --region US-EAST-1 --include-state-bucket` would list `US-EAST-1`
|
|
1023
|
+
* as an "other" region, refuse with `STATE_BUCKET_HOLDS_MARKERS` naming the
|
|
1024
|
+
* very region being torn down, and tell the user to run the command they just
|
|
1025
|
+
* ran. It aborts BEFORE the teardown, so the bucket and repo survive too.
|
|
1026
|
+
*
|
|
1027
|
+
* Other regions are deduplicated by the folded name, so a region bootstrapped
|
|
1028
|
+
* twice under two spellings is reported once rather than twice.
|
|
1029
|
+
*
|
|
1030
|
+
* The comparison folds but the REPORTED value stays the RAW key segment, and
|
|
1031
|
+
* that asymmetry is load-bearing. The error tells the user to run
|
|
1032
|
+
* `cdkd bootstrap --destroy --region <r>` for each name printed, and only the
|
|
1033
|
+
* raw spelling actually reaches the marker: `--region eu-west-1` against a
|
|
1034
|
+
* marker at `cdkd-bootstrap/EU-WEST-1.json` probes the canonical key (absent),
|
|
1035
|
+
* finds `rawMarkerKey === markerKey`, skips the second probe, and reports
|
|
1036
|
+
* "nothing to delete". Printing the folded name would hand the user a command
|
|
1037
|
+
* that cannot work.
|
|
1038
|
+
*/
|
|
1039
|
+
async function listBootstrapMarkerSiblings(stateBackend, region, resolvedMarkerKey) {
|
|
1040
|
+
const segments = (await stateBackend.listRawKeys(BOOTSTRAP_MARKER_PREFIX)).filter((k) => k.endsWith(".json")).map((k) => {
|
|
1041
|
+
const raw = k.slice(BOOTSTRAP_MARKER_PREFIX.length, -5);
|
|
1042
|
+
return {
|
|
1043
|
+
key: k,
|
|
1044
|
+
raw,
|
|
1045
|
+
folded: canonicalizeRegion(raw)
|
|
1046
|
+
};
|
|
1047
|
+
}).filter((e) => e.raw.length > 0);
|
|
1048
|
+
const byCanonical = /* @__PURE__ */ new Map();
|
|
1049
|
+
for (const { raw, folded } of segments) {
|
|
1050
|
+
if (folded === region) continue;
|
|
1051
|
+
if (!byCanonical.has(folded)) byCanonical.set(folded, raw);
|
|
1052
|
+
}
|
|
1053
|
+
const sameRegionKeys = segments.filter((e) => e.folded === region && e.key !== resolvedMarkerKey).map((e) => e.key);
|
|
1054
|
+
return {
|
|
1055
|
+
otherRegions: [...byCanonical.values()],
|
|
1056
|
+
sameRegionKeys
|
|
1057
|
+
};
|
|
1004
1058
|
}
|
|
1005
1059
|
/**
|
|
1006
1060
|
* `cdkd bootstrap --destroy` implementation. Dispatched from the bootstrap
|
|
@@ -1013,14 +1067,16 @@ async function bootstrapDestroyCommand(options) {
|
|
|
1013
1067
|
logger.debug("Options:", options);
|
|
1014
1068
|
await applyRoleArnIfSet({
|
|
1015
1069
|
roleArn: options.roleArn,
|
|
1016
|
-
region: options.region
|
|
1070
|
+
region: canonicalizeRegion(options.region)
|
|
1017
1071
|
});
|
|
1072
|
+
const rawRegion = options.region || process.env["AWS_REGION"] || "us-east-1";
|
|
1073
|
+
const region = canonicalizeRegion(rawRegion);
|
|
1074
|
+
const clientRegion = canonicalizeRegion(options.region || process.env["AWS_REGION"]) || void 0;
|
|
1018
1075
|
const awsClients = new AwsClients({
|
|
1019
|
-
...
|
|
1076
|
+
...clientRegion !== void 0 && { region: clientRegion },
|
|
1020
1077
|
...options.profile && { profile: options.profile }
|
|
1021
1078
|
});
|
|
1022
1079
|
setAwsClients(awsClients);
|
|
1023
|
-
const region = options.region || process.env["AWS_REGION"] || "us-east-1";
|
|
1024
1080
|
const accountId = (await awsClients.sts.send(new GetCallerIdentityCommand({}))).Account;
|
|
1025
1081
|
const bucketName = options.stateBucket ?? getDefaultStateBucketName(accountId);
|
|
1026
1082
|
const markerS3Client = new S3Client({
|
|
@@ -1036,9 +1092,18 @@ async function bootstrapDestroyCommand(options) {
|
|
|
1036
1092
|
});
|
|
1037
1093
|
try {
|
|
1038
1094
|
const markerKey = getBootstrapMarkerKey(region);
|
|
1095
|
+
const rawMarkerKey = getBootstrapMarkerKey(rawRegion);
|
|
1039
1096
|
let markerBody;
|
|
1097
|
+
let resolvedMarkerKey = markerKey;
|
|
1040
1098
|
try {
|
|
1041
1099
|
markerBody = await stateBackend.getRawObject(markerKey);
|
|
1100
|
+
if (markerBody === null && rawMarkerKey !== markerKey) {
|
|
1101
|
+
markerBody = await stateBackend.getRawObject(rawMarkerKey);
|
|
1102
|
+
if (markerBody !== null) {
|
|
1103
|
+
resolvedMarkerKey = rawMarkerKey;
|
|
1104
|
+
logger.debug(`Bootstrap marker found at the un-folded key '${rawMarkerKey}' (none at '${markerKey}') — an upper-cased region was used at 'cdkd bootstrap' time.`);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1042
1107
|
} catch (error) {
|
|
1043
1108
|
if (error.name === "NoSuchBucket") {
|
|
1044
1109
|
logger.info(`State bucket '${bucketName}' does not exist — this account/region was never bootstrapped; nothing to delete.`);
|
|
@@ -1046,14 +1111,29 @@ async function bootstrapDestroyCommand(options) {
|
|
|
1046
1111
|
}
|
|
1047
1112
|
throw error;
|
|
1048
1113
|
}
|
|
1114
|
+
let markerSiblings = {
|
|
1115
|
+
otherRegions: [],
|
|
1116
|
+
sameRegionKeys: []
|
|
1117
|
+
};
|
|
1118
|
+
let markerListingSucceeded = true;
|
|
1119
|
+
try {
|
|
1120
|
+
markerSiblings = await listBootstrapMarkerSiblings(stateBackend, region, resolvedMarkerKey);
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
markerListingSucceeded = false;
|
|
1123
|
+
if (options.includeStateBucket) throw new CdkdError(`Cannot verify which bootstrap markers the state bucket '${bucketName}' still holds (listing '${BOOTSTRAP_MARKER_PREFIX}' failed), and --include-state-bucket deletes every one of them. Grant s3:ListBucket on this bucket and re-run.`, "STATE_BUCKET_MARKER_SCAN_FAILED", error);
|
|
1124
|
+
logger.warn(`Could not list bootstrap markers under '${BOOTSTRAP_MARKER_PREFIX}' (${error instanceof Error ? error.message : String(error)}). Continuing — this only means a marker for this region under another spelling of its name would not be reported.`);
|
|
1125
|
+
}
|
|
1049
1126
|
let marker;
|
|
1050
1127
|
if (markerBody === null) {
|
|
1051
|
-
|
|
1128
|
+
const probed = rawMarkerKey === markerKey ? markerKey : `${markerKey}, ${rawMarkerKey}`;
|
|
1129
|
+
logger.info(`No bootstrap marker for region '${region}' (${probed}) — asset storage is not opted in for this region (or was already destroyed); nothing to delete.`);
|
|
1130
|
+
if (markerSiblings.sameRegionKeys.length > 0) for (const siblingKey of markerSiblings.sameRegionKeys) logger.warn(`A bootstrap marker for this region DOES exist at '${siblingKey}', under a different spelling of the region name — its asset bucket and ECR repository are still alive. Re-run 'cdkd bootstrap --destroy --region ${markerRegionOfKey(siblingKey)}' to tear it down.`);
|
|
1052
1131
|
if (!options.includeStateBucket) {
|
|
1053
|
-
logger.
|
|
1132
|
+
if (!markerListingSucceeded) logger.warn(`The bootstrap-marker listing could not be made, so it is NOT known whether this region has a marker under another spelling of its name. Check 's3://${bucketName}/${BOOTSTRAP_MARKER_PREFIX}' before re-bootstrapping — re-running 'cdkd bootstrap' while one exists writes a SECOND marker with DEFAULT names, and the next destroy would then tear down that storage while the storage the existing marker names survives.`);
|
|
1133
|
+
else if (markerSiblings.sameRegionKeys.length === 0) logger.info(`If the asset bucket / ECR repo still exist without a marker, re-run 'cdkd bootstrap --region ${region}' to recreate the marker, then destroy again.`);
|
|
1054
1134
|
return;
|
|
1055
1135
|
}
|
|
1056
|
-
} else marker = parseBootstrapMarker(markerBody,
|
|
1136
|
+
} else marker = parseBootstrapMarker(markerBody, resolvedMarkerKey);
|
|
1057
1137
|
if (marker && !options.force) {
|
|
1058
1138
|
logger.info("Scanning stack state for references to the asset storage...");
|
|
1059
1139
|
const referencing = await scanStateReferences(stateBackend, [marker.assetBucket, marker.containerRepo]);
|
|
@@ -1065,14 +1145,19 @@ async function bootstrapDestroyCommand(options) {
|
|
|
1065
1145
|
const listing = stateKeys.map((k) => ` - ${describeStateKey(k)} [${k}]`).join("\n");
|
|
1066
1146
|
throw new CdkdError(`Refusing to delete state bucket '${bucketName}': ${stateKeys.length} stack(s) still have state in it:\n${listing}\nDestroy every stack ('cdkd destroy' / 'cdkd state destroy') before deleting the state bucket.`, "STATE_BUCKET_NOT_EMPTY");
|
|
1067
1147
|
}
|
|
1068
|
-
|
|
1069
|
-
if (
|
|
1148
|
+
if (markerSiblings.otherRegions.length > 0) throw new CdkdError(`Refusing to delete state bucket '${bucketName}': region(s) ${markerSiblings.otherRegions.join(", ")} are still opted in to cdkd asset storage (their bootstrap markers live in this bucket). Run 'cdkd bootstrap --destroy --region <r>' for each first.`, "STATE_BUCKET_HOLDS_MARKERS");
|
|
1149
|
+
if (markerSiblings.sameRegionKeys.length > 0) {
|
|
1150
|
+
const canonicalStillPresent = markerBody !== null && resolvedMarkerKey === markerKey;
|
|
1151
|
+
const listing = markerSiblings.sameRegionKeys.map((k) => canonicalStillPresent ? ` - ${k}` : ` - ${k} (cdkd bootstrap --destroy --region ${markerRegionOfKey(k)})`).join("\n");
|
|
1152
|
+
const remedy = canonicalStillPresent ? `Run 'cdkd bootstrap --destroy --region ${region}' WITHOUT --include-state-bucket ONCE: it tears down '${resolvedMarkerKey}' and then prints the exact --region spelling for each marker still standing. Follow those, then re-run with --include-state-bucket. (No per-marker spelling is given here because '${resolvedMarkerKey}' still exists and would be resolved first — and simply repeating this same command would not clear the rest, since once it is gone this region's canonical key no longer resolves anything.)` : "Run the command shown against each marker above, then re-run with --include-state-bucket.";
|
|
1153
|
+
throw new CdkdError(`Refusing to delete state bucket '${bucketName}': region '${region}' has ${markerSiblings.sameRegionKeys.length} further bootstrap marker(s) under a different spelling of its name, which this teardown does not delete:\n${listing}\n${remedy}\nDestroy each one first — deleting the state bucket now would remove those markers while the asset storage they name survives, with no record of its names.`, "STATE_BUCKET_HOLDS_SIBLING_MARKERS");
|
|
1154
|
+
}
|
|
1070
1155
|
}
|
|
1071
1156
|
const planLines = [];
|
|
1072
1157
|
if (marker) {
|
|
1073
1158
|
planLines.push(`Asset bucket: s3://${marker.assetBucket} (region ${region}, all contents)`);
|
|
1074
1159
|
planLines.push(`Container-asset ECR repository: ${marker.containerRepo} (region ${region}, all images)`);
|
|
1075
|
-
planLines.push(`Bootstrap marker: s3://${bucketName}/${
|
|
1160
|
+
planLines.push(`Bootstrap marker: s3://${bucketName}/${resolvedMarkerKey} (deleted last)`);
|
|
1076
1161
|
}
|
|
1077
1162
|
if (options.includeStateBucket) planLines.push(`State bucket: s3://${bucketName} (ALL contents, including all versions)`);
|
|
1078
1163
|
if (!await promptBootstrapDestroyConfirm({
|
|
@@ -1085,8 +1170,10 @@ async function bootstrapDestroyCommand(options) {
|
|
|
1085
1170
|
if (marker) {
|
|
1086
1171
|
await emptyAndDeleteBucket(awsClients.s3, marker.assetBucket, accountId, "Asset bucket", logger);
|
|
1087
1172
|
await deleteContainerRepo(marker.containerRepo, region, options.profile, logger);
|
|
1088
|
-
await stateBackend.deleteRawObjects([
|
|
1089
|
-
logger.info(`✓ Deleted bootstrap marker (${
|
|
1173
|
+
await stateBackend.deleteRawObjects([resolvedMarkerKey]);
|
|
1174
|
+
logger.info(`✓ Deleted bootstrap marker (${resolvedMarkerKey})`);
|
|
1175
|
+
for (const siblingKey of markerSiblings.sameRegionKeys) logger.warn(`A further bootstrap marker for this region remains at '${siblingKey}' (this region was bootstrapped under more than one spelling of its name). It names asset storage that was NOT destroyed by this run — re-run 'cdkd bootstrap --destroy --region ${markerRegionOfKey(siblingKey)}' to tear that one down too.`);
|
|
1176
|
+
if (!markerListingSucceeded) logger.warn(`The bootstrap-marker listing could not be made, so a marker under another spelling of this region may still be live. Check 's3://${bucketName}/${BOOTSTRAP_MARKER_PREFIX}'.`);
|
|
1090
1177
|
logger.info(`\ncdkd asset storage is now OFF for region ${region}: future deploys in this region fall back to legacy mode (CDK bootstrap destinations) unless the region is bootstrapped again.`);
|
|
1091
1178
|
}
|
|
1092
1179
|
if (options.includeStateBucket) {
|
|
@@ -58636,6 +58723,18 @@ function containsSecretDynamicReference(value) {
|
|
|
58636
58723
|
if (value !== null && typeof value === "object") return Object.values(value).some(containsSecretDynamicReference);
|
|
58637
58724
|
return false;
|
|
58638
58725
|
}
|
|
58726
|
+
/**
|
|
58727
|
+
* Does this template carry a secret-bearing dynamic reference ANYWHERE?
|
|
58728
|
+
*
|
|
58729
|
+
* Exported for the DELETED nested-child path (issue #1948 review): that node
|
|
58730
|
+
* diffs its state against an EMPTY template, so it can compute nothing about
|
|
58731
|
+
* secrets from its own input and would render a pre-GHSA child's whole stored
|
|
58732
|
+
* bag in full. The caller passes the PARENT's answer instead — see
|
|
58733
|
+
* `diff-recursive.ts`'s `buildDeletedSubtree`.
|
|
58734
|
+
*/
|
|
58735
|
+
function templateHasSecretDynamicReference(template) {
|
|
58736
|
+
return containsSecretDynamicReference(template);
|
|
58737
|
+
}
|
|
58639
58738
|
/** Structural equality, mirroring the deploy engine's `outputMapsEqual` leaf rule. */
|
|
58640
58739
|
function valuesEqual(a, b) {
|
|
58641
58740
|
if (a === b) return true;
|
|
@@ -58685,13 +58784,21 @@ function valuesEqual(a, b) {
|
|
|
58685
58784
|
* resolver runs with `skipDynamicReferences`, so a `{{resolve:...}}` reference
|
|
58686
58785
|
* is never resolved to plaintext in the first place and the bag already holds
|
|
58687
58786
|
* the expression that state stores post-redaction.
|
|
58787
|
+
*
|
|
58788
|
+
* `storedOutputs` is the bag currently in state. It is consulted for exactly
|
|
58789
|
+
* one decision — the LITERAL `Export.Name` of a secret-bearing stack, see issue
|
|
58790
|
+
* #1942 at that branch — and never merged into the result: everything else here
|
|
58791
|
+
* previews what the template says, and reading state anywhere else would make
|
|
58792
|
+
* the preview agree with the past instead of with the next deploy.
|
|
58688
58793
|
*/
|
|
58689
|
-
async function resolveTemplateOutputs(template, resolveFn, conditions) {
|
|
58794
|
+
async function resolveTemplateOutputs(template, resolveFn, conditions, storedOutputs) {
|
|
58690
58795
|
const logger = getLogger().child("OutputsDiff");
|
|
58691
|
-
const outputs =
|
|
58796
|
+
const outputs = Object.create(null);
|
|
58692
58797
|
const exportNames = /* @__PURE__ */ new Set();
|
|
58693
58798
|
const failedKeys = /* @__PURE__ */ new Set();
|
|
58694
58799
|
const secretSourceKeys = /* @__PURE__ */ new Set();
|
|
58800
|
+
const declaredKeys = /* @__PURE__ */ new Set();
|
|
58801
|
+
const templateHasSecretReference = containsSecretDynamicReference(template);
|
|
58695
58802
|
let resolutionFailed = false;
|
|
58696
58803
|
const publishedOutputNames = collectPublishedOutputNames(template.Outputs ?? {}, conditions);
|
|
58697
58804
|
if (!template.Outputs) return {
|
|
@@ -58699,6 +58806,8 @@ async function resolveTemplateOutputs(template, resolveFn, conditions) {
|
|
|
58699
58806
|
exportNames,
|
|
58700
58807
|
failedKeys,
|
|
58701
58808
|
secretSourceKeys,
|
|
58809
|
+
declaredKeys,
|
|
58810
|
+
templateHasSecretReference,
|
|
58702
58811
|
resolutionFailed
|
|
58703
58812
|
};
|
|
58704
58813
|
/**
|
|
@@ -58712,6 +58821,8 @@ async function resolveTemplateOutputs(template, resolveFn, conditions) {
|
|
|
58712
58821
|
if (typeof output.Export?.Name === "string") failedKeys.add(output.Export.Name);
|
|
58713
58822
|
};
|
|
58714
58823
|
for (const [outputKey, output] of Object.entries(template.Outputs)) {
|
|
58824
|
+
declaredKeys.add(outputKey);
|
|
58825
|
+
if (typeof output.Export?.Name === "string") declaredKeys.add(output.Export.Name);
|
|
58715
58826
|
if (!containsSecretDynamicReference(output.Value)) continue;
|
|
58716
58827
|
secretSourceKeys.add(outputKey);
|
|
58717
58828
|
if (typeof output.Export?.Name === "string") secretSourceKeys.add(output.Export.Name);
|
|
@@ -58751,11 +58862,15 @@ async function resolveTemplateOutputs(template, resolveFn, conditions) {
|
|
|
58751
58862
|
}
|
|
58752
58863
|
if (typeof exportName === "string" && !isUnresolvedValue(exportName, exportSourceUsedSub)) if (declaredExportIsIntrinsic && isSecretDynamicReference(exportName)) logger.debug(`Diff skipping export alias of ${stripControlChars(outputKey)} — the name carries a secret reference`);
|
|
58753
58864
|
else if (isExportAliasCollision(exportName, outputKey, publishedOutputNames)) logger.debug(`Diff skipping export alias ${stripControlChars(exportName)} of ${stripControlChars(outputKey)} — collides with an output name`);
|
|
58754
|
-
else if (!declaredExportIsIntrinsic && secretSourceKeys.size > 0) {
|
|
58755
|
-
|
|
58865
|
+
else if (!declaredExportIsIntrinsic && secretSourceKeys.size > 0) if (storedOutputs !== void 0 && Object.prototype.hasOwnProperty.call(storedOutputs, exportName)) {
|
|
58866
|
+
outputs[exportName] = value;
|
|
58867
|
+
exportNames.add(exportName);
|
|
58868
|
+
} else {
|
|
58869
|
+
logger.debug(`Diff cannot decide the export alias of ${stripControlChars(outputKey)} — a literal name may contain a resolved secret and state does not hold that key`);
|
|
58756
58870
|
failedKeys.add(exportName);
|
|
58757
58871
|
resolutionFailed = true;
|
|
58758
|
-
}
|
|
58872
|
+
}
|
|
58873
|
+
else {
|
|
58759
58874
|
outputs[exportName] = value;
|
|
58760
58875
|
exportNames.add(exportName);
|
|
58761
58876
|
}
|
|
@@ -58767,6 +58882,8 @@ async function resolveTemplateOutputs(template, resolveFn, conditions) {
|
|
|
58767
58882
|
exportNames,
|
|
58768
58883
|
failedKeys,
|
|
58769
58884
|
secretSourceKeys,
|
|
58885
|
+
declaredKeys,
|
|
58886
|
+
templateHasSecretReference,
|
|
58770
58887
|
resolutionFailed
|
|
58771
58888
|
};
|
|
58772
58889
|
}
|
|
@@ -58796,24 +58913,73 @@ async function resolveTemplateOutputs(template, resolveFn, conditions) {
|
|
|
58796
58913
|
* {@link isSecretDynamicReference} (this resolver runs with
|
|
58797
58914
|
* `skipDynamicReferences`) while the stored side is not; and
|
|
58798
58915
|
* - `secretSourceKeys` — the template itself declares the key's value as such a
|
|
58799
|
-
* reference. This one reaches
|
|
58800
|
-
*
|
|
58801
|
-
*
|
|
58916
|
+
* reference. This one reaches a case the first cannot: a condition-skipped
|
|
58917
|
+
* secret output has NO desired side at all and would otherwise print in full
|
|
58918
|
+
* as a REMOVE row.
|
|
58802
58919
|
*
|
|
58803
58920
|
* A hit on either makes the WHOLE record suspect, not just that key: the
|
|
58804
58921
|
* record was written by a pre-GHSA binary, so every value in it is unredacted.
|
|
58805
58922
|
* Withholding is therefore record-level. The change is still REPORTED — only
|
|
58806
58923
|
* the value is withheld — so `--fail` and the exports story are unaffected.
|
|
58807
|
-
|
|
58808
|
-
|
|
58924
|
+
*
|
|
58925
|
+
* ## The key today's template cannot account for (issue #1948)
|
|
58926
|
+
*
|
|
58927
|
+
* Neither signal above covers an output DELETED from the template, and an
|
|
58928
|
+
* earlier revision of this note claimed `secretSourceKeys` did — a false
|
|
58929
|
+
* coverage claim, which is exactly what lets a reader conclude the case is
|
|
58930
|
+
* handled. It structurally cannot: that set is built from DECLARATIONS, and a
|
|
58931
|
+
* deleted output declares nothing, so a record whose ONLY secret-bearing output
|
|
58932
|
+
* has since been removed is judged not-legacy and its stored pre-GHSA plaintext
|
|
58933
|
+
* renders as the `old:` side of a REMOVE row.
|
|
58934
|
+
*
|
|
58935
|
+
* The signal has to come from the STORED bag, and the honest statement about
|
|
58936
|
+
* the stored bag is that the case is UNDECIDABLE there: post-GHSA a secret
|
|
58937
|
+
* output stores its `{{resolve:...}}` EXPRESSION, but pre-GHSA it stores a
|
|
58938
|
+
* plaintext, and a plaintext is indistinguishable from an ordinary string. So
|
|
58939
|
+
* this is a REFUSAL rather than a detection — a stored key the template cannot
|
|
58940
|
+
* account for has its value withheld — bounded by two gates that keep it off
|
|
58941
|
+
* the common benign case (deleting an output is a normal refactor):
|
|
58942
|
+
*
|
|
58943
|
+
* - `templateHasSecretReference` — the template must still prove this stack
|
|
58944
|
+
* handles secrets AT ALL (anywhere, `Resources` included). An ordinary stack
|
|
58945
|
+
* with no secret reference keeps printing its REMOVE values.
|
|
58946
|
+
* - no stored value is itself a secret-bearing expression. One that is proves
|
|
58947
|
+
* the LAST write was post-GHSA, and the bag is rewritten wholesale by
|
|
58948
|
+
* `resolveOutputs` on every deploy, so every other value in it is redacted
|
|
58949
|
+
* too. This exonerates the record.
|
|
58950
|
+
*
|
|
58951
|
+
* That reasoning holds for a DEPLOY write and is weaker for a `cdkd scrub`
|
|
58952
|
+
* one, which rewrites `state.outputs` IN PLACE: scrub redacts what it has a
|
|
58953
|
+
* needle or a template position for, and its own docs admit it can leave a
|
|
58954
|
+
* plaintext it has neither for — so a scrubbed bag holding one redacted key
|
|
58955
|
+
* can exonerate a deleted key's surviving plaintext. Recorded rather than
|
|
58956
|
+
* closed: removing the exoneration would withhold values on every clean
|
|
58957
|
+
* post-GHSA record in a secret-handling stack, which is the larger harm.
|
|
58958
|
+
*
|
|
58959
|
+
* Withholding here is PER-KEY, unlike the record-level arms above, and the
|
|
58960
|
+
* asymmetry follows from what each concludes: those conclude the record was
|
|
58961
|
+
* written by a pre-GHSA binary (a claim about the whole bag), while this one
|
|
58962
|
+
* concludes only that THIS key is undecidable. So a secret-handling stack that
|
|
58963
|
+
* deletes an ordinary output withholds that one row's value and prints the
|
|
58964
|
+
* rest.
|
|
58965
|
+
*
|
|
58966
|
+
* Residual, stated because the gate is what buys the low false-positive rate:
|
|
58967
|
+
* a stack whose ONLY secret reference WAS the deleted output has no secret left
|
|
58968
|
+
* in its template, so nothing here fires. Closing it would mean withholding
|
|
58969
|
+
* every REMOVE value on every stack, which is a worse trade.
|
|
58970
|
+
*/
|
|
58971
|
+
function computeOutputsDiff(current, desired, exportNames, secretSourceKeys = /* @__PURE__ */ new Set(), unaccountableScan = {}) {
|
|
58809
58972
|
const changes = [];
|
|
58810
58973
|
const currentBag = current ?? {};
|
|
58811
58974
|
const legacyRecord = Object.entries(currentBag).some(([name, oldValue]) => {
|
|
58812
58975
|
if (typeof oldValue === "string" && isSecretDynamicReference(oldValue)) return false;
|
|
58813
58976
|
return containsSecretDynamicReference(desired[name]) || secretSourceKeys.has(name);
|
|
58814
58977
|
});
|
|
58978
|
+
const recordProvesPostGhsa = Object.values(currentBag).some((v) => typeof v === "string" && isSecretDynamicReference(v));
|
|
58979
|
+
const declaredKeys = unaccountableScan.declaredKeys ?? /* @__PURE__ */ new Set();
|
|
58980
|
+
const unaccountable = (name) => unaccountableScan.templateHasSecretReference === true && !recordProvesPostGhsa && !declaredKeys.has(name) && !Object.prototype.hasOwnProperty.call(desired, name);
|
|
58815
58981
|
const push = (change) => {
|
|
58816
|
-
if (
|
|
58982
|
+
if (change.changeType !== "ADD" && (legacyRecord || unaccountable(change.name))) {
|
|
58817
58983
|
const { oldValue: _dropped, ...rest } = change;
|
|
58818
58984
|
changes.push({
|
|
58819
58985
|
...rest,
|
|
@@ -59824,7 +59990,8 @@ async function loadStateOrEmpty(stackName, region, stateBackend) {
|
|
|
59824
59990
|
* Pure with respect to AWS state mutation — only reads state (the resolver
|
|
59825
59991
|
* may read producer state for `Fn::ImportValue` / `Fn::GetStackOutput`).
|
|
59826
59992
|
*/
|
|
59827
|
-
async function computeStackDiff(currentState, template, region, stackName, stateBackend, diffCalculator,
|
|
59993
|
+
async function computeStackDiff(currentState, template, region, stackName, stateBackend, diffCalculator, options = {}) {
|
|
59994
|
+
const { parameters, canonicalizeProperties, cfnFallback, inheritSecretBearingTemplate } = options;
|
|
59828
59995
|
const intrinsicResolver = new IntrinsicFunctionResolver(region, { cfnFallback: cfnFallback ?? true });
|
|
59829
59996
|
let mergedParameters = parameters;
|
|
59830
59997
|
let parametersBound = false;
|
|
@@ -59866,11 +60033,17 @@ async function computeStackDiff(currentState, template, region, stackName, state
|
|
|
59866
60033
|
skipDynamicReferences: true
|
|
59867
60034
|
});
|
|
59868
60035
|
const changes = await diffCalculator.calculateDiff(currentState, effectiveTemplate, resolveFn, canonicalizeProperties);
|
|
59869
|
-
const resolved = await resolveTemplateOutputs(effectiveTemplate, resolveFn, conditions);
|
|
60036
|
+
const resolved = await resolveTemplateOutputs(effectiveTemplate, resolveFn, conditions, currentState.outputs);
|
|
59870
60037
|
let outputChanges = [];
|
|
59871
60038
|
if (resolved.resolutionFailed) {
|
|
59872
|
-
if (computeOutputsDiff(currentState.outputs, resolved.outputs, resolved.exportNames, resolved.secretSourceKeys
|
|
59873
|
-
|
|
60039
|
+
if (computeOutputsDiff(currentState.outputs, resolved.outputs, resolved.exportNames, resolved.secretSourceKeys, {
|
|
60040
|
+
declaredKeys: resolved.declaredKeys,
|
|
60041
|
+
templateHasSecretReference: resolved.templateHasSecretReference || inheritSecretBearingTemplate === true
|
|
60042
|
+
}).filter((change) => !resolved.failedKeys.has(change.name)).length > 0) logger.warn(`Outputs of stack ${stackName} may have changed, but one or more could not be resolved against current state — omitting the Outputs section from this diff. It is usually an output referencing a resource this deploy has yet to create.`);
|
|
60043
|
+
} else outputChanges = computeOutputsDiff(currentState.outputs, resolved.outputs, resolved.exportNames, resolved.secretSourceKeys, {
|
|
60044
|
+
declaredKeys: resolved.declaredKeys,
|
|
60045
|
+
templateHasSecretReference: resolved.templateHasSecretReference || inheritSecretBearingTemplate === true
|
|
60046
|
+
});
|
|
59874
60047
|
return {
|
|
59875
60048
|
changes,
|
|
59876
60049
|
outputChanges
|
|
@@ -59935,9 +60108,15 @@ async function resolveChildStackParameters(parentStackRow, parentTemplate, paren
|
|
|
59935
60108
|
* fail at deploy time.
|
|
59936
60109
|
*/
|
|
59937
60110
|
async function buildDiffTree(args) {
|
|
59938
|
-
const { stackName, displayName, region, template, nestedTemplates, recursive, stateBackend, diffCalculator, parameters, canonicalizeProperties, assetRedirect, cfnFallback } = args;
|
|
60111
|
+
const { stackName, displayName, region, template, nestedTemplates, recursive, stateBackend, diffCalculator, parentHasSecretReference, parameters, canonicalizeProperties, assetRedirect, cfnFallback } = args;
|
|
59939
60112
|
const state = await loadStateOrEmpty(stackName, region, stateBackend);
|
|
59940
|
-
const
|
|
60113
|
+
const secretBearingAbove = parentHasSecretReference === true || templateHasSecretDynamicReference(template);
|
|
60114
|
+
const { changes, outputChanges } = await computeStackDiff(state, template, region, stackName, stateBackend, diffCalculator, {
|
|
60115
|
+
...parameters && { parameters },
|
|
60116
|
+
...canonicalizeProperties && { canonicalizeProperties },
|
|
60117
|
+
...cfnFallback !== void 0 && { cfnFallback },
|
|
60118
|
+
inheritSecretBearingTemplate: false
|
|
60119
|
+
});
|
|
59941
60120
|
const node = {
|
|
59942
60121
|
stackName,
|
|
59943
60122
|
displayName,
|
|
@@ -59971,13 +60150,14 @@ async function buildDiffTree(args) {
|
|
|
59971
60150
|
parameters: childParameters,
|
|
59972
60151
|
...canonicalizeProperties && { canonicalizeProperties },
|
|
59973
60152
|
...assetRedirect && { assetRedirect },
|
|
59974
|
-
...cfnFallback !== void 0 && { cfnFallback }
|
|
60153
|
+
...cfnFallback !== void 0 && { cfnFallback },
|
|
60154
|
+
parentHasSecretReference: secretBearingAbove
|
|
59975
60155
|
}));
|
|
59976
60156
|
}
|
|
59977
60157
|
for (const [logicalId, resource] of Object.entries(state.resources)) {
|
|
59978
60158
|
if (resource.resourceType !== "AWS::CloudFormation::Stack") continue;
|
|
59979
60159
|
if (templateChildIds.has(logicalId)) continue;
|
|
59980
|
-
node.children.push(await buildDeletedSubtree(`${stackName}~${logicalId}`, region, stateBackend, diffCalculator));
|
|
60160
|
+
node.children.push(await buildDeletedSubtree(`${stackName}~${logicalId}`, region, stateBackend, diffCalculator, secretBearingAbove));
|
|
59981
60161
|
}
|
|
59982
60162
|
return node;
|
|
59983
60163
|
}
|
|
@@ -59987,9 +60167,9 @@ async function buildDiffTree(args) {
|
|
|
59987
60167
|
* (state vs empty template), recursing into state-listed grandchildren
|
|
59988
60168
|
* (also all DELETE).
|
|
59989
60169
|
*/
|
|
59990
|
-
async function buildDeletedSubtree(stackName, region, stateBackend, diffCalculator) {
|
|
60170
|
+
async function buildDeletedSubtree(stackName, region, stateBackend, diffCalculator, parentHasSecretReference) {
|
|
59991
60171
|
const state = await loadStateOrEmpty(stackName, region, stateBackend);
|
|
59992
|
-
const { changes, outputChanges } = await computeStackDiff(state, EMPTY_TEMPLATE, region, stackName, stateBackend, diffCalculator);
|
|
60172
|
+
const { changes, outputChanges } = await computeStackDiff(state, EMPTY_TEMPLATE, region, stackName, stateBackend, diffCalculator, { inheritSecretBearingTemplate: parentHasSecretReference });
|
|
59993
60173
|
const node = {
|
|
59994
60174
|
stackName,
|
|
59995
60175
|
displayName: stackName,
|
|
@@ -60001,7 +60181,7 @@ async function buildDeletedSubtree(stackName, region, stateBackend, diffCalculat
|
|
|
60001
60181
|
};
|
|
60002
60182
|
for (const [logicalId, resource] of Object.entries(state.resources)) {
|
|
60003
60183
|
if (resource.resourceType !== "AWS::CloudFormation::Stack") continue;
|
|
60004
|
-
node.children.push(await buildDeletedSubtree(`${stackName}~${logicalId}`, region, stateBackend, diffCalculator));
|
|
60184
|
+
node.children.push(await buildDeletedSubtree(`${stackName}~${logicalId}`, region, stateBackend, diffCalculator, parentHasSecretReference));
|
|
60005
60185
|
}
|
|
60006
60186
|
return node;
|
|
60007
60187
|
}
|
|
@@ -60231,25 +60411,19 @@ function renderChangeLines(changes, logFn, ccApiRoutes) {
|
|
|
60231
60411
|
delete: deleteCount
|
|
60232
60412
|
};
|
|
60233
60413
|
}
|
|
60234
|
-
/** Stand-in printed instead of a withheld legacy-plaintext output value. */
|
|
60235
|
-
const REDACTED_LEGACY_PLAINTEXT = "<redacted: legacy plaintext in state — run `cdkd scrub`>";
|
|
60236
60414
|
/**
|
|
60237
|
-
*
|
|
60238
|
-
* TERMINAL rather than merely appear in it: C0 (incl. ESC, so no ANSI sequence
|
|
60239
|
-
* survives), DEL, C1 (the 8-bit CSI forms some terminals still honor), and the
|
|
60240
|
-
* bidi / format overrides that visually reorder a rendered line.
|
|
60241
|
-
*
|
|
60242
|
-
* Applied only on the human-render path. The `--json` payload is deliberately
|
|
60243
|
-
* left byte-faithful: it is a machine interface, an export NAME is data a
|
|
60244
|
-
* consumer may match on, and silently mutating it there would trade a real
|
|
60245
|
-
* correctness regression for a display concern belonging to whatever renders
|
|
60246
|
-
* the JSON. `JSON.stringify` already escapes everything below 0x20.
|
|
60415
|
+
* Stand-in printed instead of a withheld stored output value.
|
|
60247
60416
|
*
|
|
60248
|
-
*
|
|
60249
|
-
*
|
|
60417
|
+
* Worded as a POSSIBILITY because both reasons the value is withheld are
|
|
60418
|
+
* suspicions rather than detections — a record that LOOKS pre-GHSA, and a
|
|
60419
|
+
* stored key today's template cannot account for (issue #1948), which is
|
|
60420
|
+
* undecidable by construction. Stating it as a fact would be wrong on the
|
|
60421
|
+
* benign half of each, and `cdkd scrub` finding nothing is the expected
|
|
60422
|
+
* outcome there.
|
|
60250
60423
|
*/
|
|
60424
|
+
const REDACTED_LEGACY_PLAINTEXT = "<redacted: may be legacy plaintext in state — run `cdkd scrub`>";
|
|
60251
60425
|
/**
|
|
60252
|
-
* The
|
|
60426
|
+
* The guard for an already-JSON-SERIALIZED value: C1 and the bidi marks
|
|
60253
60427
|
* only, deliberately NOT C0.
|
|
60254
60428
|
*
|
|
60255
60429
|
* `JSON.stringify` escapes every character below 0x20 that occurs INSIDE a
|
|
@@ -67801,13 +67975,13 @@ async function stateResourcesCommand(stackName, options) {
|
|
|
67801
67975
|
for (const detail of details) {
|
|
67802
67976
|
lines.push(detail.logicalId);
|
|
67803
67977
|
lines.push(` Type: ${detail.resourceType}`);
|
|
67804
|
-
lines.push(` PhysicalID: ${detail.physicalId}`);
|
|
67978
|
+
lines.push(` PhysicalID: ${stripControlChars(detail.physicalId)}`);
|
|
67805
67979
|
lines.push(` Dependencies: ${detail.dependencies.length > 0 ? detail.dependencies.join(", ") : "(none)"}`);
|
|
67806
67980
|
const attrEntries = Object.entries(detail.attributes);
|
|
67807
67981
|
if (attrEntries.length === 0) lines.push(" Attributes: (none)");
|
|
67808
67982
|
else {
|
|
67809
67983
|
lines.push(" Attributes:");
|
|
67810
|
-
for (const [k, v] of attrEntries) lines.push(` ${k}: ${formatAttributeValue(v)}`);
|
|
67984
|
+
for (const [k, v] of attrEntries) lines.push(` ${stripControlChars(k)}: ${formatAttributeValue(v)}`);
|
|
67811
67985
|
}
|
|
67812
67986
|
lines.push("");
|
|
67813
67987
|
}
|
|
@@ -67827,11 +68001,31 @@ async function stateResourcesCommand(stackName, options) {
|
|
|
67827
68001
|
*
|
|
67828
68002
|
* Scalar values render as-is; objects/arrays are JSON-encoded inline so a
|
|
67829
68003
|
* resource block stays compact even when an attribute is structured.
|
|
68004
|
+
*
|
|
68005
|
+
* Control characters are stripped HERE rather than at each call site (issue
|
|
68006
|
+
* #1948 review), because every caller has the same provenance problem and the
|
|
68007
|
+
* same answer: an Outputs value, a resource PROPERTY and a provider-returned
|
|
68008
|
+
* ATTRIBUTE are all values cdkd resolved or read back, none of them passed a
|
|
68009
|
+
* CloudFormation validator, and all three land in the same terminal. Doing it
|
|
68010
|
+
* once means a future caller cannot forget — the half-applied version of this
|
|
68011
|
+
* guard is what the review caught.
|
|
68012
|
+
*
|
|
68013
|
+
* The FULL class (C0 included), unlike `diff-recursive`'s
|
|
68014
|
+
* `stripDisplayOnlyChars`: that narrower guard exists to protect a
|
|
68015
|
+
* PRETTY-PRINTED payload's structural newlines, and this function never
|
|
68016
|
+
* pretty-prints — `JSON.stringify` is called without an indent, so a newline
|
|
68017
|
+
* here is content injected by the value, not layout.
|
|
68018
|
+
*
|
|
68019
|
+
* Only the human path routes through this; `--json` serializes the state
|
|
68020
|
+
* record directly and stays byte-faithful, which is the same split
|
|
68021
|
+
* `renderOutputChangeLines` makes.
|
|
67830
68022
|
*/
|
|
67831
68023
|
function formatAttributeValue(value) {
|
|
67832
68024
|
if (value === null) return "null";
|
|
67833
|
-
if (
|
|
67834
|
-
return
|
|
68025
|
+
if (value === void 0) return "undefined";
|
|
68026
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return stripControlChars(String(value));
|
|
68027
|
+
const json = JSON.stringify(value);
|
|
68028
|
+
return json === void 0 ? "(unserializable)" : stripControlChars(json);
|
|
67835
68029
|
}
|
|
67836
68030
|
/**
|
|
67837
68031
|
* Render a duration in milliseconds as `1m23s` / `45s`.
|
|
@@ -67933,7 +68127,7 @@ function renderStateBlock(state, lockInfo) {
|
|
|
67933
68127
|
if (outputEntries.length > 0) {
|
|
67934
68128
|
lines.push("");
|
|
67935
68129
|
lines.push("Outputs:");
|
|
67936
|
-
for (const [k, v] of outputEntries) lines.push(` ${k}: ${formatAttributeValue(v)}`);
|
|
68130
|
+
for (const [k, v] of outputEntries) lines.push(` ${stripControlChars(k)}: ${formatAttributeValue(v)}`);
|
|
67937
68131
|
}
|
|
67938
68132
|
const resourceEntries = Object.entries(state.resources ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
67939
68133
|
lines.push("");
|
|
@@ -67942,7 +68136,7 @@ function renderStateBlock(state, lockInfo) {
|
|
|
67942
68136
|
lines.push("");
|
|
67943
68137
|
lines.push(logicalId);
|
|
67944
68138
|
lines.push(` Type: ${resource.resourceType}`);
|
|
67945
|
-
lines.push(` PhysicalID: ${resource.physicalId}`);
|
|
68139
|
+
lines.push(` PhysicalID: ${stripControlChars(resource.physicalId)}`);
|
|
67946
68140
|
const provisionedBy = resource.provisionedBy ?? "(sdk, legacy default)";
|
|
67947
68141
|
lines.push(` ProvisionedBy: ${provisionedBy}`);
|
|
67948
68142
|
const deps = resource.dependencies ?? [];
|
|
@@ -67951,13 +68145,13 @@ function renderStateBlock(state, lockInfo) {
|
|
|
67951
68145
|
if (attrEntries.length === 0) lines.push(" Attributes: (none)");
|
|
67952
68146
|
else {
|
|
67953
68147
|
lines.push(" Attributes:");
|
|
67954
|
-
for (const [k, v] of attrEntries) lines.push(` ${k}: ${formatAttributeValue(v)}`);
|
|
68148
|
+
for (const [k, v] of attrEntries) lines.push(` ${stripControlChars(k)}: ${formatAttributeValue(v)}`);
|
|
67955
68149
|
}
|
|
67956
68150
|
const propEntries = Object.entries(resource.properties ?? {});
|
|
67957
68151
|
if (propEntries.length === 0) lines.push(" Properties: (none)");
|
|
67958
68152
|
else {
|
|
67959
68153
|
lines.push(" Properties:");
|
|
67960
|
-
for (const [k, v] of propEntries) lines.push(` ${k}: ${formatAttributeValue(v)}`);
|
|
68154
|
+
for (const [k, v] of propEntries) lines.push(` ${stripControlChars(k)}: ${formatAttributeValue(v)}`);
|
|
67961
68155
|
}
|
|
67962
68156
|
}
|
|
67963
68157
|
return lines;
|
|
@@ -68628,6 +68822,19 @@ async function stateRefreshObservedCommand(stackArgs, options) {
|
|
|
68628
68822
|
}
|
|
68629
68823
|
}
|
|
68630
68824
|
/**
|
|
68825
|
+
* The secrets map every redaction on this command's paths gets, and it is
|
|
68826
|
+
* EMPTY by construction (issue #1926).
|
|
68827
|
+
*
|
|
68828
|
+
* `cdkd state refresh-observed` neither synthesizes a template nor resolves a
|
|
68829
|
+
* dynamic reference, so no plaintext is ever recorded here and a VALUE scan
|
|
68830
|
+
* would have no needles at all. That is the issue #1900 shape, and it is why
|
|
68831
|
+
* the PATH pass has to carry the redaction on its own — see the call site.
|
|
68832
|
+
*
|
|
68833
|
+
* Shared rather than constructed per resource because nothing writes to it:
|
|
68834
|
+
* `redactSecretsForState` only reads its argument.
|
|
68835
|
+
*/
|
|
68836
|
+
const NO_RECORDED_SECRETS = /* @__PURE__ */ new Map();
|
|
68837
|
+
/**
|
|
68631
68838
|
* Refresh the `observedProperties` of every resource in one stack
|
|
68632
68839
|
* record. Returns counts so the caller can aggregate across `--all`.
|
|
68633
68840
|
*
|
|
@@ -68706,7 +68913,7 @@ async function refreshObservedForStack(stackName, region, stateBackend, lockMana
|
|
|
68706
68913
|
unsupported++;
|
|
68707
68914
|
return;
|
|
68708
68915
|
}
|
|
68709
|
-
resource.observedProperties = observed;
|
|
68916
|
+
resource.observedProperties = redactSecretsForState(observed, NO_RECORDED_SECRETS, resource.properties ?? {}, STATE_SOURCED_READBACK_RULES);
|
|
68710
68917
|
refreshed++;
|
|
68711
68918
|
} catch (err) {
|
|
68712
68919
|
failed++;
|
|
@@ -68747,7 +68954,7 @@ async function confirmRefresh(prompt) {
|
|
|
68747
68954
|
* Create the `state refresh-observed` subcommand.
|
|
68748
68955
|
*/
|
|
68749
68956
|
function createStateRefreshObservedCommand() {
|
|
68750
|
-
const cmd = new Command("refresh-observed").description("Refresh observedProperties for every resource in a stack by calling provider.readCurrentState — populates the drift baseline for stacks deployed before state schema v3, without redeploying.").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(stackRegionOption$1()).action(withErrorHandling(stateRefreshObservedCommand));
|
|
68957
|
+
const cmd = new Command("refresh-observed").description("Refresh observedProperties for every resource in a stack by calling provider.readCurrentState — populates the drift baseline for stacks deployed before state schema v3, without redeploying. Run cdkd scrub first on state written by a pre-GHSA binary: the readback is redacted by POSITION against the record, so a record whose own properties still hold plaintext has nothing to redact from.").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(stackRegionOption$1()).action(withErrorHandling(stateRefreshObservedCommand));
|
|
68751
68958
|
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
68752
68959
|
cmd.addOption(deprecatedRegionOption);
|
|
68753
68960
|
return cmd;
|
|
@@ -69864,6 +70071,107 @@ const KEY_TERMINATORS = "[^\\s\"'?]";
|
|
|
69864
70071
|
*/
|
|
69865
70072
|
const GC_EXTRA_URL_SUFFIXES = [];
|
|
69866
70073
|
/**
|
|
70074
|
+
* Every AWS partition's URL suffix. The asset-reference matchers match the
|
|
70075
|
+
* suffix against this CLOSED SET instead of hardcoding one literal
|
|
70076
|
+
* (issue #1781).
|
|
70077
|
+
*
|
|
70078
|
+
* Before this list, all three matchers spelled `amazonaws\.com` inline, so a
|
|
70079
|
+
* state file written in `aws-cn` / `us-iso*` / `us-isob*` (recording
|
|
70080
|
+
* `amazonaws.com.cn` / `c2s.ic.gov` / `sc2s.sgov.gov` hosts) matched none of
|
|
70081
|
+
* them and its assets read as UNREFERENCED.
|
|
70082
|
+
*
|
|
70083
|
+
* **What that actually deleted, measured rather than assumed.** The blast
|
|
70084
|
+
* radius is narrower than "every live asset" but still a silent, irreversible
|
|
70085
|
+
* delete:
|
|
70086
|
+
*
|
|
70087
|
+
* - **S3 was already rescued**, by accident. {@link CONTENT_HASH_KEY_RE}
|
|
70088
|
+
* collects `<sha256>.<ext>` tokens out of ANY string regardless of host, and
|
|
70089
|
+
* cdkd's file assets are content-addressed with an extension — so a
|
|
70090
|
+
* non-commercial `https://…amazonaws.com.cn/<sha256>.zip` still had its key
|
|
70091
|
+
* protected by the name-independent pass. Measured on a real `--dry-run`
|
|
70092
|
+
* against a seeded `cn-north-1` state file: 71 of 72 objects, including the
|
|
70093
|
+
* referenced one, were protected with the matchers still broken.
|
|
70094
|
+
* - **ECR was NOT rescued, and is where the bug actually deleted.** cdkd's
|
|
70095
|
+
* image tags are bare 64-hex with no `.<ext>` tail, so the content-hash pass
|
|
70096
|
+
* cannot see them, and digests are `sha256:<hex>` — also no tail. With the
|
|
70097
|
+
* ECR matcher missing the suffix, a referenced non-commercial image is
|
|
70098
|
+
* selected for deletion. That is the case the live test reproduced.
|
|
70099
|
+
*
|
|
70100
|
+
* Either way the failure direction is the opposite of the #1758 / #1745 sites,
|
|
70101
|
+
* which merely emit a host that does not resolve.
|
|
70102
|
+
*
|
|
70103
|
+
* Two properties of this list are load-bearing:
|
|
70104
|
+
*
|
|
70105
|
+
* - It is the union over ALL partitions, not
|
|
70106
|
+
* `derivePartitionAndUrlSuffix(region).urlSuffix` for the caller's region.
|
|
70107
|
+
* The scan reads EVERY state file in the state bucket, written by any cdkd
|
|
70108
|
+
* binary for any region, so it must match every partition at once; a single
|
|
70109
|
+
* derived literal would still delete a `cn-north-1` stack's images during a
|
|
70110
|
+
* `us-east-1` gc run.
|
|
70111
|
+
* - It is a SUPERSET of the arms {@link derivePartitionAndUrlSuffix} knows
|
|
70112
|
+
* (issue #1764). A suffix missing HERE deletes live assets, which is the
|
|
70113
|
+
* irreversible direction, so this list must lead that table rather than
|
|
70114
|
+
* follow it.
|
|
70115
|
+
*
|
|
70116
|
+
* The superset property is now MECHANICAL rather than hand-maintained (issue
|
|
70117
|
+
* #1785): the list is built FROM {@link PARTITION_TABLE} plus
|
|
70118
|
+
* {@link GC_EXTRA_URL_SUFFIXES}, so a partition arm added to that table
|
|
70119
|
+
* propagates here in the same commit instead of silently outrunning gc. It
|
|
70120
|
+
* used to be a hand-written literal, and the test meant to fence it iterated a
|
|
70121
|
+
* hand-written region list — so a new arm in the derive table redded nothing.
|
|
70122
|
+
*
|
|
70123
|
+
* It is a closed SET rather than a `[^/\s]+` wildcard so a look-alike host
|
|
70124
|
+
* (`https://<assetBucket>.s3.<region>.example.com/<key>`) is not treated as a
|
|
70125
|
+
* cdkd asset reference. That direction only ever over-PROTECTS, but it would
|
|
70126
|
+
* also let any string embedding the bucket name pin an object forever, which
|
|
70127
|
+
* quietly turns gc into a no-op.
|
|
70128
|
+
*
|
|
70129
|
+
* NOTE this is a weaker check than `src/utils/ecr-uri.ts`, which CAPTURES the
|
|
70130
|
+
* suffix and validates it against the region the host names. That is the right
|
|
70131
|
+
* shape there (one host, one caller-known region) and the wrong shape here:
|
|
70132
|
+
* the region in a scanned string is not necessarily the region whose partition
|
|
70133
|
+
* the suffix must belong to, and pairing them would re-inherit #1764's missing
|
|
70134
|
+
* arms on the irreversible side.
|
|
70135
|
+
*/
|
|
70136
|
+
const AWS_URL_SUFFIXES = [.../* @__PURE__ */ new Set([...PARTITION_TABLE.map((row) => row.urlSuffix), ...GC_EXTRA_URL_SUFFIXES])];
|
|
70137
|
+
/**
|
|
70138
|
+
* Spell an ASCII literal so it matches case-INSENSITIVELY inside a
|
|
70139
|
+
* case-SENSITIVE `RegExp`: `s3` → `[sS]3`, `amazonaws.com` →
|
|
70140
|
+
* `[aA][mM][aA][zZ][oO][nN][aA][wW][sS]\.[cC][oO][mM]`. EVERY character goes
|
|
70141
|
+
* through {@link escapeRegExp} first and the ASCII letters are folded after,
|
|
70142
|
+
* so the result is exactly as strict as an escaped literal outside the letters
|
|
70143
|
+
* it folds.
|
|
70144
|
+
*
|
|
70145
|
+
* This exists because gc must fold only PART of a pattern (issue #1847). Host
|
|
70146
|
+
* names are case-insensitive, so
|
|
70147
|
+
* `https://<assetBucket>.s3.<region>.AMAZONAWS.COM/<key>` in a state file names
|
|
70148
|
+
* the same object as the lower-cased spelling — and a reference gc misses reads
|
|
70149
|
+
* as UNREFERENCED, so the live object is DELETED. The obvious fix, the `i` flag
|
|
70150
|
+
* the ECR matcher carries, is per-REGEX and therefore all-or-nothing: it cannot
|
|
70151
|
+
* fold the host while leaving a KEY or a path-segment bucket exact, and those
|
|
70152
|
+
* segments are genuinely case-sensitive at S3. Which segment gets folded is
|
|
70153
|
+
* decided per shape at the matchers themselves — see the block above
|
|
70154
|
+
* `s3UriRe` for the authority / path-segment / DNS-label split.
|
|
70155
|
+
*
|
|
70156
|
+
* Two further properties are load-bearing:
|
|
70157
|
+
*
|
|
70158
|
+
* - Folding by character class rather than by flag leaves
|
|
70159
|
+
* {@link URL_SUFFIX_ALTERNATION} — SHARED with the ECR matcher — spelled
|
|
70160
|
+
* exactly as that matcher's own review settled it, so the S3 side cannot
|
|
70161
|
+
* quietly redefine the constant the ECR side reads (issues #1792 / #1793).
|
|
70162
|
+
* - A two-character class is not `/k/i`. Under a `u` flag the `i` flag
|
|
70163
|
+
* canonicalizes U+212A KELVIN SIGN onto ASCII `k` — the hazard
|
|
70164
|
+
* `src/utils/ecr-uri.ts` guards its region / suffix checks against —
|
|
70165
|
+
* whereas `[kK]` matches those two code points and nothing else, with or
|
|
70166
|
+
* without `u`. (Measured, and pinned by `tests/unit/cli/gc.test.ts`:
|
|
70167
|
+
* `/k/iu` matches U+212A, `/[kK]/u` does not.) The matchers below carry no
|
|
70168
|
+
* `u` flag today, so this is about what adding one later could silently
|
|
70169
|
+
* widen: with classes, nothing.
|
|
70170
|
+
*/
|
|
70171
|
+
function caseFoldLiteral(literal) {
|
|
70172
|
+
return escapeRegExp(literal).replace(/[A-Za-z]/g, (ch) => `[${ch.toLowerCase()}${ch.toUpperCase()}]`);
|
|
70173
|
+
}
|
|
70174
|
+
/**
|
|
69867
70175
|
* `(?:amazonaws\.com\.cn|amazonaws\.com|...)` — {@link AWS_URL_SUFFIXES} as a
|
|
69868
70176
|
* non-capturing alternation, LONGEST FIRST so `amazonaws.com.cn` is tried
|
|
69869
70177
|
* before its own `amazonaws.com` prefix instead of relying on backtracking.
|
|
@@ -69873,8 +70181,48 @@ const GC_EXTRA_URL_SUFFIXES = [];
|
|
|
69873
70181
|
* the ECR digest), and this alternation sits BEFORE those groups in all three
|
|
69874
70182
|
* patterns. Making it capturing would shift every index by one and silently
|
|
69875
70183
|
* collect the suffix as if it were a key.
|
|
70184
|
+
*
|
|
70185
|
+
* `spellSuffix` decides how each suffix is written out, so the SET, the
|
|
70186
|
+
* longest-first order and the non-capturing property keep exactly one
|
|
70187
|
+
* definition while the two consumers differ in case handling — see
|
|
70188
|
+
* {@link URL_SUFFIX_ALTERNATION_CASE_FOLDED}.
|
|
69876
70189
|
*/
|
|
69877
|
-
|
|
70190
|
+
function buildUrlSuffixAlternation(spellSuffix) {
|
|
70191
|
+
return `(?:${[...AWS_URL_SUFFIXES].sort((a, b) => b.length - a.length).map(spellSuffix).join("|")})`;
|
|
70192
|
+
}
|
|
70193
|
+
/**
|
|
70194
|
+
* The case-SENSITIVE spelling, consumed by {@link ECR_REGISTRY_HOST}.
|
|
70195
|
+
*
|
|
70196
|
+
* Folding THIS constant would be inert for its only consumer — the ECR matcher
|
|
70197
|
+
* carries the `i` flag, so it already matches an upper-cased suffix either way.
|
|
70198
|
+
* The constraint is the other direction: this is the spelling the ECR side's
|
|
70199
|
+
* own review settled on (issues #1792 / #1793), and the S3 matchers must widen
|
|
70200
|
+
* through their own {@link URL_SUFFIX_ALTERNATION_CASE_FOLDED} copy rather than
|
|
70201
|
+
* by editing the shared one, so an S3-side change can never reach the ECR
|
|
70202
|
+
* grammar.
|
|
70203
|
+
*/
|
|
70204
|
+
const URL_SUFFIX_ALTERNATION = buildUrlSuffixAlternation(escapeRegExp);
|
|
70205
|
+
/**
|
|
70206
|
+
* The case-FOLDED spelling, consumed by the three S3 matchers (issue #1847).
|
|
70207
|
+
* Same suffix set, same order, letters folded per {@link caseFoldLiteral} — so
|
|
70208
|
+
* an upper-cased or mixed-case host suffix collects the key. Still a CLOSED
|
|
70209
|
+
* set: folding widens the SPELLING of each suffix, never which suffixes count.
|
|
70210
|
+
*/
|
|
70211
|
+
const URL_SUFFIX_ALTERNATION_CASE_FOLDED = buildUrlSuffixAlternation(caseFoldLiteral);
|
|
70212
|
+
/**
|
|
70213
|
+
* `[sS]3`, case-folded (issue #1847). One constant for BOTH places the literal
|
|
70214
|
+
* `s3` appears: the `s3://` URI SCHEME and the `s3` HOST LABEL of the two HTTPS
|
|
70215
|
+
* shapes. The two roles differ but the spelling and the fold argument are the
|
|
70216
|
+
* same, and a single constant is what stops the three matchers drifting into
|
|
70217
|
+
* two different spellings of it.
|
|
70218
|
+
*/
|
|
70219
|
+
const S3_SEGMENT_CASE_FOLDED = caseFoldLiteral("s3");
|
|
70220
|
+
/**
|
|
70221
|
+
* `[hH][tT][tT][pP][sS]` — the URI scheme, case-folded (issue #1847). A scheme
|
|
70222
|
+
* is case-insensitive by RFC 3986 §3.1, and folding a fixed literal that is
|
|
70223
|
+
* neither the bucket name nor the key can only ever over-PROTECT.
|
|
70224
|
+
*/
|
|
70225
|
+
const HTTPS_SCHEME_CASE_FOLDED = caseFoldLiteral("https");
|
|
69878
70226
|
/**
|
|
69879
70227
|
* The HOST half of an ECR registry, built from the ONE authoritative FORM TABLE
|
|
69880
70228
|
* in `src/utils/ecr-uri.ts` (issue #1793) instead of spelled a second time here.
|
|
@@ -69967,16 +70315,21 @@ function tryDecodeBase64Text(value) {
|
|
|
69967
70315
|
* account's or region's URI can only over-protect (keep more), never
|
|
69968
70316
|
* delete more.
|
|
69969
70317
|
*
|
|
69970
|
-
* `<urlSuffix>` in the two S3 shapes is
|
|
69971
|
-
* EVERY partition's suffix at
|
|
69972
|
-
*
|
|
70318
|
+
* `<urlSuffix>` in the two S3 shapes is
|
|
70319
|
+
* {@link URL_SUFFIX_ALTERNATION_CASE_FOLDED}, i.e. EVERY partition's suffix at
|
|
70320
|
+
* once — see {@link AWS_URL_SUFFIXES} for why a per-region derived literal is
|
|
70321
|
+
* the wrong shape here (issue #1781). Every S3 HOST segment is matched
|
|
70322
|
+
* case-insensitively, including the bucket label of the virtual-hosted shape,
|
|
70323
|
+
* while the KEY and the bucket in the other two shapes stay EXACT (issue
|
|
70324
|
+
* #1847); the per-shape reasoning is in the block above `s3UriRe`.
|
|
69973
70325
|
*/
|
|
69974
70326
|
function buildReferenceExtractors(marker) {
|
|
69975
|
-
const
|
|
70327
|
+
const bucketExact = escapeRegExp(marker.assetBucket);
|
|
70328
|
+
const bucketFolded = caseFoldLiteral(marker.assetBucket);
|
|
69976
70329
|
const repo = escapeRegExp(marker.containerRepo);
|
|
69977
|
-
const s3UriRe = new RegExp(
|
|
69978
|
-
const virtualHostedRe = new RegExp(
|
|
69979
|
-
const pathStyleRe = new RegExp(
|
|
70330
|
+
const s3UriRe = new RegExp(`${S3_SEGMENT_CASE_FOLDED}://${bucketExact}/(${KEY_TERMINATORS}+)`, "g");
|
|
70331
|
+
const virtualHostedRe = new RegExp(`${HTTPS_SCHEME_CASE_FOLDED}://${bucketFolded}\\.${S3_SEGMENT_CASE_FOLDED}[^/\\s]*\\.${URL_SUFFIX_ALTERNATION_CASE_FOLDED}/(${KEY_TERMINATORS}+)`, "g");
|
|
70332
|
+
const pathStyleRe = new RegExp(`${HTTPS_SCHEME_CASE_FOLDED}://${S3_SEGMENT_CASE_FOLDED}[^/\\s]*\\.${URL_SUFFIX_ALTERNATION_CASE_FOLDED}/${bucketExact}/(${KEY_TERMINATORS}+)`, "g");
|
|
69980
70333
|
const ecrRe = new RegExp(`${ECR_REGISTRY_HOST}/${repo}(?::([A-Za-z0-9_][A-Za-z0-9._-]{0,127}))?(?:@(sha256:[0-9a-f]{64}))?`, "gi");
|
|
69981
70334
|
const extractOnce = (value, refs) => {
|
|
69982
70335
|
for (const re of [
|
|
@@ -70245,9 +70598,10 @@ async function gcCommand(options) {
|
|
|
70245
70598
|
logger.debug("Options:", options);
|
|
70246
70599
|
await applyRoleArnIfSet({
|
|
70247
70600
|
roleArn: options.roleArn,
|
|
70248
|
-
region: options.region
|
|
70601
|
+
region: canonicalizeRegion(options.region)
|
|
70249
70602
|
});
|
|
70250
|
-
const
|
|
70603
|
+
const rawRegion = options.region || process.env["AWS_REGION"] || "us-east-1";
|
|
70604
|
+
const region = canonicalizeRegion(rawRegion);
|
|
70251
70605
|
const awsClients = new AwsClients({
|
|
70252
70606
|
region,
|
|
70253
70607
|
...options.profile && { profile: options.profile }
|
|
@@ -70272,9 +70626,18 @@ async function gcCommand(options) {
|
|
|
70272
70626
|
});
|
|
70273
70627
|
try {
|
|
70274
70628
|
const markerKey = getBootstrapMarkerKey(region);
|
|
70629
|
+
const rawMarkerKey = getBootstrapMarkerKey(rawRegion);
|
|
70275
70630
|
let markerBody;
|
|
70631
|
+
let resolvedMarkerKey = markerKey;
|
|
70276
70632
|
try {
|
|
70277
70633
|
markerBody = await stateBackend.getRawObject(markerKey);
|
|
70634
|
+
if (markerBody === null && rawMarkerKey !== markerKey) {
|
|
70635
|
+
markerBody = await stateBackend.getRawObject(rawMarkerKey);
|
|
70636
|
+
if (markerBody !== null) {
|
|
70637
|
+
resolvedMarkerKey = rawMarkerKey;
|
|
70638
|
+
logger.debug(`Bootstrap marker found at the un-folded key '${rawMarkerKey}' (none at '${markerKey}') — an upper-cased region was used at 'cdkd bootstrap' time.`);
|
|
70639
|
+
}
|
|
70640
|
+
}
|
|
70278
70641
|
} catch (error) {
|
|
70279
70642
|
if (error.name === "NoSuchBucket") {
|
|
70280
70643
|
logger.info(`State bucket '${bucketName}' does not exist — this account was never bootstrapped; nothing to garbage-collect.`);
|
|
@@ -70283,10 +70646,11 @@ async function gcCommand(options) {
|
|
|
70283
70646
|
throw error;
|
|
70284
70647
|
}
|
|
70285
70648
|
if (markerBody === null) {
|
|
70286
|
-
|
|
70649
|
+
const probed = rawMarkerKey === markerKey ? markerKey : `${markerKey}, ${rawMarkerKey}`;
|
|
70650
|
+
logger.info(`No bootstrap marker for region '${region}' (${probed}) — the region is not opted in to cdkd asset storage; nothing to garbage-collect. (CDK bootstrap storage is 'cdk gc' territory.)`);
|
|
70287
70651
|
return;
|
|
70288
70652
|
}
|
|
70289
|
-
const marker = parseBootstrapMarker(markerBody,
|
|
70653
|
+
const marker = parseBootstrapMarker(markerBody, resolvedMarkerKey);
|
|
70290
70654
|
const lockKeys = await listAllLockKeys(stateBackend);
|
|
70291
70655
|
if (lockKeys.length > 0) {
|
|
70292
70656
|
const listing = lockKeys.map((k) => ` - ${describeStateKey(k, LOCK_FILE_SUFFIX)} [${k}]`).join("\n");
|
|
@@ -82117,7 +82481,7 @@ function createMigrateCommand() {
|
|
|
82117
82481
|
*/
|
|
82118
82482
|
function buildProgram() {
|
|
82119
82483
|
const program = new Command();
|
|
82120
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.
|
|
82484
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.8");
|
|
82121
82485
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
82122
82486
|
const { profile } = actionCommand.optsWithGlobals();
|
|
82123
82487
|
if (profile !== void 0) process.env["AWS_PROFILE"] = profile;
|