@go-to-k/cdkd 0.285.16 → 0.286.1
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-DoP-32eU.js → asg-provider-BocQJsjl.js} +2 -2
- package/dist/{asg-provider-DoP-32eU.js.map → asg-provider-BocQJsjl.js.map} +1 -1
- package/dist/cli.js +5 -3
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-DuFUy9D0.js → deploy-engine-D0RCEQ6D.js} +42 -57
- package/dist/deploy-engine-D0RCEQ6D.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-C-xfsrSP.js → program-x67eZ06V.js} +308 -1207
- package/dist/{program-C-xfsrSP.js.map → program-x67eZ06V.js.map} +1 -1
- package/dist/{version-C_ZRHikq.js → version-CnQK6Jeh.js} +2 -2
- package/dist/{version-C_ZRHikq.js.map → version-CnQK6Jeh.js.map} +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-DuFUy9D0.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as beginCommandInterruptScope, $n as resolveAutoAssetStorage, $r as isTransientServerError, $t as maskSecretsInError, A as formatResourceLine, An as ensureAssetStorage, At as requireConfigString, B as isExportAliasCollision, Bn as dockerSpawnEnvWithSensitive, Br as ResourceTimeoutError, Bt as INTRINSIC_KEYS, C as unsupportedFinalSnapshotError, Cn as loadPublishableAssetManifest, Cr as getAwsClients, Ct as coerceCfnBoolean, D as isStatefulRecreateTargetForReplace, Dn as AssetModeResolver, Dr as CdkdError, Dt as replayWarn, E as MULTI_REGION_RECREATE_BLOCKED_TYPES, En as stripControlChars, Et as readConfigString, F as red, Fn as validateAssetBucketName, Fr as LocalStartServiceError, Ft as s3BucketDualStackDomainName, G as collectInlinePolicyNamesManagedBySiblings, Gn as runDockerStreaming, Gt as STATE_SOURCED_CROSS_GENERATION_RULES, H as stateKeySecretExposure, Hn as getDockerCmd, Hr as StackHasActiveImportsError, Ht as withRetry, I as yellow, In as validateContainerRepoName, It as s3BucketRegionalDomainName, J as findActionableSilentDrops, Jn as Synthesizer, Jr as normalizeAwsError, Jt as carriesSecretMask, K as clearOnUpdateRemoval, Kn as AssetManifestLoader, Kt as STATE_SOURCED_READBACK_RULES, L as collectDeclaredOutputNames, Ln as buildDenyExternalAccessPolicy, Lr as NestedStackChildDirectDestroyError, Lt as s3BucketWebsiteUrl, M as cyan, Mn as isCrossRegionRedirect, Mr as DynamicReferenceRegionAmbiguousError, Mt as producerRegionsFromState, N as gray, Nn as parseBootstrapMarker, Nr as IntrinsicResolutionRefusalError, Nt as s3BucketArn, O as isStatefulRecreateTargetSync, On as BOOTSTRAP_MARKER_PREFIX, Ot as requireConfigArray, P as green, Pn as readBootstrapMarkerBody, Pr as LocalInvokeBuildError$1, Pt as s3BucketDomainName, Q as maskerOrIdentity, Qn as resolveApp, Qr as isThrottlingError, Qt as isSingleDynamicReferenceToken, R as collectPublishedOutputNames, Rn as describeAwsFailure, Rr as PartialFailureError, Rt as applyRoleArnIfSet, S as refusesFinalSnapshot, Sn as createAssetRedirectResolver, Sr as AwsClients, St as assertRegionMatch, T as extractDeploymentEventError, Tn as escapeRegExp, Tr as setAwsClients, Tt as configStringRefusal, U as getCurrentResourceSecrets, Un as partitionSensitiveEnv, Ur as StackTerminationProtectionError, Ut as DagBuilder, V as secretBearingStateKeyWarning, Vn as formatDockerLoginError, Vr as ResourceUpdateNotSupportedError, Vt as describeTypeWithThrottleRetry, W as IAMRoleProvider, Wn as runDockerForeground, Wt as TemplateParser, X as createMaskedRetryLogger, Xn as getDefaultStateBucketName, Xr as isMarkedNonRetryable, Xt as dynamicReferenceTokens, Y as findSilentDropProperties, Yn as synthesisStatusMessage, Yr as withErrorHandling, Yt as createSecretMasker, Z as maskDeep, Zn as getLegacyStateBucketName, Zr as isRetryableTransientError, Zt as errorCauseChain, _ as PRE_DELETE_SNAPSHOT_TYPES, _n as shouldRetainResource, _r as derivePartitionAndUrlSuffix, _t as parameterTypeMayLoseSecretIdentity, a as DeploymentEventsStore, an as LockManager, ar as stateBucketExistenceConfirmed, at as slowCcOperationTimeoutMs, b as createPreDeleteFinalSnapshot, bn as WorkGraph, bt as normalizeAwsTagsToCfn, c as replayFailedOperations, cn as UNRENDERABLE, cr as CFN_TEMPLATE_URL_LIMIT, ct as deleteSkipReason, d as updatePartialReason, dn as forceQuitRecoveryClause, dr as uploadCfnTemplate, dt as IntrinsicFunctionResolver, ei as markNonRetryable, en as maskSecretsInText, er as resolveCaptureObservedState, et as endCommandInterruptScope, f as withResourceDeadline, fn as CUSTOM_RESOURCE_RESPONSE_PREFIX, fr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, ft as carriesDynamicReference, g as ATOMIC_FINAL_SNAPSHOT_TYPES, gn as importableOutputs, gr as canonicalizeRegion, gt as isUnboundTemplateParameter, h as computeImplicitDeleteEdges, hn as importableOutputKeys, hr as PARTITION_TABLE, ht as getAccountInfo, i as DeploymentEventsReader, in as scrubResourceRecord, ir as resolveUseCdkBootstrapAssets, it as CloudControlProvider, j as bold, jn as getBootstrapMarkerKey, jr as DeployCancelledError, jt as classifyReplaySecretRegion, k as renderStatefulReason, kn as assertAssetBucketRegion, kr as CrossAccountSecretRefusalError, kt as requireConfigObject, l as replayRollback, ln as buildForceUnlockCommand, lr as MIGRATE_TMP_PREFIX, lt as disableInstanceApiTermination, m as IMPLICIT_DELETE_DEPENDENCIES, mn as exportNamesCarriedFrom, mr as expectedOwnerParam, mt as coerceParameterTypedValue, n as DEFAULT_RESOURCE_WARN_AFTER_MS, ni as retryClassificationText, nn as recoverMaskedOutput, nr as resolveStateBucketWithDefault, nt as isInterruptedWaitError, o as planFailedOps, on as S3StateBackend, or as warnDeprecatedNoPrefixCliFlag, ot as UNSPECIFIED_SKIP_REASON, p as maskingRetryLogger, pn as DEFAULT_STATE_PREFIX, pr as displaySafe, pt as cfnRefValueFromPhysicalId, q as ProviderRegistry, qn as getDockerImageBySourceHash, qt as TEMPLATE_SOURCED_RULES, r as DeployEngine, rn as redactSecretsForState, rr as resolveStateBucketWithDefaultAndSource, rt as startInterruptWatch, s as planRollback, sn as rebuildClientForBucketRegion, sr as CFN_TEMPLATE_BODY_LIMIT, st as deleteIndeterminateGuards, t as DEFAULT_RESOURCE_TIMEOUT_MS, ti as markRedactedCause, tn as recordMaskOnlyValue, tr as resolveSkipPrefix, tt as interruptWatchListenerCount, u as updatePartialMessage, un as buildLockContentionMessage, ur as findLargeInlineResources, ut as isTerminationProtectionPropagationError, v as buildFinalSnapshotIdentifier, vn as AssetPublisher, vr as AssemblyReader, vt as refStateLookupFromResource, w as makeCanonicalizePropertiesFn, wn as rewriteTemplateAssetReferences, wr as resetAwsClients, wt as configBooleanRefusal, x as isFinalSnapshotError, xn as buildAssetRedirectMap, xr as resolveBucketRegion, xt as resolveExplicitPhysicalId, y as ccRoutedFinalSnapshotError, yn as stringifyValue, yr as processStackMessages, yt as WAFv2WebACLProvider, z as exportAliasCollisionScrubWarning, zn as buildDockerImage, zr as ProvisioningError, zt as DiffCalculator } from "./deploy-engine-D0RCEQ6D.js";
|
|
2
2
|
import { _ as withStackName, c as PATTERN_B_NAME_OPTIONS, f as generateResourceName, g as withSkipPrefix, i as reserveStdoutForPayload, l as PATTERN_B_NAME_PROPERTIES, m as getCurrentStackName, n as getLogger, o as runStackBuffered, p as generateResourceNameWithFallback, s as getLiveRenderer, u as PATTERN_B_RESOURCE_TYPES } from "./logger-Dw-3y48G.js";
|
|
3
3
|
import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
|
|
4
|
-
import { t as getCdkdVersion } from "./version-
|
|
5
|
-
import { t as ASGProvider } from "./asg-provider-
|
|
4
|
+
import { t as getCdkdVersion } from "./version-CnQK6Jeh.js";
|
|
5
|
+
import { t as ASGProvider } from "./asg-provider-BocQJsjl.js";
|
|
6
6
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
7
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
8
8
|
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";
|
|
@@ -20,15 +20,15 @@ import { CreateSecretCommand, DeleteSecretCommand, DescribeSecretCommand, GetSec
|
|
|
20
20
|
import { AddTagsToResourceCommand, DeleteParameterCommand, DescribeParametersCommand, GetParameterCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$2, ParameterNotFound, PutParameterCommand, RemoveTagsFromResourceCommand, SSMClient } from "@aws-sdk/client-ssm";
|
|
21
21
|
import { CloudFrontClient, CreateCloudFrontOriginAccessIdentityCommand, CreateDistributionCommand, CreateDistributionWithTagsCommand, CreateOriginAccessControlCommand, DeleteCloudFrontOriginAccessIdentityCommand, DeleteDistributionCommand, DeleteOriginAccessControlCommand, DistributionAlreadyExists, GetCloudFrontOriginAccessIdentityCommand, GetDistributionCommand, GetDistributionConfigCommand, GetOriginAccessControlCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$3, NoSuchCloudFrontOriginAccessIdentity, NoSuchDistribution, NoSuchOriginAccessControl, TagResourceCommand as TagResourceCommand$6, UntagResourceCommand as UntagResourceCommand$6, UpdateCloudFrontOriginAccessIdentityCommand, UpdateDistributionCommand, UpdateOriginAccessControlCommand } from "@aws-sdk/client-cloudfront";
|
|
22
22
|
import { CloudWatchClient, DeleteAlarmsCommand, DeleteAnomalyDetectorCommand, DescribeAlarmsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$4, PutAnomalyDetectorCommand, PutMetricAlarmCommand, ResourceNotFoundException as ResourceNotFoundException$4, TagResourceCommand as TagResourceCommand$7, UntagResourceCommand as UntagResourceCommand$7 } from "@aws-sdk/client-cloudwatch";
|
|
23
|
-
import { AssociateKmsKeyCommand, CloudWatchLogsClient, CreateLogGroupCommand, DeleteDataProtectionPolicyCommand, DeleteIndexPolicyCommand, DeleteLogGroupCommand, DeleteRetentionPolicyCommand, DescribeIndexPoliciesCommand, DescribeLogGroupsCommand, DisassociateKmsKeyCommand, GetDataProtectionPolicyCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$5, PutBearerTokenAuthenticationCommand, PutDataProtectionPolicyCommand, PutIndexPolicyCommand, PutLogGroupDeletionProtectionCommand, PutRetentionPolicyCommand, ResourceAlreadyExistsException, ResourceNotFoundException as ResourceNotFoundException$5, TagResourceCommand as TagResourceCommand$8, UntagResourceCommand as UntagResourceCommand$8 } from "@aws-sdk/client-cloudwatch-logs";
|
|
23
|
+
import { AssociateKmsKeyCommand, CloudWatchLogsClient, CreateLogGroupCommand, DeleteDataProtectionPolicyCommand, DeleteIndexPolicyCommand, DeleteLogGroupCommand, DeleteRetentionPolicyCommand, DescribeIndexPoliciesCommand, DescribeLogGroupsCommand, DescribeLogStreamsCommand, DisassociateKmsKeyCommand, GetDataProtectionPolicyCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$5, PutBearerTokenAuthenticationCommand, PutDataProtectionPolicyCommand, PutIndexPolicyCommand, PutLogGroupDeletionProtectionCommand, PutRetentionPolicyCommand, ResourceAlreadyExistsException, ResourceNotFoundException as ResourceNotFoundException$5, TagResourceCommand as TagResourceCommand$8, UntagResourceCommand as UntagResourceCommand$8 } from "@aws-sdk/client-cloudwatch-logs";
|
|
24
24
|
import { BedrockAgentCoreControlClient, CreateAgentRuntimeCommand, CreateEvaluatorCommand, DeleteAgentRuntimeCommand, DeleteEvaluatorCommand, GetAgentRuntimeCommand, GetBrowserCommand, GetCodeInterpreterCommand, GetEvaluatorCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$6, ResourceNotFoundException as ResourceNotFoundException$6, TagResourceCommand as TagResourceCommand$9, UntagResourceCommand as UntagResourceCommand$9, UpdateAgentRuntimeCommand, UpdateEvaluatorCommand } from "@aws-sdk/client-bedrock-agentcore-control";
|
|
25
25
|
import { AddTagsToResourceCommand as AddTagsToResourceCommand$1, CreateCacheClusterCommand, CreateCacheSubnetGroupCommand, DeleteCacheClusterCommand, DeleteCacheSubnetGroupCommand, DescribeCacheClustersCommand, DescribeCacheSubnetGroupsCommand, ElastiCacheClient, ListTagsForResourceCommand as ListTagsForResourceCommand$7, ModifyCacheClusterCommand, ModifyCacheSubnetGroupCommand, RemoveTagsFromResourceCommand as RemoveTagsFromResourceCommand$1 } from "@aws-sdk/client-elasticache";
|
|
26
26
|
import { ACMClient, AddTagsToCertificateCommand, DeleteCertificateCommand, DescribeCertificateCommand, ListTagsForCertificateCommand, RemoveTagsFromCertificateCommand, RequestCertificateCommand, ResourceNotFoundException as ResourceNotFoundException$7, UpdateCertificateOptionsCommand } from "@aws-sdk/client-acm";
|
|
27
27
|
import { CreateMicrovmImageCommand, DeleteMicrovmImageCommand, GetMicrovmImageCommand, LambdaMicrovmsClient, ListTagsCommand as ListTagsCommand$1, ResourceNotFoundException as ResourceNotFoundException$8, TagResourceCommand as TagResourceCommand$10, UntagResourceCommand as UntagResourceCommand$10, UpdateMicrovmImageCommand } from "@aws-sdk/client-lambda-microvms";
|
|
28
28
|
import * as fs from "node:fs";
|
|
29
|
-
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync,
|
|
29
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
30
30
|
import * as path from "node:path";
|
|
31
|
-
import { basename, dirname, isAbsolute,
|
|
31
|
+
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
32
32
|
import { execFile, spawn } from "node:child_process";
|
|
33
33
|
import { tmpdir } from "node:os";
|
|
34
34
|
import { AssociateVPCWithHostedZoneCommand, ChangeResourceRecordSetsCommand, ChangeTagsForResourceCommand, CreateHostedZoneCommand, CreateQueryLoggingConfigCommand, DeleteHostedZoneCommand, DeleteQueryLoggingConfigCommand, DisassociateVPCFromHostedZoneCommand, GetHostedZoneCommand, HostedZoneAlreadyExists, ListHostedZonesByNameCommand, ListQueryLoggingConfigsCommand, ListResourceRecordSetsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$8, Route53Client, UpdateHostedZoneCommentCommand, UpdateHostedZoneFeaturesCommand } from "@aws-sdk/client-route-53";
|
|
@@ -779,8 +779,8 @@ const recreateViaSdkProviderOption = new Option("--recreate-via-sdk-provider <lo
|
|
|
779
779
|
* force would create a false sense of granularity (the user is opting
|
|
780
780
|
* into a footgun; pretending to scope it per-resource is misleading).
|
|
781
781
|
*/
|
|
782
|
-
const forceStatefulRecreationOption = new Option("--force-stateful-recreation", "Confirm a data-losing destroy + recreate of a stateful resource. Required whenever a replacement target is a stateful type
|
|
783
|
-
const replaceOption = new Option("--replace", "Replace (DELETE + CREATE) a resource whose in-place update is rejected because an immutable property changed and AWS exposes no update API for it (e.g. Lambda LayerVersion content, EFS AccessPoint, ECS TaskDefinition, ApiGatewayV2 immutable fields). Without this flag such a change fails the deploy. Stateful types (RDS / DynamoDB / EFS / S3
|
|
782
|
+
const forceStatefulRecreationOption = new Option("--force-stateful-recreation", "Confirm a data-losing destroy + recreate of a stateful resource. Required whenever a replacement target is a stateful type on the --recreate-via-cc-api / --recreate-via-sdk-provider pre-flight, with --replace, AND on the paths a plain deploy reaches with no flag: a template immutable-property change, and an in-place update the provisioning layer rejects. Stateful types are databases, filesystems, KMS keys, table / vector storage, source repositories, and more; docs/cli-deploy-safety.md carries the full list. S3 buckets and log groups are the conditional cases: at pre-flight, a non-empty bucket, or a log group with retention, with log streams, or whose emptiness the probe could not settle; mid-deploy, ANY bucket or log group, because the emptiness probe cannot run there. The two probes differ on failure — the bucket fails open, the log group closed. Destroy + recreate loses ALL data in the resource — no automatic data migration. Full opt-in for CI use: --recreate-via-cc-api <id> --force-stateful-recreation --yes.").default(false);
|
|
783
|
+
const replaceOption = new Option("--replace", "Replace (DELETE + CREATE) a resource whose in-place update is rejected because an immutable property changed and AWS exposes no update API for it (e.g. Lambda LayerVersion content, EFS AccessPoint, ECS TaskDefinition, ApiGatewayV2 immutable fields). Without this flag such a change fails the deploy. Stateful types (RDS / DynamoDB / EFS / any S3 bucket / any Logs log group / etc.) ALSO require --force-stateful-recreation since the replacement is a data-losing DELETE + CREATE — mid-deploy neither emptiness probe can run, so both conditional types count as stateful there.").default(false);
|
|
784
784
|
/**
|
|
785
785
|
* Issue #1002 PR 2 — per-invocation legacy pin for asset destinations
|
|
786
786
|
* (design §4.2). Shared by `deploy`, `diff`, `import`, and `publish-assets`
|
|
@@ -2495,12 +2495,15 @@ function isPromptAbortError(error) {
|
|
|
2495
2495
|
* no-op for fresh deploys and should error out with a clear
|
|
2496
2496
|
* message rather than silently apply.
|
|
2497
2497
|
* 3. Stateful-resource guard: every named target whose resource type
|
|
2498
|
-
* is in {@link STATEFUL_TYPES} (or conditionally stateful — S3
|
|
2499
|
-
* bucket
|
|
2500
|
-
*
|
|
2501
|
-
* first-cut runs from
|
|
2502
|
-
*
|
|
2503
|
-
* reason to `'has-objects'`
|
|
2498
|
+
* is in {@link STATEFUL_TYPES} (or conditionally stateful — an S3
|
|
2499
|
+
* bucket holding objects, a LogGroup with retention or with log
|
|
2500
|
+
* streams) MUST be matched by an explicit
|
|
2501
|
+
* `--force-stateful-recreation` flag. The sync first-cut runs from
|
|
2502
|
+
* the recorded properties alone; the live probes promote a `null`
|
|
2503
|
+
* reason afterwards — `s3:ListObjectVersions` to `'has-objects'`
|
|
2504
|
+
* when a bucket actually contains data (issue [#648]), and
|
|
2505
|
+
* `logs:DescribeLogStreams` to `'has-log-events'` when a log group
|
|
2506
|
+
* is not provably empty (issue [#2558]).
|
|
2504
2507
|
* 4. Multi-region refusal: every named target whose resource type
|
|
2505
2508
|
* is in {@link MULTI_REGION_RECREATE_BLOCKED_TYPES} (e.g.
|
|
2506
2509
|
* `AWS::DynamoDB::GlobalTable`) is refused outright. Out of
|
|
@@ -2516,11 +2519,12 @@ function isPromptAbortError(error) {
|
|
|
2516
2519
|
/**
|
|
2517
2520
|
* Plan-time validation of the user's recreate-via-cc-api list.
|
|
2518
2521
|
*
|
|
2519
|
-
* Pure with respect to AWS — does NOT probe S3 bucket emptiness
|
|
2520
|
-
* the result with
|
|
2521
|
-
*
|
|
2522
|
-
* before rendering errors. The
|
|
2523
|
-
* itself stays sync so unit tests
|
|
2522
|
+
* Pure with respect to AWS — does NOT probe S3 bucket emptiness, nor
|
|
2523
|
+
* log-group emptiness. Wrap the result with
|
|
2524
|
+
* {@link probeAndRevalidateStateful} to promote deferred targets'
|
|
2525
|
+
* `statefulReason` via a live round-trip before rendering errors. The
|
|
2526
|
+
* deploy command does this; the validator itself stays sync so unit tests
|
|
2527
|
+
* don't need AWS mocks.
|
|
2524
2528
|
*
|
|
2525
2529
|
* Input order is preserved; duplicate logical ids in the user's input
|
|
2526
2530
|
* are deduplicated.
|
|
@@ -2681,63 +2685,176 @@ function renderRecreateTargetsErrors(validation) {
|
|
|
2681
2685
|
return lines.length > 0 ? lines.join("\n") : null;
|
|
2682
2686
|
}
|
|
2683
2687
|
/**
|
|
2684
|
-
*
|
|
2685
|
-
*
|
|
2686
|
-
*
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
*
|
|
2693
|
-
*
|
|
2694
|
-
*
|
|
2695
|
-
*
|
|
2696
|
-
*
|
|
2697
|
-
*
|
|
2698
|
-
* the
|
|
2699
|
-
*
|
|
2700
|
-
*
|
|
2701
|
-
*
|
|
2702
|
-
*
|
|
2703
|
-
*
|
|
2704
|
-
*
|
|
2705
|
-
*
|
|
2706
|
-
*
|
|
2707
|
-
*
|
|
2708
|
-
*
|
|
2709
|
-
*
|
|
2710
|
-
|
|
2711
|
-
|
|
2688
|
+
* Trim-then-lower-case, the pair `CloudControlProvider` applies to both sides
|
|
2689
|
+
* of its own region assert. `canonicalizeRegion` only lower-cases, so a state
|
|
2690
|
+
* record carrying stray whitespace would still fail a `!==` compare.
|
|
2691
|
+
*/
|
|
2692
|
+
function foldRegion(region) {
|
|
2693
|
+
return canonicalizeRegion(region?.trim());
|
|
2694
|
+
}
|
|
2695
|
+
/**
|
|
2696
|
+
* Async emptiness probes for the two conditionally stateful types
|
|
2697
|
+
* (issues [#648] / [#2558]).
|
|
2698
|
+
*
|
|
2699
|
+
* For every target whose sync {@link StatefulReason} is `null` — which for
|
|
2700
|
+
* these two types means DEFER, not "not stateful" (see
|
|
2701
|
+
* {@link isStatefulRecreateTargetSync}) — issues one single-page listing and
|
|
2702
|
+
* promotes the reason when the resource is not provably empty:
|
|
2703
|
+
*
|
|
2704
|
+
* - `AWS::S3::Bucket` → `ListObjectVersions(MaxKeys=1)` against the
|
|
2705
|
+
* bucket's recorded physical id, promoting to `'has-objects'` when the
|
|
2706
|
+
* bucket has at least one current object, prior version, OR
|
|
2707
|
+
* delete-marker. `ListObjectVersions` rather than `ListObjectsV2` so the
|
|
2708
|
+
* probe mirrors the s3-bucket-provider's `emptyBucket` view: a versioned
|
|
2709
|
+
* bucket whose current keys have all been soft-deleted (so
|
|
2710
|
+
* `ListObjectsV2.KeyCount === 0`) still holds prior versions +
|
|
2711
|
+
* delete-markers that the destroy + recreate cycle would lose. Using the
|
|
2712
|
+
* same listing API as the provider ensures the probe and the destroy path
|
|
2713
|
+
* agree on "empty".
|
|
2714
|
+
* - `AWS::Logs::LogGroup` → `DescribeLogStreams(limit=1)` against the log
|
|
2715
|
+
* group's recorded name. Only ONE response shape leaves the target
|
|
2716
|
+
* un-promoted: a PRESENT, zero-length `logStreams` with no `nextToken`.
|
|
2717
|
+
* A page with a stream, an ABSENT `logStreams` (the SDK types it
|
|
2718
|
+
* optional), and a zero-length page carrying a continuation token all
|
|
2719
|
+
* promote to `'has-log-events'` — the last two are non-answers, and an
|
|
2720
|
+
* unprovable emptiness must not read as empty. **Stream presence, not
|
|
2721
|
+
* `storedBytes`,** and the choice is load-bearing in both directions:
|
|
2722
|
+
* * Every log event belongs to a log stream, so ZERO streams is a
|
|
2723
|
+
* structural proof that the group holds no events — the only kind of
|
|
2724
|
+
* "empty" this guard may act on.
|
|
2725
|
+
* * `LogStream.storedBytes` cannot be used at all: the SDK still
|
|
2726
|
+
* declares the field, and the AWS API reference says "As of June 17,
|
|
2727
|
+
* 2019, this parameter is no longer supported for log streams, and is
|
|
2728
|
+
* always reported as zero" (quoted verbatim in
|
|
2729
|
+
* `@aws-sdk/client-cloudwatch-logs`'s own `LogStream.storedBytes`
|
|
2730
|
+
* JSDoc, which also marks it `@deprecated`). A probe reading it would
|
|
2731
|
+
* report EVERY log group empty.
|
|
2732
|
+
* * The same SDK note says the log GROUP's own `storedBytes` is "not
|
|
2733
|
+
* affected", so that field is not ruled out the way the stream's is —
|
|
2734
|
+
* but cdkd does not read it either, and the ground is not a claim
|
|
2735
|
+
* about how fresh it is (this repo has measured nothing about that).
|
|
2736
|
+
* Stream presence needs no size semantics at all: zero streams is a
|
|
2737
|
+
* STRUCTURAL proof, and it over-blocks rather than under-blocks (a
|
|
2738
|
+
* group holding only empty streams counts as non-empty), which is the
|
|
2739
|
+
* side this guard must err on.
|
|
2740
|
+
*
|
|
2741
|
+
* **Probe failures fail CLOSED for the log group, OPEN for the bucket**, and
|
|
2742
|
+
* the divergence is deliberate rather than an oversight. The bucket's
|
|
2743
|
+
* soft-fail is pre-existing shipped behaviour (issue [#648]) documented on
|
|
2744
|
+
* `docs/cli-deploy-safety.md`; the log group's arm is new with issue [#2558],
|
|
2745
|
+
* whose whole subject is that an unprovable emptiness must not read as empty.
|
|
2746
|
+
* So a failed `DescribeLogStreams` (permission denied, throttling) warns AND
|
|
2747
|
+
* promotes to `'has-log-events'`: the user gets a refusal naming the remedies,
|
|
2748
|
+
* not a silent recreate. The ONE carve-out is a typed
|
|
2749
|
+
* `ResourceNotFoundException`, which is an ANSWER rather than a failure to get
|
|
2750
|
+
* one — a log group AWS says does not exist provably holds no events.
|
|
2751
|
+
*
|
|
2752
|
+
* Returns a NEW array of targets; the input is not mutated. Targets of other
|
|
2753
|
+
* types, and targets whose sync reason is already non-null, are passed through
|
|
2754
|
+
* unchanged — including a `'has-retention'` log group, whose verdict the bag
|
|
2755
|
+
* already settled and no probe may weaken.
|
|
2756
|
+
*/
|
|
2757
|
+
async function probeStatefulRecreateTargetsAsync(targets, clients, logger = getLogger().child("recreate-targets")) {
|
|
2712
2758
|
const promoted = [];
|
|
2759
|
+
const probeRetryOptions = {
|
|
2760
|
+
maxRetries: 3,
|
|
2761
|
+
initialDelayMs: 500,
|
|
2762
|
+
logger,
|
|
2763
|
+
isRetryable: (_message, error) => isThrottlingError(error),
|
|
2764
|
+
...clients.sleep && { sleep: clients.sleep }
|
|
2765
|
+
};
|
|
2713
2766
|
for (const target of targets) {
|
|
2714
|
-
if (target.
|
|
2767
|
+
if (target.statefulReason !== null) {
|
|
2715
2768
|
promoted.push({ ...target });
|
|
2716
2769
|
continue;
|
|
2717
2770
|
}
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2771
|
+
if (target.resourceType === "AWS::S3::Bucket") {
|
|
2772
|
+
try {
|
|
2773
|
+
const result = await withRetry(() => clients.s3.send(new ListObjectVersionsCommand({
|
|
2774
|
+
Bucket: target.physicalId,
|
|
2775
|
+
MaxKeys: 1
|
|
2776
|
+
})), target.logicalId, probeRetryOptions);
|
|
2777
|
+
const hasVersions = (result.Versions?.length ?? 0) > 0;
|
|
2778
|
+
const hasDeleteMarkers = (result.DeleteMarkers?.length ?? 0) > 0;
|
|
2779
|
+
const truncated = result.IsTruncated === true || !!result.NextKeyMarker || !!result.NextVersionIdMarker;
|
|
2780
|
+
if (hasVersions || hasDeleteMarkers) promoted.push({
|
|
2781
|
+
...target,
|
|
2782
|
+
statefulReason: "has-objects"
|
|
2783
|
+
});
|
|
2784
|
+
else if (truncated) {
|
|
2785
|
+
logger.warn(`--recreate-via-cc-api / --recreate-via-sdk-provider: S3 answered the emptiness probe for ${target.logicalId} (bucket ${target.physicalId}) without settling it (an empty page carrying a continuation marker); treating the bucket as NOT provably empty. Re-run to retry the probe, or — only if the bucket really is disposable — re-run with --force-stateful-recreation (that flag has NO per-resource granularity and clears the guard for every target in the run).`);
|
|
2786
|
+
promoted.push({
|
|
2787
|
+
...target,
|
|
2788
|
+
statefulReason: "has-objects"
|
|
2789
|
+
});
|
|
2790
|
+
} else promoted.push({ ...target });
|
|
2791
|
+
} catch (e) {
|
|
2792
|
+
logger.warn(`--recreate-via-cc-api / --recreate-via-sdk-provider: live S3 probe failed for ${target.logicalId} (bucket ${target.physicalId}); leaving stateful guard at the sync result. If the bucket might be non-empty, re-run with --force-stateful-recreation. Underlying error: ${e instanceof Error ? e.message : String(e)}`);
|
|
2793
|
+
promoted.push({ ...target });
|
|
2794
|
+
}
|
|
2795
|
+
continue;
|
|
2733
2796
|
}
|
|
2797
|
+
if (target.resourceType === "AWS::Logs::LogGroup") {
|
|
2798
|
+
try {
|
|
2799
|
+
const result = await withRetry(() => clients.cloudWatchLogs.send(new DescribeLogStreamsCommand({
|
|
2800
|
+
logGroupName: target.physicalId,
|
|
2801
|
+
limit: 1
|
|
2802
|
+
})), target.logicalId, probeRetryOptions);
|
|
2803
|
+
const streams = result.logStreams;
|
|
2804
|
+
if (streams !== void 0 && streams.length === 0 && !result.nextToken) promoted.push({ ...target });
|
|
2805
|
+
else if (streams !== void 0 && streams.length > 0) promoted.push({
|
|
2806
|
+
...target,
|
|
2807
|
+
statefulReason: "has-log-events"
|
|
2808
|
+
});
|
|
2809
|
+
else {
|
|
2810
|
+
logger.warn(`--recreate-via-cc-api / --recreate-via-sdk-provider: CloudWatch Logs answered the emptiness probe for ${target.logicalId} (log group ${target.physicalId}) without settling it (${streams === void 0 ? "no logStreams field in the response" : "an empty page carrying a continuation token"}); treating the log group as NOT provably empty. An unset or zero RetentionInDays is CloudWatch Logs' "never expire", so an unprovable emptiness must not read as empty. Re-run to retry the probe, or — only if the log group really is disposable — re-run with --force-stateful-recreation (that flag has NO per-resource granularity and clears the guard for every target in the run).`);
|
|
2811
|
+
promoted.push({
|
|
2812
|
+
...target,
|
|
2813
|
+
statefulReason: "has-log-events"
|
|
2814
|
+
});
|
|
2815
|
+
}
|
|
2816
|
+
} catch (e) {
|
|
2817
|
+
if (e instanceof ResourceNotFoundException$5) {
|
|
2818
|
+
let regionVerified = true;
|
|
2819
|
+
const recordedRegion = foldRegion(clients.expectedRegion);
|
|
2820
|
+
if (recordedRegion) {
|
|
2821
|
+
let clientRegion;
|
|
2822
|
+
try {
|
|
2823
|
+
clientRegion = foldRegion(await clients.cloudWatchLogs.config.region());
|
|
2824
|
+
assertRegionMatch(clientRegion, recordedRegion, target.resourceType, target.logicalId, target.physicalId);
|
|
2825
|
+
} catch {
|
|
2826
|
+
regionVerified = false;
|
|
2827
|
+
const cause = clientRegion ? `the client region (${clientRegion}) does not match the region cdkd state records for it (${recordedRegion}) — a not-found from the wrong region says nothing about the log group. Fix the region mismatch, or` : `the CloudWatch Logs client's own region could not be resolved, so the not-found cannot be attributed to the region cdkd state records (${recordedRegion}). Fix the client's region configuration, or`;
|
|
2828
|
+
logger.warn(`--recreate-via-cc-api / --recreate-via-sdk-provider: CloudWatch Logs reported ${target.logicalId} (log group ${target.physicalId}) missing, but ${cause} re-run with --force-stateful-recreation if the log group really is disposable (that flag clears the data guard for every target in the run). Until then it is treated as NOT provably empty.`);
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
if (regionVerified) {
|
|
2832
|
+
logger.debug(`--recreate-via-cc-api / --recreate-via-sdk-provider: log group ${target.physicalId} (${target.logicalId}) does not exist, so it holds no events — not stateful.`);
|
|
2833
|
+
promoted.push({ ...target });
|
|
2834
|
+
continue;
|
|
2835
|
+
}
|
|
2836
|
+
promoted.push({
|
|
2837
|
+
...target,
|
|
2838
|
+
statefulReason: "has-log-events"
|
|
2839
|
+
});
|
|
2840
|
+
continue;
|
|
2841
|
+
}
|
|
2842
|
+
logger.warn(`--recreate-via-cc-api / --recreate-via-sdk-provider: live CloudWatch Logs probe failed for ${target.logicalId} (log group ${target.physicalId}); treating the log group as NOT provably empty. An unset or zero RetentionInDays is CloudWatch Logs' "never expire", so an unprovable emptiness must not read as empty. Fixes, cheapest first: grant logs:DescribeLogStreams and re-run, or re-run as-is if this was transient (CloudWatch Logs throttles this API aggressively). Only if the log group really is disposable, re-run with --force-stateful-recreation — that flag has NO per-resource granularity and clears the guard for every target in the run. Underlying error: ${e instanceof Error ? e.message : String(e)}`);
|
|
2843
|
+
promoted.push({
|
|
2844
|
+
...target,
|
|
2845
|
+
statefulReason: "has-log-events"
|
|
2846
|
+
});
|
|
2847
|
+
}
|
|
2848
|
+
continue;
|
|
2849
|
+
}
|
|
2850
|
+
promoted.push({ ...target });
|
|
2734
2851
|
}
|
|
2735
2852
|
return promoted;
|
|
2736
2853
|
}
|
|
2737
2854
|
/**
|
|
2738
2855
|
* Async re-validation of the stateful-guard slice of a
|
|
2739
|
-
* {@link RecreateTargetsValidation}, after promoting S3 bucket
|
|
2740
|
-
* via {@link probeStatefulRecreateTargetsAsync}.
|
|
2856
|
+
* {@link RecreateTargetsValidation}, after promoting the deferred S3 bucket
|
|
2857
|
+
* and log group reasons via {@link probeStatefulRecreateTargetsAsync}.
|
|
2741
2858
|
*
|
|
2742
2859
|
* Skips the probe entirely when `forceStatefulRecreation: true` — the
|
|
2743
2860
|
* sync validation already omits the blocked list in that case, and
|
|
@@ -2750,7 +2867,7 @@ async function probeStatefulRecreateTargetsAsync(targets, s3Client, logger = get
|
|
|
2750
2867
|
*/
|
|
2751
2868
|
async function probeAndRevalidateStateful(input) {
|
|
2752
2869
|
if (input.forceStatefulRecreation) return input.validation;
|
|
2753
|
-
const promoted = await probeStatefulRecreateTargetsAsync(input.validation.targets, input.
|
|
2870
|
+
const promoted = await probeStatefulRecreateTargetsAsync(input.validation.targets, input.clients);
|
|
2754
2871
|
const blockedStatefulTargets = promoted.filter((t) => t.statefulReason !== null);
|
|
2755
2872
|
return {
|
|
2756
2873
|
...input.validation,
|
|
@@ -2853,11 +2970,27 @@ function renderDownstreamConsumers(producerStack, consumers) {
|
|
|
2853
2970
|
* like a successful skipped-deploy.
|
|
2854
2971
|
*
|
|
2855
2972
|
* The per-target plan surfaces a **DATA LOSS** prefix for stateful
|
|
2856
|
-
* targets
|
|
2857
|
-
*
|
|
2858
|
-
*
|
|
2859
|
-
*
|
|
2860
|
-
*
|
|
2973
|
+
* targets; a target reaches this prompt still carrying a stateful reason only
|
|
2974
|
+
* because the user opted in with `--force-stateful-recreation`, so the
|
|
2975
|
+
* **DATA LOSS** wording is the third "stop and think" moment.
|
|
2976
|
+
*
|
|
2977
|
+
* **The prefix is NOT read off `statefulReason` alone**, and the reason is the
|
|
2978
|
+
* flag that makes the prompt reachable in the first place (issue [#2558]'s
|
|
2979
|
+
* review round). `probeAndRevalidateStateful` returns EARLY under
|
|
2980
|
+
* `--force-stateful-recreation` — it never issues the live
|
|
2981
|
+
* `s3:ListObjectVersions` / `logs:DescribeLogStreams` probes — so under that
|
|
2982
|
+
* flag every target still carries the SYNC verdict, and for the two
|
|
2983
|
+
* CONDITIONAL types the sync verdict of a deferral is `null`. `null` there
|
|
2984
|
+
* does not mean "holds nothing": an `AWS::S3::Bucket` always defers, and an
|
|
2985
|
+
* `AWS::Logs::LogGroup` with no recorded retention is CloudWatch Logs'
|
|
2986
|
+
* NEVER EXPIRE. Rendering that as "not stateful" is precisely the reading
|
|
2987
|
+
* issue [#2558] exists to retire, and it was already wrong for a non-empty
|
|
2988
|
+
* bucket before that. So when the probe did not run, a `null` reason is
|
|
2989
|
+
* re-derived through {@link isStatefulRecreateTargetForReplace} — the
|
|
2990
|
+
* conservative mid-deploy predicate, a pure function of the type with no AWS
|
|
2991
|
+
* call, chosen over re-running the probe because re-running it under the force
|
|
2992
|
+
* flag would repopulate `blockedStatefulTargets` and turn the opt-in into a
|
|
2993
|
+
* refusal.
|
|
2861
2994
|
*/
|
|
2862
2995
|
async function promptRecreateConfirm(input) {
|
|
2863
2996
|
if (input.targets.length === 0) return true;
|
|
@@ -2869,10 +3002,13 @@ async function promptRecreateConfirm(input) {
|
|
|
2869
3002
|
else if (toCcCount > 0) logger.warn(`--recreate-via-cc-api will destroy + recreate ${toCcCount} resource(s) via Cloud Control API on stack ${input.stackName}:`);
|
|
2870
3003
|
else logger.warn(`--recreate-via-sdk-provider will destroy + recreate ${toSdkCount} resource(s) via SDK Provider on stack ${input.stackName}:`);
|
|
2871
3004
|
for (const t of input.targets) {
|
|
2872
|
-
const
|
|
3005
|
+
const rederivedReason = t.statefulReason === null && input.forceStatefulRecreation ? isStatefulRecreateTargetForReplace(t.resourceType, void 0) : null;
|
|
3006
|
+
const rederived = rederivedReason !== null;
|
|
3007
|
+
const reason = t.statefulReason ?? rederivedReason;
|
|
3008
|
+
const stateful = reason !== null;
|
|
2873
3009
|
const dataLossPrefix = stateful ? "**DATA LOSS** " : "";
|
|
2874
3010
|
const directionTag = t.direction === "to-cc-api" ? " [SDK → CC]" : " [CC → SDK]";
|
|
2875
|
-
const stateNote = stateful ? ` — stateful (${
|
|
3011
|
+
const stateNote = stateful ? ` — stateful (${rederived ? "emptiness not established — --force-stateful-recreation skips the probe" : renderStatefulReason(reason)}); --force-stateful-recreation acknowledged` : "";
|
|
2876
3012
|
logger.warn(` - ${dataLossPrefix}${t.logicalId} (${t.resourceType})${directionTag}${stateNote}`);
|
|
2877
3013
|
if (stateful) logger.warn(` DATA: all data in ${t.logicalId} will be lost (no automatic data migration)`);
|
|
2878
3014
|
}
|
|
@@ -2950,12 +3086,13 @@ const DEFAULT_CONFIRM_SUFFIX = " [y/N] ";
|
|
|
2950
3086
|
* `printf 'y' |` (a real answer with no trailing newline) and `< /dev/null`
|
|
2951
3087
|
* stay pending indefinitely.
|
|
2952
3088
|
*
|
|
2953
|
-
* REFUSE rather than auto-confirm, at every one of the
|
|
2954
|
-
*
|
|
2955
|
-
* record removal, an observed-property refresh, an orphan, an
|
|
2956
|
-
* export-then-delete-state, a drift accept/revert, a
|
|
2957
|
-
* retirement, a state-bucket migration
|
|
2958
|
-
* behalf of an absent operator is never
|
|
3089
|
+
* REFUSE rather than auto-confirm, at every one of the TEN sites that route
|
|
3090
|
+
* through this helper. Every one of them guards a MUTATION — a rollback
|
|
3091
|
+
* replay, a state record removal, an observed-property refresh, an orphan, an
|
|
3092
|
+
* import, an export-then-delete-state, a drift accept/revert, a
|
|
3093
|
+
* CloudFormation stack retirement, a state-bucket migration, an event-history
|
|
3094
|
+
* prune — so silently answering "yes" on behalf of an absent operator is never
|
|
3095
|
+
* the safe reading. `deploy.ts`'s
|
|
2959
3096
|
* auto-confirm (`options.yes || !process.stdin.isTTY` -> proceed) stays the
|
|
2960
3097
|
* deliberate exception, because a deploy that assumes "yes" is recoverable.
|
|
2961
3098
|
*
|
|
@@ -2974,10 +3111,12 @@ const DEFAULT_CONFIRM_SUFFIX = " [y/N] ";
|
|
|
2974
3111
|
* `--yes` refuse instead of proceed, which is the inverse bug.
|
|
2975
3112
|
*
|
|
2976
3113
|
* Throws {@link CdkdError} with the `NON_INTERACTIVE_CONFIRM` code, matching
|
|
2977
|
-
* `gc.ts` and `bootstrap-destroy.ts` — the only two of the
|
|
2978
|
-
* guarded prompts that carry a code (the other
|
|
2979
|
-
*
|
|
2980
|
-
*
|
|
3114
|
+
* `gc.ts` and `bootstrap-destroy.ts` — the only two of the four originally
|
|
3115
|
+
* guarded prompts that carry a code (the other two throw a bare `Error`).
|
|
3116
|
+
* `handleError` maps it to exit 1, so CI can branch on it.
|
|
3117
|
+
*
|
|
3118
|
+
* There were FIVE until `cdkd migrate` was removed (issue #2572); its prompt
|
|
3119
|
+
* threw a `LocalMigrateError`, and that class went with it.
|
|
2981
3120
|
*
|
|
2982
3121
|
* TWO GUARDED PROMPTS DELIBERATELY DO NOT ROUTE THROUGH HERE:
|
|
2983
3122
|
* `destroy-runner.ts`'s per-stack prompt (the issue #2259 fix) and
|
|
@@ -2988,10 +3127,10 @@ const DEFAULT_CONFIRM_SUFFIX = " [y/N] ";
|
|
|
2988
3127
|
* passes an abort `signal` for the issue #2117 Ctrl-C handling. Folding them
|
|
2989
3128
|
* in would also drag `destroy-runner.ts` (in the `integ-destroy` AND
|
|
2990
3129
|
* `integ-broad` gate scopes) into a pure refactor's blast radius, buying a
|
|
2991
|
-
* real-AWS run for no behaviour change. The other
|
|
3130
|
+
* real-AWS run for no behaviour change. The other four pre-existing guarded
|
|
2992
3131
|
* prompts (`gc.ts`, `bootstrap-destroy.ts`, `recreate-confirm-prompt.ts`,
|
|
2993
|
-
* `prefix-migration-check.ts
|
|
2994
|
-
*
|
|
3132
|
+
* `prefix-migration-check.ts`) stay put for the same reason: each guards its
|
|
3133
|
+
* own flow with its own error type.
|
|
2995
3134
|
*
|
|
2996
3135
|
* @param prompt the question, WITHOUT its trailing `[y/N]` marker
|
|
2997
3136
|
* @returns `true` only for `y` / `yes` (any case, trimmed)
|
|
@@ -21731,9 +21870,8 @@ function warmThroughputDiagnostic(raw, coercion, indexName) {
|
|
|
21731
21870
|
* The CFn schema forbids `ProvisionedThroughput` / `OnDemandThroughput` /
|
|
21732
21871
|
* `*ThroughputOverride` on `AWS::DynamoDB::GlobalTable`, so this only fires
|
|
21733
21872
|
* for pre-#1387 cdkd state and hand-authored templates — but those ARE a
|
|
21734
|
-
* first-class cdkd surface (`cdkd import --migrate-from-cloudformation
|
|
21735
|
-
*
|
|
21736
|
-
* block VERBATIM. Three defects followed, and fixing any subset leaves a
|
|
21873
|
+
* first-class cdkd surface (`cdkd import --migrate-from-cloudformation`), and
|
|
21874
|
+
* the pre-fix code forwarded such a block VERBATIM. Three defects followed, and fixing any subset leaves a
|
|
21737
21875
|
* live one, which is why this helper does all three at once:
|
|
21738
21876
|
*
|
|
21739
21877
|
* 1. **Coercion.** The verbatim forward was the ONE path that skipped
|
|
@@ -22937,7 +23075,7 @@ var LogsLogGroupProvider = class {
|
|
|
22937
23075
|
const normalizeClass = (v) => typeof v === "string" && v.length > 0 ? v : "STANDARD";
|
|
22938
23076
|
const prevClass = normalizeClass(previousProperties["LogGroupClass"]);
|
|
22939
23077
|
const nextClass = normalizeClass(properties["LogGroupClass"]);
|
|
22940
|
-
if (prevClass !== nextClass) throw new ResourceUpdateNotSupportedError("AWS::Logs::LogGroup", logicalId, `the LogGroupClass ('${prevClass}' -> '${nextClass}') cannot be changed after creation. Re-deploy with
|
|
23078
|
+
if (prevClass !== nextClass) throw new ResourceUpdateNotSupportedError("AWS::Logs::LogGroup", logicalId, `the LogGroupClass ('${prevClass}' -> '${nextClass}') cannot be changed after creation. Re-deploy with --replace --force-stateful-recreation to delete + recreate the log group under the new class (its stored log events are lost), or revert the LogGroupClass change. Note --force-stateful-recreation has NO per-resource granularity: it clears the data guard for every replacement target in the run`);
|
|
22941
23079
|
const newKmsKeyId = properties["KmsKeyId"];
|
|
22942
23080
|
if (newKmsKeyId !== previousProperties["KmsKeyId"]) {
|
|
22943
23081
|
if (newKmsKeyId) await this.logsClient.send(new AssociateKmsKeyCommand({
|
|
@@ -47289,9 +47427,32 @@ function isAccountIdPseudoParameter(value) {
|
|
|
47289
47427
|
* [#1675](https://github.com/go-to-k/cdkd/issues/1675)).
|
|
47290
47428
|
*
|
|
47291
47429
|
* This is {@link importableString} plus a NUMBER arm, and the number arm is
|
|
47292
|
-
* load-bearing rather than defensive
|
|
47293
|
-
*
|
|
47294
|
-
*
|
|
47430
|
+
* load-bearing rather than defensive, and the reason is NOT the one this
|
|
47431
|
+
* comment gave for three revisions. It claimed
|
|
47432
|
+
* `cdkd import --migrate-from-cloudformation` (earlier: `cdkd migrate`) read
|
|
47433
|
+
* the source stack's ORIGINAL CloudFormation template and handed those raw
|
|
47434
|
+
* properties to providers. It never did. Both commands take properties from
|
|
47435
|
+
* the SYNTH assembly; `cdkd migrate` reached `runImport` with
|
|
47436
|
+
* `app: <assemblyDir>` plus a logical-id mapping, and the ORIGINAL template it
|
|
47437
|
+
* fetched was consumed only by transform detection and by the Pass-2
|
|
47438
|
+
* property-equality MATCH, never forwarded. The `GetTemplate` that survives in
|
|
47439
|
+
* `retire-cfn-stack.ts` feeds Retain injection and the re-upload, and reaches
|
|
47440
|
+
* no properties bag either. Do not restore any of that.
|
|
47441
|
+
*
|
|
47442
|
+
* The two producers that ARE real, both current:
|
|
47443
|
+
*
|
|
47444
|
+
* 1. A numeric literal in the synth template itself — `new CfnResource({
|
|
47445
|
+
* properties: { CatalogId: 123456789012 } })`, or an
|
|
47446
|
+
* `addPropertyOverride`. CDK emits the number verbatim; nothing between
|
|
47447
|
+
* synth and this provider stringifies it.
|
|
47448
|
+
* 2. The macro-expanded deploy template. `src/synthesis/macro-expander.ts`
|
|
47449
|
+
* fetches `GetTemplate` at `TemplateStage: 'Processed'` and parses the
|
|
47450
|
+
* JSON body as the template deploy / diff / import then use, so a macro
|
|
47451
|
+
* that emits an unquoted number lands here as a number.
|
|
47452
|
+
*
|
|
47453
|
+
* The DELETE path reaches it too, from the other side: `destroy-runner.ts`
|
|
47454
|
+
* hands the recorded `resource.properties` straight to `provider.delete`, so a
|
|
47455
|
+
* state row holding either shape above replays it at teardown. Rejecting it
|
|
47295
47456
|
* as "not a string" would drop the field and silently retarget the call at the
|
|
47296
47457
|
* account's DEFAULT catalog — which on a DELETE can destroy a same-named
|
|
47297
47458
|
* resource that happens to live there. `Number.isFinite` keeps `NaN` /
|
|
@@ -49041,9 +49202,12 @@ const TARGET_DATABASE_MEMBERS = [
|
|
|
49041
49202
|
const FEDERATED_DATABASE_MEMBERS = ["ConnectionName", "Identifier"];
|
|
49042
49203
|
/**
|
|
49043
49204
|
* Is this leaf value one cdkd can put on the wire? A STRING or a NUMBER — CFn
|
|
49044
|
-
* is stringly typed and an
|
|
49045
|
-
* reason
|
|
49046
|
-
*
|
|
49205
|
+
* is stringly typed, and an account id can reach a provider as a JSON number
|
|
49206
|
+
* (the reason {@link catalogIdForApi} exists; that function's note carries the
|
|
49207
|
+
* two producers, neither of which is a YAML read — an earlier version of this
|
|
49208
|
+
* sentence said "an unquoted YAML account id" and was wrong the same way) —
|
|
49209
|
+
* and nothing else, so an unresolved intrinsic cannot ride an unchecked cast
|
|
49210
|
+
* into the request.
|
|
49047
49211
|
*/
|
|
49048
49212
|
const isSendableLeaf = (value) => typeof value === "string" || typeof value === "number" && Number.isFinite(value);
|
|
49049
49213
|
/**
|
|
@@ -62468,7 +62632,11 @@ async function deployCommand(stacks, options) {
|
|
|
62468
62632
|
forceStatefulRecreation: options.forceStatefulRecreation ?? false,
|
|
62469
62633
|
hasSdkProvider: (rt) => stackProviderRegistry.getProviderType(rt) === "sdk"
|
|
62470
62634
|
}),
|
|
62471
|
-
|
|
62635
|
+
clients: {
|
|
62636
|
+
s3: stackAwsClients.s3,
|
|
62637
|
+
cloudWatchLogs: stackAwsClients.cloudWatchLogs,
|
|
62638
|
+
expectedRegion: stateForRecreateCheck?.state.region
|
|
62639
|
+
},
|
|
62472
62640
|
forceStatefulRecreation: options.forceStatefulRecreation ?? false
|
|
62473
62641
|
});
|
|
62474
62642
|
const errorBlock = renderRecreateTargetsErrors(validation);
|
|
@@ -62486,6 +62654,7 @@ async function deployCommand(stacks, options) {
|
|
|
62486
62654
|
stackName: stackInfo.stackName,
|
|
62487
62655
|
targets: validation.targets,
|
|
62488
62656
|
yes: options.yes ?? false,
|
|
62657
|
+
forceStatefulRecreation: options.forceStatefulRecreation ?? false,
|
|
62489
62658
|
downstreamConsumers
|
|
62490
62659
|
})) return;
|
|
62491
62660
|
}
|
|
@@ -63040,10 +63209,11 @@ function computeOutputsDiff(current, desired, exportNames, secretSourceKeys = /*
|
|
|
63040
63209
|
* unhealthy state — callers gate AWS-side mutations behind this set so
|
|
63041
63210
|
* the user can settle the source before paying for further work.
|
|
63042
63211
|
*
|
|
63043
|
-
*
|
|
63044
|
-
*
|
|
63045
|
-
* `cdkd
|
|
63046
|
-
*
|
|
63212
|
+
* Consumed by the `cdkd import --migrate-from-cloudformation` retirement
|
|
63213
|
+
* flow (`src/cli/commands/retire-cfn-stack.ts`). It had a second consumer,
|
|
63214
|
+
* `cdkd migrate`'s pre-flight check, until that command was removed (issue
|
|
63215
|
+
* #2572); the module stays separate so the next CloudFormation-reading
|
|
63216
|
+
* surface shares the set instead of re-spelling it.
|
|
63047
63217
|
*/
|
|
63048
63218
|
const STABLE_TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
63049
63219
|
"CREATE_COMPLETE",
|
|
@@ -63857,20 +64027,20 @@ async function walkCfnStackTree(stackName, physicalId, cfnClient) {
|
|
|
63857
64027
|
}
|
|
63858
64028
|
/**
|
|
63859
64029
|
* The CloudFormation-stack retirement confirmation prompt, reached from
|
|
63860
|
-
* `cdkd import --migrate-from-cloudformation`
|
|
63861
|
-
*
|
|
63862
|
-
* site is inside the `if (!yes)` block above, which is what keeps
|
|
64030
|
+
* `cdkd import --migrate-from-cloudformation` (skip flag `-y` / `--yes`). Its
|
|
64031
|
+
* only call site is inside the `if (!yes)` block above, which is what keeps
|
|
63863
64032
|
* `confirmOrRefuse`'s non-interactive refusal (issue #2275) from firing on a
|
|
63864
64033
|
* `--yes` run.
|
|
63865
64034
|
*
|
|
63866
|
-
*
|
|
63867
|
-
*
|
|
63868
|
-
*
|
|
63869
|
-
*
|
|
63870
|
-
*
|
|
64035
|
+
* The refusal NAMES that command, as every other site does, because
|
|
64036
|
+
* `confirmOrRefuse`'s contract is that the refusal is the only thing a CI user
|
|
64037
|
+
* sees: it has to say which invocation the missing flag belongs on. It named
|
|
64038
|
+
* TWO commands until `cdkd migrate --retire-cfn-stack` was removed with the
|
|
64039
|
+
* command itself (issue #2572); `cdkd import --migrate-from-cloudformation` is
|
|
64040
|
+
* now the sole way to reach this prompt.
|
|
63871
64041
|
*
|
|
63872
64042
|
* It also states that cdkd state has ALREADY been written. This prompt is the
|
|
63873
|
-
* only one of the
|
|
64043
|
+
* only one of the ten that fires AFTER its command's state write, so a
|
|
63874
64044
|
* refusal here leaves a genuine split brain — cdkd claims the resources while
|
|
63875
64045
|
* the CloudFormation stack is still live — and the operator has to know that
|
|
63876
64046
|
* before deciding whether to re-run or finish by hand.
|
|
@@ -63878,7 +64048,7 @@ async function walkCfnStackTree(stackName, physicalId, cfnClient) {
|
|
|
63878
64048
|
* Exported for unit testing — internal to the command flow otherwise.
|
|
63879
64049
|
*/
|
|
63880
64050
|
async function confirmPrompt$6(prompt) {
|
|
63881
|
-
return confirmOrRefuse(prompt, { refusal: "The CloudFormation stack retirement confirmation prompt cannot run in a non-interactive environment. Pass -y / --yes on
|
|
64051
|
+
return confirmOrRefuse(prompt, { refusal: "The CloudFormation stack retirement confirmation prompt cannot run in a non-interactive environment. Pass -y / --yes on cdkd import --migrate-from-cloudformation. Alternatively, run it from a real terminal. NOTE cdkd state has already been written: the resources are recorded in cdkd state while the CloudFormation stack is still live, so re-run with -y / --yes (or retire the stack by hand) to finish the migration." });
|
|
63882
64052
|
}
|
|
63883
64053
|
/**
|
|
63884
64054
|
* Flat `logicalId -> physicalId` map for ONE CloudFormation stack, or `null`
|
|
@@ -78732,6 +78902,7 @@ async function forceUnlockCommand(stackArgs, options) {
|
|
|
78732
78902
|
setAwsClients(awsClients);
|
|
78733
78903
|
const region = namedCliRegion(options.region) ?? "us-east-1";
|
|
78734
78904
|
const stateBucket = await resolveStateBucketWithDefault(options.stateBucket, region);
|
|
78905
|
+
const failures = [];
|
|
78735
78906
|
try {
|
|
78736
78907
|
const stateConfig = {
|
|
78737
78908
|
bucket: stateBucket,
|
|
@@ -78756,13 +78927,17 @@ async function forceUnlockCommand(stackArgs, options) {
|
|
|
78756
78927
|
} catch (error) {
|
|
78757
78928
|
const message = error instanceof Error ? error.message : String(error);
|
|
78758
78929
|
if (message.includes("No lock found") || message.includes("NoSuchKey")) logger.info(`No lock found for stack: ${where}`);
|
|
78759
|
-
else
|
|
78930
|
+
else {
|
|
78931
|
+
logger.error(`Failed to unlock stack ${where}: ${message}`);
|
|
78932
|
+
failures.push(`${where}: ${message}`);
|
|
78933
|
+
}
|
|
78760
78934
|
}
|
|
78761
78935
|
}
|
|
78762
78936
|
}
|
|
78763
78937
|
} finally {
|
|
78764
78938
|
awsClients.destroy();
|
|
78765
78939
|
}
|
|
78940
|
+
if (failures.length > 0) throw new CdkdError(`Failed to release ${failures.length} lock(s):\n` + failures.map((f) => ` - ${f}`).join("\n"), "FORCE_UNLOCK_FAILED");
|
|
78766
78941
|
}
|
|
78767
78942
|
/**
|
|
78768
78943
|
* Create force-unlock command
|
|
@@ -78780,9 +78955,6 @@ function createForceUnlockCommand() {
|
|
|
78780
78955
|
|
|
78781
78956
|
//#endregion
|
|
78782
78957
|
//#region src/cli/commands/import.ts
|
|
78783
|
-
async function runImport(stackArg, options) {
|
|
78784
|
-
return importCommand(stackArg, options);
|
|
78785
|
-
}
|
|
78786
78958
|
async function importCommand(stackArg, options) {
|
|
78787
78959
|
const logger = getLogger();
|
|
78788
78960
|
if (options.verbose) {
|
|
@@ -79667,7 +79839,7 @@ function validateNestedStackShape(template, tree, parentStackName, nestedTemplat
|
|
|
79667
79839
|
* **parent-first acquire, leaves-first release** (each level's
|
|
79668
79840
|
* `finally` releases its child lock before sibling iteration
|
|
79669
79841
|
* continues, and the root lock is the outermost — held by
|
|
79670
|
-
* `
|
|
79842
|
+
* `importCommand`'s `try` / `finally`). This is the conventional
|
|
79671
79843
|
* hierarchical lock pattern: parent-first acquire prevents a
|
|
79672
79844
|
* second cdkd import from racing past the root lock; leaves-first
|
|
79673
79845
|
* release on success/failure means a mid-walk error never strands
|
|
@@ -81890,7 +82062,7 @@ async function applyCrossStackResolverToTask(task, context) {
|
|
|
81890
82062
|
|
|
81891
82063
|
//#endregion
|
|
81892
82064
|
//#region src/local/docker-runner.ts
|
|
81893
|
-
const execFileAsync$
|
|
82065
|
+
const execFileAsync$3 = promisify(execFile);
|
|
81894
82066
|
/**
|
|
81895
82067
|
* Wraps `docker pull` / `docker run` / `docker rm` for `cdkd local invoke`.
|
|
81896
82068
|
*
|
|
@@ -81994,7 +82166,7 @@ async function runDetached(opts) {
|
|
|
81994
82166
|
logger.debug(`${getDockerCmd()} ${redactAwsCredentialsInArgs(args).join(" ")}`);
|
|
81995
82167
|
if (collisions.length > 0) logger.warn(`Env var(s) ${collisions.map((k) => JSON.stringify(k)).join(", ")} share a name with a docker-client environment variable or have a malformed name (empty, or containing '=' / NUL), and were NOT passed to the container at all (a colliding name would hijack the docker client; a malformed name cannot form a valid environment variable). Rename the env var if the container needs it.`);
|
|
81996
82168
|
try {
|
|
81997
|
-
const { stdout } = await execFileAsync$
|
|
82169
|
+
const { stdout } = await execFileAsync$3(getDockerCmd(), args, {
|
|
81998
82170
|
maxBuffer: 10 * 1024 * 1024,
|
|
81999
82171
|
env: dockerSpawnEnvWithSensitive(passthroughEnv)
|
|
82000
82172
|
});
|
|
@@ -82034,7 +82206,7 @@ async function removeContainer(containerId) {
|
|
|
82034
82206
|
if (!containerId) return;
|
|
82035
82207
|
const logger = getLogger().child("docker");
|
|
82036
82208
|
try {
|
|
82037
|
-
await execFileAsync$
|
|
82209
|
+
await execFileAsync$3(getDockerCmd(), [
|
|
82038
82210
|
"rm",
|
|
82039
82211
|
"-f",
|
|
82040
82212
|
containerId
|
|
@@ -82053,7 +82225,7 @@ async function removeContainer(containerId) {
|
|
|
82053
82225
|
async function ensureDockerAvailable() {
|
|
82054
82226
|
const cmd = getDockerCmd();
|
|
82055
82227
|
try {
|
|
82056
|
-
await execFileAsync$
|
|
82228
|
+
await execFileAsync$3(cmd, [
|
|
82057
82229
|
"version",
|
|
82058
82230
|
"--format",
|
|
82059
82231
|
"{{.Server.Version}}"
|
|
@@ -85067,7 +85239,7 @@ function createLocalStartApiCommand() {
|
|
|
85067
85239
|
|
|
85068
85240
|
//#endregion
|
|
85069
85241
|
//#region src/local/ecs-network.ts
|
|
85070
|
-
const execFileAsync$
|
|
85242
|
+
const execFileAsync$2 = promisify(execFile);
|
|
85071
85243
|
/**
|
|
85072
85244
|
* Docker network + AWS-published metadata-endpoints sidecar lifecycle for
|
|
85073
85245
|
* `cdkd local run-task`. The sidecar (a small Go binary maintained by
|
|
@@ -85120,7 +85292,7 @@ async function createNetworkAndSidecar(args) {
|
|
|
85120
85292
|
await pullImage(METADATA_ENDPOINT_IMAGE, skipPull);
|
|
85121
85293
|
logger.info(`Creating docker network ${networkName} (subnet ${cidr})...`);
|
|
85122
85294
|
try {
|
|
85123
|
-
await execFileAsync$
|
|
85295
|
+
await execFileAsync$2(getDockerCmd(), [
|
|
85124
85296
|
"network",
|
|
85125
85297
|
"create",
|
|
85126
85298
|
"--driver",
|
|
@@ -85157,7 +85329,7 @@ async function createNetworkAndSidecar(args) {
|
|
|
85157
85329
|
sidecarArgs.push(METADATA_ENDPOINT_IMAGE);
|
|
85158
85330
|
logger.info(`Starting ECS local-container-endpoints sidecar at ${sidecarIp}...`);
|
|
85159
85331
|
try {
|
|
85160
|
-
const { stdout } = await execFileAsync$
|
|
85332
|
+
const { stdout } = await execFileAsync$2(getDockerCmd(), sidecarArgs, {
|
|
85161
85333
|
maxBuffer: 10 * 1024 * 1024,
|
|
85162
85334
|
env: dockerSpawnEnvWithSensitive(sidecarSensitiveEnv)
|
|
85163
85335
|
});
|
|
@@ -85228,7 +85400,7 @@ async function destroyNetworkOnly(networkName) {
|
|
|
85228
85400
|
if (!networkName) return;
|
|
85229
85401
|
const logger = getLogger().child("ecs-network");
|
|
85230
85402
|
try {
|
|
85231
|
-
await execFileAsync$
|
|
85403
|
+
await execFileAsync$2(getDockerCmd(), [
|
|
85232
85404
|
"network",
|
|
85233
85405
|
"rm",
|
|
85234
85406
|
networkName
|
|
@@ -85368,7 +85540,7 @@ async function resolveSsm(entry, shape, client) {
|
|
|
85368
85540
|
|
|
85369
85541
|
//#endregion
|
|
85370
85542
|
//#region src/local/ecs-task-runner.ts
|
|
85371
|
-
const execFileAsync$
|
|
85543
|
+
const execFileAsync$1 = promisify(execFile);
|
|
85372
85544
|
/**
|
|
85373
85545
|
* Top-level orchestrator for `cdkd local run-task`. Coordinates image
|
|
85374
85546
|
* preparation, secret resolution, docker-network bring-up, container
|
|
@@ -85428,7 +85600,7 @@ async function cleanupEcsRun(state, options) {
|
|
|
85428
85600
|
if (state.network && !state.network.ownedByCaller) await destroyTaskNetwork(state.network);
|
|
85429
85601
|
state.network = void 0;
|
|
85430
85602
|
for (const v of state.dockerVolumeNames) try {
|
|
85431
|
-
await execFileAsync$
|
|
85603
|
+
await execFileAsync$1(getDockerCmd(), [
|
|
85432
85604
|
"volume",
|
|
85433
85605
|
"rm",
|
|
85434
85606
|
v
|
|
@@ -85523,7 +85695,7 @@ async function runEcsTask(task, options, state) {
|
|
|
85523
85695
|
let id;
|
|
85524
85696
|
try {
|
|
85525
85697
|
const sensitiveEnv = dockerEnvs.get(container.name);
|
|
85526
|
-
const { stdout } = await execFileAsync$
|
|
85698
|
+
const { stdout } = await execFileAsync$1(getDockerCmd(), args, {
|
|
85527
85699
|
maxBuffer: 10 * 1024 * 1024,
|
|
85528
85700
|
env: dockerSpawnEnvWithSensitive(sensitiveEnv)
|
|
85529
85701
|
});
|
|
@@ -85635,7 +85807,7 @@ async function waitForContainerHealthy(containerId, displayName) {
|
|
|
85635
85807
|
let lastStatus = "";
|
|
85636
85808
|
while (Date.now() < deadline) {
|
|
85637
85809
|
try {
|
|
85638
|
-
const { stdout } = await execFileAsync$
|
|
85810
|
+
const { stdout } = await execFileAsync$1(getDockerCmd(), [
|
|
85639
85811
|
"inspect",
|
|
85640
85812
|
"--format",
|
|
85641
85813
|
"{{.State.Health.Status}}",
|
|
@@ -85658,7 +85830,7 @@ async function waitForContainerHealthy(containerId, displayName) {
|
|
|
85658
85830
|
}
|
|
85659
85831
|
async function waitForContainerExit(containerId) {
|
|
85660
85832
|
try {
|
|
85661
|
-
const { stdout } = await execFileAsync$
|
|
85833
|
+
const { stdout } = await execFileAsync$1(getDockerCmd(), ["wait", containerId], { maxBuffer: 1024 * 1024 });
|
|
85662
85834
|
const code = Number.parseInt(stdout.trim(), 10);
|
|
85663
85835
|
return Number.isFinite(code) ? code : 1;
|
|
85664
85836
|
} catch (err) {
|
|
@@ -85668,7 +85840,7 @@ async function waitForContainerExit(containerId) {
|
|
|
85668
85840
|
}
|
|
85669
85841
|
async function stopContainer(containerId, graceSeconds) {
|
|
85670
85842
|
try {
|
|
85671
|
-
await execFileAsync$
|
|
85843
|
+
await execFileAsync$1(getDockerCmd(), [
|
|
85672
85844
|
"stop",
|
|
85673
85845
|
"-t",
|
|
85674
85846
|
String(graceSeconds),
|
|
@@ -85793,7 +85965,7 @@ async function realizeDockerVolumes(volumes, state) {
|
|
|
85793
85965
|
const dockerVolumeName = `cdkd-local-${v.name}-${randHex(4)}`;
|
|
85794
85966
|
args.push(dockerVolumeName);
|
|
85795
85967
|
try {
|
|
85796
|
-
await execFileAsync$
|
|
85968
|
+
await execFileAsync$1(getDockerCmd(), args);
|
|
85797
85969
|
state.dockerVolumeNames.push(dockerVolumeName);
|
|
85798
85970
|
logger.debug(`Created docker volume ${dockerVolumeName} for task volume '${v.name}'`);
|
|
85799
85971
|
} catch (err) {
|
|
@@ -86374,7 +86546,7 @@ function createLocalStartServiceCommand() {
|
|
|
86374
86546
|
* after each reload (cdkd-specific — cdk-local treats single-shot as a
|
|
86375
86547
|
* no-op WARN).
|
|
86376
86548
|
*/
|
|
86377
|
-
const execFileAsync
|
|
86549
|
+
const execFileAsync = promisify(execFile);
|
|
86378
86550
|
/**
|
|
86379
86551
|
* Run the `--watch` reload loop. Returns when the invocation closes naturally
|
|
86380
86552
|
* with no pending reload (one-shot finished, or `--ws` agent closed), or when
|
|
@@ -86525,7 +86697,7 @@ async function softReloadAgentContainer(containerId, newAssetSourceDir) {
|
|
|
86525
86697
|
const dockerCmd = getDockerCmd();
|
|
86526
86698
|
let workdir;
|
|
86527
86699
|
try {
|
|
86528
|
-
const { stdout } = await execFileAsync
|
|
86700
|
+
const { stdout } = await execFileAsync(dockerCmd, [
|
|
86529
86701
|
"inspect",
|
|
86530
86702
|
"--format",
|
|
86531
86703
|
"{{.Config.WorkingDir}}",
|
|
@@ -86538,7 +86710,7 @@ async function softReloadAgentContainer(containerId, newAssetSourceDir) {
|
|
|
86538
86710
|
const workdirDest = workdir.endsWith("/") ? workdir : `${workdir}/`;
|
|
86539
86711
|
logger.info(`Soft-reload: docker cp ${newAssetSourceDir} -> ${containerId}:${workdirDest}; restart.`);
|
|
86540
86712
|
try {
|
|
86541
|
-
await execFileAsync
|
|
86713
|
+
await execFileAsync(dockerCmd, [
|
|
86542
86714
|
"cp",
|
|
86543
86715
|
`${newAssetSourceDir}/.`,
|
|
86544
86716
|
`${containerId}:${workdirDest}`
|
|
@@ -86547,7 +86719,7 @@ async function softReloadAgentContainer(containerId, newAssetSourceDir) {
|
|
|
86547
86719
|
throw new CdkdError(`softReloadAgentContainer: docker cp into '${containerId}:${workdir}' failed: ${describeExecError(err)}.`, "LOCAL_INVOKE_AGENTCORE_WATCH_SOFT_RELOAD_CP_FAILED");
|
|
86548
86720
|
}
|
|
86549
86721
|
try {
|
|
86550
|
-
await execFileAsync
|
|
86722
|
+
await execFileAsync(dockerCmd, ["restart", containerId]);
|
|
86551
86723
|
} catch (err) {
|
|
86552
86724
|
throw new CdkdError(`softReloadAgentContainer: docker restart of '${containerId}' failed: ${describeExecError(err)}.`, "LOCAL_INVOKE_AGENTCORE_WATCH_SOFT_RELOAD_RESTART_FAILED");
|
|
86553
86725
|
}
|
|
@@ -88733,1076 +88905,6 @@ function createLocalCommand() {
|
|
|
88733
88905
|
return local;
|
|
88734
88906
|
}
|
|
88735
88907
|
|
|
88736
|
-
//#endregion
|
|
88737
|
-
//#region src/cli/commands/migrate/cdk-cli-check.ts
|
|
88738
|
-
const execFileAsync = promisify(execFile);
|
|
88739
|
-
/**
|
|
88740
|
-
* Minimum aws-cdk CLI version where `cdk migrate --from-stack` is
|
|
88741
|
-
* considered stable (per the design doc at docs/design/465-cfn-migrate.md
|
|
88742
|
-
* §4 — "Version requirement"). Below this we WARN but do not block,
|
|
88743
|
-
* because users may have a working older release and a hard rejection
|
|
88744
|
-
* would be more disruptive than a warning.
|
|
88745
|
-
*/
|
|
88746
|
-
const RECOMMENDED_MIN_VERSION = "2.124.0";
|
|
88747
|
-
/**
|
|
88748
|
-
* Verify that the upstream `cdk` CLI is available on PATH (or at the
|
|
88749
|
-
* override path passed via `--cdk-bin`) and report its version.
|
|
88750
|
-
*
|
|
88751
|
-
* Hard-errors with {@link MissingCdkCliError} when `cdk` cannot be
|
|
88752
|
-
* spawned (ENOENT, permission denied, etc.). Emits a `warn` field on
|
|
88753
|
-
* the result when the version string is below the recommended minimum
|
|
88754
|
-
* — the caller decides how to surface it.
|
|
88755
|
-
*
|
|
88756
|
-
* `cdk --version` output format empirically (verified 2026-05-22
|
|
88757
|
-
* against cdk@2.1112.0): `<MAJOR.MINOR.PATCH> (build <id>)`. The
|
|
88758
|
-
* version is the first whitespace-separated token; the trailing
|
|
88759
|
-
* `(build <id>)` is informational and ignored.
|
|
88760
|
-
*
|
|
88761
|
-
* @param cdkBinPath - Path to the `cdk` binary. Defaults to `'cdk'`
|
|
88762
|
-
* which uses the system PATH for resolution.
|
|
88763
|
-
*/
|
|
88764
|
-
async function verifyCdkCliAvailable(cdkBinPath = "cdk") {
|
|
88765
|
-
let stdout;
|
|
88766
|
-
try {
|
|
88767
|
-
stdout = (await execFileAsync(cdkBinPath, ["--version"])).stdout ?? "";
|
|
88768
|
-
} catch (err) {
|
|
88769
|
-
throw new MissingCdkCliError(`Failed to run '${cdkBinPath} --version': ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0);
|
|
88770
|
-
}
|
|
88771
|
-
const version = parseCdkVersion(stdout);
|
|
88772
|
-
if (!version) throw new MissingCdkCliError(`'${cdkBinPath} --version' produced unexpected output: ${JSON.stringify(stdout.trim())}`);
|
|
88773
|
-
if (compareSemver(version, RECOMMENDED_MIN_VERSION) < 0) return {
|
|
88774
|
-
version,
|
|
88775
|
-
warn: `cdk CLI version ${version} is older than the recommended minimum ${RECOMMENDED_MIN_VERSION}. 'cdkd migrate' relies on the stabilized 'cdk migrate --from-stack' flag; upgrade with 'npm install -g aws-cdk@latest' if you hit codegen issues.`
|
|
88776
|
-
};
|
|
88777
|
-
return { version };
|
|
88778
|
-
}
|
|
88779
|
-
/**
|
|
88780
|
-
* Parse the first semver-shaped token (`<MAJOR>.<MINOR>.<PATCH>`) out of
|
|
88781
|
-
* `cdk --version` stdout. Tolerates a trailing `(build <id>)` suffix.
|
|
88782
|
-
*
|
|
88783
|
-
* Returns `undefined` when no semver-shaped token is present so the
|
|
88784
|
-
* caller can surface the raw stdout in the error message.
|
|
88785
|
-
*/
|
|
88786
|
-
function parseCdkVersion(stdout) {
|
|
88787
|
-
const match = stdout.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
88788
|
-
if (!match) return void 0;
|
|
88789
|
-
return `${match[1]}.${match[2]}.${match[3]}`;
|
|
88790
|
-
}
|
|
88791
|
-
/**
|
|
88792
|
-
* Compare two semver strings (`MAJOR.MINOR.PATCH`). Returns a negative
|
|
88793
|
-
* number when `a < b`, zero when `a === b`, positive when `a > b`.
|
|
88794
|
-
*
|
|
88795
|
-
* Strictly numeric comparison per component — no pre-release / build
|
|
88796
|
-
* suffix handling needed for this use case (`cdk --version` always
|
|
88797
|
-
* emits a stable `X.Y.Z` triple).
|
|
88798
|
-
*/
|
|
88799
|
-
function compareSemver(a, b) {
|
|
88800
|
-
const aParts = a.split(".").map((n) => Number.parseInt(n, 10));
|
|
88801
|
-
const bParts = b.split(".").map((n) => Number.parseInt(n, 10));
|
|
88802
|
-
for (let i = 0; i < 3; i++) {
|
|
88803
|
-
const aN = aParts[i] ?? 0;
|
|
88804
|
-
const bN = bParts[i] ?? 0;
|
|
88805
|
-
if (aN !== bN) return aN - bN;
|
|
88806
|
-
}
|
|
88807
|
-
return 0;
|
|
88808
|
-
}
|
|
88809
|
-
|
|
88810
|
-
//#endregion
|
|
88811
|
-
//#region src/cli/commands/migrate/cfn-stack-prefetch.ts
|
|
88812
|
-
/**
|
|
88813
|
-
* Resource types that `cdkd migrate` refuses to adopt under any
|
|
88814
|
-
* circumstances. The rationale per type:
|
|
88815
|
-
*
|
|
88816
|
-
* - `AWS::CloudFormation::CustomResource` — Lambda-backed Custom
|
|
88817
|
-
* Resources whose response protocol (cfn-response over a
|
|
88818
|
-
* pre-signed S3 URL) is incompatible with cdkd's import flow. The
|
|
88819
|
-
* backing Lambda's onCreate handler would need to be re-invoked
|
|
88820
|
-
* for a cdkd-side adoption to make sense, and that's structurally
|
|
88821
|
-
* different from the metadata-transfer migration this command
|
|
88822
|
-
* provides.
|
|
88823
|
-
*
|
|
88824
|
-
* - `AWS::CloudFormation::Stack` — nested stacks. cdkd's recursive
|
|
88825
|
-
* `NestedStackProvider` handles fresh `cdkd deploy` (#459), but
|
|
88826
|
-
* `cdk migrate` flattens nested stacks into separate generated apps
|
|
88827
|
-
* in a way that doesn't round-trip cleanly into cdkd's parent~child
|
|
88828
|
-
* state-key layout, so the migrate command keeps rejecting them.
|
|
88829
|
-
* Adopting an existing CFn-managed nested-stack hierarchy is
|
|
88830
|
-
* deferred to issue
|
|
88831
|
-
* [#464](https://github.com/go-to-k/cdkd/issues/464).
|
|
88832
|
-
*
|
|
88833
|
-
* - `Custom::*` — any user-defined Custom Resource type prefix.
|
|
88834
|
-
* Same rationale as `AWS::CloudFormation::CustomResource`.
|
|
88835
|
-
*/
|
|
88836
|
-
const HARD_REJECT_RESOURCE_TYPES = /* @__PURE__ */ new Set(["AWS::CloudFormation::CustomResource", "AWS::CloudFormation::Stack"]);
|
|
88837
|
-
/**
|
|
88838
|
-
* Fetch the source CFn stack's current state + resources + transform
|
|
88839
|
-
* info. Issues 3 read-only AWS API calls (`DescribeStacks`,
|
|
88840
|
-
* `DescribeStackResources`, `GetTemplate(Stage=Original)`); none of
|
|
88841
|
-
* them mutate AWS state. The result is fed into
|
|
88842
|
-
* {@link validatePrefetchResult} for the hard-reject check and
|
|
88843
|
-
* surfaced to {@link runMigrateLibrary}'s caller (PR B) as part of
|
|
88844
|
-
* `RunMigrateLibraryResult`.
|
|
88845
|
-
*
|
|
88846
|
-
* `DescribeStackResources` is preferred over `ListStackResources`
|
|
88847
|
-
* because the response is unpaginated up to 500 resources (CFn's hard
|
|
88848
|
-
* stack cap) — no pagination loop needed for the migration use case.
|
|
88849
|
-
*/
|
|
88850
|
-
async function prefetchCfnStack(stackName, cfnClient) {
|
|
88851
|
-
const stack = (await cfnClient.send(new DescribeStacksCommand({ StackName: stackName }))).Stacks?.[0];
|
|
88852
|
-
if (!stack) throw new LocalMigrateError(`CloudFormation stack '${stackName}' not found.`);
|
|
88853
|
-
const stackStatus = stack.StackStatus ?? "";
|
|
88854
|
-
const resourcesResp = await cfnClient.send(new DescribeStackResourcesCommand({ StackName: stackName }));
|
|
88855
|
-
const resources = [];
|
|
88856
|
-
for (const r of resourcesResp.StackResources ?? []) {
|
|
88857
|
-
if (!r.LogicalResourceId || !r.PhysicalResourceId || !r.ResourceType) continue;
|
|
88858
|
-
resources.push({
|
|
88859
|
-
LogicalResourceId: r.LogicalResourceId,
|
|
88860
|
-
PhysicalResourceId: r.PhysicalResourceId,
|
|
88861
|
-
ResourceType: r.ResourceType
|
|
88862
|
-
});
|
|
88863
|
-
}
|
|
88864
|
-
let transformInfo = {
|
|
88865
|
-
hasSamTransform: false,
|
|
88866
|
-
hasIncludeTransform: false
|
|
88867
|
-
};
|
|
88868
|
-
let sourceCfnTemplate = null;
|
|
88869
|
-
try {
|
|
88870
|
-
const tplResp = await cfnClient.send(new GetTemplateCommand({
|
|
88871
|
-
StackName: stackName,
|
|
88872
|
-
TemplateStage: "Original"
|
|
88873
|
-
}));
|
|
88874
|
-
if (tplResp.TemplateBody) {
|
|
88875
|
-
try {
|
|
88876
|
-
sourceCfnTemplate = parseCfnTemplate(tplResp.TemplateBody);
|
|
88877
|
-
} catch {
|
|
88878
|
-
sourceCfnTemplate = null;
|
|
88879
|
-
}
|
|
88880
|
-
transformInfo = detectTransformsFromParsed(sourceCfnTemplate);
|
|
88881
|
-
}
|
|
88882
|
-
} catch (err) {
|
|
88883
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
88884
|
-
getLogger().warn(`[migrate] GetTemplate failed for '${stackName}': ${detail}. Skipping transform detection.`);
|
|
88885
|
-
}
|
|
88886
|
-
return {
|
|
88887
|
-
stackStatus,
|
|
88888
|
-
resources,
|
|
88889
|
-
transformInfo,
|
|
88890
|
-
sourceCfnTemplate
|
|
88891
|
-
};
|
|
88892
|
-
}
|
|
88893
|
-
/**
|
|
88894
|
-
* Pre-flight reject when the source CFn stack contains any
|
|
88895
|
-
* un-migratable resource type OR is in a non-terminal state. Surfaces
|
|
88896
|
-
* SAM / Include transforms via the result's `transformInfo` instead —
|
|
88897
|
-
* the caller is responsible for emitting INFO logs.
|
|
88898
|
-
*
|
|
88899
|
-
* Per #465 Q2 (parent-session decision): every hard-reject case
|
|
88900
|
-
* routes the user to the standalone `cdk migrate --from-stack <name>
|
|
88901
|
-
* --output-dir ./tmp` flow so they can manually inspect the generated
|
|
88902
|
-
* code and decide how to proceed (e.g. delete the Custom Resource
|
|
88903
|
-
* from the source CFn stack before re-running cdkd migrate).
|
|
88904
|
-
*/
|
|
88905
|
-
function validatePrefetchResult(result) {
|
|
88906
|
-
if (!STABLE_TERMINAL_STATUSES.has(result.stackStatus)) throw new LocalMigrateError(`CloudFormation stack is in status '${result.stackStatus}', which is not a stable terminal state. Wait for the stack to settle (or roll back) before running 'cdkd migrate'.`);
|
|
88907
|
-
const offenders = [];
|
|
88908
|
-
for (const r of result.resources) if (isHardRejectType(r.ResourceType)) offenders.push({
|
|
88909
|
-
LogicalResourceId: r.LogicalResourceId,
|
|
88910
|
-
ResourceType: r.ResourceType
|
|
88911
|
-
});
|
|
88912
|
-
if (offenders.length === 0) return;
|
|
88913
|
-
throw new LocalMigrateError(`Source CloudFormation stack contains resource types that 'cdkd migrate' cannot adopt:\n${offenders.map((o) => ` - ${o.LogicalResourceId} (${o.ResourceType})`).join("\n")}\n\nLambda-backed Custom Resources and nested CloudFormation stacks are out of scope for #465.\nTo migrate the rest of the stack, either:\n 1. Delete the unsupported resources from the source CFn stack first, OR\n 2. Run upstream 'cdk migrate' standalone to inspect the generated code:\n cdk migrate --from-stack --stack-name <source-stack> --output-path ./tmp\n then hand-author CDK constructs to replace the Custom Resource semantics.`);
|
|
88914
|
-
}
|
|
88915
|
-
/**
|
|
88916
|
-
* Whether the given resource type is in the hard-reject set. Matches
|
|
88917
|
-
* literal types from {@link HARD_REJECT_RESOURCE_TYPES} AND the
|
|
88918
|
-
* `Custom::*` prefix pattern (user-defined Custom Resource types).
|
|
88919
|
-
*/
|
|
88920
|
-
function isHardRejectType(resourceType) {
|
|
88921
|
-
if (HARD_REJECT_RESOURCE_TYPES.has(resourceType)) return true;
|
|
88922
|
-
if (resourceType.startsWith("Custom::")) return true;
|
|
88923
|
-
return false;
|
|
88924
|
-
}
|
|
88925
|
-
/**
|
|
88926
|
-
* Scan an already-parsed CFn template object for stack-level
|
|
88927
|
-
* Transform blocks. Accepts both the scalar form (`Transform:
|
|
88928
|
-
* AWS::Serverless-2016-10-31`) and the array form (`Transform:
|
|
88929
|
-
* [AWS::Serverless-2016-10-31, ...]`).
|
|
88930
|
-
*
|
|
88931
|
-
* The caller is responsible for parsing the raw template body via the
|
|
88932
|
-
* CFn-aware codec; this helper operates on the parsed object so the
|
|
88933
|
-
* `GetTemplate` response can be parsed once and reused.
|
|
88934
|
-
*/
|
|
88935
|
-
function detectTransformsFromParsed(parsed) {
|
|
88936
|
-
if (!parsed || typeof parsed !== "object") return {
|
|
88937
|
-
hasSamTransform: false,
|
|
88938
|
-
hasIncludeTransform: false
|
|
88939
|
-
};
|
|
88940
|
-
const transformRaw = parsed["Transform"];
|
|
88941
|
-
const transforms = Array.isArray(transformRaw) ? transformRaw.map((t) => String(t)) : typeof transformRaw === "string" ? [transformRaw] : [];
|
|
88942
|
-
return {
|
|
88943
|
-
hasSamTransform: transforms.some((t) => t.startsWith("AWS::Serverless")),
|
|
88944
|
-
hasIncludeTransform: transforms.some((t) => t === "AWS::Include" || t.startsWith("AWS::Include"))
|
|
88945
|
-
};
|
|
88946
|
-
}
|
|
88947
|
-
|
|
88948
|
-
//#endregion
|
|
88949
|
-
//#region src/cli/commands/migrate/output-dir-guard.ts
|
|
88950
|
-
/**
|
|
88951
|
-
* Refuse to run `cdkd migrate` when the upstream `cdk migrate` would
|
|
88952
|
-
* write its output into an existing non-empty directory. `cdk migrate`
|
|
88953
|
-
* itself silently overwrites in that case; cdkd surfaces the collision
|
|
88954
|
-
* up-front with the exact recovery command so the user is not left
|
|
88955
|
-
* with a half-overwritten CDK app.
|
|
88956
|
-
*
|
|
88957
|
-
* Behavior matrix (`outputPath = <user-supplied dir>`, `stackName =
|
|
88958
|
-
* <CFn stack name>`; cdk writes to `<outputPath>/<stackName>`):
|
|
88959
|
-
* - target dir does not exist → OK
|
|
88960
|
-
* - target dir exists but is empty → OK (treated as fresh)
|
|
88961
|
-
* - target path is a file, not a directory → typed error
|
|
88962
|
-
* - target dir exists and contains entries → typed error with
|
|
88963
|
-
* the canonical
|
|
88964
|
-
* `rm -rf` recovery
|
|
88965
|
-
* command in the
|
|
88966
|
-
* message
|
|
88967
|
-
*
|
|
88968
|
-
* The parent `outputPath` is NOT checked — `cdk migrate` creates
|
|
88969
|
-
* `<outputPath>` itself if missing, so a non-existent parent is fine.
|
|
88970
|
-
*/
|
|
88971
|
-
function assertOutputDirAvailable(outputPath, stackName) {
|
|
88972
|
-
const targetDir = resolve(outputPath, stackName);
|
|
88973
|
-
if (!existsSync(targetDir)) return;
|
|
88974
|
-
if (!statSync(targetDir).isDirectory()) throw new LocalMigrateError(`Output path '${targetDir}' exists but is not a directory. Remove it (or pass --output-dir <NEW_PATH>) and retry:\n rm -f ${targetDir} && cdkd migrate --from-cfn-stack ${stackName}`);
|
|
88975
|
-
const entries = readdirSync(targetDir);
|
|
88976
|
-
if (entries.length === 0) return;
|
|
88977
|
-
throw new LocalMigrateError(`Output dir '${targetDir}' already exists and is non-empty (e.g. '${join(targetDir, entries[0])}'); remove it or pass --output-dir <NEW_PATH>:\n rm -rf ${targetDir} && cdkd migrate --from-cfn-stack ${stackName}`);
|
|
88978
|
-
}
|
|
88979
|
-
|
|
88980
|
-
//#endregion
|
|
88981
|
-
//#region src/cli/commands/migrate/cdk-migrate-spawn.ts
|
|
88982
|
-
/**
|
|
88983
|
-
* Spawn `cdk migrate --from-stack` as a subprocess and stream its
|
|
88984
|
-
* output through cdkd's logger.
|
|
88985
|
-
*
|
|
88986
|
-
* Why subprocess (not Node module import): the upstream `cdk` CLI is
|
|
88987
|
-
* a bundled CommonJS app with no stable public API; pinning a
|
|
88988
|
-
* runtime-injected `aws-cdk-lib` from cdkd is impractical. Subprocess
|
|
88989
|
-
* isolation also gives us a clean failure boundary — non-zero exit
|
|
88990
|
-
* surfaces as {@link LocalMigrateError} with the captured streams
|
|
88991
|
-
* folded into the error message.
|
|
88992
|
-
*
|
|
88993
|
-
* Streaming is preferred over a buffered `execFile` because `cdk
|
|
88994
|
-
* migrate` emits progress lines (downloading the resource schema,
|
|
88995
|
-
* synthesizing the L1 code for each resource) that users expect to
|
|
88996
|
-
* see in real time. Captured streams are still returned for caller
|
|
88997
|
-
* inspection.
|
|
88998
|
-
*/
|
|
88999
|
-
async function spawnCdkMigrate(opts) {
|
|
89000
|
-
const logger = getLogger();
|
|
89001
|
-
const cdkBin = opts.cdkBinPath ?? "cdk";
|
|
89002
|
-
const language = opts.language ?? "typescript";
|
|
89003
|
-
const outputDir = resolve(opts.outputPath, opts.stackName);
|
|
89004
|
-
const args = [
|
|
89005
|
-
"migrate",
|
|
89006
|
-
"--from-stack",
|
|
89007
|
-
"--stack-name",
|
|
89008
|
-
opts.fromStackName,
|
|
89009
|
-
"--output-path",
|
|
89010
|
-
opts.outputPath,
|
|
89011
|
-
"--language",
|
|
89012
|
-
language
|
|
89013
|
-
];
|
|
89014
|
-
if (opts.region) args.push("--region", opts.region);
|
|
89015
|
-
if (opts.account) args.push("--account", opts.account);
|
|
89016
|
-
if (opts.profile) args.push("--profile", opts.profile);
|
|
89017
|
-
for (const filter of opts.filters ?? []) args.push("--filter", filter);
|
|
89018
|
-
const env = {
|
|
89019
|
-
...process.env,
|
|
89020
|
-
...opts.extraEnv ?? {}
|
|
89021
|
-
};
|
|
89022
|
-
logger.info(`[cdk-migrate] ${cdkBin} ${args.join(" ")}`);
|
|
89023
|
-
return await new Promise((resolvePromise, rejectPromise) => {
|
|
89024
|
-
let stdout = "";
|
|
89025
|
-
let stderr = "";
|
|
89026
|
-
let child;
|
|
89027
|
-
try {
|
|
89028
|
-
child = spawn(cdkBin, args, {
|
|
89029
|
-
env,
|
|
89030
|
-
stdio: [
|
|
89031
|
-
"ignore",
|
|
89032
|
-
"pipe",
|
|
89033
|
-
"pipe"
|
|
89034
|
-
]
|
|
89035
|
-
});
|
|
89036
|
-
} catch (err) {
|
|
89037
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
89038
|
-
rejectPromise(new LocalMigrateError(`Failed to spawn '${cdkBin} migrate': ${detail}`, err instanceof Error ? err : void 0));
|
|
89039
|
-
return;
|
|
89040
|
-
}
|
|
89041
|
-
child.stdout?.on("data", (chunk) => {
|
|
89042
|
-
const text = chunk.toString("utf-8");
|
|
89043
|
-
stdout += text;
|
|
89044
|
-
const trimmed = text.replace(/\n$/, "");
|
|
89045
|
-
if (trimmed) logger.info(`[cdk-migrate] ${trimmed}`);
|
|
89046
|
-
});
|
|
89047
|
-
child.stderr?.on("data", (chunk) => {
|
|
89048
|
-
const text = chunk.toString("utf-8");
|
|
89049
|
-
stderr += text;
|
|
89050
|
-
const trimmed = text.replace(/\n$/, "");
|
|
89051
|
-
if (trimmed) logger.warn(`[cdk-migrate] ${trimmed}`);
|
|
89052
|
-
});
|
|
89053
|
-
child.on("error", (err) => {
|
|
89054
|
-
rejectPromise(new LocalMigrateError(`'${cdkBin} migrate' subprocess error: ${err.message}`, err));
|
|
89055
|
-
});
|
|
89056
|
-
child.on("close", (code, signal) => {
|
|
89057
|
-
if (code === 0) {
|
|
89058
|
-
resolvePromise({
|
|
89059
|
-
outputDir,
|
|
89060
|
-
stdout,
|
|
89061
|
-
stderr
|
|
89062
|
-
});
|
|
89063
|
-
return;
|
|
89064
|
-
}
|
|
89065
|
-
const exitDetail = signal !== null ? `killed by signal ${signal}` : code !== null ? `exited with code ${code}` : "process closed without a code or signal";
|
|
89066
|
-
rejectPromise(new LocalMigrateError(`'${cdkBin} migrate' ${exitDetail}.\n--- stdout ---\n${stdout || "(empty)"}\n--- stderr ---\n${stderr || "(empty)"}\n`));
|
|
89067
|
-
});
|
|
89068
|
-
});
|
|
89069
|
-
}
|
|
89070
|
-
|
|
89071
|
-
//#endregion
|
|
89072
|
-
//#region src/cli/commands/migrate/synth-after-migrate.ts
|
|
89073
|
-
/**
|
|
89074
|
-
* Run `npm install` inside the generated CDK app directory so the
|
|
89075
|
-
* `cdk synth` step (which dynamically loads `aws-cdk-lib`) has its
|
|
89076
|
-
* dependencies in place.
|
|
89077
|
-
*
|
|
89078
|
-
* Skippable via `skipInstall: true` — CI users with a pre-populated
|
|
89079
|
-
* node_modules cache can save the ~30s `npm install` round-trip.
|
|
89080
|
-
*
|
|
89081
|
-
* Streams stdout / stderr through cdkd's logger so users see progress
|
|
89082
|
-
* for the often-slow npm step. Non-zero exit surfaces as a typed
|
|
89083
|
-
* {@link LocalMigrateError} with the captured streams folded into
|
|
89084
|
-
* the error message.
|
|
89085
|
-
*/
|
|
89086
|
-
async function installGeneratedAppDeps(outputDir, opts) {
|
|
89087
|
-
const logger = getLogger();
|
|
89088
|
-
if (opts.skipInstall) {
|
|
89089
|
-
logger.info(`[cdk-migrate] Skipping 'npm install' (--skip-install).`);
|
|
89090
|
-
return;
|
|
89091
|
-
}
|
|
89092
|
-
const absDir = resolve(outputDir);
|
|
89093
|
-
if (!existsSync(absDir)) throw new LocalMigrateError(`Generated app directory '${absDir}' does not exist; cannot run 'npm install'.`);
|
|
89094
|
-
logger.info(`[cdk-migrate] Running 'npm install' in ${absDir}...`);
|
|
89095
|
-
await runStreamingCommand("npm", ["install"], absDir, opts.extraEnv ? {
|
|
89096
|
-
...process.env,
|
|
89097
|
-
...opts.extraEnv
|
|
89098
|
-
} : void 0, "npm install");
|
|
89099
|
-
}
|
|
89100
|
-
/**
|
|
89101
|
-
* Run `cdk synth --quiet` inside the generated CDK app directory and
|
|
89102
|
-
* return both the assembly dir path and the parsed root-stack
|
|
89103
|
-
* template.
|
|
89104
|
-
*
|
|
89105
|
-
* `--quiet` suppresses the noisy template echo that `cdk synth`
|
|
89106
|
-
* defaults to on stdout — we read the template from disk
|
|
89107
|
-
* (`cdk.out/<StackName>.template.json`) instead.
|
|
89108
|
-
*
|
|
89109
|
-
* Skippable via `skipSynth: true` — useful when PR B's orchestrator
|
|
89110
|
-
* needs the codegen output without running synth (e.g. preview /
|
|
89111
|
-
* dry-run modes).
|
|
89112
|
-
*/
|
|
89113
|
-
async function synthGeneratedApp(outputDir, opts) {
|
|
89114
|
-
const logger = getLogger();
|
|
89115
|
-
const absDir = resolve(outputDir);
|
|
89116
|
-
const assemblyDir = join(absDir, "cdk.out");
|
|
89117
|
-
if (opts.skipSynth) {
|
|
89118
|
-
logger.info(`[cdk-migrate] Skipping 'cdk synth' (--skip-synth).`);
|
|
89119
|
-
return {
|
|
89120
|
-
assemblyDir,
|
|
89121
|
-
templateBody: null
|
|
89122
|
-
};
|
|
89123
|
-
}
|
|
89124
|
-
if (!existsSync(absDir)) throw new LocalMigrateError(`Generated app directory '${absDir}' does not exist; cannot run 'cdk synth'.`);
|
|
89125
|
-
const cdkBin = opts.cdkBinPath ?? "cdk";
|
|
89126
|
-
const env = {
|
|
89127
|
-
...process.env,
|
|
89128
|
-
...opts.extraEnv ?? {}
|
|
89129
|
-
};
|
|
89130
|
-
logger.info(`[cdk-migrate] Running '${cdkBin} synth --quiet' in ${absDir}...`);
|
|
89131
|
-
await runStreamingCommand(cdkBin, ["synth", "--quiet"], absDir, env, `${cdkBin} synth`);
|
|
89132
|
-
if (!existsSync(assemblyDir)) throw new LocalMigrateError(`'cdk synth' completed but produced no '${assemblyDir}' directory.`);
|
|
89133
|
-
const templates = readdirSync(assemblyDir).filter((f) => f.endsWith(".template.json"));
|
|
89134
|
-
if (templates.length === 0) return {
|
|
89135
|
-
assemblyDir,
|
|
89136
|
-
templateBody: null
|
|
89137
|
-
};
|
|
89138
|
-
const templatePath = join(assemblyDir, templates[0]);
|
|
89139
|
-
let templateBody;
|
|
89140
|
-
try {
|
|
89141
|
-
const raw = readFileSync(templatePath, "utf-8");
|
|
89142
|
-
templateBody = JSON.parse(raw);
|
|
89143
|
-
} catch (err) {
|
|
89144
|
-
throw new LocalMigrateError(`Failed to parse generated template '${templatePath}': ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0);
|
|
89145
|
-
}
|
|
89146
|
-
return {
|
|
89147
|
-
assemblyDir,
|
|
89148
|
-
templateBody
|
|
89149
|
-
};
|
|
89150
|
-
}
|
|
89151
|
-
/**
|
|
89152
|
-
* Helper — spawn a subprocess in the given working directory, stream
|
|
89153
|
-
* its output through cdkd's logger, and reject with a typed
|
|
89154
|
-
* {@link LocalMigrateError} on non-zero exit.
|
|
89155
|
-
*
|
|
89156
|
-
* Factored out so the `npm install` and `cdk synth` call sites use the
|
|
89157
|
-
* same shape; both expose progress to the user and surface failures
|
|
89158
|
-
* with the captured streams.
|
|
89159
|
-
*/
|
|
89160
|
-
async function runStreamingCommand(bin, args, cwd, env, label) {
|
|
89161
|
-
const logger = getLogger();
|
|
89162
|
-
return await new Promise((resolvePromise, rejectPromise) => {
|
|
89163
|
-
let stdout = "";
|
|
89164
|
-
let stderr = "";
|
|
89165
|
-
let child;
|
|
89166
|
-
try {
|
|
89167
|
-
child = spawn(bin, args, {
|
|
89168
|
-
cwd,
|
|
89169
|
-
env: env ?? process.env,
|
|
89170
|
-
stdio: [
|
|
89171
|
-
"ignore",
|
|
89172
|
-
"pipe",
|
|
89173
|
-
"pipe"
|
|
89174
|
-
]
|
|
89175
|
-
});
|
|
89176
|
-
} catch (err) {
|
|
89177
|
-
rejectPromise(new LocalMigrateError(`Failed to spawn ${label}: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0));
|
|
89178
|
-
return;
|
|
89179
|
-
}
|
|
89180
|
-
child.stdout?.on("data", (chunk) => {
|
|
89181
|
-
const text = chunk.toString("utf-8");
|
|
89182
|
-
stdout += text;
|
|
89183
|
-
const trimmed = text.replace(/\n$/, "");
|
|
89184
|
-
if (trimmed) logger.info(`[${label}] ${trimmed}`);
|
|
89185
|
-
});
|
|
89186
|
-
child.stderr?.on("data", (chunk) => {
|
|
89187
|
-
const text = chunk.toString("utf-8");
|
|
89188
|
-
stderr += text;
|
|
89189
|
-
const trimmed = text.replace(/\n$/, "");
|
|
89190
|
-
if (trimmed) logger.warn(`[${label}] ${trimmed}`);
|
|
89191
|
-
});
|
|
89192
|
-
child.on("error", (err) => {
|
|
89193
|
-
rejectPromise(new LocalMigrateError(`${label} subprocess error: ${err.message}`, err));
|
|
89194
|
-
});
|
|
89195
|
-
child.on("close", (code, signal) => {
|
|
89196
|
-
if (code === 0) {
|
|
89197
|
-
resolvePromise();
|
|
89198
|
-
return;
|
|
89199
|
-
}
|
|
89200
|
-
rejectPromise(new LocalMigrateError(`${label} ${signal !== null ? `killed by signal ${signal}` : code !== null ? `exited with code ${code}` : "process closed without a code or signal"}.\n--- stdout ---\n${stdout || "(empty)"}\n--- stderr ---\n${stderr || "(empty)"}\n`));
|
|
89201
|
-
});
|
|
89202
|
-
});
|
|
89203
|
-
}
|
|
89204
|
-
|
|
89205
|
-
//#endregion
|
|
89206
|
-
//#region src/cli/commands/migrate/index.ts
|
|
89207
|
-
/**
|
|
89208
|
-
* Build an extra-env bag for subprocesses (`npm install`, `cdk synth`)
|
|
89209
|
-
* so they inherit the AWS profile the caller selected via `--profile`.
|
|
89210
|
-
*
|
|
89211
|
-
* Without this, `cdk synth` falls back to the SDK default credential
|
|
89212
|
-
* chain (or no credentials at all) when the user runs cdkd under a
|
|
89213
|
-
* non-default profile — which silently produces a template synthesized
|
|
89214
|
-
* against the wrong account / region context.
|
|
89215
|
-
*/
|
|
89216
|
-
function buildAwsEnv(opts) {
|
|
89217
|
-
return opts.profile ? { AWS_PROFILE: opts.profile } : {};
|
|
89218
|
-
}
|
|
89219
|
-
/**
|
|
89220
|
-
* PR A library entry point — orchestrates the codegen + synth half of
|
|
89221
|
-
* `cdkd migrate --from-cfn-stack` without writing cdkd state, running
|
|
89222
|
-
* import, or touching the source CFn stack.
|
|
89223
|
-
*
|
|
89224
|
-
* Flow:
|
|
89225
|
-
* 1. `verifyCdkCliAvailable` — hard-fail if `cdk` is missing.
|
|
89226
|
-
* 2. `prefetchCfnStack` — read source stack state + resources +
|
|
89227
|
-
* transform info.
|
|
89228
|
-
* 3. `validatePrefetchResult` — reject CR / nested-stack / non-
|
|
89229
|
-
* terminal state; INFO log SAM / Include transforms.
|
|
89230
|
-
* 4. `assertOutputDirAvailable` — refuse pre-existing non-empty
|
|
89231
|
-
* output dir.
|
|
89232
|
-
* 5. `spawnCdkMigrate` — run `cdk migrate --from-stack` subprocess.
|
|
89233
|
-
* 6. `installGeneratedAppDeps` (gated by `skipInstall`).
|
|
89234
|
-
* 7. `synthGeneratedApp` (gated by `skipSynth`).
|
|
89235
|
-
* 8. Return every artifact PR B will consume.
|
|
89236
|
-
*
|
|
89237
|
-
* Per #465 Q4 (parent-session decision): PR A is library-only. The CLI
|
|
89238
|
-
* command (`cdkd migrate`), state writes, retire flow, and resource-
|
|
89239
|
-
* mapping algorithm all live in PR B.
|
|
89240
|
-
*/
|
|
89241
|
-
async function runMigrateLibrary(opts) {
|
|
89242
|
-
const logger = getLogger();
|
|
89243
|
-
const stackName = opts.fromCfnStack;
|
|
89244
|
-
const outputPath = opts.outputDir ?? resolve(process.cwd(), stackName);
|
|
89245
|
-
logger.info(`[migrate] Verifying upstream 'cdk' CLI...`);
|
|
89246
|
-
const cliCheck = await verifyCdkCliAvailable(opts.cdkBinPath);
|
|
89247
|
-
logger.info(`[migrate] Using cdk CLI v${cliCheck.version}`);
|
|
89248
|
-
if (cliCheck.warn) logger.warn(`[migrate] ${cliCheck.warn}`);
|
|
89249
|
-
logger.info(`[migrate] Pre-fetching CloudFormation stack '${stackName}'...`);
|
|
89250
|
-
const cfnClient = buildCfnClient(opts);
|
|
89251
|
-
let prefetch;
|
|
89252
|
-
try {
|
|
89253
|
-
prefetch = await prefetchCfnStack(stackName, cfnClient);
|
|
89254
|
-
} finally {
|
|
89255
|
-
cfnClient.destroy?.();
|
|
89256
|
-
}
|
|
89257
|
-
validatePrefetchResult(prefetch);
|
|
89258
|
-
if (prefetch.transformInfo.hasSamTransform) logger.info("[migrate] INFO: source CFn stack uses 'AWS::Serverless' transform; cdk migrate will expand it client-side and the generated CDK code will use plain Lambda + API Gateway L1 constructs (not SAM).");
|
|
89259
|
-
if (prefetch.transformInfo.hasIncludeTransform) logger.info("[migrate] INFO: source CFn stack uses 'AWS::Include' transform; cdk migrate will expand it client-side and the generated CDK code will inline the included template content.");
|
|
89260
|
-
assertOutputDirAvailable(outputPath, stackName);
|
|
89261
|
-
const spawnResult = await spawnCdkMigrate({
|
|
89262
|
-
stackName,
|
|
89263
|
-
fromStackName: stackName,
|
|
89264
|
-
outputPath,
|
|
89265
|
-
...opts.language && { language: opts.language },
|
|
89266
|
-
...opts.region && { region: opts.region },
|
|
89267
|
-
...opts.account && { account: opts.account },
|
|
89268
|
-
...opts.filters && { filters: opts.filters },
|
|
89269
|
-
...opts.profile && { profile: opts.profile },
|
|
89270
|
-
...opts.cdkBinPath && { cdkBinPath: opts.cdkBinPath }
|
|
89271
|
-
});
|
|
89272
|
-
await installGeneratedAppDeps(spawnResult.outputDir, {
|
|
89273
|
-
skipInstall: opts.skipInstall ?? false,
|
|
89274
|
-
extraEnv: buildAwsEnv(opts)
|
|
89275
|
-
});
|
|
89276
|
-
const synthResult = await synthGeneratedApp(spawnResult.outputDir, {
|
|
89277
|
-
skipSynth: opts.skipSynth ?? false,
|
|
89278
|
-
...opts.cdkBinPath && { cdkBinPath: opts.cdkBinPath },
|
|
89279
|
-
extraEnv: buildAwsEnv(opts)
|
|
89280
|
-
});
|
|
89281
|
-
if (prefetch.sourceCfnTemplate === null) throw new LocalMigrateError(`Could not read the source CloudFormation template for '${stackName}' (GetTemplate failed during pre-flight). Re-run after granting cloudformation:GetTemplate on the stack, or check the earlier warning for details.`);
|
|
89282
|
-
return {
|
|
89283
|
-
outputDir: spawnResult.outputDir,
|
|
89284
|
-
assemblyDir: synthResult.assemblyDir,
|
|
89285
|
-
templateBody: synthResult.templateBody,
|
|
89286
|
-
sourceCfnTemplate: prefetch.sourceCfnTemplate,
|
|
89287
|
-
sourceResources: prefetch.resources
|
|
89288
|
-
};
|
|
89289
|
-
}
|
|
89290
|
-
/**
|
|
89291
|
-
* Build a CloudFormation client using cdkd's shared {@link AwsClients}
|
|
89292
|
-
* factory so the credential chain matches every other cdkd command.
|
|
89293
|
-
*
|
|
89294
|
-
* Keeping the construction in one place makes the per-command CFn-
|
|
89295
|
-
* client recipe a single readable hook for future plumbing (e.g.
|
|
89296
|
-
* threading PR B's `--role-arn` STS-assume).
|
|
89297
|
-
*/
|
|
89298
|
-
function buildCfnClient(opts) {
|
|
89299
|
-
const config = {};
|
|
89300
|
-
if (opts.region) config.region = opts.region;
|
|
89301
|
-
if (opts.profile) config.profile = opts.profile;
|
|
89302
|
-
return new AwsClients(config).getCloudFormationClient();
|
|
89303
|
-
}
|
|
89304
|
-
|
|
89305
|
-
//#endregion
|
|
89306
|
-
//#region src/cli/commands/migrate/resource-mapper.ts
|
|
89307
|
-
/**
|
|
89308
|
-
* Run the 2-pass mapping algorithm against `(sourceCfnTemplate,
|
|
89309
|
-
* synthTemplate, sourceResources)` and return the resolved mapping plus
|
|
89310
|
-
* any unmatched entries.
|
|
89311
|
-
*
|
|
89312
|
-
* Pure-functional: no AWS calls, no I/O, no mutation of inputs. Safe to
|
|
89313
|
-
* call repeatedly in tests against shared fixtures.
|
|
89314
|
-
*
|
|
89315
|
-
* Throws when an override references a synth id that does not exist —
|
|
89316
|
-
* this is a user error in the hand-edited mapping JSON and surfacing
|
|
89317
|
-
* the available synth ids in the message is the path back to a working
|
|
89318
|
-
* config.
|
|
89319
|
-
*/
|
|
89320
|
-
function buildResourceMapping(opts) {
|
|
89321
|
-
const sourceEntries = extractSourceResources(opts.sourceCfnTemplate);
|
|
89322
|
-
const synthEntries = extractSynthResources(opts.synthTemplate);
|
|
89323
|
-
const physicalIdByLogicalId = /* @__PURE__ */ new Map();
|
|
89324
|
-
for (const r of opts.sourceResources) physicalIdByLogicalId.set(r.LogicalResourceId, r.PhysicalResourceId);
|
|
89325
|
-
const overrides = opts.overrides ?? {};
|
|
89326
|
-
const synthLogicalIds = new Set(synthEntries.map((s) => s.logicalId));
|
|
89327
|
-
for (const [srcId, synthId] of Object.entries(overrides)) if (!synthLogicalIds.has(synthId)) throw new Error(`Resource-mapping override targets synth logical id '${synthId}' (for source id '${srcId}'), but that id is not in the synthesized template. Available synth ids: ${[...synthLogicalIds].sort().join(", ")}`);
|
|
89328
|
-
const missingPhysicalIds = [];
|
|
89329
|
-
for (const src of sourceEntries) if (!physicalIdByLogicalId.has(src.logicalId)) missingPhysicalIds.push(` - ${src.logicalId} (${src.type})`);
|
|
89330
|
-
if (missingPhysicalIds.length > 0) throw new Error(`Source CFn resource(s) not returned by DescribeStackResources — cannot migrate:\n${missingPhysicalIds.join("\n")}\n\nThe stack may have been mid-operation; wait for it to settle (CREATE_COMPLETE / UPDATE_COMPLETE / etc.) and retry.`);
|
|
89331
|
-
const synthByLastPathSegment = indexSynthByLastPathSegment(synthEntries);
|
|
89332
|
-
const synthByLogicalId = new Map(synthEntries.map((s) => [s.logicalId, s]));
|
|
89333
|
-
const claimedSynthIds = /* @__PURE__ */ new Set();
|
|
89334
|
-
const pairs = [];
|
|
89335
|
-
const unmatched = [];
|
|
89336
|
-
const sourcesHandledByOverride = /* @__PURE__ */ new Set();
|
|
89337
|
-
for (const [srcId, synthId] of Object.entries(overrides)) {
|
|
89338
|
-
const src = sourceEntries.find((e) => e.logicalId === srcId);
|
|
89339
|
-
if (!src) {
|
|
89340
|
-
const availableSrc = sourceEntries.map((e) => e.logicalId).sort();
|
|
89341
|
-
throw new Error(`Resource-mapping override references source logical id '${srcId}', but no resource with that id exists in the source CloudFormation template. Available source ids: ${availableSrc.join(", ")}`);
|
|
89342
|
-
}
|
|
89343
|
-
const synth = synthByLogicalId.get(synthId);
|
|
89344
|
-
if (!synth) continue;
|
|
89345
|
-
const physicalId = physicalIdByLogicalId.get(src.logicalId);
|
|
89346
|
-
pairs.push({
|
|
89347
|
-
sourceLogicalId: src.logicalId,
|
|
89348
|
-
synthLogicalId: synth.logicalId,
|
|
89349
|
-
physicalId,
|
|
89350
|
-
resourceType: src.type
|
|
89351
|
-
});
|
|
89352
|
-
claimedSynthIds.add(synth.logicalId);
|
|
89353
|
-
sourcesHandledByOverride.add(src.logicalId);
|
|
89354
|
-
}
|
|
89355
|
-
const passOnePending = [];
|
|
89356
|
-
for (const src of sourceEntries) {
|
|
89357
|
-
if (sourcesHandledByOverride.has(src.logicalId)) continue;
|
|
89358
|
-
const candidatesForLastSegment = synthByLastPathSegment.get(src.logicalId) ?? [];
|
|
89359
|
-
const rawCandidateCount = candidatesForLastSegment.length;
|
|
89360
|
-
const availableCandidates = candidatesForLastSegment.filter((c) => !claimedSynthIds.has(c.logicalId));
|
|
89361
|
-
if (rawCandidateCount === 1 && availableCandidates.length === 1) {
|
|
89362
|
-
const synth = availableCandidates[0];
|
|
89363
|
-
pairs.push({
|
|
89364
|
-
sourceLogicalId: src.logicalId,
|
|
89365
|
-
synthLogicalId: synth.logicalId,
|
|
89366
|
-
physicalId: physicalIdByLogicalId.get(src.logicalId),
|
|
89367
|
-
resourceType: src.type
|
|
89368
|
-
});
|
|
89369
|
-
claimedSynthIds.add(synth.logicalId);
|
|
89370
|
-
} else passOnePending.push(src);
|
|
89371
|
-
}
|
|
89372
|
-
for (const src of passOnePending) {
|
|
89373
|
-
const candidatesSameType = synthEntries.filter((s) => s.type === src.type && !claimedSynthIds.has(s.logicalId));
|
|
89374
|
-
const propertyMatches = candidatesSameType.filter((s) => deepEqualIgnoreNoValue(src.properties, s.properties));
|
|
89375
|
-
if (propertyMatches.length === 1) {
|
|
89376
|
-
const synth = propertyMatches[0];
|
|
89377
|
-
pairs.push({
|
|
89378
|
-
sourceLogicalId: src.logicalId,
|
|
89379
|
-
synthLogicalId: synth.logicalId,
|
|
89380
|
-
physicalId: physicalIdByLogicalId.get(src.logicalId),
|
|
89381
|
-
resourceType: src.type
|
|
89382
|
-
});
|
|
89383
|
-
claimedSynthIds.add(synth.logicalId);
|
|
89384
|
-
} else {
|
|
89385
|
-
const isCollision = (synthByLastPathSegment.get(src.logicalId) ?? []).length >= 2;
|
|
89386
|
-
unmatched.push({
|
|
89387
|
-
sourceLogicalId: src.logicalId,
|
|
89388
|
-
resourceType: src.type,
|
|
89389
|
-
candidates: candidatesSameType.map((c) => c.logicalId).sort(),
|
|
89390
|
-
reason: isCollision ? "logical-id-collision" : "no-match"
|
|
89391
|
-
});
|
|
89392
|
-
}
|
|
89393
|
-
}
|
|
89394
|
-
const mapping = {};
|
|
89395
|
-
for (const p of pairs) mapping[p.sourceLogicalId] = p.synthLogicalId;
|
|
89396
|
-
return {
|
|
89397
|
-
mapping,
|
|
89398
|
-
pairs,
|
|
89399
|
-
unmatched
|
|
89400
|
-
};
|
|
89401
|
-
}
|
|
89402
|
-
/**
|
|
89403
|
-
* Walk a parsed CFn template's `Resources` and return one entry per
|
|
89404
|
-
* resource. `Type` defaults to `''` and `Properties` to `{}` when the
|
|
89405
|
-
* template omits them (CFn allows resources with only `DeletionPolicy`
|
|
89406
|
-
* + `Type`); the mapper's downstream deep-equal handles `{}` cleanly.
|
|
89407
|
-
*
|
|
89408
|
-
* Top-level keys other than `Resources` (Conditions / Parameters /
|
|
89409
|
-
* Rules / Outputs / Mappings / Metadata) are NOT walked — the mapping
|
|
89410
|
-
* is resource-to-resource only, and §5.5 of the design doc spells out
|
|
89411
|
-
* the four CDK-synth injections that live in those sibling keys.
|
|
89412
|
-
*/
|
|
89413
|
-
function extractSourceResources(template) {
|
|
89414
|
-
if (!template || typeof template !== "object") return [];
|
|
89415
|
-
const resources = template["Resources"];
|
|
89416
|
-
if (!resources || typeof resources !== "object") return [];
|
|
89417
|
-
const out = [];
|
|
89418
|
-
for (const [logicalId, raw] of Object.entries(resources)) {
|
|
89419
|
-
if (!raw || typeof raw !== "object") continue;
|
|
89420
|
-
const r = raw;
|
|
89421
|
-
const type = typeof r["Type"] === "string" ? r["Type"] : "";
|
|
89422
|
-
const props = r["Properties"] && typeof r["Properties"] === "object" ? r["Properties"] : {};
|
|
89423
|
-
out.push({
|
|
89424
|
-
logicalId,
|
|
89425
|
-
type,
|
|
89426
|
-
properties: props
|
|
89427
|
-
});
|
|
89428
|
-
}
|
|
89429
|
-
return out;
|
|
89430
|
-
}
|
|
89431
|
-
/**
|
|
89432
|
-
* Walk the synth template's `Resources` and return one entry per
|
|
89433
|
-
* resource, EXCLUDING `AWS::CDK::Metadata` (synth-only sentinel that has
|
|
89434
|
-
* no source counterpart). Captures `Metadata['aws:cdk:path']` so Pass 1
|
|
89435
|
-
* can match against the last `/`-separated segment.
|
|
89436
|
-
*
|
|
89437
|
-
* The four other CDK-synth injections (CDKMetadataAvailable Condition,
|
|
89438
|
-
* BootstrapVersion Parameter, CheckBootstrapVersion Rule) live outside
|
|
89439
|
-
* `Resources` and are structurally excluded by this function.
|
|
89440
|
-
*/
|
|
89441
|
-
function extractSynthResources(template) {
|
|
89442
|
-
if (!template || typeof template !== "object") return [];
|
|
89443
|
-
const resources = template["Resources"];
|
|
89444
|
-
if (!resources || typeof resources !== "object") return [];
|
|
89445
|
-
const out = [];
|
|
89446
|
-
for (const [logicalId, raw] of Object.entries(resources)) {
|
|
89447
|
-
if (!raw || typeof raw !== "object") continue;
|
|
89448
|
-
const r = raw;
|
|
89449
|
-
const type = typeof r["Type"] === "string" ? r["Type"] : "";
|
|
89450
|
-
if (type === "AWS::CDK::Metadata") continue;
|
|
89451
|
-
const props = r["Properties"] && typeof r["Properties"] === "object" ? r["Properties"] : {};
|
|
89452
|
-
const meta = r["Metadata"] && typeof r["Metadata"] === "object" ? r["Metadata"] : {};
|
|
89453
|
-
const path = typeof meta["aws:cdk:path"] === "string" ? meta["aws:cdk:path"] : "";
|
|
89454
|
-
out.push({
|
|
89455
|
-
logicalId,
|
|
89456
|
-
type,
|
|
89457
|
-
properties: props,
|
|
89458
|
-
awsCdkPath: path
|
|
89459
|
-
});
|
|
89460
|
-
}
|
|
89461
|
-
return out;
|
|
89462
|
-
}
|
|
89463
|
-
/**
|
|
89464
|
-
* Group synth entries by the LAST `/`-separated segment of their
|
|
89465
|
-
* `aws:cdk:path` metadata so Pass 1 can look up the source logical id
|
|
89466
|
-
* directly. Per [docs/design/465-cfn-migrate.md](../../../../docs/design/465-cfn-migrate.md) §5.5:
|
|
89467
|
-
* `cdk migrate` emits `<StackName>/<LogicalId>` as the metadata value,
|
|
89468
|
-
* so the last segment IS the source logical id in the typical case.
|
|
89469
|
-
*
|
|
89470
|
-
* Entries without an `aws:cdk:path` are silently skipped — they cannot
|
|
89471
|
-
* be addressed by source logical id and will only be reachable via
|
|
89472
|
-
* Pass 2's Properties match.
|
|
89473
|
-
*/
|
|
89474
|
-
function indexSynthByLastPathSegment(entries) {
|
|
89475
|
-
const out = /* @__PURE__ */ new Map();
|
|
89476
|
-
for (const e of entries) {
|
|
89477
|
-
if (!e.awsCdkPath) continue;
|
|
89478
|
-
const lastSegment = e.awsCdkPath.split("/").pop() ?? "";
|
|
89479
|
-
if (!lastSegment) continue;
|
|
89480
|
-
const bucket = out.get(lastSegment);
|
|
89481
|
-
if (bucket) bucket.push(e);
|
|
89482
|
-
else out.set(lastSegment, [e]);
|
|
89483
|
-
}
|
|
89484
|
-
return out;
|
|
89485
|
-
}
|
|
89486
|
-
/**
|
|
89487
|
-
* Recursive deep equality between two values, with two CFn-specific
|
|
89488
|
-
* accommodations:
|
|
89489
|
-
*
|
|
89490
|
-
* 1. Object key order is NOT significant — `JSON.stringify` would
|
|
89491
|
-
* diverge on the canonical-source-vs-synth-alphabetized case
|
|
89492
|
-
* (verified by PR A's empirical run, §5.5 point 3) so we walk the
|
|
89493
|
-
* keys explicitly.
|
|
89494
|
-
* 2. `AWS::NoValue` placeholder values are stripped before compare —
|
|
89495
|
-
* `cdk migrate`'s codegen sometimes emits the placeholder for a
|
|
89496
|
-
* property the source template simply omitted, and the structural
|
|
89497
|
-
* intent matches. Source side never has the placeholder; synth side
|
|
89498
|
-
* can on either side of a nested object.
|
|
89499
|
-
*
|
|
89500
|
-
* Arrays are compared positionally (order IS significant — Tags arrays
|
|
89501
|
-
* and similar carry ordering semantics).
|
|
89502
|
-
*/
|
|
89503
|
-
function deepEqualIgnoreNoValue(a, b) {
|
|
89504
|
-
if (a === b) return true;
|
|
89505
|
-
if (isAwsNoValue(a) !== isAwsNoValue(b)) return false;
|
|
89506
|
-
if (isAwsNoValue(a) && isAwsNoValue(b)) return true;
|
|
89507
|
-
if (typeof a !== typeof b) return false;
|
|
89508
|
-
if (a === null || b === null) return a === b;
|
|
89509
|
-
if (typeof a !== "object") return a === b;
|
|
89510
|
-
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
89511
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
89512
|
-
if (a.length !== b.length) return false;
|
|
89513
|
-
for (let i = 0; i < a.length; i++) if (!deepEqualIgnoreNoValue(a[i], b[i])) return false;
|
|
89514
|
-
return true;
|
|
89515
|
-
}
|
|
89516
|
-
const aObj = a;
|
|
89517
|
-
const bObj = b;
|
|
89518
|
-
const aKeys = Object.keys(aObj).filter((k) => !isAwsNoValue(aObj[k]) && aObj[k] !== void 0);
|
|
89519
|
-
const bKeys = Object.keys(bObj).filter((k) => !isAwsNoValue(bObj[k]) && bObj[k] !== void 0);
|
|
89520
|
-
if (aKeys.length !== bKeys.length) return false;
|
|
89521
|
-
const aKeySet = new Set(aKeys);
|
|
89522
|
-
for (const k of bKeys) {
|
|
89523
|
-
if (!aKeySet.has(k)) return false;
|
|
89524
|
-
if (!deepEqualIgnoreNoValue(aObj[k], bObj[k])) return false;
|
|
89525
|
-
}
|
|
89526
|
-
return true;
|
|
89527
|
-
}
|
|
89528
|
-
/**
|
|
89529
|
-
* True when the value is the CFn `AWS::NoValue` placeholder
|
|
89530
|
-
* (`{Ref: 'AWS::NoValue'}`). `cdk migrate`'s codegen emits this for
|
|
89531
|
-
* conditional properties the source template omitted; the structural
|
|
89532
|
-
* intent matches and deep-equal must treat them as absent.
|
|
89533
|
-
*/
|
|
89534
|
-
function isAwsNoValue(v) {
|
|
89535
|
-
if (!v || typeof v !== "object") return false;
|
|
89536
|
-
const obj = v;
|
|
89537
|
-
const keys = Object.keys(obj);
|
|
89538
|
-
return keys.length === 1 && keys[0] === "Ref" && obj["Ref"] === "AWS::NoValue";
|
|
89539
|
-
}
|
|
89540
|
-
|
|
89541
|
-
//#endregion
|
|
89542
|
-
//#region src/cli/commands/migrate/resource-mapping-file.ts
|
|
89543
|
-
/** Conventional filename inside the migrate output dir. */
|
|
89544
|
-
const RESOURCE_MAPPING_FILENAME = "cdkd-resource-mapping.json";
|
|
89545
|
-
/**
|
|
89546
|
-
* Build the on-disk file shape from a {@link ResourceMappingResult} +
|
|
89547
|
-
* the source / output stack names, then write it to
|
|
89548
|
-
* `<outputDir>/cdkd-resource-mapping.json` (overwriting any prior file
|
|
89549
|
-
* on the same path — same idempotency contract as `cdk migrate`'s own
|
|
89550
|
-
* codegen, and the user expects a re-run to refresh stale data).
|
|
89551
|
-
*
|
|
89552
|
-
* Returns the absolute on-disk path so the orchestrator can name it in
|
|
89553
|
-
* the error message when the mapping is incomplete.
|
|
89554
|
-
*/
|
|
89555
|
-
function writeMappingFile(outputDir, args) {
|
|
89556
|
-
const path = join(outputDir, RESOURCE_MAPPING_FILENAME);
|
|
89557
|
-
const file = {
|
|
89558
|
-
version: 1,
|
|
89559
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
89560
|
-
sourceStack: args.sourceStack,
|
|
89561
|
-
outputStack: args.outputStack,
|
|
89562
|
-
mapping: args.result.mapping
|
|
89563
|
-
};
|
|
89564
|
-
if (args.result.unmatched.length > 0) file._unmatched = args.result.unmatched;
|
|
89565
|
-
writeFileSync(path, JSON.stringify(file, null, 2) + "\n", "utf-8");
|
|
89566
|
-
return path;
|
|
89567
|
-
}
|
|
89568
|
-
/**
|
|
89569
|
-
* Read a user-supplied resource-mapping file from disk and return the
|
|
89570
|
-
* `{<srcLogicalId>: <synthLogicalId>}` overrides for the mapper.
|
|
89571
|
-
*
|
|
89572
|
-
* Tolerates `_unmatched` on input (a user that re-runs against a
|
|
89573
|
-
* partial-failure file without editing should hit the same failure on
|
|
89574
|
-
* the resolution side — not a parse error here). Hard-errors on
|
|
89575
|
-
* structurally invalid input (missing `mapping`, non-object root,
|
|
89576
|
-
* non-string values, wrong `version`).
|
|
89577
|
-
*
|
|
89578
|
-
* Surfaces every shape error as `LocalMigrateError` (exit code 2) so
|
|
89579
|
-
* the CLI handler routes it through the normal error path.
|
|
89580
|
-
*/
|
|
89581
|
-
function readMappingFile(path) {
|
|
89582
|
-
if (!existsSync(path)) throw new LocalMigrateError(`Resource-mapping file not found: ${path}. Drop the --resource-mapping flag or supply a valid path.`);
|
|
89583
|
-
let raw;
|
|
89584
|
-
try {
|
|
89585
|
-
raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
89586
|
-
} catch (err) {
|
|
89587
|
-
throw new LocalMigrateError(`Resource-mapping file '${path}' is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
89588
|
-
}
|
|
89589
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new LocalMigrateError(`Resource-mapping file '${path}' must contain a JSON object at the top level.`);
|
|
89590
|
-
const obj = raw;
|
|
89591
|
-
if (obj["version"] !== 1) throw new LocalMigrateError(`Resource-mapping file '${path}' has unsupported version '${String(obj["version"])}'. Expected version 1.`);
|
|
89592
|
-
if (typeof obj["sourceStack"] !== "string" || obj["sourceStack"].length === 0) throw new LocalMigrateError(`Resource-mapping file '${path}' is missing the required 'sourceStack' field.`);
|
|
89593
|
-
if (typeof obj["outputStack"] !== "string" || obj["outputStack"].length === 0) throw new LocalMigrateError(`Resource-mapping file '${path}' is missing the required 'outputStack' field.`);
|
|
89594
|
-
if (typeof obj["generatedAt"] !== "string") throw new LocalMigrateError(`Resource-mapping file '${path}' is missing the required 'generatedAt' field.`);
|
|
89595
|
-
const mappingRaw = obj["mapping"];
|
|
89596
|
-
if (!mappingRaw || typeof mappingRaw !== "object" || Array.isArray(mappingRaw)) throw new LocalMigrateError(`Resource-mapping file '${path}' is missing the required 'mapping' object.`);
|
|
89597
|
-
const mapping = {};
|
|
89598
|
-
for (const [k, v] of Object.entries(mappingRaw)) {
|
|
89599
|
-
if (typeof v !== "string") throw new LocalMigrateError(`Resource-mapping file '${path}' has a non-string value for key '${k}'.`);
|
|
89600
|
-
mapping[k] = v;
|
|
89601
|
-
}
|
|
89602
|
-
const result = {
|
|
89603
|
-
version: 1,
|
|
89604
|
-
generatedAt: obj["generatedAt"],
|
|
89605
|
-
sourceStack: obj["sourceStack"],
|
|
89606
|
-
outputStack: obj["outputStack"],
|
|
89607
|
-
mapping
|
|
89608
|
-
};
|
|
89609
|
-
if (Array.isArray(obj["_unmatched"])) {
|
|
89610
|
-
const cleaned = [];
|
|
89611
|
-
for (const entry of obj["_unmatched"]) {
|
|
89612
|
-
if (!entry || typeof entry !== "object") continue;
|
|
89613
|
-
const e = entry;
|
|
89614
|
-
if (typeof e["sourceLogicalId"] !== "string" || typeof e["resourceType"] !== "string" || !Array.isArray(e["candidates"]) || e["reason"] !== "no-match" && e["reason"] !== "logical-id-collision") continue;
|
|
89615
|
-
cleaned.push({
|
|
89616
|
-
sourceLogicalId: e["sourceLogicalId"],
|
|
89617
|
-
resourceType: e["resourceType"],
|
|
89618
|
-
candidates: e["candidates"].filter((c) => typeof c === "string"),
|
|
89619
|
-
reason: e["reason"]
|
|
89620
|
-
});
|
|
89621
|
-
}
|
|
89622
|
-
if (cleaned.length > 0) result._unmatched = cleaned;
|
|
89623
|
-
}
|
|
89624
|
-
return result;
|
|
89625
|
-
}
|
|
89626
|
-
|
|
89627
|
-
//#endregion
|
|
89628
|
-
//#region src/cli/commands/migrate-command.ts
|
|
89629
|
-
/**
|
|
89630
|
-
* `cdkd migrate --from-cfn-stack <name>` end-to-end orchestrator.
|
|
89631
|
-
*
|
|
89632
|
-
* Closes the CLI half of [#465](https://github.com/go-to-k/cdkd/issues/465).
|
|
89633
|
-
* Reads the source CFn stack, runs upstream `cdk migrate` via the PR A
|
|
89634
|
-
* library, builds the source → synth logical-ID mapping with the 2-pass
|
|
89635
|
-
* algorithm, writes a `cdkd-resource-mapping.json` audit file, prompts
|
|
89636
|
-
* for confirmation, invokes `cdkd import` to write state under the
|
|
89637
|
-
* synth logical IDs, and (when `--retire-cfn-stack` is set) finally
|
|
89638
|
-
* retires the source CFn stack so management responsibility transfers
|
|
89639
|
-
* fully to cdkd.
|
|
89640
|
-
*
|
|
89641
|
-
* AWS resources are never modified — the migration is a metadata
|
|
89642
|
-
* transfer only. See [docs/design/465-cfn-migrate.md](../../../docs/design/465-cfn-migrate.md) §7
|
|
89643
|
-
* for the post-migration state matrix.
|
|
89644
|
-
*/
|
|
89645
|
-
async function migrateCommandAction(positionalStack, options) {
|
|
89646
|
-
const logger = getLogger();
|
|
89647
|
-
if (options.verbose) {
|
|
89648
|
-
logger.setLevel("debug");
|
|
89649
|
-
process.env["CDKD_NO_LIVE"] = "1";
|
|
89650
|
-
}
|
|
89651
|
-
const sourceCfnStackName = options.fromCfnStack ?? positionalStack;
|
|
89652
|
-
if (!sourceCfnStackName) throw new LocalMigrateError("Missing required argument: --from-cfn-stack <name> (or pass the stack name positionally).");
|
|
89653
|
-
if (options.retireCfnStack && options.skipSynth) throw new LocalMigrateError("--retire-cfn-stack is incompatible with --skip-synth: the post-state-write retirement (UpdateStack + DeleteStack) cannot run without a synthesized template + cdkd state.");
|
|
89654
|
-
if (options.retireCfnStack && options.dryRun) throw new LocalMigrateError("--retire-cfn-stack is incompatible with --dry-run: retirement issues real AWS calls (UpdateStack injects Retain policies, then DeleteStack).");
|
|
89655
|
-
if (options.retireCfnStack && options.filter && options.filter.length > 0) throw new LocalMigrateError("--retire-cfn-stack is incompatible with --filter: a partial migration that retires the whole source CFn stack would strand the un-migrated resources. Drop one of the flags or migrate the rest of the stack first.");
|
|
89656
|
-
foldRegionOption(options);
|
|
89657
|
-
await applyRoleArnIfSet({
|
|
89658
|
-
roleArn: options.roleArn,
|
|
89659
|
-
region: options.region
|
|
89660
|
-
});
|
|
89661
|
-
const region = namedCliRegion(options.region) ?? "us-east-1";
|
|
89662
|
-
const outputDir = resolve(options.outputDir ?? sourceCfnStackName);
|
|
89663
|
-
logger.info(`[migrate] Source CFn stack: ${sourceCfnStackName}`);
|
|
89664
|
-
logger.info(`[migrate] Output directory: ${outputDir}`);
|
|
89665
|
-
const libResult = await runMigrateLibrary({
|
|
89666
|
-
fromCfnStack: sourceCfnStackName,
|
|
89667
|
-
outputDir,
|
|
89668
|
-
language: options.language ?? "typescript",
|
|
89669
|
-
...options.region && { region: options.region },
|
|
89670
|
-
...options.account && { account: options.account },
|
|
89671
|
-
...options.filter && options.filter.length > 0 && { filters: options.filter },
|
|
89672
|
-
...options.profile && { profile: options.profile },
|
|
89673
|
-
...options.cdkBin && { cdkBinPath: options.cdkBin },
|
|
89674
|
-
skipInstall: options.skipInstall ?? false,
|
|
89675
|
-
skipSynth: options.skipSynth ?? false
|
|
89676
|
-
});
|
|
89677
|
-
if (options.skipSynth || !libResult.templateBody) {
|
|
89678
|
-
logger.info(`[migrate] --skip-synth: generated CDK app at ${libResult.outputDir}. Re-run without --skip-synth to write cdkd state.`);
|
|
89679
|
-
return;
|
|
89680
|
-
}
|
|
89681
|
-
let overrides;
|
|
89682
|
-
if (options.resourceMapping) {
|
|
89683
|
-
const mappingPath = resolve(options.resourceMapping);
|
|
89684
|
-
logger.info(`[migrate] Loading user-supplied resource mapping from ${mappingPath}`);
|
|
89685
|
-
overrides = readMappingFile(mappingPath).mapping;
|
|
89686
|
-
}
|
|
89687
|
-
let mappingResult;
|
|
89688
|
-
try {
|
|
89689
|
-
mappingResult = buildResourceMapping({
|
|
89690
|
-
sourceCfnTemplate: libResult.sourceCfnTemplate,
|
|
89691
|
-
synthTemplate: libResult.templateBody,
|
|
89692
|
-
sourceResources: libResult.sourceResources,
|
|
89693
|
-
...overrides && { overrides }
|
|
89694
|
-
});
|
|
89695
|
-
} catch (err) {
|
|
89696
|
-
throw new LocalMigrateError(err instanceof Error ? err.message : String(err));
|
|
89697
|
-
}
|
|
89698
|
-
const outputStack = sourceCfnStackName;
|
|
89699
|
-
const mappingFilePath = writeMappingFile(libResult.outputDir, {
|
|
89700
|
-
sourceStack: sourceCfnStackName,
|
|
89701
|
-
outputStack,
|
|
89702
|
-
result: mappingResult
|
|
89703
|
-
});
|
|
89704
|
-
logger.info(`[migrate] Resource mapping written to ${mappingFilePath}`);
|
|
89705
|
-
if (mappingResult.unmatched.length > 0) {
|
|
89706
|
-
const lines = mappingResult.unmatched.map((u) => {
|
|
89707
|
-
const candidatesStr = u.candidates.length > 0 ? ` (synth candidates of same Type: ${u.candidates.join(", ")})` : " (no synth resource has the same Type)";
|
|
89708
|
-
return ` - ${u.sourceLogicalId} (${u.resourceType}) [${u.reason}]${candidatesStr}`;
|
|
89709
|
-
});
|
|
89710
|
-
throw new LocalMigrateError(`Could not auto-map ${mappingResult.unmatched.length} of ${mappingResult.unmatched.length + mappingResult.pairs.length} source resource(s) to the synthesized CDK template:\n${lines.join("\n")}\n\nEdit '${mappingFilePath}'\nto add the correct '<sourceLogicalId>: <synthLogicalId>' pairs under "mapping", then re-run:\n cdkd migrate --from-cfn-stack ${sourceCfnStackName} --resource-mapping ${mappingFilePath} --output-dir ${libResult.outputDir}\n(The --output-dir override re-uses the already-generated CDK code; drop it if you let the default location be used.)`);
|
|
89711
|
-
}
|
|
89712
|
-
printMappingTable(mappingResult, sourceCfnStackName, outputStack);
|
|
89713
|
-
if (options.dryRun) {
|
|
89714
|
-
logger.info(`[migrate] --dry-run: would import ${mappingResult.pairs.length} resource(s) into cdkd state under stack '${outputStack}' (region '${region}'). State NOT written.`);
|
|
89715
|
-
return;
|
|
89716
|
-
}
|
|
89717
|
-
if (!options.yes) {
|
|
89718
|
-
if (!await promptConfirm(`Import ${mappingResult.pairs.length} resource(s) into cdkd state for stack '${outputStack}' (${region})?`)) {
|
|
89719
|
-
logger.info("[migrate] Cancelled by user. No state written.");
|
|
89720
|
-
return;
|
|
89721
|
-
}
|
|
89722
|
-
}
|
|
89723
|
-
const importMapping = {};
|
|
89724
|
-
for (const p of mappingResult.pairs) importMapping[p.synthLogicalId] = p.physicalId;
|
|
89725
|
-
const resolvedStateBucket = await resolveStateBucketWithDefault(options.stateBucket, region);
|
|
89726
|
-
await runImport(outputStack, {
|
|
89727
|
-
app: libResult.assemblyDir,
|
|
89728
|
-
statePrefix: options.statePrefix ?? "cdkd",
|
|
89729
|
-
resourceMappingInline: JSON.stringify(importMapping),
|
|
89730
|
-
auto: false,
|
|
89731
|
-
dryRun: false,
|
|
89732
|
-
yes: true,
|
|
89733
|
-
force: false,
|
|
89734
|
-
verbose: options.verbose ?? false,
|
|
89735
|
-
stateBucket: resolvedStateBucket,
|
|
89736
|
-
...options.region && { region: options.region },
|
|
89737
|
-
...options.profile && { profile: options.profile },
|
|
89738
|
-
...options.roleArn && { roleArn: options.roleArn }
|
|
89739
|
-
});
|
|
89740
|
-
if (options.retireCfnStack) {
|
|
89741
|
-
logger.info(`[migrate] Retiring source CloudFormation stack '${sourceCfnStackName}'...`);
|
|
89742
|
-
const cfnConfig = {};
|
|
89743
|
-
if (options.region) cfnConfig.region = options.region;
|
|
89744
|
-
if (options.profile) cfnConfig.profile = options.profile;
|
|
89745
|
-
const awsClients = new AwsClients(cfnConfig);
|
|
89746
|
-
const cfnClient = awsClients.cloudFormation;
|
|
89747
|
-
try {
|
|
89748
|
-
await retireCloudFormationStack({
|
|
89749
|
-
cfnStackName: sourceCfnStackName,
|
|
89750
|
-
cfnClient,
|
|
89751
|
-
yes: options.yes ?? false,
|
|
89752
|
-
stateBucket: resolvedStateBucket,
|
|
89753
|
-
...options.profile && { s3ClientOpts: { profile: options.profile } }
|
|
89754
|
-
});
|
|
89755
|
-
} finally {
|
|
89756
|
-
awsClients.destroy();
|
|
89757
|
-
}
|
|
89758
|
-
}
|
|
89759
|
-
logger.info(`[migrate] Migrated ${mappingResult.pairs.length} resource(s) from CloudFormation stack '${sourceCfnStackName}' to cdkd state for stack '${outputStack}'.`);
|
|
89760
|
-
logger.info(`[migrate] Generated CDK app at ${libResult.outputDir}`);
|
|
89761
|
-
if (!options.retireCfnStack) logger.info(`[migrate] Source CloudFormation stack '${sourceCfnStackName}' is unchanged. Re-run with --retire-cfn-stack to retire it (or do so manually later).`);
|
|
89762
|
-
}
|
|
89763
|
-
/**
|
|
89764
|
-
* Print a tabular `(sourceLogicalId → synthLogicalId physicalId)`
|
|
89765
|
-
* summary so users can see what's about to be imported before the
|
|
89766
|
-
* confirmation prompt. Matches the visual shape of `cdkd import`'s
|
|
89767
|
-
* summary table for consistency.
|
|
89768
|
-
*/
|
|
89769
|
-
function printMappingTable(result, sourceStack, outputStack) {
|
|
89770
|
-
const logger = getLogger();
|
|
89771
|
-
logger.info("");
|
|
89772
|
-
logger.info(`[migrate] Resolved mapping (${sourceStack} → ${outputStack}):`);
|
|
89773
|
-
for (const p of result.pairs) logger.info(` ${p.sourceLogicalId} → ${p.synthLogicalId} [${p.resourceType}] ${p.physicalId}`);
|
|
89774
|
-
logger.info("");
|
|
89775
|
-
}
|
|
89776
|
-
/**
|
|
89777
|
-
* Minimal `(y/N)` confirmation prompt using `node:readline`. Mirrors
|
|
89778
|
-
* the shape of [src/cli/commands/import.ts](import.ts)'s `confirmPrompt`
|
|
89779
|
-
* so the user UX is consistent across the import + migrate surfaces.
|
|
89780
|
-
* Non-TTY callers (CI) should pass `--yes` to skip this entirely.
|
|
89781
|
-
*/
|
|
89782
|
-
async function promptConfirm(message) {
|
|
89783
|
-
if (!process.stdin.isTTY) throw new LocalMigrateError(`Non-interactive shell detected and --yes was not supplied. Re-run with --yes to confirm: "${message}"`);
|
|
89784
|
-
const rl = (await import("node:readline/promises")).createInterface({
|
|
89785
|
-
input: process.stdin,
|
|
89786
|
-
output: process.stdout
|
|
89787
|
-
});
|
|
89788
|
-
try {
|
|
89789
|
-
const v = (await rl.question(`${message} (y/N) `)).trim().toLowerCase();
|
|
89790
|
-
return v === "y" || v === "yes";
|
|
89791
|
-
} finally {
|
|
89792
|
-
rl.close();
|
|
89793
|
-
}
|
|
89794
|
-
}
|
|
89795
|
-
/**
|
|
89796
|
-
* Commander factory for `cdkd migrate`. Registered in `src/cli/index.ts`
|
|
89797
|
-
* alongside the existing top-level commands. Matches the design doc §3
|
|
89798
|
-
* flag set; the action handler routes through {@link migrateCommandAction}
|
|
89799
|
-
* via `withErrorHandling` so library callers see exceptions but CLI
|
|
89800
|
-
* callers get the standard exit-code-2 routing.
|
|
89801
|
-
*/
|
|
89802
|
-
function createMigrateCommand() {
|
|
89803
|
-
return new Command("migrate").description("Adopt a plain (non-CDK) CloudFormation stack into a cdkd-managed CDK app. Generates new CDK code via upstream `cdk migrate`, builds a logical-ID mapping between the source CFn template and the synth template, writes cdkd state, and (optionally) retires the source CFn stack. AWS resources are never modified.").argument("[stack]", "Source CFn stack name. Alias for --from-cfn-stack.").addOption(new Option("--from-cfn-stack <name>", "Source CloudFormation stack name to adopt. Required (or pass positionally).")).addOption(new Option("--output-dir <dir>", "Directory to write the generated CDK app to. Defaults to <cwd>/<CfnStackName>.")).addOption(new Option("--language <choice>", "Generated code language. v1: typescript only.").choices(["typescript"]).default("typescript")).addOption(new Option("--region <region>", "AWS region. Defaults to AWS_REGION env / profile.")).addOption(new Option("--account <id>", "AWS account ID. Auto-detected via STS when omitted.")).addOption(new Option("--retire-cfn-stack", "After cdkd state is written, inject DeletionPolicy=Retain on every resource in the source CFn stack and DeleteStack. AWS resources stay; the CFn stack record is gone. Off by default.").default(false)).addOption(new Option("--filter <key=value>", "Pass-through to `cdk migrate --filter` for resource subsetting. Repeatable.").argParser((value, previous) => [...previous ?? [], value]).default([])).addOption(new Option("--skip-install", "Skip `npm install` after codegen.").default(false)).addOption(new Option("--skip-synth", "Skip `cdk synth` (does NOT write cdkd state). Mutually exclusive with --retire-cfn-stack.").default(false)).addOption(new Option("--dry-run", "Print the import plan without writing state or retiring the CFn stack. Mutually exclusive with --retire-cfn-stack.").default(false)).addOption(new Option("-y, --yes", "Auto-confirm the import + retirement prompts.").default(false)).addOption(new Option("--cdk-bin <path>", "Override the `cdk` binary path.")).addOption(new Option("--resource-mapping <file>", `Path to a JSON file of {sourceLogicalId: synthLogicalId} overrides. Same shape as the auto-written ${RESOURCE_MAPPING_FILENAME}.`)).addOption(new Option("--state-bucket <name>", "cdkd state bucket. Defaults to cdkd-state-<accountId>.")).addOption(new Option("--state-prefix <prefix>", "cdkd state prefix inside the bucket.").default(DEFAULT_STATE_PREFIX)).addOption(new Option("--profile <name>", "AWS profile name.")).addOption(new Option("--role-arn <arn>", "IAM role to assume before any AWS call.")).addOption(new Option("--verbose", "Enable debug-level logging.").default(false)).action(withErrorHandling(migrateCommandAction));
|
|
89804
|
-
}
|
|
89805
|
-
|
|
89806
88908
|
//#endregion
|
|
89807
88909
|
//#region src/cli/program.ts
|
|
89808
88910
|
/**
|
|
@@ -89846,11 +88948,10 @@ function buildProgram() {
|
|
|
89846
88948
|
program.addCommand(createStateCommand());
|
|
89847
88949
|
program.addCommand(createLocalCommand());
|
|
89848
88950
|
program.addCommand(createExportCommand());
|
|
89849
|
-
program.addCommand(createMigrateCommand());
|
|
89850
88951
|
guardStackRegionOptions(program);
|
|
89851
88952
|
return program;
|
|
89852
88953
|
}
|
|
89853
88954
|
|
|
89854
88955
|
//#endregion
|
|
89855
88956
|
export { buildProgram };
|
|
89856
|
-
//# sourceMappingURL=program-
|
|
88957
|
+
//# sourceMappingURL=program-x67eZ06V.js.map
|