@go-to-k/cdkd 0.281.23 → 0.281.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/{asg-provider-CCSQkGwO.js → asg-provider-Cm5r4o8r.js} +2 -2
- package/dist/{asg-provider-CCSQkGwO.js.map → asg-provider-Cm5r4o8r.js.map} +1 -1
- package/dist/cli.js +862 -83
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-uxo_FCje.js → deploy-engine-C8Kq3Cei.js} +3 -3
- package/dist/{deploy-engine-uxo_FCje.js.map → deploy-engine-C8Kq3Cei.js.map} +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as configStringRefusal, $t as MIGRATE_TMP_PREFIX, A as green, At as validateContainerRepoName, B as slowCcOperationTimeoutMs, Bt as getDefaultStateBucketName, C as MULTI_REGION_RECREATE_BLOCKED_TYPES, Cn as PartialFailureError, Ct as rewriteTemplateAssetReferences, D as bold, Dn as StackHasActiveImportsError, Dt as getBootstrapMarkerKey, E as formatResourceLine, En as ResourceUpdateNotSupportedError, Et as ensureAssetStorage, F as clearOnUpdateRemoval, Ft as runDockerStreaming, G as getAccountInfo, Gt as resolveSkipPrefix, H as isTerminationProtectionPropagationError, Ht as resolveApp, I as ProviderRegistry, It as AssetManifestLoader, J as normalizeAwsTagsToCfn, Jt as resolveUseCdkBootstrapAssets, K as refStateLookupFromResource, Kt as resolveStateBucketWithDefault, L as findActionableSilentDrops, Lt as getDockerImageBySourceHash, M as yellow, Mt as formatDockerLoginError, N as IAMRoleProvider, Nn as normalizeAwsError, Nt as getDockerCmd, O as cyan, On as StackTerminationProtectionError, Ot as parseBootstrapMarker, P as collectInlinePolicyNamesManagedBySiblings, Pn as withErrorHandling, Pt as runDockerForeground, Q as configBooleanRefusal, Qt as CFN_TEMPLATE_URL_LIMIT, R as findSilentDropProperties, Rt as Synthesizer, S as extractDeploymentEventError, Sn as NestedStackChildDirectDestroyError, St as loadPublishableAssetManifest, T as renderStatefulReason, Tn as ResourceTimeoutError, Tt as BOOTSTRAP_MARKER_PREFIX, U as IntrinsicFunctionResolver, Ut as resolveAutoAssetStorage, V as disableInstanceApiTermination, Vt as getLegacyStateBucketName, W as cfnRefValueFromPhysicalId, Wt as resolveCaptureObservedState, X as assertRegionMatch, Xt as warnDeprecatedNoPrefixCliFlag, Y as resolveExplicitPhysicalId, Yt as stateBucketExistenceConfirmed, Z as coerceCfnBoolean, Zt as CFN_TEMPLATE_BODY_LIMIT, _ as createPreDeleteFinalSnapshot, _n as LocalInvokeBuildError$1, _t as AssetPublisher, a as DeploymentEventsStore, an as processStackMessages, at as applyRoleArnIfSet, b as unsupportedFinalSnapshotError, bt as buildAssetRedirectMap, c as replayFailedOperations, cn as AwsClients, ct as withRetry, d as IMPLICIT_DELETE_DEPENDENCIES, dn as setAwsClients, dt as DagBuilder, en as findLargeInlineResources, et as readConfigString, f as computeImplicitDeleteEdges, ft as TemplateParser, g as ccRoutedFinalSnapshotError, gn as DeployCancelledError, gt as shouldRetainResource, h as buildFinalSnapshotIdentifier, ht as rebuildClientForBucketRegion, i as DeploymentEventsReader, in as AssemblyReader, it as requireConfigString, j as red, jt as buildDockerImage, k as gray, kt as validateAssetBucketName, l as replayRollback, ln as getAwsClients, lt as isRetryableTransientError, m as PRE_DELETE_SNAPSHOT_TYPES, mt as S3StateBackend, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as expectedOwnerParam, nt as requireConfigArray, o as planFailedOps, ot as DiffCalculator, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as CdkdError, pt as LockManager, q as WAFv2WebACLProvider, qt as resolveStateBucketWithDefaultAndSource, r as DeployEngine, rn as derivePartitionAndUrlSuffix, rt as requireConfigObject, s as planRollback, sn as resolveBucketRegion, st as describeTypeWithThrottleRetry, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as uploadCfnTemplate, tt as replayWarn, u as withResourceDeadline, un as resetAwsClients, ut as isThrottlingError, v as isFinalSnapshotError, vn as LocalMigrateError, vt as stringifyValue, w as isStatefulRecreateTargetSync, wn as ProvisioningError, wt as AssetModeResolver, x as makeCanonicalizePropertiesFn, xn as MissingCdkCliError, xt as createAssetRedirectResolver, y as refusesFinalSnapshot, yn as LocalStartServiceError, yt as WorkGraph, z as CloudControlProvider, zt as synthesisStatusMessage } from "./deploy-engine-
|
|
2
|
+
import { $ as configStringRefusal, $t as MIGRATE_TMP_PREFIX, A as green, At as validateContainerRepoName, B as slowCcOperationTimeoutMs, Bt as getDefaultStateBucketName, C as MULTI_REGION_RECREATE_BLOCKED_TYPES, Cn as PartialFailureError, Ct as rewriteTemplateAssetReferences, D as bold, Dn as StackHasActiveImportsError, Dt as getBootstrapMarkerKey, E as formatResourceLine, En as ResourceUpdateNotSupportedError, Et as ensureAssetStorage, F as clearOnUpdateRemoval, Ft as runDockerStreaming, G as getAccountInfo, Gt as resolveSkipPrefix, H as isTerminationProtectionPropagationError, Ht as resolveApp, I as ProviderRegistry, It as AssetManifestLoader, J as normalizeAwsTagsToCfn, Jt as resolveUseCdkBootstrapAssets, K as refStateLookupFromResource, Kt as resolveStateBucketWithDefault, L as findActionableSilentDrops, Lt as getDockerImageBySourceHash, M as yellow, Mt as formatDockerLoginError, N as IAMRoleProvider, Nn as normalizeAwsError, Nt as getDockerCmd, O as cyan, On as StackTerminationProtectionError, Ot as parseBootstrapMarker, P as collectInlinePolicyNamesManagedBySiblings, Pn as withErrorHandling, Pt as runDockerForeground, Q as configBooleanRefusal, Qt as CFN_TEMPLATE_URL_LIMIT, R as findSilentDropProperties, Rt as Synthesizer, S as extractDeploymentEventError, Sn as NestedStackChildDirectDestroyError, St as loadPublishableAssetManifest, T as renderStatefulReason, Tn as ResourceTimeoutError, Tt as BOOTSTRAP_MARKER_PREFIX, U as IntrinsicFunctionResolver, Ut as resolveAutoAssetStorage, V as disableInstanceApiTermination, Vt as getLegacyStateBucketName, W as cfnRefValueFromPhysicalId, Wt as resolveCaptureObservedState, X as assertRegionMatch, Xt as warnDeprecatedNoPrefixCliFlag, Y as resolveExplicitPhysicalId, Yt as stateBucketExistenceConfirmed, Z as coerceCfnBoolean, Zt as CFN_TEMPLATE_BODY_LIMIT, _ as createPreDeleteFinalSnapshot, _n as LocalInvokeBuildError$1, _t as AssetPublisher, a as DeploymentEventsStore, an as processStackMessages, at as applyRoleArnIfSet, b as unsupportedFinalSnapshotError, bt as buildAssetRedirectMap, c as replayFailedOperations, cn as AwsClients, ct as withRetry, d as IMPLICIT_DELETE_DEPENDENCIES, dn as setAwsClients, dt as DagBuilder, en as findLargeInlineResources, et as readConfigString, f as computeImplicitDeleteEdges, ft as TemplateParser, g as ccRoutedFinalSnapshotError, gn as DeployCancelledError, gt as shouldRetainResource, h as buildFinalSnapshotIdentifier, ht as rebuildClientForBucketRegion, i as DeploymentEventsReader, in as AssemblyReader, it as requireConfigString, j as red, jt as buildDockerImage, k as gray, kt as validateAssetBucketName, l as replayRollback, ln as getAwsClients, lt as isRetryableTransientError, m as PRE_DELETE_SNAPSHOT_TYPES, mt as S3StateBackend, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as expectedOwnerParam, nt as requireConfigArray, o as planFailedOps, ot as DiffCalculator, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as CdkdError, pt as LockManager, q as WAFv2WebACLProvider, qt as resolveStateBucketWithDefaultAndSource, r as DeployEngine, rn as derivePartitionAndUrlSuffix, rt as requireConfigObject, s as planRollback, sn as resolveBucketRegion, st as describeTypeWithThrottleRetry, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as uploadCfnTemplate, tt as replayWarn, u as withResourceDeadline, un as resetAwsClients, ut as isThrottlingError, v as isFinalSnapshotError, vn as LocalMigrateError, vt as stringifyValue, w as isStatefulRecreateTargetSync, wn as ProvisioningError, wt as AssetModeResolver, x as makeCanonicalizePropertiesFn, xn as MissingCdkCliError, xt as createAssetRedirectResolver, y as refusesFinalSnapshot, yn as LocalStartServiceError, yt as WorkGraph, z as CloudControlProvider, zt as synthesisStatusMessage } from "./deploy-engine-C8Kq3Cei.js";
|
|
3
3
|
import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, h as withStackName, i as runStackBuffered, m as withSkipPrefix, n as getLogger, o as PATTERN_B_NAME_OPTIONS, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-zRrlbaQt.js";
|
|
4
|
-
import { t as ASGProvider } from "./asg-provider-
|
|
4
|
+
import { t as ASGProvider } from "./asg-provider-Cm5r4o8r.js";
|
|
5
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
6
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
7
7
|
import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketEncryptionCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketOwnershipControlsCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
|
|
@@ -5172,10 +5172,81 @@ function foldToleratedAliases(bag, aliases) {
|
|
|
5172
5172
|
}
|
|
5173
5173
|
return out ?? bag;
|
|
5174
5174
|
}
|
|
5175
|
+
/** The transition date member, held in a CONST so the fold writes it COMPUTED. */
|
|
5176
|
+
const TRANSITION_DATE_KEY = "TransitionDate";
|
|
5175
5177
|
/** The lifecycle `Transitions[]` aliases the applier accepts (issue #1748). */
|
|
5176
5178
|
const TRANSITION_ALIASES = [["TransitionInDays", "Days"], ["TransitionDate", "Date"]];
|
|
5177
5179
|
/** The lifecycle `NoncurrentVersionTransitions[]` alias (issue #1748). */
|
|
5178
5180
|
const NONCURRENT_VERSION_TRANSITION_ALIASES = [["TransitionInDays", "NoncurrentDays"]];
|
|
5181
|
+
/** The refusal path for the notification EventBridge boolean (issue #1759). */
|
|
5182
|
+
const EVENTBRIDGE_CONFIG_PATH = "AWS::S3::Bucket NotificationConfiguration.EventBridgeConfiguration";
|
|
5183
|
+
/**
|
|
5184
|
+
* The block's own key, held in a CONST so the fold below writes it as a
|
|
5185
|
+
* COMPUTED key.
|
|
5186
|
+
*
|
|
5187
|
+
* Load-bearing, not style: `gen-nested-key-coverage`'s write-evidence walk reads
|
|
5188
|
+
* a bag-derived spread-and-patch literal that NAMES a member
|
|
5189
|
+
* (`{ ...config, EventBridgeConfiguration: … }`) as the #1475 whole-blob
|
|
5190
|
+
* HAND-OFF and wildcard-credits everything beneath that path as DELIVERED — so
|
|
5191
|
+
* a DIFF-side fold spelled that way would switch the write pass off for the
|
|
5192
|
+
* whole notification subtree, and the critic's only visible complaint would be
|
|
5193
|
+
* that some allow-list entries went "stale". Same reason
|
|
5194
|
+
* {@link canonicalizeBlock} exists.
|
|
5195
|
+
*/
|
|
5196
|
+
const EVENTBRIDGE_CONFIG_KEY = "EventBridgeConfiguration";
|
|
5197
|
+
/**
|
|
5198
|
+
* What a DECLARED `EventBridgeConfiguration` block with no `EventBridgeEnabled`
|
|
5199
|
+
* key means. CFn marks the member required, so the block's presence is the
|
|
5200
|
+
* affirmative declaration; this is also the pre-#1759 behavior for that shape.
|
|
5201
|
+
*/
|
|
5202
|
+
const EVENTBRIDGE_DEFAULT_ENABLED = true;
|
|
5203
|
+
/**
|
|
5204
|
+
* The boolean the wire derives from a USABLE `EventBridgeConfiguration` block
|
|
5205
|
+
* (issue #1759) — the ONE definition `applyNotificationConfiguration` and
|
|
5206
|
+
* {@link effectiveNotificationConfiguration} share, so the value that goes on
|
|
5207
|
+
* the wire and the value recorded into state cannot drift apart.
|
|
5208
|
+
*
|
|
5209
|
+
* Callers MUST have cleared `configBooleanRefusal` first; the `??` is only
|
|
5210
|
+
* correct once every declared-but-unusable value has been refused.
|
|
5211
|
+
*/
|
|
5212
|
+
function eventBridgeEnabledOf(eb) {
|
|
5213
|
+
return coerceCfnBoolean(isPlainObject$1(eb) ? eb["EventBridgeEnabled"] : void 0) ?? EVENTBRIDGE_DEFAULT_ENABLED;
|
|
5214
|
+
}
|
|
5215
|
+
/** The notification families whose empty declaration is never SENT (issue #1759). */
|
|
5216
|
+
const NOTIFICATION_LIST_KEYS = [
|
|
5217
|
+
"TopicConfigurations",
|
|
5218
|
+
"QueueConfigurations",
|
|
5219
|
+
"LambdaConfigurations"
|
|
5220
|
+
];
|
|
5221
|
+
/**
|
|
5222
|
+
* Drop a declared-but-EMPTY collection the applier never sends (issue #1759).
|
|
5223
|
+
*
|
|
5224
|
+
* MEASURED before choosing this answer, because the sibling shape answers the
|
|
5225
|
+
* opposite way: the lifecycle / CORS empty-collection skip RECORDS its
|
|
5226
|
+
* placeholder (issue #1718) precisely because `readLifecycle` / `readCors`
|
|
5227
|
+
* always emit `{Rules: []}` / `{CorsRules: []}` for an unconfigured bucket, so
|
|
5228
|
+
* the declared empty collection already equals the readback. `readNotification`
|
|
5229
|
+
* does NOT: each family is emitted only under
|
|
5230
|
+
* `resp.<Family> && resp.<Family>.length > 0`, so an unconfigured bucket comes
|
|
5231
|
+
* back with the key ABSENT. A record keeping `[]` therefore describes a key set
|
|
5232
|
+
* AWS can never report, and the honest answer here is the DROP.
|
|
5233
|
+
*
|
|
5234
|
+
* Scoped to a genuinely EMPTY ARRAY. A non-array declaration is skipped by the
|
|
5235
|
+
* applier's `Array.isArray(...)` gate too, but it is MALFORMED: dropping it
|
|
5236
|
+
* would make the desired side compare equal and stop the difference being
|
|
5237
|
+
* reported, which is the concealment `.claude/rules/providers.md` finding 3
|
|
5238
|
+
* refuses. It is left visible instead.
|
|
5239
|
+
*/
|
|
5240
|
+
function dropEmptyNotificationFamilies(config) {
|
|
5241
|
+
let out;
|
|
5242
|
+
for (const listKey of NOTIFICATION_LIST_KEYS) {
|
|
5243
|
+
const list = config[listKey];
|
|
5244
|
+
if (!Array.isArray(list) || list.length > 0) continue;
|
|
5245
|
+
out ??= { ...config };
|
|
5246
|
+
delete out[listKey];
|
|
5247
|
+
}
|
|
5248
|
+
return out ?? config;
|
|
5249
|
+
}
|
|
5179
5250
|
/** The `NotificationConfiguration` ARN aliases, per family (issue #1748). */
|
|
5180
5251
|
const NOTIFICATION_ARN_ALIASES = [
|
|
5181
5252
|
["TopicConfigurations", [["Topic", "TopicArn"]]],
|
|
@@ -5248,6 +5319,439 @@ function effectiveNotificationConfiguration(config) {
|
|
|
5248
5319
|
if (!isPlainObject$1(config)) return config;
|
|
5249
5320
|
let out = config;
|
|
5250
5321
|
for (const [listKey, aliases] of NOTIFICATION_ARN_ALIASES) out = canonicalizeItemList(out, listKey, (item) => foldNotificationEvents(foldToleratedAliases(item, aliases)));
|
|
5322
|
+
out = dropEmptyNotificationFamilies(out);
|
|
5323
|
+
return foldEventBridgeConfiguration(out);
|
|
5324
|
+
}
|
|
5325
|
+
/**
|
|
5326
|
+
* Normalize `EventBridgeConfiguration` onto the shape `readNotification` emits
|
|
5327
|
+
* (issue #1759, items 2 and 3 of the same block).
|
|
5328
|
+
*
|
|
5329
|
+
* CFn is stringly typed, so `EventBridgeEnabled: 'true'` is a legitimate
|
|
5330
|
+
* declaration; the wire turns the value into presence/absence and
|
|
5331
|
+
* `readNotification` emits the BOOLEAN (`{EventBridgeEnabled: <bool>}`, the
|
|
5332
|
+
* #1430 shape). A record holding the STRING can therefore never match the
|
|
5333
|
+
* readback. This is #1633's lossless-coercion arm and it meets the #1643 bar —
|
|
5334
|
+
* what AWS reports back is exactly the boolean cdkd derived at send time. A
|
|
5335
|
+
* declared `null` folds to `false` for the same reason: the applier reads it as
|
|
5336
|
+
* "block omitted" and sends nothing.
|
|
5337
|
+
*
|
|
5338
|
+
* Keyed off the DECLARED shape and off the wire's own predicate, never off a
|
|
5339
|
+
* refusal: a value `configBooleanRefusal` rejects is left COMPLETELY alone (no
|
|
5340
|
+
* patch, and the key not dropped), because the wire SKIPS the whole Put for it
|
|
5341
|
+
* and AWS keeps the previous configuration. Folding it would collapse the
|
|
5342
|
+
* desired side onto the retained value, the provider would never be called, and
|
|
5343
|
+
* the skip warning would stop — the "mirror what the wire does" rule.
|
|
5344
|
+
*
|
|
5345
|
+
* MEASURED AND DELIBERATELY NOT FOLDED (1) — the per-item `Id`, which issue
|
|
5346
|
+
* #1755 asked to be measured rather than assumed. The applier sends
|
|
5347
|
+
* `Id: t['Id']`, so a template declaring none sends none, AWS GENERATES one, and
|
|
5348
|
+
* `readNotification` emits it (`if (t.Id !== undefined) e['Id'] = t.Id`). That is
|
|
5349
|
+
* an AWS-COMPUTED value, not a spelling: it belongs in `observedProperties`,
|
|
5350
|
+
* which `readCurrentState` already populates, and `.claude/rules/providers.md` is
|
|
5351
|
+
* explicit that a computed value in `properties` makes the DESIRED baseline drift
|
|
5352
|
+
* from the template and disables the #1160 absent-field removal derivation. There
|
|
5353
|
+
* is also nothing to fold TOWARDS — the value does not exist at send time, which
|
|
5354
|
+
* is the #1643 "knowable at send time" test failing. So: no fold, by measurement
|
|
5355
|
+
* rather than by omission.
|
|
5356
|
+
*
|
|
5357
|
+
* MEASURED AND DELIBERATELY NOT FOLDED (2): `readNotification` emits
|
|
5358
|
+
* `EventBridgeConfiguration` UNCONDITIONALLY, so a notification configuration
|
|
5359
|
+
* that declares no EventBridge block at all is one key short of the readback.
|
|
5360
|
+
* That is NOT this class — nothing was declared, so there is no spelling to
|
|
5361
|
+
* normalize, and supplying the key would record one the template does not have
|
|
5362
|
+
* on EVERY notification-configured bucket (the shape #1723 records as the
|
|
5363
|
+
* reason its own arm was left unanswered). `readNotification`'s own comment
|
|
5364
|
+
* already scopes that difference: it is a ONE-TIME, self-clearing drift on the
|
|
5365
|
+
* `properties`-fallback baseline, deliberately accepted by #1430, not the
|
|
5366
|
+
* permanent phantom drift this fold exists to remove.
|
|
5367
|
+
*
|
|
5368
|
+
* @returns `config` BY IDENTITY when nothing is declared, when the declared
|
|
5369
|
+
* block is already the emitted shape, or when it is unfoldable.
|
|
5370
|
+
*/
|
|
5371
|
+
function foldEventBridgeConfiguration(config) {
|
|
5372
|
+
const eb = config[EVENTBRIDGE_CONFIG_KEY];
|
|
5373
|
+
if (eb === void 0) return config;
|
|
5374
|
+
if (eb !== null && configBooleanRefusal(eb, "EventBridgeEnabled", EVENTBRIDGE_CONFIG_PATH)) return config;
|
|
5375
|
+
const enabled = eb === null ? false : eventBridgeEnabledOf(eb);
|
|
5376
|
+
const declared = isPlainObject$1(eb) ? eb["EventBridgeEnabled"] : void 0;
|
|
5377
|
+
if (isPlainObject$1(eb) && Object.keys(eb).length === 1 && Object.is(declared, enabled)) return config;
|
|
5378
|
+
return {
|
|
5379
|
+
...config,
|
|
5380
|
+
[EVENTBRIDGE_CONFIG_KEY]: { EventBridgeEnabled: enabled }
|
|
5381
|
+
};
|
|
5382
|
+
}
|
|
5383
|
+
/** The refusal paths the lifecycle applier and its fold SHARE (issue #1755). */
|
|
5384
|
+
const LIFECYCLE_RULE_PATH = "AWS::S3::Bucket LifecycleConfiguration.Rules[]";
|
|
5385
|
+
const LIFECYCLE_FILTER_PATH = `${LIFECYCLE_RULE_PATH}.Filter`;
|
|
5386
|
+
const LIFECYCLE_TAG_FILTERS_PATH = `${LIFECYCLE_RULE_PATH}.TagFilters`;
|
|
5387
|
+
/** What `readConfigString(rule, 'Status', ...)` substitutes for an ABSENT key. */
|
|
5388
|
+
const LIFECYCLE_DEFAULT_STATUS = "Enabled";
|
|
5389
|
+
/** The rule's status member, held in a CONST so the fold writes it COMPUTED. */
|
|
5390
|
+
const LIFECYCLE_STATUS_KEY = "Status";
|
|
5391
|
+
/**
|
|
5392
|
+
* Gather a rule's full location scope — module-level so the applier and the
|
|
5393
|
+
* fold run the SAME function rather than two copies that can disagree about
|
|
5394
|
+
* which key wins (`.claude/rules/providers.md`: share ONE helper with the
|
|
5395
|
+
* provisioning path, or state and template end up narrowed to different keys
|
|
5396
|
+
* and the fix becomes the bug).
|
|
5397
|
+
*
|
|
5398
|
+
* `TagFilters` lives at the RULE level in the CFn schema and CDK synthesizes it
|
|
5399
|
+
* there; the `Filter`-side reads are cdkd's accommodation for SDK-shaped /
|
|
5400
|
+
* imported input.
|
|
5401
|
+
*/
|
|
5402
|
+
function lifecycleRuleScope(rule) {
|
|
5403
|
+
const filter = rule["Filter"];
|
|
5404
|
+
return {
|
|
5405
|
+
prefix: filter?.["Prefix"] ?? rule["Prefix"],
|
|
5406
|
+
tagFilters: filter?.["TagFilters"] ?? rule["TagFilters"],
|
|
5407
|
+
sizeGt: filter?.["ObjectSizeGreaterThan"] ?? rule["ObjectSizeGreaterThan"],
|
|
5408
|
+
sizeLt: filter?.["ObjectSizeLessThan"] ?? rule["ObjectSizeLessThan"]
|
|
5409
|
+
};
|
|
5410
|
+
}
|
|
5411
|
+
/**
|
|
5412
|
+
* Can this rule stay in the deprecated V1 (top-level `Prefix`) form? See
|
|
5413
|
+
* {@link lifecycleUsesFilterForm} — S3 forbids MIXING the two forms in one
|
|
5414
|
+
* configuration, so the answer is decided across the whole rule list.
|
|
5415
|
+
*/
|
|
5416
|
+
function lifecycleRuleIsPlainPrefixOnly(rule) {
|
|
5417
|
+
if (rule["Filter"] != null) return false;
|
|
5418
|
+
const s = lifecycleRuleScope(rule);
|
|
5419
|
+
return s.prefix !== void 0 && (s.tagFilters == null || s.tagFilters.length === 0) && s.sizeGt === void 0 && s.sizeLt === void 0;
|
|
5420
|
+
}
|
|
5421
|
+
/** V1 vs V2 is decided ONCE for the whole configuration — see the applier's note. */
|
|
5422
|
+
function lifecycleUsesFilterForm(rules) {
|
|
5423
|
+
return !rules.every(lifecycleRuleIsPlainPrefixOnly);
|
|
5424
|
+
}
|
|
5425
|
+
/** Discards the message; the fold only needs the PREDICATE's answer. */
|
|
5426
|
+
const IGNORE_REFUSAL_MESSAGE = () => {};
|
|
5427
|
+
/**
|
|
5428
|
+
* Would `applyLifecycleConfiguration` REFUSE this rule, and therefore skip the
|
|
5429
|
+
* whole Put? (issue #1755)
|
|
5430
|
+
*
|
|
5431
|
+
* Runs the applier's own three guards rather than restating them — the
|
|
5432
|
+
* "do not HAND-ROLL the mirror, run the wire's own predicate" rule, whose
|
|
5433
|
+
* cautionary tale is the S3 schedule fold, where an approximation disagreed
|
|
5434
|
+
* with the wire on three of nine container shapes.
|
|
5435
|
+
*/
|
|
5436
|
+
function lifecycleRuleRefused(rule) {
|
|
5437
|
+
const rawFilter = rule["Filter"];
|
|
5438
|
+
if (rawFilter != null && requireConfigObject(rawFilter, LIFECYCLE_FILTER_PATH, { onUnusable: IGNORE_REFUSAL_MESSAGE }) === void 0) return true;
|
|
5439
|
+
const rawTagFilters = rawFilter?.["TagFilters"] ?? rule["TagFilters"];
|
|
5440
|
+
if (rawTagFilters != null && requireConfigArray(rawTagFilters, LIFECYCLE_TAG_FILTERS_PATH, { onUnusable: IGNORE_REFUSAL_MESSAGE }) === void 0) return true;
|
|
5441
|
+
return configStringRefusal(rule, "Status", LIFECYCLE_DEFAULT_STATUS, LIFECYCLE_RULE_PATH) !== void 0;
|
|
5442
|
+
}
|
|
5443
|
+
/**
|
|
5444
|
+
* Deep, key-ORDER-insensitive equality — the identity test for a fold that
|
|
5445
|
+
* rebuilds nested objects (`Filter`) even when nothing about them changed.
|
|
5446
|
+
*
|
|
5447
|
+
* `Object.is` alone would report every folded rule as changed and rewrite the
|
|
5448
|
+
* state record of every lifecycle-configured bucket; `JSON.stringify` would
|
|
5449
|
+
* report a pure reordering as a change, which is not one worth recording.
|
|
5450
|
+
*/
|
|
5451
|
+
function deepSameValue(a, b) {
|
|
5452
|
+
if (Object.is(a, b)) return true;
|
|
5453
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
5454
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
5455
|
+
return a.every((element, index) => deepSameValue(element, b[index]));
|
|
5456
|
+
}
|
|
5457
|
+
if (isPlainObject$1(a) && isPlainObject$1(b)) {
|
|
5458
|
+
const keysA = Object.keys(a);
|
|
5459
|
+
if (keysA.length !== Object.keys(b).length) return false;
|
|
5460
|
+
return keysA.every((key) => key in b && deepSameValue(a[key], b[key]));
|
|
5461
|
+
}
|
|
5462
|
+
return false;
|
|
5463
|
+
}
|
|
5464
|
+
/**
|
|
5465
|
+
* The ISO string `readLifecycle` emits for a date the applier sends as
|
|
5466
|
+
* `new Date(value)`.
|
|
5467
|
+
*
|
|
5468
|
+
* DETERMINISM IS A PRECONDITION, not a nicety (PR review). `new Date(str)`
|
|
5469
|
+
* resolves an offset-less string in the HOST's local zone — `new Date('0')` is
|
|
5470
|
+
* `1999-12-31T15:00:00Z` under JST and `2000-01-01T00:00:00Z` under UTC, and
|
|
5471
|
+
* `'2030-01-01T00:00:00'` (ISO, no offset) is local by spec. The APPLIER shares
|
|
5472
|
+
* that dependence, so the wire and the readback still agree on any one machine —
|
|
5473
|
+
* but this feeds `canonicalizeDesiredProperties`, a DIFF-side pure function that
|
|
5474
|
+
* runs wherever the CLI runs. A CI runner in UTC and a laptop in JST would
|
|
5475
|
+
* canonicalize the SAME template to different ISO strings and manufacture a diff
|
|
5476
|
+
* neither machine can clear, which is strictly worse than the phantom drift the
|
|
5477
|
+
* fold exists to remove. So only the forms whose meaning is fixed by the ISO
|
|
5478
|
+
* 8601 spec are folded: a bare `YYYY-MM-DD` (spec-UTC), a date-time carrying
|
|
5479
|
+
* `Z` or a `±HH:MM` offset, and an epoch NUMBER. Everything else — `'Jan 1
|
|
5480
|
+
* 2030'`, `'0'`, an offset-less date-time — is left ALONE, which keeps the
|
|
5481
|
+
* record machine-independent at the cost of not normalizing a shape almost no
|
|
5482
|
+
* template uses.
|
|
5483
|
+
*
|
|
5484
|
+
* @returns `undefined` for a value that cannot become a valid `Date`, or whose
|
|
5485
|
+
* `Date` would depend on the host timezone — in both cases there is no
|
|
5486
|
+
* effective value this can claim AWS will report, so the fold leaves the rule
|
|
5487
|
+
* alone.
|
|
5488
|
+
*/
|
|
5489
|
+
const TIMEZONE_INDEPENDENT_DATE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2}))?$/;
|
|
5490
|
+
function cfnDateToIso(value) {
|
|
5491
|
+
if (typeof value === "string" && !TIMEZONE_INDEPENDENT_DATE.test(value)) return void 0;
|
|
5492
|
+
if (typeof value !== "string" && typeof value !== "number") return void 0;
|
|
5493
|
+
const parsed = new Date(value);
|
|
5494
|
+
return Number.isNaN(parsed.getTime()) ? void 0 : parsed.toISOString();
|
|
5495
|
+
}
|
|
5496
|
+
/** The legacy SINGULAR action objects the CFn schema still declares (issue #1754). */
|
|
5497
|
+
const LEGACY_SINGULAR_TRANSITIONS = [["Transitions", "Transition"], ["NoncurrentVersionTransitions", "NoncurrentVersionTransition"]];
|
|
5498
|
+
/**
|
|
5499
|
+
* Merge the legacy singular `Transition` / `NoncurrentVersionTransition` into
|
|
5500
|
+
* the plural array, exactly as the wire does (issue #1754).
|
|
5501
|
+
*
|
|
5502
|
+
* The CFn `Rule` definition still declares BOTH spellings and
|
|
5503
|
+
* `applyLifecycleConfiguration` folds the singular into the plural array via
|
|
5504
|
+
* {@link mergeLegacySingular}, so `readLifecycle` can only ever emit the plural.
|
|
5505
|
+
* A rule declaring the singular therefore recorded a key AWS can never
|
|
5506
|
+
* report — the #1686 never-emitted-KEY class through the SHAPE — with no
|
|
5507
|
+
* warning anywhere, because nothing is malformed and nothing is substituted.
|
|
5508
|
+
*
|
|
5509
|
+
* It shares `mergeLegacySingular` itself rather than re-deriving the collision
|
|
5510
|
+
* rule, per the share-ONE-helper rule. What it does NOT do is pass a no-op
|
|
5511
|
+
* callback and fold regardless: on a `StorageClass` COLLISION the wire DROPS the
|
|
5512
|
+
* singular and WARNS, so a colliding rule is left COMPLETELY alone (both keys
|
|
5513
|
+
* kept). The NON-colliding case, which is the population this exists for, has
|
|
5514
|
+
* no fault to report and folds.
|
|
5515
|
+
*
|
|
5516
|
+
* Be precise about WHICH signal that preserves, because the obvious answer is
|
|
5517
|
+
* wrong (PR review). It is NOT the deploy-time warning: the record and the
|
|
5518
|
+
* canonicalized template are folded by the same function either way, so they
|
|
5519
|
+
* compare equal after the first deploy and the Put stops re-firing whichever
|
|
5520
|
+
* choice is made. What leaving it alone preserves is the `cdkd drift` report —
|
|
5521
|
+
* state keeps a `Transition` key `readLifecycle` can never emit, so drift
|
|
5522
|
+
* re-reports the rule until the template drops one of the two declarations.
|
|
5523
|
+
* Folding it would make state match the readback exactly and render a
|
|
5524
|
+
* template fault S3 itself rejects completely invisible.
|
|
5525
|
+
*
|
|
5526
|
+
* @returns `rule` BY IDENTITY when no legacy singular is declared.
|
|
5527
|
+
*/
|
|
5528
|
+
function foldLegacySingularTransitions(rule) {
|
|
5529
|
+
let out;
|
|
5530
|
+
for (const [pluralKey, singularKey] of LEGACY_SINGULAR_TRANSITIONS) {
|
|
5531
|
+
const singular = rule[singularKey];
|
|
5532
|
+
const hasSingular = isPlainObject$1(singular);
|
|
5533
|
+
const plural = rule[pluralKey];
|
|
5534
|
+
const needsFilter = Array.isArray(plural) && plural.some((element) => !isPlainObject$1(element));
|
|
5535
|
+
if (!hasSingular && !needsFilter) continue;
|
|
5536
|
+
let collided = false;
|
|
5537
|
+
const merged = mergeLegacySingular(plural, hasSingular ? singular : void 0, () => {
|
|
5538
|
+
collided = true;
|
|
5539
|
+
});
|
|
5540
|
+
out ??= { ...rule };
|
|
5541
|
+
if (collided) {
|
|
5542
|
+
if (needsFilter) out[pluralKey] = merged;
|
|
5543
|
+
continue;
|
|
5544
|
+
}
|
|
5545
|
+
out[pluralKey] = merged;
|
|
5546
|
+
if (hasSingular) delete out[singularKey];
|
|
5547
|
+
}
|
|
5548
|
+
return out ?? rule;
|
|
5549
|
+
}
|
|
5550
|
+
/**
|
|
5551
|
+
* Emit a transition's `TransitionDate` as the ISO string the readback emits
|
|
5552
|
+
* (PR review of issues #1754 / #1755).
|
|
5553
|
+
*
|
|
5554
|
+
* The alias fold above only RENAMES `Date` -> `TransitionDate`, but the applier
|
|
5555
|
+
* sends `new Date(t['TransitionDate'] ?? t['Date'])` and `readLifecycle` emits
|
|
5556
|
+
* `t.Date.toISOString()` — so a hand-written / imported `TransitionDate:
|
|
5557
|
+
* '2030-01-01'` recorded a value the readback can never match, with no warning
|
|
5558
|
+
* and nothing malformed. Exactly the class {@link foldLifecycleExpiration}
|
|
5559
|
+
* closes for the RULE-level `ExpirationDate` one member over; found by review
|
|
5560
|
+
* because the fold and the readback were compared per member rather than
|
|
5561
|
+
* per item.
|
|
5562
|
+
*
|
|
5563
|
+
* `NoncurrentVersionTransitions` needs no twin of this: `toSdkNvt` has no date
|
|
5564
|
+
* member at all.
|
|
5565
|
+
*
|
|
5566
|
+
* @returns the item BY IDENTITY when no date is declared, or when it is one the
|
|
5567
|
+
* applier would put on the wire as an Invalid Date (nothing to record).
|
|
5568
|
+
*/
|
|
5569
|
+
function foldTransitionDate(item) {
|
|
5570
|
+
const declared = item[TRANSITION_DATE_KEY];
|
|
5571
|
+
if (!declared) return item;
|
|
5572
|
+
const iso = cfnDateToIso(declared);
|
|
5573
|
+
if (iso === void 0 || iso === declared) return item;
|
|
5574
|
+
const out = { ...item };
|
|
5575
|
+
out[TRANSITION_DATE_KEY] = iso;
|
|
5576
|
+
return out;
|
|
5577
|
+
}
|
|
5578
|
+
/**
|
|
5579
|
+
* Drop a declared-but-EMPTY transition list (issue #1755).
|
|
5580
|
+
*
|
|
5581
|
+
* The applier sets `sdkRule.Transitions` / `.NoncurrentVersionTransitions` only
|
|
5582
|
+
* when the merged array is non-empty, and `readLifecycle` emits each only under
|
|
5583
|
+
* `r.<List> && r.<List>.length > 0` — so an empty declaration is one key the
|
|
5584
|
+
* readback can never carry. Measured on the reverse-mapper, not assumed: the
|
|
5585
|
+
* lifecycle block's OWN empty-collection placeholder answers the opposite way
|
|
5586
|
+
* (issue #1718) because `readLifecycle` DOES emit `{Rules: []}`.
|
|
5587
|
+
*/
|
|
5588
|
+
function dropEmptyTransitionLists(rule) {
|
|
5589
|
+
let out;
|
|
5590
|
+
for (const [pluralKey] of LEGACY_SINGULAR_TRANSITIONS) {
|
|
5591
|
+
const list = rule[pluralKey];
|
|
5592
|
+
if (!Array.isArray(list) || list.length > 0) continue;
|
|
5593
|
+
out ??= { ...rule };
|
|
5594
|
+
delete out[pluralKey];
|
|
5595
|
+
}
|
|
5596
|
+
return out ?? rule;
|
|
5597
|
+
}
|
|
5598
|
+
/**
|
|
5599
|
+
* Reshape the rule's expiration onto the CFn spellings `readLifecycle` emits
|
|
5600
|
+
* (issue #1755 item 1).
|
|
5601
|
+
*
|
|
5602
|
+
* The CFn `Rule` definition declares `ExpirationDate` / `ExpirationInDays` and
|
|
5603
|
+
* the RULE-level `ExpiredObjectDeleteMarker`, and has NO `Expiration` member;
|
|
5604
|
+
* the applier additionally accepts a cdkd-only `Expiration` OBJECT as a third
|
|
5605
|
+
* source. The readback used to emit only the SDK-shaped `Expiration` object, so
|
|
5606
|
+
* state and the readback carried different key sets for essentially every
|
|
5607
|
+
* lifecycle-configured bucket CDK creates. The fix is split, per the issue: the
|
|
5608
|
+
* READBACK now emits the CFn scalars (recording the SDK shape instead would put
|
|
5609
|
+
* a spelling in state that no CFn template can declare), and this is the desired
|
|
5610
|
+
* side folding onto the same choice.
|
|
5611
|
+
*
|
|
5612
|
+
* The rule is left COMPLETELY alone in three cases, each because the wire does
|
|
5613
|
+
* something this fold must not conceal:
|
|
5614
|
+
*
|
|
5615
|
+
* - a rule-level `ExpiredObjectDeleteMarker` the wire cannot read
|
|
5616
|
+
* (`coerceCfnBoolean` answers `undefined`): the wire silently ignores it, and
|
|
5617
|
+
* nothing warns, so the difference is the user's only signal;
|
|
5618
|
+
* - a date the wire would send as an Invalid Date, or one whose `Date` depends
|
|
5619
|
+
* on the host timezone (see {@link cfnDateToIso});
|
|
5620
|
+
* - a rule-level `ExpiredObjectDeleteMarker: true` ALONGSIDE a `Days` / `Date`:
|
|
5621
|
+
* S3 forbids the combination, so the applier sends the `Days` / `Date` and
|
|
5622
|
+
* WARNS that the marker was not applied. Folding the marker away on both sides
|
|
5623
|
+
* would stop that difference being reported, so `cdkd drift` re-reporting it
|
|
5624
|
+
* until the template drops one of the two is the correct standing signal.
|
|
5625
|
+
*
|
|
5626
|
+
* @returns `rule` BY IDENTITY when nothing expiration-shaped is declared.
|
|
5627
|
+
*/
|
|
5628
|
+
function foldLifecycleExpiration(rule) {
|
|
5629
|
+
const declaredMarker = rule["ExpiredObjectDeleteMarker"];
|
|
5630
|
+
const ruleLevelMarker = coerceCfnBoolean(declaredMarker);
|
|
5631
|
+
if (declaredMarker !== void 0 && ruleLevelMarker === void 0) return rule;
|
|
5632
|
+
if (rule["ExpirationInDays"] === void 0 && rule["ExpirationDate"] === void 0 && rule["Expiration"] === void 0 && declaredMarker === void 0) return rule;
|
|
5633
|
+
const expiration = rule["ExpirationInDays"] || rule["ExpirationDate"] || rule["Expiration"];
|
|
5634
|
+
let days;
|
|
5635
|
+
let date;
|
|
5636
|
+
let marker;
|
|
5637
|
+
if (typeof expiration === "number") days = expiration;
|
|
5638
|
+
else if (typeof expiration === "string") {
|
|
5639
|
+
const iso = cfnDateToIso(expiration);
|
|
5640
|
+
if (iso === void 0) return rule;
|
|
5641
|
+
date = iso;
|
|
5642
|
+
} else if (isPlainObject$1(expiration)) {
|
|
5643
|
+
if (expiration["Days"] !== void 0) days = expiration["Days"];
|
|
5644
|
+
if (expiration["Date"]) {
|
|
5645
|
+
const iso = cfnDateToIso(expiration["Date"]);
|
|
5646
|
+
if (iso === void 0) return rule;
|
|
5647
|
+
date = iso;
|
|
5648
|
+
}
|
|
5649
|
+
const nested = expiration["ExpiredObjectDeleteMarker"];
|
|
5650
|
+
if (nested !== void 0) {
|
|
5651
|
+
if (typeof nested !== "boolean") return rule;
|
|
5652
|
+
marker = nested;
|
|
5653
|
+
}
|
|
5654
|
+
} else if (expiration) return rule;
|
|
5655
|
+
if (ruleLevelMarker === true) {
|
|
5656
|
+
if (days !== void 0 || date !== void 0) return rule;
|
|
5657
|
+
days = void 0;
|
|
5658
|
+
date = void 0;
|
|
5659
|
+
marker = true;
|
|
5660
|
+
}
|
|
5661
|
+
const out = { ...rule };
|
|
5662
|
+
delete out["ExpirationInDays"];
|
|
5663
|
+
delete out["ExpirationDate"];
|
|
5664
|
+
delete out["Expiration"];
|
|
5665
|
+
delete out["ExpiredObjectDeleteMarker"];
|
|
5666
|
+
if (days !== void 0) out["ExpirationInDays"] = days;
|
|
5667
|
+
if (date !== void 0) out["ExpirationDate"] = date;
|
|
5668
|
+
if (marker !== void 0) out["ExpiredObjectDeleteMarker"] = marker;
|
|
5669
|
+
return out;
|
|
5670
|
+
}
|
|
5671
|
+
/**
|
|
5672
|
+
* Fold the legacy scalar `NoncurrentVersionExpirationInDays` onto the OBJECT
|
|
5673
|
+
* form `readLifecycle` emits (issue #1755 item 2).
|
|
5674
|
+
*
|
|
5675
|
+
* CFn declares BOTH spellings here — unlike the expiration sibling above — so
|
|
5676
|
+
* the readback has to pick one and the desired side has to fold to the same
|
|
5677
|
+
* choice. The OBJECT wins because it is the only one that can express
|
|
5678
|
+
* `NewerNoncurrentVersions`, so folding towards it is lossless in both
|
|
5679
|
+
* directions; and it is also what the wire already prefers when both are
|
|
5680
|
+
* declared, which is why a rule carrying both simply DROPS the scalar here.
|
|
5681
|
+
*
|
|
5682
|
+
* `coerceCfnNumber`, not `typeof === 'number'`: it is the predicate the wire
|
|
5683
|
+
* runs, and the wire sends the COERCED number, so a hand-written `"365"` is
|
|
5684
|
+
* recorded as `365` — exactly what AWS reports back.
|
|
5685
|
+
*/
|
|
5686
|
+
function foldNoncurrentVersionExpiration(rule) {
|
|
5687
|
+
const legacy = rule["NoncurrentVersionExpirationInDays"];
|
|
5688
|
+
if (legacy === void 0) return rule;
|
|
5689
|
+
const declaredObject = rule["NoncurrentVersionExpiration"];
|
|
5690
|
+
if (declaredObject !== void 0 && !isPlainObject$1(declaredObject)) return rule;
|
|
5691
|
+
const out = { ...rule };
|
|
5692
|
+
delete out["NoncurrentVersionExpirationInDays"];
|
|
5693
|
+
if (isPlainObject$1(declaredObject)) return out;
|
|
5694
|
+
const days = coerceCfnNumber(legacy);
|
|
5695
|
+
if (days === void 0) return rule;
|
|
5696
|
+
out["NoncurrentVersionExpiration"] = { NoncurrentDays: days };
|
|
5697
|
+
return out;
|
|
5698
|
+
}
|
|
5699
|
+
/**
|
|
5700
|
+
* Rebuild the rule's location scope in the shape `readLifecycle` emits for the
|
|
5701
|
+
* `Filter` the applier actually sends (issue #1755 item 3).
|
|
5702
|
+
*
|
|
5703
|
+
* This is not a spelling at all but a defaulted-but-SENT member (#1718's
|
|
5704
|
+
* class): a rule with no declared scope is SENT `Filter: {Prefix: ''}` in a V2
|
|
5705
|
+
* configuration, AWS holds it, the readback reports it, and the record was one
|
|
5706
|
+
* key short. The fold is wider than the issue's headline because the same round
|
|
5707
|
+
* trip moves three more shapes — a bare top-level `Prefix` becomes
|
|
5708
|
+
* `Filter: {Prefix}`, rule-level `TagFilters` become `Filter: {TagFilters}`, and
|
|
5709
|
+
* rule-level `ObjectSize*` move under `Filter` — all of which the readback emits
|
|
5710
|
+
* only under `Filter`.
|
|
5711
|
+
*
|
|
5712
|
+
* `useFilterForm` is decided ACROSS ALL RULES, which is why this takes it as a
|
|
5713
|
+
* parameter instead of being a pure per-rule function: in a V1 configuration
|
|
5714
|
+
* (every rule scoped by exactly one top-level `Prefix`) nothing moves at all.
|
|
5715
|
+
*
|
|
5716
|
+
* WHY THIS RECORDS `Filter` WHEN THE EXPIRATION SIBLING REFUSED THE SDK SHAPE,
|
|
5717
|
+
* since the two answer the same objection opposite ways one member apart (PR
|
|
5718
|
+
* review — without this, the next author "fixes" one to match the other). The
|
|
5719
|
+
* CFn `Rule` definition declares no `Filter` member either, so by the
|
|
5720
|
+
* `Expiration` argument this too should have been fixed on the READBACK side.
|
|
5721
|
+
* It is not, for a reason that does not apply there: the rule-level CFn scalars
|
|
5722
|
+
* can express every `Expiration` the SDK returns, and so the reverse map is
|
|
5723
|
+
* LOSSLESS — whereas the V2 `Filter` carries a distinction with no rule-level
|
|
5724
|
+
* spelling at all. A scope-less rule is SENT `Filter: {Prefix: ''}`, and the
|
|
5725
|
+
* only rule-level rendering of that is a top-level `Prefix: ''`, which is the V1
|
|
5726
|
+
* form — a different configuration that S3 forbids mixing with V2. Reverse-
|
|
5727
|
+
* mapping would therefore destroy the V1/V2 distinction state needs to
|
|
5728
|
+
* reproduce the call. `readLifecycle` has emitted `Filter` since issue #1388, so
|
|
5729
|
+
* both sides already meet there; this fold makes the desired side agree rather
|
|
5730
|
+
* than moving the meeting point.
|
|
5731
|
+
*/
|
|
5732
|
+
function foldLifecycleScope(rule, useFilterForm) {
|
|
5733
|
+
const { prefix, tagFilters, sizeGt, sizeLt } = lifecycleRuleScope(rule);
|
|
5734
|
+
const hasTags = tagFilters != null && tagFilters.length > 0;
|
|
5735
|
+
const componentCount = (hasTags ? 1 : 0) + (prefix !== void 0 ? 1 : 0) + (sizeGt !== void 0 ? 1 : 0) + (sizeLt !== void 0 ? 1 : 0);
|
|
5736
|
+
let filter;
|
|
5737
|
+
if (componentCount > 1) {
|
|
5738
|
+
filter = {};
|
|
5739
|
+
if (prefix !== void 0) filter["Prefix"] = prefix;
|
|
5740
|
+
if (hasTags) filter["TagFilters"] = tagFilters;
|
|
5741
|
+
if (sizeGt !== void 0) filter["ObjectSizeGreaterThan"] = sizeGt;
|
|
5742
|
+
if (sizeLt !== void 0) filter["ObjectSizeLessThan"] = sizeLt;
|
|
5743
|
+
} else if (hasTags) filter = { TagFilters: tagFilters };
|
|
5744
|
+
else if (sizeGt !== void 0) filter = { ObjectSizeGreaterThan: sizeGt };
|
|
5745
|
+
else if (sizeLt !== void 0) filter = { ObjectSizeLessThan: sizeLt };
|
|
5746
|
+
else if (useFilterForm) filter = { Prefix: prefix ?? "" };
|
|
5747
|
+
const out = { ...rule };
|
|
5748
|
+
delete out["Filter"];
|
|
5749
|
+
delete out["Prefix"];
|
|
5750
|
+
delete out["TagFilters"];
|
|
5751
|
+
delete out["ObjectSizeGreaterThan"];
|
|
5752
|
+
delete out["ObjectSizeLessThan"];
|
|
5753
|
+
if (filter !== void 0) out["Filter"] = filter;
|
|
5754
|
+
else if (prefix !== void 0) out["Prefix"] = prefix;
|
|
5251
5755
|
return out;
|
|
5252
5756
|
}
|
|
5253
5757
|
/**
|
|
@@ -5268,29 +5772,65 @@ function effectiveNotificationConfiguration(config) {
|
|
|
5268
5772
|
* the same two item shapes, so fixing only the reported one would have left its
|
|
5269
5773
|
* siblings broken — the "diff the WHOLE blob, not the reported key" rule.
|
|
5270
5774
|
*
|
|
5271
|
-
*
|
|
5272
|
-
* the legacy SINGULAR `Transition` /
|
|
5273
|
-
*
|
|
5274
|
-
*
|
|
5275
|
-
*
|
|
5276
|
-
*
|
|
5277
|
-
*
|
|
5278
|
-
* `
|
|
5279
|
-
*
|
|
5280
|
-
*
|
|
5281
|
-
*
|
|
5282
|
-
* #1755 lands — which is the honest state, not a claim this makes lifecycle
|
|
5283
|
-
* whole.
|
|
5284
|
-
*
|
|
5285
|
-
* @returns `config` BY IDENTITY when no tolerated spelling is declared.
|
|
5775
|
+
* Issues #1754 / #1755 finished the block, so the "still differs on those keys"
|
|
5776
|
+
* caveat this doc carried is gone: the legacy SINGULAR `Transition` /
|
|
5777
|
+
* `NoncurrentVersionTransition` objects are MERGED here exactly as the wire
|
|
5778
|
+
* merges them ({@link foldLegacySingularTransitions}), the rule-level
|
|
5779
|
+
* `Expiration` / `NoncurrentVersionExpiration` reshapes are folded onto the
|
|
5780
|
+
* spellings the readback emits ({@link foldLifecycleExpiration} /
|
|
5781
|
+
* {@link foldNoncurrentVersionExpiration}), and the sent-but-unrecorded
|
|
5782
|
+
* `Filter` is supplied from the rule's full gathered SCOPE
|
|
5783
|
+
* ({@link foldLifecycleScope}).
|
|
5784
|
+
*
|
|
5785
|
+
* @returns `config` BY IDENTITY when the declared bag IS what goes on the wire.
|
|
5286
5786
|
*/
|
|
5287
5787
|
function effectiveLifecycleConfiguration(config) {
|
|
5288
5788
|
if (!isPlainObject$1(config)) return config;
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5789
|
+
const rules = config["Rules"];
|
|
5790
|
+
if (!Array.isArray(rules)) return config;
|
|
5791
|
+
const objectRules = rules.filter(isPlainObject$1);
|
|
5792
|
+
if (objectRules.length !== rules.length || objectRules.length === 0) return config;
|
|
5793
|
+
if (objectRules.some(lifecycleRuleRefused)) return config;
|
|
5794
|
+
const useFilterForm = lifecycleUsesFilterForm(objectRules);
|
|
5795
|
+
return canonicalizeItemList(config, "Rules", (rule) => effectiveLifecycleRule(rule, useFilterForm));
|
|
5796
|
+
}
|
|
5797
|
+
/**
|
|
5798
|
+
* The one rule fold both sides share — the CFn-shaped bag `readLifecycle` emits
|
|
5799
|
+
* for the SDK rule `applyLifecycleConfiguration` builds from this rule.
|
|
5800
|
+
*
|
|
5801
|
+
* Ordered the way the wire is: the legacy singular is merged into the plural
|
|
5802
|
+
* BEFORE the alias fold runs, because the singular can carry the SDK day/date
|
|
5803
|
+
* spellings too.
|
|
5804
|
+
*
|
|
5805
|
+
* MEASURED against the REAL synthesized `s3-lifecycle` fixture templates (every
|
|
5806
|
+
* bucket, both phases): the recorded rule and the emitted one carry identical
|
|
5807
|
+
* KEYS and identical VALUES, and differ only in key ORDER — the record keeps the
|
|
5808
|
+
* template's, the readback uses the reverse-mapper's. That is pre-existing and
|
|
5809
|
+
* unchanged here (the record was always template-ordered), and it does not reach
|
|
5810
|
+
* `cdkd drift`, whose baseline is `observedProperties` — captured from this very
|
|
5811
|
+
* reverse-mapper, so both sides carry its order. It is stated because a reader
|
|
5812
|
+
* comparing the two by `JSON.stringify` will see a difference and should know it
|
|
5813
|
+
* was measured rather than missed.
|
|
5814
|
+
*
|
|
5815
|
+
* @returns `rule` BY IDENTITY when the declared rule already IS that bag, so an
|
|
5816
|
+
* ordinary CFn-spelled template records byte-for-byte as before. The compare
|
|
5817
|
+
* is deep and key-ORDER-insensitive for the same reason: a reordering is not a
|
|
5818
|
+
* difference worth rewriting a state record for.
|
|
5819
|
+
*/
|
|
5820
|
+
function effectiveLifecycleRule(rule, useFilterForm) {
|
|
5821
|
+
let out = foldLegacySingularTransitions(rule);
|
|
5822
|
+
out = canonicalizeItemList(out, "Transitions", (t) => foldTransitionDate(foldToleratedAliases(t, TRANSITION_ALIASES)));
|
|
5823
|
+
out = canonicalizeItemList(out, "NoncurrentVersionTransitions", (nvt) => foldToleratedAliases(nvt, NONCURRENT_VERSION_TRANSITION_ALIASES));
|
|
5824
|
+
out = dropEmptyTransitionLists(out);
|
|
5825
|
+
out = foldLifecycleExpiration(out);
|
|
5826
|
+
out = foldNoncurrentVersionExpiration(out);
|
|
5827
|
+
out = foldLifecycleScope(out, useFilterForm);
|
|
5828
|
+
if (out[LIFECYCLE_STATUS_KEY] === void 0) {
|
|
5829
|
+
const withStatus = { ...out };
|
|
5830
|
+
withStatus[LIFECYCLE_STATUS_KEY] = LIFECYCLE_DEFAULT_STATUS;
|
|
5831
|
+
out = withStatus;
|
|
5832
|
+
}
|
|
5833
|
+
return deepSameValue(rule, out) ? rule : out;
|
|
5294
5834
|
}
|
|
5295
5835
|
/**
|
|
5296
5836
|
* Apply a WHOLE-BLOCK fold to one top-level property on BOTH sides of a diff —
|
|
@@ -5649,32 +6189,18 @@ var S3BucketProvider = class S3BucketProvider {
|
|
|
5649
6189
|
async applyLifecycleConfiguration(bucketName, lifecycleConfig, onUnusable) {
|
|
5650
6190
|
for (const rule of lifecycleConfig.Rules) {
|
|
5651
6191
|
const rawFilter = rule["Filter"];
|
|
5652
|
-
if (rawFilter != null && requireConfigObject(rawFilter,
|
|
6192
|
+
if (rawFilter != null && requireConfigObject(rawFilter, LIFECYCLE_FILTER_PATH, { ...onUnusable ? { onUnusable } : {} }) === void 0) return false;
|
|
5653
6193
|
const rawTagFilters = rawFilter?.["TagFilters"] ?? rule["TagFilters"];
|
|
5654
6194
|
if (rawTagFilters != null) {
|
|
5655
|
-
if (requireConfigArray(rawTagFilters,
|
|
6195
|
+
if (requireConfigArray(rawTagFilters, LIFECYCLE_TAG_FILTERS_PATH, { ...onUnusable ? { onUnusable } : {} }) === void 0) return false;
|
|
5656
6196
|
}
|
|
5657
|
-
if (this.skipOnUnusableConfigString(rule, "Status",
|
|
6197
|
+
if (this.skipOnUnusableConfigString(rule, "Status", LIFECYCLE_DEFAULT_STATUS, LIFECYCLE_RULE_PATH, "Leaving the whole lifecycle configuration unapplied here", onUnusable)) return false;
|
|
5658
6198
|
}
|
|
5659
|
-
const
|
|
5660
|
-
const filter = rule["Filter"];
|
|
5661
|
-
return {
|
|
5662
|
-
prefix: filter?.["Prefix"] ?? rule["Prefix"],
|
|
5663
|
-
tagFilters: filter?.["TagFilters"] ?? rule["TagFilters"],
|
|
5664
|
-
sizeGt: filter?.["ObjectSizeGreaterThan"] ?? rule["ObjectSizeGreaterThan"],
|
|
5665
|
-
sizeLt: filter?.["ObjectSizeLessThan"] ?? rule["ObjectSizeLessThan"]
|
|
5666
|
-
};
|
|
5667
|
-
};
|
|
5668
|
-
const isPlainPrefixOnly = (rule) => {
|
|
5669
|
-
if (rule["Filter"] != null) return false;
|
|
5670
|
-
const s = gatherScope(rule);
|
|
5671
|
-
return s.prefix !== void 0 && (s.tagFilters == null || s.tagFilters.length === 0) && s.sizeGt === void 0 && s.sizeLt === void 0;
|
|
5672
|
-
};
|
|
5673
|
-
const useFilterForm = !lifecycleConfig.Rules.every(isPlainPrefixOnly);
|
|
6199
|
+
const useFilterForm = lifecycleUsesFilterForm(lifecycleConfig.Rules);
|
|
5674
6200
|
const rules = lifecycleConfig.Rules.map((rule) => {
|
|
5675
6201
|
const sdkRule = {
|
|
5676
6202
|
ID: rule["Id"],
|
|
5677
|
-
Status: readConfigString(rule, "Status",
|
|
6203
|
+
Status: readConfigString(rule, "Status", LIFECYCLE_DEFAULT_STATUS, LIFECYCLE_RULE_PATH),
|
|
5678
6204
|
Prefix: useFilterForm ? void 0 : rule["Prefix"]
|
|
5679
6205
|
};
|
|
5680
6206
|
const expiration = rule["ExpirationInDays"] || rule["ExpirationDate"] || rule["Expiration"];
|
|
@@ -5720,7 +6246,7 @@ var S3BucketProvider = class S3BucketProvider {
|
|
|
5720
6246
|
if (allTransitions.length > 0) sdkRule.Transitions = allTransitions.map(toSdkTransition);
|
|
5721
6247
|
const abort = isPlainObject$1(rule["AbortIncompleteMultipartUpload"]) ? rule["AbortIncompleteMultipartUpload"] : void 0;
|
|
5722
6248
|
if (abort) sdkRule.AbortIncompleteMultipartUpload = { DaysAfterInitiation: abort["DaysAfterInitiation"] };
|
|
5723
|
-
const { prefix, tagFilters, sizeGt, sizeLt } =
|
|
6249
|
+
const { prefix, tagFilters, sizeGt, sizeLt } = lifecycleRuleScope(rule);
|
|
5724
6250
|
const hasTags = tagFilters != null && tagFilters.length > 0;
|
|
5725
6251
|
if ((hasTags ? 1 : 0) + (prefix !== void 0 ? 1 : 0) + (sizeGt !== void 0 ? 1 : 0) + (sizeLt !== void 0 ? 1 : 0) > 1) sdkRule.Filter = { And: {
|
|
5726
6252
|
Prefix: prefix,
|
|
@@ -5906,7 +6432,7 @@ var S3BucketProvider = class S3BucketProvider {
|
|
|
5906
6432
|
* SDK uses the same structure (PutBucketNotificationConfiguration replaces
|
|
5907
6433
|
* the entire notification configuration in one call).
|
|
5908
6434
|
*/
|
|
5909
|
-
async applyNotificationConfiguration(bucketName, notifConfig) {
|
|
6435
|
+
async applyNotificationConfiguration(bucketName, notifConfig, onUnusable) {
|
|
5910
6436
|
const cfg = {};
|
|
5911
6437
|
if (notifConfig) {
|
|
5912
6438
|
const topics = notifConfig["TopicConfigurations"];
|
|
@@ -5931,13 +6457,22 @@ var S3BucketProvider = class S3BucketProvider {
|
|
|
5931
6457
|
Filter: this.cfnNotifFilterToSdk(l["Filter"])
|
|
5932
6458
|
}));
|
|
5933
6459
|
const eb = notifConfig["EventBridgeConfiguration"];
|
|
5934
|
-
if (eb
|
|
6460
|
+
if (eb != null) {
|
|
6461
|
+
const ebRefusal = configBooleanRefusal(eb, "EventBridgeEnabled", EVENTBRIDGE_CONFIG_PATH);
|
|
6462
|
+
if (ebRefusal !== void 0) {
|
|
6463
|
+
if (!onUnusable) throw new Error(`${ebRefusal}. Omit the block entirely to leave EventBridge delivery disabled`);
|
|
6464
|
+
onUnusable(`${ebRefusal}. Leaving the whole notification configuration unapplied here: PutBucketNotificationConfiguration is a full replace, and treating an unreadable value as ${EVENTBRIDGE_DEFAULT_ENABLED} would ENABLE EventBridge delivery the template may have been disabling; the same value is REFUSED on a template-path create`);
|
|
6465
|
+
return false;
|
|
6466
|
+
}
|
|
6467
|
+
if (eventBridgeEnabledOf(eb)) cfg.EventBridgeConfiguration = {};
|
|
6468
|
+
}
|
|
5935
6469
|
}
|
|
5936
6470
|
await this.s3Client.send(new PutBucketNotificationConfigurationCommand({
|
|
5937
6471
|
Bucket: bucketName,
|
|
5938
6472
|
NotificationConfiguration: cfg
|
|
5939
6473
|
}));
|
|
5940
6474
|
this.logger.debug(`Applied notification configuration to bucket ${bucketName}`);
|
|
6475
|
+
return true;
|
|
5941
6476
|
}
|
|
5942
6477
|
/**
|
|
5943
6478
|
* Convert CFn notification Filter ({ S3Key: { Rules: [{ Name, Value }] } })
|
|
@@ -6951,9 +7486,11 @@ var S3BucketProvider = class S3BucketProvider {
|
|
|
6951
7486
|
await this.applyLoggingConfiguration(bucketName, void 0);
|
|
6952
7487
|
});
|
|
6953
7488
|
await this.diffSubConfig(bucketName, previousProperties["NotificationConfiguration"], properties["NotificationConfiguration"], async (cfg) => {
|
|
6954
|
-
await this.applyNotificationConfiguration(bucketName, cfg);
|
|
6955
|
-
recordEffectiveFold("NotificationConfiguration", effectiveNotificationConfiguration(cfg));
|
|
6956
|
-
}, async () =>
|
|
7489
|
+
if (!await this.applyNotificationConfiguration(bucketName, cfg, (m) => this.logger.warn(m))) retainPrevious("NotificationConfiguration");
|
|
7490
|
+
else recordEffectiveFold("NotificationConfiguration", effectiveNotificationConfiguration(cfg));
|
|
7491
|
+
}, async () => {
|
|
7492
|
+
await this.applyNotificationConfiguration(bucketName, void 0);
|
|
7493
|
+
});
|
|
6957
7494
|
await this.diffSubConfig(bucketName, previousProperties["ReplicationConfiguration"], properties["ReplicationConfiguration"], async (cfg) => {
|
|
6958
7495
|
if (!await this.applyReplicationConfiguration(bucketName, cfg, (m) => this.logger.warn(m))) retainPrevious("ReplicationConfiguration");
|
|
6959
7496
|
}, async () => {
|
|
@@ -7067,10 +7604,8 @@ var S3BucketProvider = class S3BucketProvider {
|
|
|
7067
7604
|
};
|
|
7068
7605
|
const replayOnUnusable = replayWarn(this.logger, context).onUnusable;
|
|
7069
7606
|
const notifConfig = properties["NotificationConfiguration"];
|
|
7070
|
-
if (notifConfig)
|
|
7071
|
-
|
|
7072
|
-
recordEffectiveFold("NotificationConfiguration", effectiveNotificationConfiguration(notifConfig));
|
|
7073
|
-
}
|
|
7607
|
+
if (notifConfig) if (!await this.applyNotificationConfiguration(bucketName, notifConfig, replayOnUnusable)) overrides.set("NotificationConfiguration", void 0);
|
|
7608
|
+
else recordEffectiveFold("NotificationConfiguration", effectiveNotificationConfiguration(notifConfig));
|
|
7074
7609
|
const corsConfig = properties["CorsConfiguration"];
|
|
7075
7610
|
if (corsConfig?.CorsRules && Array.isArray(corsConfig.CorsRules) && corsConfig.CorsRules.length > 0) await this.applyCorsConfiguration(bucketName, corsConfig);
|
|
7076
7611
|
else if (corsConfig != null) this.announceCreateEmptyCollectionSkip("CorsConfiguration", "CorsConfiguration.CorsRules");
|
|
@@ -7421,11 +7956,9 @@ var S3BucketProvider = class S3BucketProvider {
|
|
|
7421
7956
|
if (r.Status !== void 0) out["Status"] = r.Status;
|
|
7422
7957
|
if (r.Prefix !== void 0) out["Prefix"] = r.Prefix;
|
|
7423
7958
|
if (r.Expiration) {
|
|
7424
|
-
|
|
7425
|
-
if (r.Expiration.
|
|
7426
|
-
if (r.Expiration.
|
|
7427
|
-
if (r.Expiration.ExpiredObjectDeleteMarker !== void 0) exp["ExpiredObjectDeleteMarker"] = r.Expiration.ExpiredObjectDeleteMarker;
|
|
7428
|
-
out["Expiration"] = exp;
|
|
7959
|
+
if (r.Expiration.Days !== void 0) out["ExpirationInDays"] = r.Expiration.Days;
|
|
7960
|
+
if (r.Expiration.Date !== void 0) out["ExpirationDate"] = r.Expiration.Date.toISOString();
|
|
7961
|
+
if (r.Expiration.ExpiredObjectDeleteMarker !== void 0) out["ExpiredObjectDeleteMarker"] = r.Expiration.ExpiredObjectDeleteMarker;
|
|
7429
7962
|
}
|
|
7430
7963
|
if (r.Transitions && r.Transitions.length > 0) out["Transitions"] = r.Transitions.map((t) => {
|
|
7431
7964
|
const item = {};
|
|
@@ -58338,9 +58871,17 @@ function stackTreeToJson(roots) {
|
|
|
58338
58871
|
* PR B2 — until that lands, the orchestrator hard-errors at submission
|
|
58339
58872
|
* time with a clear pointer.
|
|
58340
58873
|
*
|
|
58341
|
-
* The list is intentionally narrow
|
|
58342
|
-
*
|
|
58343
|
-
*
|
|
58874
|
+
* The list is intentionally narrow, and stays hand-written because each member
|
|
58875
|
+
* is a cdkd / CDK construct rather than something the registry describes.
|
|
58876
|
+
*
|
|
58877
|
+
* It is NOT the only refusal, though — the "defer to the CreateChangeSet
|
|
58878
|
+
* error" note this comment used to end on was revisited in issue
|
|
58879
|
+
* [#1659](https://github.com/go-to-k/cdkd/issues/1659). Types AWS refuses for
|
|
58880
|
+
* IMPORT because their registry schema declares no `read` handler are now
|
|
58881
|
+
* caught by {@link buildImportPlan}'s pre-flight instead, which needs no
|
|
58882
|
+
* hand-maintained list (the signal is derived from the schema cdkd already
|
|
58883
|
+
* fetches for the identifier) and names every offender in one pass — AWS's own
|
|
58884
|
+
* error names only some of them.
|
|
58344
58885
|
*/
|
|
58345
58886
|
const NEVER_IMPORTABLE_TYPES = /* @__PURE__ */ new Set(["AWS::CDK::Metadata"]);
|
|
58346
58887
|
/**
|
|
@@ -58513,6 +59054,16 @@ const COMPOSITE_ID_SPLITTERS = {
|
|
|
58513
59054
|
propertiesOverlay: { ApiId: apiId }
|
|
58514
59055
|
};
|
|
58515
59056
|
},
|
|
59057
|
+
"AWS::S3Tables::Namespace": (physicalId) => {
|
|
59058
|
+
const parts = physicalId.split("|");
|
|
59059
|
+
if (parts.length !== 2) throw new Error(`expected 2 parts ('<tableBucketARN>|<namespaceName>'), got ${parts.length}: '${physicalId}'`);
|
|
59060
|
+
const [tableBucketARN, namespace] = parts;
|
|
59061
|
+
if (!tableBucketARN || !namespace) throw new Error(`empty part in '<tableBucketARN>|<namespaceName>': '${physicalId}'`);
|
|
59062
|
+
return { resourceIdentifier: {
|
|
59063
|
+
TableBucketARN: tableBucketARN,
|
|
59064
|
+
Namespace: namespace
|
|
59065
|
+
} };
|
|
59066
|
+
},
|
|
58516
59067
|
"AWS::Lambda::Permission": (physicalId, properties) => {
|
|
58517
59068
|
const functionName = readStringProperty(properties, "FunctionName", "AWS::Lambda::Permission");
|
|
58518
59069
|
return {
|
|
@@ -58598,6 +59149,144 @@ function readStringProperty(properties, key, resourceType) {
|
|
|
58598
59149
|
return v;
|
|
58599
59150
|
}
|
|
58600
59151
|
/**
|
|
59152
|
+
* Types whose cdkd physicalId is a COMPOSITE (`|`-joined) string while
|
|
59153
|
+
* CloudFormation's `primaryIdentifier` is a SINGLE field — so the physicalId is
|
|
59154
|
+
* NOT the identifier value, and it is not a SPLIT of the composite either
|
|
59155
|
+
* (issue [#1659](https://github.com/go-to-k/cdkd/issues/1659)).
|
|
59156
|
+
*
|
|
59157
|
+
* ## Why this table exists next to `COMPOSITE_ID_SPLITTERS`
|
|
59158
|
+
*
|
|
59159
|
+
* `resolveResourceIdentifier` used to conflate two independent questions: the
|
|
59160
|
+
* ARITY of the CFn identifier, and the FORMAT of cdkd's physical id. The
|
|
59161
|
+
* single-field branch assumed "the physicalId IS the identifier value", which
|
|
59162
|
+
* holds for every type whose id is a scalar and silently produces a wrong value
|
|
59163
|
+
* for a composite-id type whose CFn identifier happens to be single-field —
|
|
59164
|
+
* `{ TableARN: 'arn:aws:s3tables:...|analytics|events' }` for an
|
|
59165
|
+
* `AWS::S3Tables::Table`. `COMPOSITE_ID_SPLITTERS` cannot fix that: it is only
|
|
59166
|
+
* consulted on the multi-field branch, so registering a type there would add an
|
|
59167
|
+
* entry no code path reaches.
|
|
59168
|
+
*
|
|
59169
|
+
* This table is consulted BEFORE the arity branch, so the two concerns are
|
|
59170
|
+
* separated: a registered type never takes the "physicalId is the identifier"
|
|
59171
|
+
* assumption regardless of what the schema's field count says.
|
|
59172
|
+
*
|
|
59173
|
+
* ## Why a RESOLVE and not a split
|
|
59174
|
+
*
|
|
59175
|
+
* The correct identifier for each member is a different value entirely, not a
|
|
59176
|
+
* segment of the composite — an ARN AWS minted, or an `sgr-` rule id. So each
|
|
59177
|
+
* entry reads the value cdkd recorded in state (`attributes`), and refuses when
|
|
59178
|
+
* it is absent rather than guessing.
|
|
59179
|
+
*
|
|
59180
|
+
* ## Live measurement (us-east-1, 2026-08-13)
|
|
59181
|
+
*
|
|
59182
|
+
* Every field below is BOTH the type's single-field `primaryIdentifier` AND
|
|
59183
|
+
* `readOnlyProperties`, which is why each resolution returns an EMPTY
|
|
59184
|
+
* `propertiesOverlay`: writing a read-only property into the synth template's
|
|
59185
|
+
* `Properties` block is rejected by CFn at changeset-create.
|
|
59186
|
+
*
|
|
59187
|
+
* | Type | `primaryIdentifier` | recorded as |
|
|
59188
|
+
* |---|---|---|
|
|
59189
|
+
* | `AWS::AppSync::DataSource` | `DataSourceArn` | `attributes.DataSourceArn` |
|
|
59190
|
+
* | `AWS::AppSync::Resolver` | `ResolverArn` | `attributes.ResolverArn` |
|
|
59191
|
+
* | `AWS::S3Tables::Table` | `TableARN` | `attributes.TableARN` |
|
|
59192
|
+
* | `AWS::EC2::SecurityGroupIngress` | `Id` | NOTHING (issue #1761) |
|
|
59193
|
+
*
|
|
59194
|
+
* All four declare a `read` handler, so CFn genuinely does accept them for
|
|
59195
|
+
* IMPORT — unlike `AWS::Glue::Table`, the type issue #1659's title names, which
|
|
59196
|
+
* CFn rejects outright (see {@link typeIsImportUnsupported}).
|
|
59197
|
+
*/
|
|
59198
|
+
const COMPOSITE_PHYSICAL_ID_IDENTIFIERS = {
|
|
59199
|
+
"AWS::AppSync::DataSource": recordedArnIdentifier({
|
|
59200
|
+
resourceType: "AWS::AppSync::DataSource",
|
|
59201
|
+
field: "DataSourceArn",
|
|
59202
|
+
physicalIdShape: "<apiId>|<name>",
|
|
59203
|
+
arnServiceSegment: "appsync"
|
|
59204
|
+
}),
|
|
59205
|
+
"AWS::AppSync::Resolver": recordedArnIdentifier({
|
|
59206
|
+
resourceType: "AWS::AppSync::Resolver",
|
|
59207
|
+
field: "ResolverArn",
|
|
59208
|
+
physicalIdShape: "<apiId>|<typeName>|<fieldName>",
|
|
59209
|
+
arnServiceSegment: "appsync"
|
|
59210
|
+
}),
|
|
59211
|
+
"AWS::S3Tables::Table": recordedArnIdentifier({
|
|
59212
|
+
resourceType: "AWS::S3Tables::Table",
|
|
59213
|
+
field: "TableARN",
|
|
59214
|
+
physicalIdShape: "<tableBucketARN>|<namespace>|<name>",
|
|
59215
|
+
arnServiceSegment: "s3tables"
|
|
59216
|
+
}),
|
|
59217
|
+
"AWS::EC2::SecurityGroupIngress": {
|
|
59218
|
+
field: "Id",
|
|
59219
|
+
resolve: ({ logicalId }) => {
|
|
59220
|
+
throw new Error(`cdkd's physical id for AWS::EC2::SecurityGroupIngress is the composite '<groupId>|<ipProtocol>|<fromPort>|<toPort>', but CloudFormation IMPORT identifies the rule by the 'sgr-...' security-group rule id AWS assigns, which cdkd does not record in state (tracked in https://github.com/go-to-k/cdkd/issues/1761). Remove '${logicalId}' from the stack before exporting (the rule stays in AWS and can be re-declared in CloudFormation afterwards), or destroy it first and let CloudFormation create it fresh.`);
|
|
59221
|
+
}
|
|
59222
|
+
}
|
|
59223
|
+
};
|
|
59224
|
+
/**
|
|
59225
|
+
* Build the {@link CompositePhysicalIdIdentifier} for a type whose CFn
|
|
59226
|
+
* identifier is an ARN cdkd records in `attributes` under the SAME name as the
|
|
59227
|
+
* `primaryIdentifier` field.
|
|
59228
|
+
*
|
|
59229
|
+
* Two accepted sources, mirroring the splitter family's "both shapes exist in
|
|
59230
|
+
* state in the wild" contract:
|
|
59231
|
+
*
|
|
59232
|
+
* 1. the recorded attribute, which is what a fresh deploy and a cdkd `import`
|
|
59233
|
+
* both write; and
|
|
59234
|
+
* 2. a physicalId that is ALREADY the bare ARN. Two live producers:
|
|
59235
|
+
* `S3TablesProvider.importTable` records it for a Cloud-Control-routed
|
|
59236
|
+
* table (CC's identifier for the type IS the ARN), and an AppSync child
|
|
59237
|
+
* adopted through `cdkd import --migrate-from-cloudformation` records
|
|
59238
|
+
* CloudFormation's `PhysicalResourceId` verbatim, which is likewise the
|
|
59239
|
+
* ARN.
|
|
59240
|
+
*
|
|
59241
|
+
* **Both sources are validated by the SAME predicate.** An earlier cut checked
|
|
59242
|
+
* the ARN's shape only on source 2 — the rare one — while source 1, which
|
|
59243
|
+
* fires on essentially every export, accepted any non-blank string verbatim.
|
|
59244
|
+
* That is the wrong way round: a hand-edited state row, a row written by an
|
|
59245
|
+
* older binary that recorded the composite under the attribute name, or an ARN
|
|
59246
|
+
* for a different service sitting under `attributes.<field>` would each have
|
|
59247
|
+
* been shipped as the IMPORT identifier and silently adopted the WRONG
|
|
59248
|
+
* resource, which is precisely what this table exists to prevent.
|
|
59249
|
+
*
|
|
59250
|
+
* Anything failing both sources is refused. That covers the degraded record
|
|
59251
|
+
* `docs/state-management.md` describes — written by a cdkd older than the fix
|
|
59252
|
+
* that started recording the ARN, or by an import that could not reach STS —
|
|
59253
|
+
* and the remedy is the one that doc already gives.
|
|
59254
|
+
*/
|
|
59255
|
+
function recordedArnIdentifier(options) {
|
|
59256
|
+
const { resourceType, field, physicalIdShape, arnServiceSegment } = options;
|
|
59257
|
+
/**
|
|
59258
|
+
* Is this value usable AS the type's CFn identifier?
|
|
59259
|
+
*
|
|
59260
|
+
* - `arn:` prefix and the type's own SERVICE segment: an ARN naming another
|
|
59261
|
+
* service is a different value entirely. Partition-agnostic by
|
|
59262
|
+
* construction, so `arn:aws-cn:s3tables:...` matches.
|
|
59263
|
+
* - no `|`: cdkd's composite for `AWS::S3Tables::Table` is built FROM the
|
|
59264
|
+
* bucket ARN, so it satisfies both checks above — the separator test is
|
|
59265
|
+
* what distinguishes the composite from the identifier, on BOTH sources.
|
|
59266
|
+
*/
|
|
59267
|
+
const isUsableIdentifier = (value) => {
|
|
59268
|
+
const trimmed = value.trim();
|
|
59269
|
+
return trimmed.startsWith("arn:") && trimmed.includes(`:${arnServiceSegment}:`) && !trimmed.includes("|");
|
|
59270
|
+
};
|
|
59271
|
+
return {
|
|
59272
|
+
field,
|
|
59273
|
+
resolve: ({ logicalId, physicalId, attributes }) => {
|
|
59274
|
+
const recorded = attributes[field];
|
|
59275
|
+
if (typeof recorded === "string" && isUsableIdentifier(recorded)) return recorded.trim();
|
|
59276
|
+
if (isUsableIdentifier(physicalId)) return physicalId.trim();
|
|
59277
|
+
const recordedNote = typeof recorded === "string" && recorded.trim() ? `attributes.${field} is recorded as '${recorded.trim()}', which is not a ${arnServiceSegment} ARN` : `attributes.${field} is missing or empty`;
|
|
59278
|
+
throw new Error(`cdkd's physical id for ${resourceType} is the composite '${physicalIdShape}', but CloudFormation IMPORT identifies the resource by its ${field}, which cdkd state does not record usably for '${logicalId}' (${recordedNote}). Re-deploy the stack once so cdkd records ${field}, then re-run cdkd export.`);
|
|
59279
|
+
}
|
|
59280
|
+
};
|
|
59281
|
+
}
|
|
59282
|
+
/**
|
|
59283
|
+
* Every type registered in {@link COMPOSITE_PHYSICAL_ID_IDENTIFIERS}. Exported
|
|
59284
|
+
* so a test can assert the table's SIZE, not just membership — a new entry
|
|
59285
|
+
* needs a measurement row in the table's doc comment, and a size-blind test
|
|
59286
|
+
* would let one land silently.
|
|
59287
|
+
*/
|
|
59288
|
+
const COMPOSITE_PHYSICAL_ID_IDENTIFIER_TYPES = Object.keys(COMPOSITE_PHYSICAL_ID_IDENTIFIERS);
|
|
59289
|
+
/**
|
|
58601
59290
|
* Resource types AWS does NOT support in IMPORT changesets but DOES
|
|
58602
59291
|
* support normal CREATE for. cdkd handles these via a pre-delete + phase-2
|
|
58603
59292
|
* CREATE dance (see {@link RecreateBeforePhase2Entry}). When the user
|
|
@@ -58751,17 +59440,20 @@ async function exportCommand(stackArg, options) {
|
|
|
58751
59440
|
}
|
|
58752
59441
|
}
|
|
58753
59442
|
}
|
|
59443
|
+
const { phase1Imports, phase2Creates, recreateBeforePhase2, nestedStackRows, blocked } = await buildImportPlan(state, template, awsClients.cloudFormation, resolvedStackName, {
|
|
59444
|
+
recreateImportUnsupported: options.recreateImportUnsupported,
|
|
59445
|
+
skipImportSupportPreflight: options.skipImportSupportPreflight
|
|
59446
|
+
});
|
|
59447
|
+
if (blocked.length > 0) {
|
|
59448
|
+
logger.error("The following resources block migration:");
|
|
59449
|
+
for (const b of blocked) logger.error(` - ${b.logicalId} (${b.resourceType}): ${b.reason}`);
|
|
59450
|
+
throw new Error(`${blocked.length} resource(s) block migration. Either destroy them first (cdkd destroy / cdkd state destroy cherry-picked), or remove them from the CDK app and re-synthesize.`);
|
|
59451
|
+
}
|
|
58754
59452
|
const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
|
|
58755
59453
|
if (!options.dryRun) {
|
|
58756
59454
|
if (!await lockManager.acquireLock(resolvedStackName, targetRegion, owner, "export")) throw new Error(`Could not acquire lock for stack '${resolvedStackName}' (${targetRegion}) — another cdkd process holds it. Wait for it to finish, or run 'cdkd force-unlock ${resolvedStackName}' if you are certain no other process is active.`);
|
|
58757
59455
|
}
|
|
58758
59456
|
try {
|
|
58759
|
-
const { phase1Imports, phase2Creates, recreateBeforePhase2, nestedStackRows, blocked } = await buildImportPlan(state, template, awsClients.cloudFormation, resolvedStackName, { recreateImportUnsupported: options.recreateImportUnsupported });
|
|
58760
|
-
if (blocked.length > 0) {
|
|
58761
|
-
logger.error("The following resources block migration:");
|
|
58762
|
-
for (const b of blocked) logger.error(` - ${b.logicalId} (${b.resourceType}): ${b.reason}`);
|
|
58763
|
-
throw new Error(`${blocked.length} resource(s) block migration. Either destroy them first (cdkd destroy / cdkd state destroy cherry-picked), or remove them from the CDK app and re-synthesize.`);
|
|
58764
|
-
}
|
|
58765
59457
|
if (nestedStackRows.length > 0) {
|
|
58766
59458
|
const nestedStackTree = await buildCdkdStateStackTree(resolvedStackName, targetRegion, stateBackend, state);
|
|
58767
59459
|
reportDriftBaselineGaps(state, logger);
|
|
@@ -58804,7 +59496,8 @@ async function exportCommand(stackArg, options) {
|
|
|
58804
59496
|
dryRun: options.dryRun,
|
|
58805
59497
|
yes: options.yes,
|
|
58806
59498
|
includeNonImportable: options.includeNonImportable,
|
|
58807
|
-
recreateImportUnsupported: options.recreateImportUnsupported
|
|
59499
|
+
recreateImportUnsupported: options.recreateImportUnsupported,
|
|
59500
|
+
skipImportSupportPreflight: options.skipImportSupportPreflight
|
|
58808
59501
|
}
|
|
58809
59502
|
})).outcome === "success") printNextSteps({
|
|
58810
59503
|
cfnStackName: options.cfnStackName ?? cdkd2cfnStackName(resolvedStackName),
|
|
@@ -59343,7 +60036,16 @@ async function buildImportPlan(state, template, cfnClient, parentStackName, opti
|
|
|
59343
60036
|
}
|
|
59344
60037
|
let resolved;
|
|
59345
60038
|
try {
|
|
59346
|
-
|
|
60039
|
+
const schemaInfo = await cachedTypeSchemaInfo(resourceType, cfnClient, identifierCache);
|
|
60040
|
+
if (!options.skipImportSupportPreflight && typeIsImportUnsupported(schemaInfo)) {
|
|
60041
|
+
blocked.push({
|
|
60042
|
+
logicalId,
|
|
60043
|
+
resourceType,
|
|
60044
|
+
reason: `AWS CloudFormation does not support ${resourceType} in IMPORT changesets: its CloudFormation registry schema declares no 'read' handler (and reports the type as NON_PROVISIONABLE), and IMPORT needs that handler to look the resource up by identifier. CreateChangeSet would reject the whole export with "ResourceTypes [${resourceType}] are not supported for Import". Remove the resource from the stack before exporting (it stays in AWS and can be re-declared in CloudFormation afterwards), or destroy it first and let CloudFormation create it fresh. If AWS has since added IMPORT support for this type, please open a cdkd issue.`
|
|
60045
|
+
});
|
|
60046
|
+
continue;
|
|
60047
|
+
}
|
|
60048
|
+
resolved = resolveResourceIdentifier(resourceType, logicalId, stateEntry.physicalId, stateEntry.properties ?? {}, stateEntry.attributes ?? {}, schemaInfo);
|
|
59347
60049
|
} catch (err) {
|
|
59348
60050
|
blocked.push({
|
|
59349
60051
|
logicalId,
|
|
@@ -59373,6 +60075,11 @@ async function buildImportPlan(state, template, cfnClient, parentStackName, opti
|
|
|
59373
60075
|
* `ResourcesToImport[].ResourceIdentifier` for the given resource type
|
|
59374
60076
|
* and cdkd state's physical ID.
|
|
59375
60077
|
*
|
|
60078
|
+
* Types whose cdkd physicalId is COMPOSITE while the CFn identifier is a
|
|
60079
|
+
* single field (`COMPOSITE_PHYSICAL_ID_IDENTIFIERS`): resolved from cdkd
|
|
60080
|
+
* state's recorded attributes, BEFORE the field-count branch below — the
|
|
60081
|
+
* physicalId is neither the identifier nor a source to split (issue #1659).
|
|
60082
|
+
*
|
|
59376
60083
|
* Single-key types: the map has a single entry keyed by the schema's
|
|
59377
60084
|
* primaryIdentifier field name (e.g. `{ BucketName: 'my-bucket' }`).
|
|
59378
60085
|
*
|
|
@@ -59382,16 +60089,27 @@ async function buildImportPlan(state, template, cfnClient, parentStackName, opti
|
|
|
59382
60089
|
* into one entry per field. Composite types without a registered
|
|
59383
60090
|
* splitter surface a clear error pointing at where to add one.
|
|
59384
60091
|
*
|
|
59385
|
-
*
|
|
59386
|
-
*
|
|
59387
|
-
*
|
|
59388
|
-
*
|
|
59389
|
-
|
|
59390
|
-
|
|
59391
|
-
|
|
59392
|
-
|
|
59393
|
-
|
|
59394
|
-
|
|
60092
|
+
* `entry` is the caller's already-resolved registry-schema info — see
|
|
60093
|
+
* {@link cachedTypeSchemaInfo}, which prefers `DescribeType` (the authoritative
|
|
60094
|
+
* AWS-internal registry) and falls back to a hardcoded single-key table when
|
|
60095
|
+
* DescribeType fails (insufficient permissions, throttling, obscure type
|
|
60096
|
+
* without a registry entry). Taking it as a parameter rather than fetching is
|
|
60097
|
+
* what keeps `buildImportPlan` to ONE DescribeType per type across both the
|
|
60098
|
+
* IMPORT pre-flight and this resolution.
|
|
60099
|
+
*/
|
|
60100
|
+
function resolveResourceIdentifier(resourceType, logicalId, physicalId, properties, attributes, entry) {
|
|
60101
|
+
const compositeIdentifier = COMPOSITE_PHYSICAL_ID_IDENTIFIERS[resourceType];
|
|
60102
|
+
if (compositeIdentifier) {
|
|
60103
|
+
if (entry.fields.length !== 1 || entry.fields[0] !== compositeIdentifier.field) throw new Error(`${resourceType} is registered in COMPOSITE_PHYSICAL_ID_IDENTIFIERS as identified by '${compositeIdentifier.field}', but CloudFormation now reports primaryIdentifier [${entry.fields.join(", ")}]. The registry schema changed under cdkd — update the entry in src/cli/commands/export.ts (or move the type to COMPOSITE_ID_SPLITTERS if its identifier is now composite too)`);
|
|
60104
|
+
const value = compositeIdentifier.resolve({
|
|
60105
|
+
logicalId,
|
|
60106
|
+
physicalId,
|
|
60107
|
+
attributes
|
|
60108
|
+
});
|
|
60109
|
+
return {
|
|
60110
|
+
resourceIdentifier: { [compositeIdentifier.field]: value },
|
|
60111
|
+
propertiesOverlay: {}
|
|
60112
|
+
};
|
|
59395
60113
|
}
|
|
59396
60114
|
if (entry.fields.length === 1) return { resourceIdentifier: { [entry.fields[0]]: physicalId } };
|
|
59397
60115
|
const splitter = COMPOSITE_ID_SPLITTERS[resourceType];
|
|
@@ -59406,6 +60124,53 @@ async function resolveResourceIdentifier(resourceType, physicalId, properties, c
|
|
|
59406
60124
|
return result;
|
|
59407
60125
|
}
|
|
59408
60126
|
/**
|
|
60127
|
+
* Cache-consulting wrapper around {@link fetchPrimaryIdentifier}. Shared by
|
|
60128
|
+
* `resolveResourceIdentifier` and `buildImportPlan`'s pre-flight so one
|
|
60129
|
+
* DescribeType per type answers both questions.
|
|
60130
|
+
*/
|
|
60131
|
+
async function cachedTypeSchemaInfo(resourceType, cfnClient, cache) {
|
|
60132
|
+
const cached = cache.get(resourceType);
|
|
60133
|
+
if (cached !== void 0) return cached;
|
|
60134
|
+
const entry = await fetchPrimaryIdentifier(resourceType, cfnClient);
|
|
60135
|
+
cache.set(resourceType, entry);
|
|
60136
|
+
return entry;
|
|
60137
|
+
}
|
|
60138
|
+
/**
|
|
60139
|
+
* True when the registry schema says CloudFormation will refuse the type in an
|
|
60140
|
+
* IMPORT changeset (`ValidationError: ResourceTypes [<T>] are not supported
|
|
60141
|
+
* for Import`) — IMPORT looks the resource up by identifier through the type's
|
|
60142
|
+
* `read` handler, so a type without one cannot be adopted.
|
|
60143
|
+
*
|
|
60144
|
+
* Live-measured (us-east-1, 2026-08-13) against `CreateChangeSet
|
|
60145
|
+
* --change-set-type IMPORT`: `AWS::Glue::Table` / `AWS::Route53::RecordSet` /
|
|
60146
|
+
* `AWS::AppSync::ApiKey` / `AWS::EC2::NetworkAclEntry` /
|
|
60147
|
+
* `AWS::Route53::RecordSetGroup` / `AWS::SQS::QueuePolicy` /
|
|
60148
|
+
* `AWS::SNS::TopicPolicy` all lack a `read` handler, are all
|
|
60149
|
+
* `NON_PROVISIONABLE`, and were all rejected; `AWS::S3::Bucket`
|
|
60150
|
+
* (`FULLY_MUTABLE`, read handler present) passed the type check. Both shapes
|
|
60151
|
+
* of "no read handler" occur and both count: NO `handlers` block at all (the
|
|
60152
|
+
* legacy types) and a `handlers` block whose keys are only create / update /
|
|
60153
|
+
* delete (`AWS::EC2::NetworkAclEntry`).
|
|
60154
|
+
*
|
|
60155
|
+
* **Why the verdict needs TWO agreeing fields.** Reading "the `handlers` key is
|
|
60156
|
+
* missing" as a refusal on its own makes any partial or unusual response block
|
|
60157
|
+
* an export that works today — the one regression this pre-flight could cause,
|
|
60158
|
+
* and strictly worse than the status quo of letting AWS answer. So the refusal
|
|
60159
|
+
* also requires `ProvisioningType: NON_PROVISIONABLE`; a response satisfying
|
|
60160
|
+
* only one of the two resolves to `unknown` and is left to CreateChangeSet.
|
|
60161
|
+
*
|
|
60162
|
+
* The two were perfectly CORRELATED across the 21 types sampled on 2026-08-13
|
|
60163
|
+
* (every no-read-handler type was NON_PROVISIONABLE and vice versa), so on
|
|
60164
|
+
* today's registry the conjunction's real work is requiring a COMPLETE
|
|
60165
|
+
* DescribeType response before refusing. The `read` handler stays the primary
|
|
60166
|
+
* signal because it is the mechanistic reason CFn accepts a type for IMPORT —
|
|
60167
|
+
* refusing on `ProvisioningType` alone would reject a NON_PROVISIONABLE type
|
|
60168
|
+
* that does declare one, should AWS ever publish that combination.
|
|
60169
|
+
*/
|
|
60170
|
+
function typeIsImportUnsupported(entry) {
|
|
60171
|
+
return entry.importSupport === "unsupported";
|
|
60172
|
+
}
|
|
60173
|
+
/**
|
|
59409
60174
|
* Fetch the primary identifier field names for a resource type, with a
|
|
59410
60175
|
* hardcoded single-key fallback when DescribeType is unavailable.
|
|
59411
60176
|
*/
|
|
@@ -59413,15 +60178,26 @@ async function fetchPrimaryIdentifier(resourceType, cfnClient) {
|
|
|
59413
60178
|
try {
|
|
59414
60179
|
const resp = await describeTypeWithThrottleRetry(resourceType, cfnClient);
|
|
59415
60180
|
if (resp.Schema) {
|
|
59416
|
-
const
|
|
59417
|
-
|
|
60181
|
+
const parsed = JSON.parse(resp.Schema);
|
|
60182
|
+
const primary = parsed.primaryIdentifier;
|
|
60183
|
+
if (Array.isArray(primary) && primary.length > 0 && primary.every((p) => typeof p === "string")) {
|
|
60184
|
+
const fields = primary.map((p) => p.replace(/^\/properties\//, ""));
|
|
60185
|
+
const handlers = parsed.handlers;
|
|
60186
|
+
return {
|
|
60187
|
+
fields,
|
|
60188
|
+
importSupport: typeof handlers === "object" && handlers !== null && Object.prototype.hasOwnProperty.call(handlers, "read") ? "importable" : resp.ProvisioningType === "NON_PROVISIONABLE" ? "unsupported" : "unknown"
|
|
60189
|
+
};
|
|
60190
|
+
}
|
|
59418
60191
|
}
|
|
59419
60192
|
} catch (err) {
|
|
59420
60193
|
const msg = err instanceof Error ? err.message : String(err);
|
|
59421
60194
|
getLogger().debug(`DescribeType failed for ${resourceType}: ${msg} — using fallback`);
|
|
59422
60195
|
}
|
|
59423
60196
|
const fallback = PRIMARY_IDENTIFIER_FALLBACK[resourceType];
|
|
59424
|
-
if (fallback) return {
|
|
60197
|
+
if (fallback) return {
|
|
60198
|
+
fields: [fallback],
|
|
60199
|
+
importSupport: "unknown"
|
|
60200
|
+
};
|
|
59425
60201
|
throw new Error(`primary identifier unknown (DescribeType returned no usable schema and no fallback is registered). Add ${resourceType} to PRIMARY_IDENTIFIER_FALLBACK in export.ts, or open an issue.`);
|
|
59426
60202
|
}
|
|
59427
60203
|
/**
|
|
@@ -60347,7 +61123,10 @@ async function runPerStackImportLoop(args) {
|
|
|
60347
61123
|
const perStackPlans = [];
|
|
60348
61124
|
for (const node of leafFirst) {
|
|
60349
61125
|
const meta = nodesByCdkdName.get(node.stackName);
|
|
60350
|
-
const plan = await buildImportPlan(meta.state, meta.template, deps.cfnClient, meta.cdkdStackName, {
|
|
61126
|
+
const plan = await buildImportPlan(meta.state, meta.template, deps.cfnClient, meta.cdkdStackName, {
|
|
61127
|
+
recreateImportUnsupported: options.recreateImportUnsupported,
|
|
61128
|
+
skipImportSupportPreflight: options.skipImportSupportPreflight === true
|
|
61129
|
+
});
|
|
60351
61130
|
if (plan.blocked.length > 0) {
|
|
60352
61131
|
const lines = plan.blocked.map((b) => ` - ${b.logicalId} (${b.resourceType}): ${b.reason}`);
|
|
60353
61132
|
throw new Error(`Stack '${meta.cdkdStackName}' has ${plan.blocked.length} resource(s) that block migration:\n${lines.join("\n")}`);
|
|
@@ -60763,7 +61542,7 @@ async function confirmPrompt$3(prompt) {
|
|
|
60763
61542
|
}
|
|
60764
61543
|
}
|
|
60765
61544
|
function createExportCommand() {
|
|
60766
|
-
const cmd = new Command("export").description("Hand a cdkd-managed stack over to CloudFormation via CFn IMPORT (changeset). AWS resources are unchanged; cdkd state for the stack is deleted on success. Mirror of `cdkd import` (AWS → cdkd) in the reverse direction (cdkd → CFn). Accepts JSON and YAML templates (YAML via a CFn-aware codec that preserves !Ref / !GetAtt / !Sub shorthand). Aborts if any resource is not CFn-importable.").argument("[stack]", "Stack name to export (auto-detected for single-stack apps)").option("--cfn-stack-name <name>", "Name of the destination CloudFormation stack for the root stack. Defaults to the cdkd stack name (or the cdkd2cfnStackName mapping when the name contains '~'). For per-nested-child overrides, use --cfn-child-stack-name.").option("--cfn-child-stack-name <pair...>", "Per-nested-child CFn stack-name override. Repeatable. Format: '<cdkdName>=<cfnName>' (e.g. --cfn-child-stack-name 'MyApp~Database=my-app-db'). The cdkd stack name for a nested child is '<parent>~<childLogicalId>' (v6 state-key form). Without an override the CFn name is derived by replacing '~' with '-'. Only consulted when the exported stack tree contains nested children; ignored for flat stacks.").option("--template <path>", "Path to a pre-rendered CloudFormation template (JSON or YAML — format auto-detected). Skips synth.").option("--stack-region <region>", "Region of the cdkd state record to operate on. Required when the same stack name has state in multiple regions.").option("--dry-run", "Print the import plan without creating a changeset.", false).option("--accept-transient-context", "Allow CLI -c key=value overrides at export time even though they are not persisted to cdk.json / cdk.context.json (default: refuse). When set, the user is responsible for passing the same -c flags to every future cdk deploy.", false).option("--include-non-importable", "Run a 2-phase migration when the stack contains non-importable resources (Custom::*). Phase 1 imports the importable resources; phase 2 CFn-CREATEs the non-importable ones, which re-invokes each Custom Resource's onCreate handler. Make sure onCreate is idempotent before enabling.", false).option("--parameter <key=value...>", "CFn template Parameter override, repeatable. Required when the synthesized template has Parameters without Default values; otherwise overrides the template's default value. Format: --parameter Key=Value.").option("--strict-cross-stack", "Refuse to export when sibling cdkd stacks in the same CDK app reference the exporting stack via Fn::GetStackOutput. Without the flag, cdkd warns and proceeds — the consumers keep resolving via the CloudFormation fallback (weak reference) unless they deploy with --no-cfn-fallback.", false).option("--no-recreate-import-unsupported", "Block instead of auto-handling resource types AWS does NOT support in IMPORT changesets (currently only AWS::ApiGatewayV2::Stage, emitted by CDK HttpApi). Default behavior: cdkd skips these from phase 1, deletes the AWS-side resource between phases, and lets CFn re-CREATE in phase 2 (brief unavailability window). With this flag, the export aborts with a clear error instead.").action(withErrorHandling(exportCommand));
|
|
61545
|
+
const cmd = new Command("export").description("Hand a cdkd-managed stack over to CloudFormation via CFn IMPORT (changeset). AWS resources are unchanged; cdkd state for the stack is deleted on success. Mirror of `cdkd import` (AWS → cdkd) in the reverse direction (cdkd → CFn). Accepts JSON and YAML templates (YAML via a CFn-aware codec that preserves !Ref / !GetAtt / !Sub shorthand). Aborts if any resource is not CFn-importable.").argument("[stack]", "Stack name to export (auto-detected for single-stack apps)").option("--cfn-stack-name <name>", "Name of the destination CloudFormation stack for the root stack. Defaults to the cdkd stack name (or the cdkd2cfnStackName mapping when the name contains '~'). For per-nested-child overrides, use --cfn-child-stack-name.").option("--cfn-child-stack-name <pair...>", "Per-nested-child CFn stack-name override. Repeatable. Format: '<cdkdName>=<cfnName>' (e.g. --cfn-child-stack-name 'MyApp~Database=my-app-db'). The cdkd stack name for a nested child is '<parent>~<childLogicalId>' (v6 state-key form). Without an override the CFn name is derived by replacing '~' with '-'. Only consulted when the exported stack tree contains nested children; ignored for flat stacks.").option("--template <path>", "Path to a pre-rendered CloudFormation template (JSON or YAML — format auto-detected). Skips synth.").option("--stack-region <region>", "Region of the cdkd state record to operate on. Required when the same stack name has state in multiple regions.").option("--dry-run", "Print the import plan without creating a changeset.", false).option("--accept-transient-context", "Allow CLI -c key=value overrides at export time even though they are not persisted to cdk.json / cdk.context.json (default: refuse). When set, the user is responsible for passing the same -c flags to every future cdk deploy.", false).option("--include-non-importable", "Run a 2-phase migration when the stack contains non-importable resources (Custom::*). Phase 1 imports the importable resources; phase 2 CFn-CREATEs the non-importable ones, which re-invokes each Custom Resource's onCreate handler. Make sure onCreate is idempotent before enabling.", false).option("--parameter <key=value...>", "CFn template Parameter override, repeatable. Required when the synthesized template has Parameters without Default values; otherwise overrides the template's default value. Format: --parameter Key=Value.").option("--strict-cross-stack", "Refuse to export when sibling cdkd stacks in the same CDK app reference the exporting stack via Fn::GetStackOutput. Without the flag, cdkd warns and proceeds — the consumers keep resolving via the CloudFormation fallback (weak reference) unless they deploy with --no-cfn-fallback.", false).option("--skip-import-support-preflight", "Skip the pre-flight that refuses resource types whose CloudFormation registry schema says CFn cannot IMPORT them (no read handler AND NON_PROVISIONABLE), and let CreateChangeSet answer instead. The pre-flight is a registry HEURISTIC, not AWS's supported-for-import list, so this is the escape hatch for a type AWS has since made importable. Expect the changeset to be rejected if the heuristic was right.", false).option("--no-recreate-import-unsupported", "Block instead of auto-handling resource types AWS does NOT support in IMPORT changesets (currently only AWS::ApiGatewayV2::Stage, emitted by CDK HttpApi). Default behavior: cdkd skips these from phase 1, deletes the AWS-side resource between phases, and lets CFn re-CREATE in phase 2 (brief unavailability window). With this flag, the export aborts with a clear error instead.").action(withErrorHandling(exportCommand));
|
|
60767
61546
|
[
|
|
60768
61547
|
...commonOptions,
|
|
60769
61548
|
...appOptions,
|
|
@@ -74608,7 +75387,7 @@ function createMigrateCommand() {
|
|
|
74608
75387
|
*/
|
|
74609
75388
|
function buildProgram() {
|
|
74610
75389
|
const program = new Command();
|
|
74611
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.281.
|
|
75390
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.281.25");
|
|
74612
75391
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
74613
75392
|
const { profile } = actionCommand.optsWithGlobals();
|
|
74614
75393
|
if (profile !== void 0) process.env["AWS_PROFILE"] = profile;
|