@go-to-k/cdkd 0.284.38 → 0.284.40
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-kqBWElNc.js → asg-provider-BeKDzUe9.js} +2 -2
- package/dist/{asg-provider-kqBWElNc.js.map → asg-provider-BeKDzUe9.js.map} +1 -1
- package/dist/cli.js +470 -60
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-DyxASW-O.js → deploy-engine-B3MQFW1K.js} +60 -13
- package/dist/deploy-engine-B3MQFW1K.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-DyxASW-O.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as startInterruptWatch, $t as
|
|
2
|
+
import { $ as startInterruptWatch, $n as CdkdError, $t as WorkGraph, A as renderStatefulReason, An as resolveStateBucketWithDefault, At as redactSecretsForState, B as exportAliasCollisionScrubWarning, Bn as expectedOwnerParam, Bt as DiffCalculator, C as isFinalSnapshotError, Cn as synthesisStatusMessage, Cr as isMarkedNonRetryable, Ct as TEMPLATE_SOURCED_RULES, D as extractDeploymentEventError, Dn as resolveAutoAssetStorage, Dt as isSingleDynamicReferenceToken, E as makeCanonicalizePropertiesFn, En as resolveApp, Er as markNonRetryable, Et as errorCauseChain, F as green, Fn as CFN_TEMPLATE_BODY_LIMIT, Ft as s3BucketDomainName, G as collectInlinePolicyNamesManagedBySiblings, Gn as processStackMessages, Gt as TemplateParser, H as secretBearingStateKeyWarning, Hn as canonicalizeRegion, Ht as describeTypeWithThrottleRetry, I as red, In as CFN_TEMPLATE_URL_LIMIT, It as s3BucketDualStackDomainName, J as findActionableSilentDrops, Jn as AwsClients, Jt as S3StateBackend, K as clearOnUpdateRemoval, Kt as LockManager, L as yellow, Ln as MIGRATE_TMP_PREFIX, Lt as s3BucketRegionalDomainName, M as bold, Mn as resolveUseCdkBootstrapAssets, Mt as classifyReplaySecretRegion, N as cyan, Nn as stateBucketExistenceConfirmed, Nt as producerRegionsFromState, O as MULTI_REGION_RECREATE_BLOCKED_TYPES, On as resolveCaptureObservedState, Ot as maskSecretsInError, P as gray, Pn as warnDeprecatedNoPrefixCliFlag, Pt as s3BucketArn, Q as isInterruptedWaitError, Qt as stringifyValue, R as collectDeclaredOutputNames, Rn as findLargeInlineResources, Rt as s3BucketWebsiteUrl, S as createPreDeleteFinalSnapshot, Sn as Synthesizer, Sr as withErrorHandling, St as STATE_SOURCED_READBACK_RULES, T as unsupportedFinalSnapshotError, Tn as getLegacyStateBucketName, Tr as isThrottlingError, Tt as dynamicReferenceTokens, U as stateKeySecretExposure, Un as derivePartitionAndUrlSuffix, Ut as withRetry, V as isExportAliasCollision, Vn as PARTITION_TABLE, Vt as INTRINSIC_KEYS, W as IAMRoleProvider, Wn as AssemblyReader, Wt as DagBuilder, X as beginCommandInterruptScope, Xn as resetAwsClients, Xt as shouldRetainResource, Y as findSilentDropProperties, Yn as getAwsClients, Yt as rebuildClientForBucketRegion, Z as endCommandInterruptScope, Zn as setAwsClients, Zt as AssetPublisher, _ as computeImplicitDeleteEdges, _n as getDockerCmd, _t as replayWarn, a as DeploymentEventsStore, an as stripControlChars, ar as LocalInvokeBuildError$1, at as carriesDynamicReference, b as buildFinalSnapshotIdentifier, bn as AssetManifestLoader, bt as requireConfigString, c as replayFailedOperations, cn as ensureAssetStorage, ct as refStateLookupFromResource, d as updatePartialReason, dn as readBootstrapMarkerBody, dr as PartialFailureError, dt as resolveExplicitPhysicalId, en as buildAssetRedirectMap, et as CloudControlProvider, f as UNSPECIFIED_SKIP_REASON, fn as validateAssetBucketName, fr as ProvisioningError, ft as assertRegionMatch, g as IMPLICIT_DELETE_DEPENDENCIES, gn as formatDockerLoginError, gr as StackTerminationProtectionError, gt as readConfigString, h as maskingRetryLogger, hn as buildDockerImage, hr as StackHasActiveImportsError, ht as configStringRefusal, i as DeploymentEventsReader, in as escapeRegExp, ir as DynamicReferenceRegionAmbiguousError, it as IntrinsicFunctionResolver, j as formatResourceLine, jn as resolveStateBucketWithDefaultAndSource, jt as scrubResourceRecord, k as isStatefulRecreateTargetSync, kn as resolveSkipPrefix, kt as maskSecretsInText, l as replayRollback, ln as getBootstrapMarkerKey, lr as MissingCdkCliError, lt as WAFv2WebACLProvider, m as withResourceDeadline, mn as buildDenyExternalAccessPolicy, mr as ResourceUpdateNotSupportedError, mt as configBooleanRefusal, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as loadPublishableAssetManifest, nt as disableInstanceApiTermination, o as planFailedOps, on as AssetModeResolver, or as LocalMigrateError, ot as cfnRefValueFromPhysicalId, p as deleteSkipReason, pn as validateContainerRepoName, pr as ResourceTimeoutError, pt as coerceCfnBoolean, q as ProviderRegistry, qn as resolveBucketRegion, qt as displaySafe, r as DeployEngine, rn as rewriteTemplateAssetReferences, rr as DeployCancelledError, rt as isTerminationProtectionPropagationError, s as planRollback, sn as BOOTSTRAP_MARKER_PREFIX, sr as LocalStartServiceError, st as getAccountInfo, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as createAssetRedirectResolver, tr as CrossAccountSecretRefusalError, tt as slowCcOperationTimeoutMs, u as updatePartialMessage, un as parseBootstrapMarker, ur as NestedStackChildDirectDestroyError, ut as normalizeAwsTagsToCfn, v as ATOMIC_FINAL_SNAPSHOT_TYPES, vn as runDockerForeground, vt as requireConfigArray, w as refusesFinalSnapshot, wn as getDefaultStateBucketName, wr as isRetryableTransientError, wt as createSecretMasker, x as ccRoutedFinalSnapshotError, xn as getDockerImageBySourceHash, xr as normalizeAwsError, xt as STATE_SOURCED_CROSS_GENERATION_RULES, y as PRE_DELETE_SNAPSHOT_TYPES, yn as runDockerStreaming, yt as requireConfigObject, z as collectPublishedOutputNames, zn as uploadCfnTemplate, zt as applyRoleArnIfSet } from "./deploy-engine-B3MQFW1K.js";
|
|
3
3
|
import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, f as getCurrentStackName, 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-BeKDzUe9.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";
|
|
@@ -77,6 +77,23 @@ import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
|
77
77
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
78
78
|
import { WebSocketServer } from "ws";
|
|
79
79
|
|
|
80
|
+
//#region src/state/state-prefix.ts
|
|
81
|
+
/**
|
|
82
|
+
* The default S3 key prefix for cdkd state.
|
|
83
|
+
*
|
|
84
|
+
* Homed in the STATE layer rather than in `src/cli/commands/state-file-keys.ts`
|
|
85
|
+
* (which re-exports it, so its four existing importers are unchanged) because
|
|
86
|
+
* `src/state/lock-contention-message.ts` needs it to decide whether a recovery
|
|
87
|
+
* hint should spell `--state-prefix` at all, and a `src/state/**` module
|
|
88
|
+
* importing from `src/cli/commands/**` inverts the layering — the CLI sits
|
|
89
|
+
* ABOVE the state layer in the 7-layer architecture, not below it.
|
|
90
|
+
*
|
|
91
|
+
* Note this is only the DEFAULT. Other commands accept `--state-prefix`, so
|
|
92
|
+
* whole-bucket listings deliberately do not scope to it.
|
|
93
|
+
*/
|
|
94
|
+
const DEFAULT_STATE_PREFIX = "cdkd";
|
|
95
|
+
|
|
96
|
+
//#endregion
|
|
80
97
|
//#region src/cli/commands/state-file-keys.ts
|
|
81
98
|
/**
|
|
82
99
|
* Shared helpers for enumerating and describing cdkd state-bucket keys.
|
|
@@ -94,20 +111,6 @@ import { WebSocketServer } from "ws";
|
|
|
94
111
|
*/
|
|
95
112
|
const STATE_FILE_SUFFIX = "/state.json";
|
|
96
113
|
/**
|
|
97
|
-
* The prefix `cdkd bootstrap` writes under, and the one `gc` /
|
|
98
|
-
* `bootstrap --destroy` configure their own state backend with.
|
|
99
|
-
*
|
|
100
|
-
* Shared here rather than spelled per command for the same reason this module
|
|
101
|
-
* exists at all: `bootstrap-destroy.ts` and `gc.ts` each held their own copy,
|
|
102
|
-
* and it is now also the `knownPrefix` argument to {@link describeStateKey},
|
|
103
|
-
* where a copy that drifted from the backend's actual prefix would silently
|
|
104
|
-
* send every key back to the shape heuristic instead of the exact depth rule.
|
|
105
|
-
*
|
|
106
|
-
* Note this is only the DEFAULT. Other commands accept `--state-prefix`, so
|
|
107
|
-
* the whole-bucket listings deliberately do not scope to it.
|
|
108
|
-
*/
|
|
109
|
-
const DEFAULT_STATE_PREFIX = "cdkd";
|
|
110
|
-
/**
|
|
111
114
|
* Segments left under a known prefix for the legacy `{prefix}/{stack}` layout.
|
|
112
115
|
*
|
|
113
116
|
* The twin of `S3StateBackend`'s `LEGACY_KEY_DEPTH`, which counts the
|
|
@@ -23916,9 +23919,10 @@ const generations = /* @__PURE__ */ new Map();
|
|
|
23916
23919
|
* long-lived process cannot grow the map without bound.
|
|
23917
23920
|
*/
|
|
23918
23921
|
const MAX_TRACKED_GENERATIONS = 1e5;
|
|
23919
|
-
const tokenKey = (scope, logicalId) => `${scope}\u0000${process.env["AWS_REGION"] ?? ""}\u0000${getCurrentStackName() ?? ""}\u0000${logicalId}`;
|
|
23920
|
-
const derive = (key, generation, maxLength) => {
|
|
23921
|
-
|
|
23922
|
+
const tokenKey = (scope, logicalId, maxLength, charset) => `${scope}\u0000${process.env["AWS_REGION"] ?? ""}\u0000${getCurrentStackName() ?? ""}\u0000${logicalId}\u0000${maxLength}\u0000${charset}`;
|
|
23923
|
+
const derive = (key, generation, maxLength, charset) => {
|
|
23924
|
+
const digest = createHash("sha256").update(`${PROCESS_NONCE}\u0000${key}\u0000${generation}`).digest("hex");
|
|
23925
|
+
return `cdkd${charset === "alphanumeric" ? "" : "-"}${digest}`.slice(0, maxLength);
|
|
23922
23926
|
};
|
|
23923
23927
|
/**
|
|
23924
23928
|
* Get the idempotency token for one logical create, minting it on first use and
|
|
@@ -23932,10 +23936,10 @@ const derive = (key, generation, maxLength) => {
|
|
|
23932
23936
|
* ```
|
|
23933
23937
|
*/
|
|
23934
23938
|
function acquireIdempotencyToken(options) {
|
|
23935
|
-
const { scope, logicalId, maxLength = 64 } = options;
|
|
23936
|
-
const key = tokenKey(scope, logicalId);
|
|
23939
|
+
const { scope, logicalId, maxLength = 64, charset = "default" } = options;
|
|
23940
|
+
const key = tokenKey(scope, logicalId, maxLength, charset);
|
|
23937
23941
|
const existing = inFlight.get(key);
|
|
23938
|
-
const value = existing ?? derive(key, generations.get(key) ?? 0, maxLength);
|
|
23942
|
+
const value = existing ?? derive(key, generations.get(key) ?? 0, maxLength, charset);
|
|
23939
23943
|
if (existing === void 0) inFlight.set(key, value);
|
|
23940
23944
|
return {
|
|
23941
23945
|
value,
|
|
@@ -58162,6 +58166,104 @@ var ECRProvider = class {
|
|
|
58162
58166
|
}
|
|
58163
58167
|
};
|
|
58164
58168
|
|
|
58169
|
+
//#endregion
|
|
58170
|
+
//#region src/state/lock-contention-message.ts
|
|
58171
|
+
/**
|
|
58172
|
+
* The message every fail-fast lock-contention site raises.
|
|
58173
|
+
*
|
|
58174
|
+
* Issue #2161 made six commands throw on `acquireLock` returning `false`
|
|
58175
|
+
* instead of running under a foreign lock. Issue #2170 is the follow-up: the
|
|
58176
|
+
* message those sites raise asked the user to decide "is another process
|
|
58177
|
+
* active?" while printing none of the evidence that would answer it, and the
|
|
58178
|
+
* recovery command it suggested could resolve against the wrong AWS account.
|
|
58179
|
+
*
|
|
58180
|
+
* Both matter more than they look, because of WHICH locks reach this message.
|
|
58181
|
+
* `LockManager.acquireLock` reaps an EXPIRED foreign lock and retries, so a
|
|
58182
|
+
* `false` return means the lock is LIVE — in practice a running `cdkd deploy`.
|
|
58183
|
+
* The user who follows a bare `cdkd force-unlock <stack>` suggestion therefore
|
|
58184
|
+
* deletes a live owner's lock and reproduces issue #2161's harm by hand, which
|
|
58185
|
+
* is the outcome #2161 exists to prevent.
|
|
58186
|
+
*
|
|
58187
|
+
* Centralising the text here also settles the third finding: the nine sites
|
|
58188
|
+
* had drifted to three spellings (`for stack` / `for nested stack` /
|
|
58189
|
+
* `for nested-stack child`), so a user grepping CI logs for one of them found
|
|
58190
|
+
* two of three. `subject` now varies only the noun.
|
|
58191
|
+
*/
|
|
58192
|
+
/**
|
|
58193
|
+
* Stand-in for a value with nothing renderable left after sanitization. Named
|
|
58194
|
+
* rather than inlined so the message and the command-suppression branch cannot
|
|
58195
|
+
* disagree about what "unrenderable" looks like.
|
|
58196
|
+
*/
|
|
58197
|
+
const UNRENDERABLE = "<unrenderable>";
|
|
58198
|
+
/** Render `expiresIn` without implying more precision than a clock skew allows. */
|
|
58199
|
+
function formatRemaining(ms) {
|
|
58200
|
+
if (!Number.isFinite(ms)) return "at an unknown time";
|
|
58201
|
+
if (ms <= 0) return "already expired";
|
|
58202
|
+
const minutes = Math.round(ms / 6e4);
|
|
58203
|
+
if (minutes < 1) return "in under a minute";
|
|
58204
|
+
return `in ~${minutes}m`;
|
|
58205
|
+
}
|
|
58206
|
+
function shellQuote(value) {
|
|
58207
|
+
return /^[A-Za-z0-9._/@:+-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
58208
|
+
}
|
|
58209
|
+
/**
|
|
58210
|
+
* The `cdkd force-unlock ...` line, carrying every flag that decides which
|
|
58211
|
+
* lock object it resolves to.
|
|
58212
|
+
*/
|
|
58213
|
+
function buildForceUnlockCommand(stackName, region, recovery) {
|
|
58214
|
+
const safeStack = displaySafe(stackName, { asciiOnly: true });
|
|
58215
|
+
const stackIsExact = safeStack === stackName;
|
|
58216
|
+
const safeRegion = region === void 0 ? void 0 : displaySafe(region, { asciiOnly: true });
|
|
58217
|
+
if (!safeStack || safeRegion === "" || !stackIsExact || !(region === void 0 || safeRegion === region)) return "";
|
|
58218
|
+
const parts = [safeRegion === void 0 ? `cdkd force-unlock ${shellQuote(safeStack)}` : `cdkd force-unlock ${shellQuote(safeStack)} --stack-region ${shellQuote(safeRegion)}`];
|
|
58219
|
+
if (recovery?.profile) parts.push(`--profile ${shellQuote(recovery.profile)}`);
|
|
58220
|
+
if (recovery?.stateBucket) parts.push(`--state-bucket ${shellQuote(recovery.stateBucket)}`);
|
|
58221
|
+
if (recovery?.statePrefix && recovery.statePrefix !== "cdkd") parts.push(`--state-prefix ${shellQuote(recovery.statePrefix)}`);
|
|
58222
|
+
return parts.join(" ");
|
|
58223
|
+
}
|
|
58224
|
+
/**
|
|
58225
|
+
* The force-quit banner's recovery sentence.
|
|
58226
|
+
*
|
|
58227
|
+
* Exported so the two `destroy-runner.ts` banners do not each decide what to
|
|
58228
|
+
* say when {@link buildForceUnlockCommand} suppresses — a banner ending in a
|
|
58229
|
+
* bare `run: ` is the shape the review found, and two copies of the branch is
|
|
58230
|
+
* how the next one drifts. Returns a leading-space clause so the caller can
|
|
58231
|
+
* concatenate it unconditionally.
|
|
58232
|
+
*/
|
|
58233
|
+
function forceQuitRecoveryClause(stackName, region, recovery) {
|
|
58234
|
+
const command = buildForceUnlockCommand(stackName, region, recovery);
|
|
58235
|
+
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.";
|
|
58236
|
+
}
|
|
58237
|
+
/**
|
|
58238
|
+
* Build the contention message, reading the holder's identity best-effort.
|
|
58239
|
+
*
|
|
58240
|
+
* The `getLockInfo` read is one GetObject and is deliberately NOT allowed to
|
|
58241
|
+
* fail the command: the caller is already on its way to throwing, and turning
|
|
58242
|
+
* a contention refusal into an S3 error would lose the reason. A failed or
|
|
58243
|
+
* absent read degrades to the evidence-free wording rather than to a crash.
|
|
58244
|
+
*/
|
|
58245
|
+
async function buildLockContentionMessage(args) {
|
|
58246
|
+
const { lockManager, stackName, region, subject = "stack", recovery, heldClause, suffix } = args;
|
|
58247
|
+
let held = heldClause ?? "another cdkd process holds it";
|
|
58248
|
+
let sawHolder = false;
|
|
58249
|
+
try {
|
|
58250
|
+
const info = await lockManager.getLockInfo(stackName, region);
|
|
58251
|
+
if (info) {
|
|
58252
|
+
const operation = info.operation ? `, operation: ${displaySafe(info.operation)}` : "";
|
|
58253
|
+
const expires = formatRemaining(info.expiresAt - Date.now());
|
|
58254
|
+
const owner = displaySafe(info.owner);
|
|
58255
|
+
const holder = owner ? `held by ${owner}${operation}` : `held by an unnamed holder`;
|
|
58256
|
+
held = `${heldClause ? `${heldClause} — ` : ""}${holder}, expires ${expires}`;
|
|
58257
|
+
if (owner) sawHolder = true;
|
|
58258
|
+
}
|
|
58259
|
+
} catch {}
|
|
58260
|
+
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`;
|
|
58261
|
+
const recoveryCommand = buildForceUnlockCommand(stackName, region, recovery);
|
|
58262
|
+
const head = `Could not acquire lock for ${subject} '${displaySafe(stackName, { asciiOnly: true }) || "<unrenderable>"}' (${displaySafe(region, { asciiOnly: true }) || "<unrenderable>"}) — ${held}.` + (suffix ? ` ${suffix}` : "");
|
|
58263
|
+
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.`;
|
|
58264
|
+
return `${head} ${advice}, run: ${recoveryCommand}`;
|
|
58265
|
+
}
|
|
58266
|
+
|
|
58165
58267
|
//#endregion
|
|
58166
58268
|
//#region src/cli/commands/destroy-runner.ts
|
|
58167
58269
|
/**
|
|
@@ -58298,9 +58400,64 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
58298
58400
|
const regionForState = state.region ?? ctx.baseRegion;
|
|
58299
58401
|
if (resourceCount === 0) {
|
|
58300
58402
|
logger.info(`Stack ${stackName} has no resources, cleaning up state...`);
|
|
58301
|
-
|
|
58302
|
-
|
|
58403
|
+
let emptyLockHeld = false;
|
|
58404
|
+
let emptyInterrupted = false;
|
|
58405
|
+
const emptySigintHandler = () => {
|
|
58406
|
+
if (emptyInterrupted) {
|
|
58407
|
+
if (emptyLockHeld) {
|
|
58408
|
+
ctx.lockManager.releaseLock(stackName, regionForState).catch(() => {});
|
|
58409
|
+
process.stderr.write(`\nForce-quit: stack lock may not be released.${forceQuitRecoveryClause(stackName, regionForState, {
|
|
58410
|
+
profile: ctx.profile,
|
|
58411
|
+
stateBucket: ctx.stateBucket,
|
|
58412
|
+
statePrefix: ctx.statePrefix
|
|
58413
|
+
})}\n`);
|
|
58414
|
+
}
|
|
58415
|
+
process.exit(130);
|
|
58416
|
+
}
|
|
58417
|
+
emptyInterrupted = true;
|
|
58418
|
+
try {
|
|
58419
|
+
process.stderr.write(`\nInterrupt received - finishing the state cleanup for ${stackName} (press Ctrl-C again to force-quit)\n`);
|
|
58420
|
+
} catch {}
|
|
58421
|
+
};
|
|
58422
|
+
process.setMaxListeners(Math.max(process.getMaxListeners(), 100));
|
|
58423
|
+
process.on("SIGINT", emptySigintHandler);
|
|
58424
|
+
let emptyAcquired;
|
|
58425
|
+
try {
|
|
58426
|
+
emptyAcquired = await ctx.lockManager.acquireLock(stackName, regionForState, void 0, "destroy");
|
|
58427
|
+
} catch (acquireErr) {
|
|
58428
|
+
process.removeListener("SIGINT", emptySigintHandler);
|
|
58429
|
+
throw acquireErr;
|
|
58430
|
+
}
|
|
58431
|
+
if (!emptyAcquired) {
|
|
58432
|
+
process.removeListener("SIGINT", emptySigintHandler);
|
|
58433
|
+
throw new Error(await buildLockContentionMessage({
|
|
58434
|
+
lockManager: ctx.lockManager,
|
|
58435
|
+
stackName,
|
|
58436
|
+
region: regionForState,
|
|
58437
|
+
recovery: {
|
|
58438
|
+
profile: ctx.profile,
|
|
58439
|
+
stateBucket: ctx.stateBucket,
|
|
58440
|
+
statePrefix: ctx.statePrefix
|
|
58441
|
+
}
|
|
58442
|
+
}));
|
|
58443
|
+
}
|
|
58444
|
+
emptyLockHeld = true;
|
|
58445
|
+
try {
|
|
58446
|
+
const recheck = await ctx.stateBackend.getState(stackName, regionForState);
|
|
58447
|
+
if (!(!recheck || Object.keys(recheck.state.resources).length === 0)) throw new Error(`Stack '${stackName}' (${regionForState}) was empty when this run started but now has ${Object.keys(recheck.state.resources).length} resource(s) — another cdkd process deployed into it. Re-run the destroy to act on the current state.`);
|
|
58448
|
+
await ctx.stateBackend.deleteState(stackName, regionForState);
|
|
58449
|
+
logger.info(`${green("✓")} State deleted`);
|
|
58450
|
+
} finally {
|
|
58451
|
+
try {
|
|
58452
|
+
await ctx.lockManager.releaseLock(stackName, regionForState);
|
|
58453
|
+
} catch (releaseErr) {
|
|
58454
|
+
logger.warn(`Failed to release lock after empty-state cleanup: ${releaseErr instanceof Error ? releaseErr.message : String(releaseErr)}`);
|
|
58455
|
+
} finally {
|
|
58456
|
+
process.removeListener("SIGINT", emptySigintHandler);
|
|
58457
|
+
}
|
|
58458
|
+
}
|
|
58303
58459
|
result.skippedEmpty = true;
|
|
58460
|
+
result.interrupted ||= emptyInterrupted;
|
|
58304
58461
|
return result;
|
|
58305
58462
|
}
|
|
58306
58463
|
const needsStrongRefCheck = !!(state.outputs && Object.keys(state.outputs).length > 0);
|
|
@@ -58377,7 +58534,11 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
58377
58534
|
if (draining) {
|
|
58378
58535
|
if (lockHeld) {
|
|
58379
58536
|
ctx.lockManager.releaseLock(stackName, regionForState).catch(() => {});
|
|
58380
|
-
process.stderr.write(`\nForce-quit: stack lock may not be released
|
|
58537
|
+
process.stderr.write(`\nForce-quit: stack lock may not be released.${forceQuitRecoveryClause(stackName, regionForState, {
|
|
58538
|
+
profile: ctx.profile,
|
|
58539
|
+
stateBucket: ctx.stateBucket,
|
|
58540
|
+
statePrefix: ctx.statePrefix
|
|
58541
|
+
})}\n`);
|
|
58381
58542
|
}
|
|
58382
58543
|
process.exit(130);
|
|
58383
58544
|
}
|
|
@@ -58390,7 +58551,16 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
58390
58551
|
process.on("SIGINT", sigintHandler);
|
|
58391
58552
|
try {
|
|
58392
58553
|
logger.info(`\nAcquiring lock for stack ${stackName}...`);
|
|
58393
|
-
if (!await ctx.lockManager.acquireLock(stackName, regionForState, void 0, "destroy")) throw new Error(
|
|
58554
|
+
if (!await ctx.lockManager.acquireLock(stackName, regionForState, void 0, "destroy")) throw new Error(await buildLockContentionMessage({
|
|
58555
|
+
lockManager: ctx.lockManager,
|
|
58556
|
+
stackName,
|
|
58557
|
+
region: regionForState,
|
|
58558
|
+
recovery: {
|
|
58559
|
+
profile: ctx.profile,
|
|
58560
|
+
stateBucket: ctx.stateBucket,
|
|
58561
|
+
statePrefix: ctx.statePrefix
|
|
58562
|
+
}
|
|
58563
|
+
}));
|
|
58394
58564
|
} catch (error) {
|
|
58395
58565
|
restoreBaseRegionAndClients();
|
|
58396
58566
|
process.removeListener("SIGINT", sigintHandler);
|
|
@@ -58984,6 +59154,7 @@ var NestedStackProvider = class {
|
|
|
58984
59154
|
skipConfirmation: true,
|
|
58985
59155
|
...ctx.exportIndexStore && { exportIndexStore: ctx.exportIndexStore },
|
|
58986
59156
|
...ctx.destroyOptions?.profile && { profile: ctx.destroyOptions.profile },
|
|
59157
|
+
...ctx.destroyOptions?.statePrefix !== void 0 && { statePrefix: ctx.destroyOptions.statePrefix },
|
|
58987
59158
|
...deleteContext?.removeProtection === true && { removeProtection: true },
|
|
58988
59159
|
...(ctx.destroyOptions?.skipFinalSnapshot === true || ctx.options?.skipFinalSnapshot === true) && { skipFinalSnapshot: true },
|
|
58989
59160
|
...ctx.destroyOptions?.resourceWarnAfterMs !== void 0 && { resourceWarnAfterMs: ctx.destroyOptions.resourceWarnAfterMs },
|
|
@@ -59210,6 +59381,19 @@ function isCertificateInUseError(error) {
|
|
|
59210
59381
|
* consumers (CloudFront, ALB) will fail to start if they reach the cert
|
|
59211
59382
|
* before it issues, but that's the documented trade-off.
|
|
59212
59383
|
*
|
|
59384
|
+
* **A create that fails after requesting DELETES its certificate** (issue
|
|
59385
|
+
* [#2169](https://github.com/go-to-k/cdkd/issues/2169)). `RequestCertificate`
|
|
59386
|
+
* materializes the certificate immediately and everything after it can fail —
|
|
59387
|
+
* most commonly the ISSUED wait, which a DNS-validated certificate whose
|
|
59388
|
+
* validation records are not live yet exhausts by construction. Before this,
|
|
59389
|
+
* the ARN was thrown away with the error: the certificate existed in AWS with
|
|
59390
|
+
* nothing naming it (absent from state, so invisible to `cdkd state show` and
|
|
59391
|
+
* unreachable by `cdkd destroy`), and the next `cdkd deploy` requested another
|
|
59392
|
+
* one — an orphan per attempt. `create()` now retires the certificate it just
|
|
59393
|
+
* requested before re-throwing, so a failed deploy leaves nothing behind and a
|
|
59394
|
+
* retry starts clean. See {@link ACMCertificateProvider.cleanupRequestedCertificate}
|
|
59395
|
+
* for why deleting is safe and why it is preferred over recording the remnant.
|
|
59396
|
+
*
|
|
59213
59397
|
* **CloudFront cross-region note**: ACM certificates referenced by a
|
|
59214
59398
|
* CloudFront Distribution MUST live in `us-east-1`. cdkd does not enforce
|
|
59215
59399
|
* this — it's the developer's responsibility to deploy the certificate
|
|
@@ -59259,12 +59443,22 @@ var ACMCertificateProvider = class {
|
|
|
59259
59443
|
if (Object.keys(options).length > 0) input["Options"] = options;
|
|
59260
59444
|
const tags = properties["Tags"];
|
|
59261
59445
|
if (tags && Array.isArray(tags) && tags.length > 0) input["Tags"] = tags;
|
|
59446
|
+
const idempotencyToken = acquireIdempotencyToken({
|
|
59447
|
+
scope: "RequestCertificate",
|
|
59448
|
+
logicalId,
|
|
59449
|
+
maxLength: 32,
|
|
59450
|
+
charset: "alphanumeric"
|
|
59451
|
+
});
|
|
59452
|
+
input["IdempotencyToken"] = idempotencyToken.value;
|
|
59453
|
+
let requestedArn;
|
|
59262
59454
|
try {
|
|
59263
59455
|
const certificateArn = (await this.acmClient.send(new RequestCertificateCommand(input))).CertificateArn;
|
|
59264
59456
|
if (!certificateArn) throw new ProvisioningError(`RequestCertificate succeeded but no CertificateArn returned for ${logicalId}`, resourceType, logicalId);
|
|
59457
|
+
requestedArn = certificateArn;
|
|
59265
59458
|
this.logger.debug(`Requested ACM certificate: ${certificateArn}`);
|
|
59266
|
-
if (!(process.env["CDKD_NO_WAIT"] === "true")) await this.waitForCertificateIssued(certificateArn, logicalId);
|
|
59459
|
+
if (!(process.env["CDKD_NO_WAIT"] === "true")) await this.waitForCertificateIssued(certificateArn, logicalId, resourceType);
|
|
59267
59460
|
else this.logger.warn(`Skipping wait for ACM certificate ${logicalId} (CDKD_NO_WAIT=true). Downstream consumers (CloudFront / ALB) will fail until the cert reaches ISSUED.`);
|
|
59461
|
+
idempotencyToken.release();
|
|
59268
59462
|
return {
|
|
59269
59463
|
physicalId: certificateArn,
|
|
59270
59464
|
attributes: {
|
|
@@ -59273,9 +59467,84 @@ var ACMCertificateProvider = class {
|
|
|
59273
59467
|
}
|
|
59274
59468
|
};
|
|
59275
59469
|
} catch (error) {
|
|
59276
|
-
|
|
59470
|
+
const survivorNote = requestedArn ? await this.cleanupRequestedCertificate(requestedArn, logicalId, idempotencyToken) : void 0;
|
|
59471
|
+
if (error instanceof CdkdError) {
|
|
59472
|
+
if (survivorNote === void 0) throw error;
|
|
59473
|
+
throw new ProvisioningError(`${error.message} ${survivorNote}`, resourceType, logicalId, requestedArn, error);
|
|
59474
|
+
}
|
|
59277
59475
|
const cause = error instanceof Error ? error : void 0;
|
|
59278
|
-
throw new ProvisioningError(`Failed to create ACM certificate ${logicalId}: ${error instanceof Error ? error.message : String(error)}
|
|
59476
|
+
throw new ProvisioningError(`Failed to create ACM certificate ${logicalId}: ${error instanceof Error ? error.message : String(error)}` + (survivorNote === void 0 ? "" : ` ${survivorNote}`), resourceType, logicalId, requestedArn, cause);
|
|
59477
|
+
}
|
|
59478
|
+
}
|
|
59479
|
+
/**
|
|
59480
|
+
* Retire the certificate a FAILED `create()` just requested, so the failure
|
|
59481
|
+
* leaves nothing behind (issue
|
|
59482
|
+
* [#2169](https://github.com/go-to-k/cdkd/issues/2169)).
|
|
59483
|
+
*
|
|
59484
|
+
* Returns `undefined` when the certificate is gone, or a one-line note naming
|
|
59485
|
+
* the survivor when it could not be deleted. Never throws: the original
|
|
59486
|
+
* create error is what the user needs to see, and a cleanup failure must
|
|
59487
|
+
* degrade that message rather than replace it.
|
|
59488
|
+
*
|
|
59489
|
+
* ## Why DELETING is right, when the obvious objection is that it throws away
|
|
59490
|
+
* the user's DNS work
|
|
59491
|
+
*
|
|
59492
|
+
* It does not. ACM derives a domain's validation CNAME from the domain and
|
|
59493
|
+
* the account, not from the certificate, and documents that the record "must
|
|
59494
|
+
* be added to your DNS database only once": *"Without the need to repeat
|
|
59495
|
+
* validation, you can request additional ACM certificates for your fully
|
|
59496
|
+
* qualified domain name (FQDN) for as long as the CNAME record remains in
|
|
59497
|
+
* place ... You can also replace a deleted certificate."*
|
|
59498
|
+
* (https://docs.aws.amazon.com/acm/latest/userguide/dns-validation.html)
|
|
59499
|
+
* So the CNAMEs a user adds after seeing this failure validate the NEXT
|
|
59500
|
+
* certificate too, and the retry issues normally. The intuition that a
|
|
59501
|
+
* `PENDING_VALIDATION` certificate is precious does not hold for ACM.
|
|
59502
|
+
*
|
|
59503
|
+
* ## Why not RECORD the remnant in cdkd state instead
|
|
59504
|
+
*
|
|
59505
|
+
* That was this fix's first shape, and it does not work. A state record makes
|
|
59506
|
+
* the next deploy diff the resource as `NO_CHANGE` -- the recorded properties
|
|
59507
|
+
* ARE the template's -- so cdkd would never touch that certificate again:
|
|
59508
|
+
* `cdkd deploy` would print "No changes detected" and exit 0 over a
|
|
59509
|
+
* certificate that is still unusable, and the CloudFront / ALB consuming it
|
|
59510
|
+
* would fail with no explanation. Turning a loud failure into a silent one is
|
|
59511
|
+
* worse than the orphan it fixes. Making the diff re-provision it instead
|
|
59512
|
+
* needs a marker on the state record -- a schema bump -- to buy behaviour
|
|
59513
|
+
* that deleting gives for free.
|
|
59514
|
+
*
|
|
59515
|
+
* ## The "keep it" escape hatch already exists
|
|
59516
|
+
*
|
|
59517
|
+
* A user who WANTS the certificate to outlive the deploy has
|
|
59518
|
+
* `CDKD_NO_WAIT=true` (`cdkd deploy --no-wait`), which returns success with
|
|
59519
|
+
* the certificate recorded in state and never reaches this path. "Wait for
|
|
59520
|
+
* it" and "keep it even if the wait fails" are different requests, and cdkd
|
|
59521
|
+
* already has a flag for the second.
|
|
59522
|
+
*/
|
|
59523
|
+
/**
|
|
59524
|
+
* The region an ACM ARN names, for the manual retire command. Taken from the
|
|
59525
|
+
* ARN rather than from the client so the command a user pastes matches the
|
|
59526
|
+
* certificate it names even when their shell default differs. Falls back to
|
|
59527
|
+
* the literal placeholder for a shape this cannot parse -- a wrong region
|
|
59528
|
+
* would make the command silently address a different account/region.
|
|
59529
|
+
*/
|
|
59530
|
+
regionOfArn(arn) {
|
|
59531
|
+
return arn.split(":")[3] || "<region>";
|
|
59532
|
+
}
|
|
59533
|
+
async cleanupRequestedCertificate(certificateArn, logicalId, idempotencyToken) {
|
|
59534
|
+
try {
|
|
59535
|
+
await this.acmClient.send(new DeleteCertificateCommand({ CertificateArn: certificateArn }));
|
|
59536
|
+
this.logger.info(`Deleted the ACM certificate ${certificateArn} that the failed create of ${logicalId} requested, so nothing is left behind. DNS validation records you have already added stay valid for the retry.`);
|
|
59537
|
+
idempotencyToken.release();
|
|
59538
|
+
return;
|
|
59539
|
+
} catch (cleanupError) {
|
|
59540
|
+
if (cleanupError instanceof ResourceNotFoundException$7 || cleanupError instanceof Error && cleanupError.name === "ResourceNotFoundException") {
|
|
59541
|
+
this.logger.info(`The ACM certificate ${certificateArn} that the failed create of ${logicalId} requested was already gone; nothing to clean up.`);
|
|
59542
|
+
idempotencyToken.release();
|
|
59543
|
+
return;
|
|
59544
|
+
}
|
|
59545
|
+
const detail = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
59546
|
+
this.logger.warn(`Could not delete the ACM certificate ${certificateArn} that the failed create of ${logicalId} requested: ${detail}`);
|
|
59547
|
+
return `The certificate ${certificateArn} this attempt created could NOT be deleted, and cdkd is not tracking it -- retire it with \`aws acm delete-certificate --certificate-arn ${certificateArn} --region ${this.regionOfArn(certificateArn)}\` (the reason is in the warning above).`;
|
|
59279
59548
|
}
|
|
59280
59549
|
}
|
|
59281
59550
|
async update(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
@@ -59448,11 +59717,25 @@ var ACMCertificateProvider = class {
|
|
|
59448
59717
|
* Throws on `VALIDATION_TIMED_OUT` / `FAILED` (terminal failures) and on
|
|
59449
59718
|
* polling-cap exhaustion (treated as timeout). SIGINT short-circuits the
|
|
59450
59719
|
* loop and returns control to the deploy engine's cleanup path.
|
|
59720
|
+
*
|
|
59721
|
+
* Both throws are `ProvisioningError`s carrying `certificateArn` as their
|
|
59722
|
+
* `physicalId` (issue
|
|
59723
|
+
* [#2169](https://github.com/go-to-k/cdkd/issues/2169)). They used to be
|
|
59724
|
+
* plain `Error`s whose only reference to the certificate was the message
|
|
59725
|
+
* TEXT, which is not something a caller can act on. The certificate is real
|
|
59726
|
+
* by the time either fires, so the error that reports the failure is the one
|
|
59727
|
+
* piece of the failure path that should still be able to name it.
|
|
59728
|
+
*
|
|
59729
|
+
* Says nothing about what happens to the certificate afterwards: the caller
|
|
59730
|
+
* owns that. `create()`'s catch retires it (see
|
|
59731
|
+
* {@link ACMCertificateProvider.cleanupRequestedCertificate}) and appends a
|
|
59732
|
+
* note to this message only when that cleanup could NOT.
|
|
59451
59733
|
*/
|
|
59452
|
-
async waitForCertificateIssued(certificateArn, logicalId) {
|
|
59734
|
+
async waitForCertificateIssued(certificateArn, logicalId, resourceType) {
|
|
59453
59735
|
this.logger.debug(`Waiting for ACM certificate ${certificateArn} to reach ISSUED status...`);
|
|
59454
59736
|
let interrupted = false;
|
|
59455
|
-
|
|
59737
|
+
/** The last validation-record block printed, so we re-print only on a CHANGE. */
|
|
59738
|
+
let lastValidationOptions;
|
|
59456
59739
|
const sigintHandler = () => {
|
|
59457
59740
|
interrupted = true;
|
|
59458
59741
|
};
|
|
@@ -59470,17 +59753,20 @@ var ACMCertificateProvider = class {
|
|
|
59470
59753
|
this.logger.debug(`ACM certificate ${certificateArn} is ISSUED`);
|
|
59471
59754
|
return;
|
|
59472
59755
|
}
|
|
59473
|
-
if (status === "FAILED" || status === "VALIDATION_TIMED_OUT" || status === "INACTIVE" || status === "REVOKED" || status === "EXPIRED") throw new
|
|
59474
|
-
if (status === "PENDING_VALIDATION"
|
|
59475
|
-
this.
|
|
59476
|
-
|
|
59756
|
+
if (status === "FAILED" || status === "VALIDATION_TIMED_OUT" || status === "INACTIVE" || status === "REVOKED" || status === "EXPIRED") throw new ProvisioningError(`ACM certificate ${logicalId} (${certificateArn}) entered terminal status ${status} during validation. Check ACM console / DNS records to diagnose.`, resourceType, logicalId, certificateArn);
|
|
59757
|
+
if (status === "PENDING_VALIDATION") {
|
|
59758
|
+
const rendered = this.renderValidationOptions(validations);
|
|
59759
|
+
if (rendered !== void 0 && rendered !== lastValidationOptions) {
|
|
59760
|
+
this.logger.info(rendered);
|
|
59761
|
+
lastValidationOptions = rendered;
|
|
59762
|
+
}
|
|
59477
59763
|
}
|
|
59478
59764
|
this.logger.debug(`ACM certificate ${certificateArn} status: ${status} (attempt ${attempt}/${this.maxPollAttempts})`);
|
|
59479
59765
|
const sleepEnd = Date.now() + this.pollIntervalMs;
|
|
59480
59766
|
const tickMs = Math.min(1e3, this.pollIntervalMs);
|
|
59481
59767
|
while (Date.now() < sleepEnd && !interrupted) await new Promise((resolve) => setTimeout(resolve, tickMs));
|
|
59482
59768
|
}
|
|
59483
|
-
throw new
|
|
59769
|
+
throw new ProvisioningError(`ACM certificate ${logicalId} (${certificateArn}) did not reach ISSUED status within ${this.maxPollAttempts * this.pollIntervalMs / 1e3}s. ` + (lastValidationOptions === void 0 ? "AWS had not published this certificate's validation records before the wait ran out, so none were printed — re-run the deploy to request them again. " : `Add the validation records printed above to your DNS zone and re-run the deploy. `) + "ACM reuses a domain's validation CNAME across certificates, so records you add now validate the next attempt. To wait LONGER, raise CDKD_ACM_POLL_ATTEMPTS / CDKD_ACM_POLL_INTERVAL_MS — this cap is the provider's OWN and is what just fired; --resource-timeout alone will not lengthen it. Past ~30m also raise --resource-timeout AWS::CertificateManager::Certificate=<duration>, or the engine's per-resource deadline becomes the shorter of the two. Or set CDKD_NO_WAIT=true to keep the certificate and validate it out of band.", resourceType, logicalId, certificateArn);
|
|
59484
59770
|
} finally {
|
|
59485
59771
|
process.removeListener("SIGINT", sigintHandler);
|
|
59486
59772
|
}
|
|
@@ -59490,16 +59776,13 @@ var ACMCertificateProvider = class {
|
|
|
59490
59776
|
* user can copy / paste them into Route 53 / Cloudflare / etc. while the
|
|
59491
59777
|
* cert is still PENDING_VALIDATION.
|
|
59492
59778
|
*/
|
|
59493
|
-
|
|
59779
|
+
renderValidationOptions(validations) {
|
|
59494
59780
|
const lines = ["ACM certificate is PENDING_VALIDATION. Add the following DNS records to validate:"];
|
|
59495
59781
|
for (const v of validations) if (v.ValidationMethod === "DNS" && v.ResourceRecord) {
|
|
59496
59782
|
const r = v.ResourceRecord;
|
|
59497
59783
|
lines.push(` ${v.DomainName} — ${r.Type} ${r.Name} -> ${r.Value}`);
|
|
59498
|
-
} else if (v.ValidationMethod === "EMAIL") {
|
|
59499
|
-
|
|
59500
|
-
lines.push(` ${v.DomainName} — confirmation email sent to: ${emails || "<none>"}`);
|
|
59501
|
-
}
|
|
59502
|
-
this.logger.info(lines.join("\n"));
|
|
59784
|
+
} else if (v.ValidationMethod === "EMAIL" && (v.ValidationEmails ?? []).length > 0) lines.push(` ${v.DomainName} — confirmation email sent to: ${v.ValidationEmails.join(", ")}`);
|
|
59785
|
+
return lines.length === 1 ? void 0 : lines.join("\n");
|
|
59503
59786
|
}
|
|
59504
59787
|
async updateTags(certificateArn, newTags, oldTags) {
|
|
59505
59788
|
const newTagMap = new Map((newTags || []).map((t) => [t.Key, t.Value]));
|
|
@@ -59905,7 +60188,7 @@ async function deployCommand(stacks, options) {
|
|
|
59905
60188
|
let deployInterrupted = false;
|
|
59906
60189
|
const topLevelSigintHandler = () => {
|
|
59907
60190
|
if (deployInterrupted) {
|
|
59908
|
-
process.stderr.write("\nForce-quit: stack locks may not be released. If the next run reports a lock, run: cdkd force-unlock <stackName>\n");
|
|
60191
|
+
process.stderr.write("\nForce-quit: stack locks may not be released. If the next run reports a lock, run this for EACH stack it names (the region-qualified form — see the message that run prints): cdkd force-unlock <stackName> --stack-region <region>\n");
|
|
59909
60192
|
process.exit(130);
|
|
59910
60193
|
}
|
|
59911
60194
|
process.stderr.write("\nInterrupted — waiting for in-progress operations to complete...\n");
|
|
@@ -60172,6 +60455,10 @@ async function deployCommand(stacks, options) {
|
|
|
60172
60455
|
dagBuilder,
|
|
60173
60456
|
diffCalculator,
|
|
60174
60457
|
options: deployEngineOptions,
|
|
60458
|
+
destroyOptions: {
|
|
60459
|
+
statePrefix: options.statePrefix,
|
|
60460
|
+
...options.profile && { profile: options.profile }
|
|
60461
|
+
},
|
|
60175
60462
|
...assetRedirect && { assetRedirect }
|
|
60176
60463
|
}, () => stackDeployEngine.deploy(stackInfo.stackName, stackInfo.template));
|
|
60177
60464
|
logger.info(`\n${bold("Deployment Summary:")}`);
|
|
@@ -66142,6 +66429,11 @@ function setAtPath(target, path, value) {
|
|
|
66142
66429
|
*/
|
|
66143
66430
|
async function runAccept(reports, stateBackend, stateConfig, awsClients, options) {
|
|
66144
66431
|
const logger = getLogger();
|
|
66432
|
+
const lockRecovery = {
|
|
66433
|
+
profile: options.profile,
|
|
66434
|
+
stateBucket: stateConfig.bucket,
|
|
66435
|
+
statePrefix: stateConfig.prefix
|
|
66436
|
+
};
|
|
66145
66437
|
printAcceptPlan(reports);
|
|
66146
66438
|
if (options.dryRun) {
|
|
66147
66439
|
logger.info("--dry-run: state will NOT be written. Re-run without --dry-run to apply.");
|
|
@@ -66164,7 +66456,12 @@ async function runAccept(reports, stateBackend, stateConfig, awsClients, options
|
|
|
66164
66456
|
skipped: () => []
|
|
66165
66457
|
}));
|
|
66166
66458
|
if (driftedOutcomes.length === 0) continue;
|
|
66167
|
-
if (!await lockManager.acquireLock(report.stackName, report.region, owner, "drift-accept")) throw new Error(
|
|
66459
|
+
if (!await lockManager.acquireLock(report.stackName, report.region, owner, "drift-accept")) throw new Error(await buildLockContentionMessage({
|
|
66460
|
+
lockManager,
|
|
66461
|
+
stackName: report.stackName,
|
|
66462
|
+
region: report.region,
|
|
66463
|
+
recovery: lockRecovery
|
|
66464
|
+
}));
|
|
66168
66465
|
try {
|
|
66169
66466
|
const resources = { ...report.state.resources };
|
|
66170
66467
|
for (const outcome of driftedOutcomes) {
|
|
@@ -66761,6 +67058,11 @@ function deepEqualUnordered(a, b) {
|
|
|
66761
67058
|
*/
|
|
66762
67059
|
async function runRevert(reports, providerRegistry, stateBackend, stateConfig, awsClients, options) {
|
|
66763
67060
|
const logger = getLogger();
|
|
67061
|
+
const lockRecovery = {
|
|
67062
|
+
profile: options.profile,
|
|
67063
|
+
stateBucket: stateConfig.bucket,
|
|
67064
|
+
statePrefix: stateConfig.prefix
|
|
67065
|
+
};
|
|
66764
67066
|
printRevertPlan(reports);
|
|
66765
67067
|
if (options.dryRun) {
|
|
66766
67068
|
logger.info("--dry-run: AWS will NOT be modified. Re-run without --dry-run to apply.");
|
|
@@ -66788,11 +67090,16 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
|
|
|
66788
67090
|
skipped: () => []
|
|
66789
67091
|
}));
|
|
66790
67092
|
if (driftedOutcomes.length === 0) continue;
|
|
66791
|
-
if (!await lockManager.acquireLock(report.stackName, report.region, owner, "drift-revert")) throw new Error(
|
|
67093
|
+
if (!await lockManager.acquireLock(report.stackName, report.region, owner, "drift-revert")) throw new Error(await buildLockContentionMessage({
|
|
67094
|
+
lockManager,
|
|
67095
|
+
stackName: report.stackName,
|
|
67096
|
+
region: report.region,
|
|
67097
|
+
recovery: lockRecovery
|
|
67098
|
+
}));
|
|
66792
67099
|
const narrowedByLogicalId = /* @__PURE__ */ new Map();
|
|
66793
|
-
const revertSecretResolvers = new DriftSecretResolvers(report.region);
|
|
66794
|
-
const revertProducerRegions = producerRegionsFromState(report.state);
|
|
66795
67100
|
try {
|
|
67101
|
+
const revertSecretResolvers = new DriftSecretResolvers(report.region);
|
|
67102
|
+
const revertProducerRegions = producerRegionsFromState(report.state);
|
|
66796
67103
|
await runWithConcurrency(driftedOutcomes.map((outcome) => async () => {
|
|
66797
67104
|
const stateResource = report.state.resources[outcome.logicalId];
|
|
66798
67105
|
if (!stateResource) {
|
|
@@ -67415,6 +67722,7 @@ async function destroyCommand(stackArgs, options) {
|
|
|
67415
67722
|
exportIndexStore,
|
|
67416
67723
|
destroyOptions: {
|
|
67417
67724
|
...options.profile && { profile: options.profile },
|
|
67725
|
+
statePrefix: options.statePrefix,
|
|
67418
67726
|
...options.removeProtection === true && { removeProtection: true },
|
|
67419
67727
|
...options.skipFinalSnapshot === true && { skipFinalSnapshot: true },
|
|
67420
67728
|
...options.resourceWarnAfter?.globalMs !== void 0 && { resourceWarnAfterMs: options.resourceWarnAfter.globalMs },
|
|
@@ -67430,6 +67738,7 @@ async function destroyCommand(stackArgs, options) {
|
|
|
67430
67738
|
baseRegion: region,
|
|
67431
67739
|
...options.profile && { profile: options.profile },
|
|
67432
67740
|
stateBucket,
|
|
67741
|
+
statePrefix: options.statePrefix,
|
|
67433
67742
|
skipConfirmation: options.yes || options.force,
|
|
67434
67743
|
removeProtection: options.removeProtection === true,
|
|
67435
67744
|
skipFinalSnapshot: options.skipFinalSnapshot === true,
|
|
@@ -69003,6 +69312,11 @@ async function exportCommand(stackArg, options) {
|
|
|
69003
69312
|
bucket: stateBucket,
|
|
69004
69313
|
prefix: options.statePrefix
|
|
69005
69314
|
};
|
|
69315
|
+
const lockRecovery = {
|
|
69316
|
+
profile: options.profile,
|
|
69317
|
+
stateBucket,
|
|
69318
|
+
statePrefix: options.statePrefix
|
|
69319
|
+
};
|
|
69006
69320
|
const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
|
|
69007
69321
|
...options.region && { region: options.region },
|
|
69008
69322
|
...options.profile && { profile: options.profile }
|
|
@@ -69078,7 +69392,12 @@ async function exportCommand(stackArg, options) {
|
|
|
69078
69392
|
}
|
|
69079
69393
|
const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
|
|
69080
69394
|
if (!options.dryRun) {
|
|
69081
|
-
if (!await lockManager.acquireLock(resolvedStackName, targetRegion, owner, "export")) throw new Error(
|
|
69395
|
+
if (!await lockManager.acquireLock(resolvedStackName, targetRegion, owner, "export")) throw new Error(await buildLockContentionMessage({
|
|
69396
|
+
lockManager,
|
|
69397
|
+
stackName: resolvedStackName,
|
|
69398
|
+
region: targetRegion,
|
|
69399
|
+
recovery: lockRecovery
|
|
69400
|
+
}));
|
|
69082
69401
|
}
|
|
69083
69402
|
try {
|
|
69084
69403
|
if (nestedStackRows.length > 0) {
|
|
@@ -69098,6 +69417,7 @@ async function exportCommand(stackArg, options) {
|
|
|
69098
69417
|
if (missingNested.length > 0) throw new Error(`Template requires parameter(s) without defaults: ${missingNested.join(", ")}. Pass each one as --parameter Key=Value (or set a Default in the CDK code).`);
|
|
69099
69418
|
const childOverrides = parseCfnChildStackNameOverrides(options.cfnChildStackName);
|
|
69100
69419
|
if ((await runPerStackImportLoop({
|
|
69420
|
+
lockRecovery,
|
|
69101
69421
|
rootStackName: resolvedStackName,
|
|
69102
69422
|
rootRegion: targetRegion,
|
|
69103
69423
|
rootStackInfoNestedTemplates: rootNestedTemplatePaths,
|
|
@@ -70994,7 +71314,15 @@ async function runPerStackImportLoop(args) {
|
|
|
70994
71314
|
try {
|
|
70995
71315
|
await deps.lockManager.acquireLockWithRetry(node.stackName, node.region, deps.lockOwner, "export", CHILD_LOCK_RETRY_MAX_ATTEMPTS, CHILD_LOCK_RETRY_DELAY_MS);
|
|
70996
71316
|
} catch (err) {
|
|
70997
|
-
throw new Error(
|
|
71317
|
+
throw new Error(await buildLockContentionMessage({
|
|
71318
|
+
lockManager: deps.lockManager,
|
|
71319
|
+
stackName: node.stackName,
|
|
71320
|
+
region: node.region,
|
|
71321
|
+
subject: "nested-stack child",
|
|
71322
|
+
heldClause: "another cdkd process held it through the retry window",
|
|
71323
|
+
recovery: args.lockRecovery,
|
|
71324
|
+
suffix: "No CloudFormation changeset has been submitted; cdkd state is unchanged."
|
|
71325
|
+
}), { cause: err instanceof Error ? err : void 0 });
|
|
70998
71326
|
}
|
|
70999
71327
|
acquiredChildLocks.push({
|
|
71000
71328
|
stackName: node.stackName,
|
|
@@ -71930,8 +72258,12 @@ async function stateOrphanCommand(stackArgs, options) {
|
|
|
71930
72258
|
}
|
|
71931
72259
|
if (!options.force) {
|
|
71932
72260
|
for (const target of targets) if (await setup.lockManager.isLocked(stackName, target.region)) {
|
|
71933
|
-
const where = target.region
|
|
71934
|
-
throw new Error(`Stack '${stackName}' (${where}) is locked. Run
|
|
72261
|
+
const where = target.region ? displaySafe(target.region, { asciiOnly: true }) || "<unrenderable>" : "(legacy)";
|
|
72262
|
+
throw new Error(`Stack '${displaySafe(stackName, { asciiOnly: true })}' (${where}) is locked. Run: ${buildForceUnlockCommand(stackName, target.region, {
|
|
72263
|
+
profile: options.profile,
|
|
72264
|
+
stateBucket: setup.bucket,
|
|
72265
|
+
statePrefix: options.statePrefix
|
|
72266
|
+
}) || "a command cdkd cannot show safely: the recorded name or region would not survive a command line intact, so inspect the lock object directly"} first, or pass --force to remove anyway.`);
|
|
71935
72267
|
}
|
|
71936
72268
|
}
|
|
71937
72269
|
if (!options.yes && !options.force) {
|
|
@@ -71951,9 +72283,13 @@ async function stateOrphanCommand(stackArgs, options) {
|
|
|
71951
72283
|
}
|
|
71952
72284
|
for (const target of targets) {
|
|
71953
72285
|
if (target.region) {
|
|
72286
|
+
await warnOnLiveForeignLock(setup.lockManager, stackName, target.region, logger);
|
|
71954
72287
|
await setup.stateBackend.deleteState(stackName, target.region);
|
|
71955
72288
|
await setup.lockManager.forceReleaseLock(stackName, target.region);
|
|
71956
|
-
} else
|
|
72289
|
+
} else {
|
|
72290
|
+
await warnOnLiveForeignLock(setup.lockManager, stackName, void 0, logger);
|
|
72291
|
+
await setup.lockManager.forceReleaseLock(stackName, void 0);
|
|
72292
|
+
}
|
|
71957
72293
|
logger.info(`✓ Removed state for stack: ${formatStackRef(target)}`);
|
|
71958
72294
|
}
|
|
71959
72295
|
}
|
|
@@ -72085,6 +72421,7 @@ async function stateDestroyCommand(stackArgs, options) {
|
|
|
72085
72421
|
exportIndexStore: setup.exportIndexStore,
|
|
72086
72422
|
destroyOptions: {
|
|
72087
72423
|
...options.profile && { profile: options.profile },
|
|
72424
|
+
statePrefix: options.statePrefix,
|
|
72088
72425
|
...options.removeProtection === true && { removeProtection: true },
|
|
72089
72426
|
...options.skipFinalSnapshot === true && { skipFinalSnapshot: true },
|
|
72090
72427
|
...options.resourceWarnAfter?.globalMs !== void 0 && { resourceWarnAfterMs: options.resourceWarnAfter.globalMs },
|
|
@@ -72100,6 +72437,7 @@ async function stateDestroyCommand(stackArgs, options) {
|
|
|
72100
72437
|
baseRegion: setup.region,
|
|
72101
72438
|
...options.profile && { profile: options.profile },
|
|
72102
72439
|
stateBucket: setup.bucket,
|
|
72440
|
+
statePrefix: options.statePrefix,
|
|
72103
72441
|
skipConfirmation: options.yes || options.all === true,
|
|
72104
72442
|
removeProtection: options.removeProtection === true,
|
|
72105
72443
|
skipFinalSnapshot: options.skipFinalSnapshot === true,
|
|
@@ -72461,7 +72799,12 @@ async function stateRefreshObservedCommand(stackArgs, options) {
|
|
|
72461
72799
|
if (!target.region) throw new Error(`Stack '${target.stackName}' has only a legacy state record without a region. Run 'cdkd deploy ${target.stackName}' (or any cdkd write) first to migrate it to the region-scoped layout, then re-run refresh-observed.`);
|
|
72462
72800
|
const counts = await refreshObservedForStack(target.stackName, target.region, setup.stateBackend, setup.lockManager, providerRegistry, {
|
|
72463
72801
|
dryRun: options.dryRun ?? false,
|
|
72464
|
-
logger
|
|
72802
|
+
logger,
|
|
72803
|
+
lockRecovery: {
|
|
72804
|
+
profile: options.profile,
|
|
72805
|
+
stateBucket: setup.bucket,
|
|
72806
|
+
statePrefix: options.statePrefix
|
|
72807
|
+
}
|
|
72465
72808
|
});
|
|
72466
72809
|
totalRefreshed += counts.refreshed;
|
|
72467
72810
|
totalUnsupported += counts.unsupported;
|
|
@@ -72488,6 +72831,34 @@ async function stateRefreshObservedCommand(stackArgs, options) {
|
|
|
72488
72831
|
*/
|
|
72489
72832
|
const NO_RECORDED_SECRETS = /* @__PURE__ */ new Map();
|
|
72490
72833
|
/**
|
|
72834
|
+
* Warn before `cdkd state orphan` force-releases a lock that is still LIVE.
|
|
72835
|
+
*
|
|
72836
|
+
* `forceReleaseLock` is unconditional by design — a stuck lock must never make
|
|
72837
|
+
* a state record unremovable — but it deletes an in-flight `cdkd deploy`'s lock
|
|
72838
|
+
* just as readily as a stale one, and it did so silently (issue #2171). The
|
|
72839
|
+
* write it enables has already happened by the time anyone notices, so the
|
|
72840
|
+
* only useful thing is to name the owner at the moment of the release.
|
|
72841
|
+
*
|
|
72842
|
+
* Best-effort in both directions: an expired lock is not worth a line, and a
|
|
72843
|
+
* failed read must not block the removal the user asked for.
|
|
72844
|
+
*
|
|
72845
|
+
* (Issue #2171's own text called this command `cdkd state rm`; no such command
|
|
72846
|
+
* exists -- this file registers it as `orphan`.)
|
|
72847
|
+
*/
|
|
72848
|
+
async function warnOnLiveForeignLock(lockManager, stackName, region, logger) {
|
|
72849
|
+
try {
|
|
72850
|
+
const info = await lockManager.getLockInfo(stackName, region);
|
|
72851
|
+
if (!info) return;
|
|
72852
|
+
const expiryKnown = Number.isFinite(info.expiresAt);
|
|
72853
|
+
if (expiryKnown && info.expiresAt <= Date.now()) return;
|
|
72854
|
+
const safeStack = displaySafe(stackName, { asciiOnly: true }) || "<unrenderable>";
|
|
72855
|
+
const where = region ? `${safeStack} (${displaySafe(region, { asciiOnly: true }) || "<unrenderable>"})` : `${safeStack} (legacy lock key)`;
|
|
72856
|
+
const owner = info.owner;
|
|
72857
|
+
const operation = info.operation ? `, operation: ${info.operation}` : "";
|
|
72858
|
+
logger.warn(`Force-releasing a LIVE lock on ${where} ${owner ? `held by ${owner}${operation}` : "held by an unnamed holder"}. ` + (owner && expiryKnown ? `That process is still running and will keep writing; its next state write ` : expiryKnown ? `The lock has not expired, so a process may still be writing; its next state write ` : `The lock records no readable expiry, so a process may still be writing; its next state write `) + `may recreate the record being removed here.`);
|
|
72859
|
+
} catch {}
|
|
72860
|
+
}
|
|
72861
|
+
/**
|
|
72491
72862
|
* Refresh the `observedProperties` of every resource in one stack
|
|
72492
72863
|
* record. Returns counts so the caller can aggregate across `--all`.
|
|
72493
72864
|
*
|
|
@@ -72497,7 +72868,7 @@ const NO_RECORDED_SECRETS = /* @__PURE__ */ new Map();
|
|
|
72497
72868
|
* for the underlying SDK reads.
|
|
72498
72869
|
*/
|
|
72499
72870
|
async function refreshObservedForStack(stackName, region, stateBackend, lockManager, providerRegistry, opts) {
|
|
72500
|
-
const { logger } = opts;
|
|
72871
|
+
const { logger, lockRecovery } = opts;
|
|
72501
72872
|
const result = await stateBackend.getState(stackName, region);
|
|
72502
72873
|
if (!result) throw new Error(`No state found for stack '${stackName}' (${region}). Run 'cdkd state list' to see available stacks.`);
|
|
72503
72874
|
const { state, etag, migrationPending } = result;
|
|
@@ -72535,7 +72906,12 @@ async function refreshObservedForStack(stackName, region, stateBackend, lockMana
|
|
|
72535
72906
|
};
|
|
72536
72907
|
}
|
|
72537
72908
|
const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
|
|
72538
|
-
if (!await lockManager.acquireLock(stackName, region, owner, "state-refresh-observed")) throw new Error(
|
|
72909
|
+
if (!await lockManager.acquireLock(stackName, region, owner, "state-refresh-observed")) throw new Error(await buildLockContentionMessage({
|
|
72910
|
+
lockManager,
|
|
72911
|
+
stackName,
|
|
72912
|
+
region,
|
|
72913
|
+
recovery: lockRecovery
|
|
72914
|
+
}));
|
|
72539
72915
|
try {
|
|
72540
72916
|
let refreshed = 0;
|
|
72541
72917
|
let unsupported = 0;
|
|
@@ -72878,7 +73254,10 @@ async function rollbackCommand(stackArg, options) {
|
|
|
72878
73254
|
awsClients: setup.awsClients,
|
|
72879
73255
|
stateBucket: setup.bucket,
|
|
72880
73256
|
exportIndexStore: setup.exportIndexStore,
|
|
72881
|
-
destroyOptions: {
|
|
73257
|
+
destroyOptions: {
|
|
73258
|
+
...options.profile && { profile: options.profile },
|
|
73259
|
+
statePrefix: options.statePrefix
|
|
73260
|
+
}
|
|
72882
73261
|
}, () => withStackName(stackName, async () => {
|
|
72883
73262
|
let failedOpFailures = 0;
|
|
72884
73263
|
let failedOpWarnings = 0;
|
|
@@ -74915,7 +75294,16 @@ async function orphanCommand(pathArgs, options) {
|
|
|
74915
75294
|
logger.info(`Target: ${stackInfo.stackName} (${targetRegion}); orphaning ${orphanLogicalIds.length} resource(s): ${orphanLogicalIds.join(", ")}`);
|
|
74916
75295
|
const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
|
|
74917
75296
|
if (!options.dryRun) {
|
|
74918
|
-
if (!await lockManager.acquireLock(stackInfo.stackName, targetRegion, owner, "orphan")) throw new Error(
|
|
75297
|
+
if (!await lockManager.acquireLock(stackInfo.stackName, targetRegion, owner, "orphan")) throw new Error(await buildLockContentionMessage({
|
|
75298
|
+
lockManager,
|
|
75299
|
+
stackName: stackInfo.stackName,
|
|
75300
|
+
region: targetRegion,
|
|
75301
|
+
recovery: {
|
|
75302
|
+
profile: options.profile,
|
|
75303
|
+
stateBucket,
|
|
75304
|
+
statePrefix: options.statePrefix
|
|
75305
|
+
}
|
|
75306
|
+
}));
|
|
74919
75307
|
}
|
|
74920
75308
|
try {
|
|
74921
75309
|
const stateData = await stateBackend.getState(stackInfo.stackName, targetRegion);
|
|
@@ -75401,6 +75789,11 @@ async function importCommand(stackArg, options) {
|
|
|
75401
75789
|
bucket: stateBucket,
|
|
75402
75790
|
prefix: options.statePrefix
|
|
75403
75791
|
};
|
|
75792
|
+
const lockRecovery = {
|
|
75793
|
+
profile: options.profile,
|
|
75794
|
+
stateBucket,
|
|
75795
|
+
statePrefix: options.statePrefix
|
|
75796
|
+
};
|
|
75404
75797
|
const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
|
|
75405
75798
|
...options.region && { region: options.region },
|
|
75406
75799
|
...options.profile && { profile: options.profile }
|
|
@@ -75501,7 +75894,16 @@ async function importCommand(stackArg, options) {
|
|
|
75501
75894
|
}
|
|
75502
75895
|
for (const overrideId of overrides.keys()) if (!templateLogicalIds.has(overrideId)) throw new Error(`--resource / --resource-mapping references logical ID '${overrideId}' which is not in the synthesized template for stack '${stackInfo.stackName}'. Available IDs: ${[...templateLogicalIds].join(", ")}`);
|
|
75503
75896
|
const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
|
|
75504
|
-
if (!await lockManager.acquireLock(stackInfo.stackName, targetRegion, owner, "import")) throw new Error(
|
|
75897
|
+
if (!await lockManager.acquireLock(stackInfo.stackName, targetRegion, owner, "import")) throw new Error(await buildLockContentionMessage({
|
|
75898
|
+
lockManager,
|
|
75899
|
+
stackName: stackInfo.stackName,
|
|
75900
|
+
region: targetRegion,
|
|
75901
|
+
recovery: {
|
|
75902
|
+
profile: options.profile,
|
|
75903
|
+
stateBucket,
|
|
75904
|
+
statePrefix: options.statePrefix
|
|
75905
|
+
}
|
|
75906
|
+
}));
|
|
75505
75907
|
try {
|
|
75506
75908
|
const rows = [];
|
|
75507
75909
|
for (const { logicalId, resource } of resources) {
|
|
@@ -75565,6 +75967,7 @@ async function importCommand(stackArg, options) {
|
|
|
75565
75967
|
logger.info(`✓ State written: ${stackInfo.stackName} (${targetRegion})`);
|
|
75566
75968
|
logger.info(` ${importedRows.length} resource(s) imported. Run 'cdkd diff' to see how the imported state lines up with the template.`);
|
|
75567
75969
|
if (migrationCfnStackName && migrationTree && accountIdForNestedSynth) await importNestedStackChildrenRecursive({
|
|
75970
|
+
lockRecovery,
|
|
75568
75971
|
parentStackName: stackInfo.stackName,
|
|
75569
75972
|
parentRegion: targetRegion,
|
|
75570
75973
|
parentNestedTemplates: stackInfo.nestedTemplates ?? {},
|
|
@@ -76230,7 +76633,13 @@ async function importNestedStackChildrenRecursive(args) {
|
|
|
76230
76633
|
const childRewritten = rewriteTemplateAssetReferences(childTemplate, assetRedirect);
|
|
76231
76634
|
if (childRewritten > 0) logger.info(`Note: ${childRewritten} asset reference(s) in nested stack ${childStackName} are recorded in state at their pre-rewrite (CDK bootstrap) values, so the next 'cdkd deploy' repoints any resource that still holds them to cdkd asset storage.`);
|
|
76232
76635
|
}
|
|
76233
|
-
if (!await lockManager.acquireLock(childStackName, childRegion, lockOwner, "import")) throw new Error(
|
|
76636
|
+
if (!await lockManager.acquireLock(childStackName, childRegion, lockOwner, "import")) throw new Error(await buildLockContentionMessage({
|
|
76637
|
+
lockManager,
|
|
76638
|
+
stackName: childStackName,
|
|
76639
|
+
region: childRegion,
|
|
76640
|
+
subject: "nested stack",
|
|
76641
|
+
recovery: args.lockRecovery
|
|
76642
|
+
}));
|
|
76234
76643
|
try {
|
|
76235
76644
|
const childResources = collectImportableResources(childTemplate);
|
|
76236
76645
|
const childTemplateLogicalIds = new Set(childResources.map((r) => r.logicalId));
|
|
@@ -76272,6 +76681,7 @@ async function importNestedStackChildrenRecursive(args) {
|
|
|
76272
76681
|
await stateBackend.saveState(childStackName, childRegion, childStackState);
|
|
76273
76682
|
logger.info(`✓ Nested stack state written: ${childStackName} (${childRegion}) — ${rows.filter((r) => r.outcome === "imported").length} resource(s) imported.`);
|
|
76274
76683
|
if (childTreeNode.nested.size > 0) await importNestedStackChildrenRecursive({
|
|
76684
|
+
lockRecovery: args.lockRecovery,
|
|
76275
76685
|
parentStackName: childStackName,
|
|
76276
76686
|
parentRegion: childRegion,
|
|
76277
76687
|
parentNestedTemplates: indexGrandchildTemplatePaths(childTemplate, childTemplatePath),
|
|
@@ -86145,7 +86555,7 @@ function createMigrateCommand() {
|
|
|
86145
86555
|
*/
|
|
86146
86556
|
function buildProgram() {
|
|
86147
86557
|
const program = new Command();
|
|
86148
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.
|
|
86558
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.40");
|
|
86149
86559
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
86150
86560
|
const { profile } = actionCommand.optsWithGlobals();
|
|
86151
86561
|
if (profile !== void 0) process.env["AWS_PROFILE"] = profile;
|