@go-to-k/cdkd 0.284.16 → 0.284.17
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-D7DqNvVo.js → asg-provider-BbLu5LGL.js} +2 -2
- package/dist/{asg-provider-D7DqNvVo.js.map → asg-provider-BbLu5LGL.js.map} +1 -1
- package/dist/cli.js +406 -10
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-CI0a6yLX.js → deploy-engine-Bnu1WuRI.js} +24 -13
- package/dist/deploy-engine-Bnu1WuRI.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-CI0a6yLX.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as isTerminationProtectionPropagationError, $n as ResourceUpdateNotSupportedError, $t as validateAssetBucketName, A as renderStatefulReason, An as derivePartitionAndUrlSuffix, At as INTRINSIC_KEYS, B as exportAliasCollisionScrubWarning, Bn as CdkdError, Bt as stringifyValue, C as isFinalSnapshotError, Cn as CFN_TEMPLATE_URL_LIMIT, Ct as s3BucketArn, D as extractDeploymentEventError, Dn as expectedOwnerParam, Dt as s3BucketWebsiteUrl, E as makeCanonicalizePropertiesFn, En as uploadCfnTemplate, Et as s3BucketRegionalDomainName, F as green, Fn as AwsClients, Ft as LockManager, G as collectInlinePolicyNamesManagedBySiblings, Gn as LocalMigrateError, Gt as rewriteTemplateAssetReferences, H as secretBearingStateKeyWarning, Ht as buildAssetRedirectMap, I as red, In as getAwsClients, It as S3StateBackend, J as findActionableSilentDrops, Jn as MissingCdkCliError, Jt as AssetModeResolver, K as clearOnUpdateRemoval, Kn as LocalStartServiceError, Kt as escapeRegExp, L as yellow, Ln as resetAwsClients, Lt as rebuildClientForBucketRegion, M as bold, Mn as processStackMessages, Mt as withRetry, N as cyan, Nt as DagBuilder, O as MULTI_REGION_RECREATE_BLOCKED_TYPES, On as PARTITION_TABLE, Ot as applyRoleArnIfSet, P as gray, Pn as resolveBucketRegion, Pt as TemplateParser, Q as disableInstanceApiTermination, Qn as ResourceTimeoutError, Qt as parseBootstrapMarker, R as collectDeclaredOutputNames, Rn as setAwsClients, Rt as shouldRetainResource, S as createPreDeleteFinalSnapshot, Sn as CFN_TEMPLATE_BODY_LIMIT, St as scrubResourceRecord, T as unsupportedFinalSnapshotError, Tn as findLargeInlineResources, Tt as s3BucketDualStackDomainName, U as stateKeySecretExposure, Un as DeployCancelledError, Ut as createAssetRedirectResolver, V as isExportAliasCollision, Vt as WorkGraph, W as IAMRoleProvider, Wn as LocalInvokeBuildError$1, Wt as loadPublishableAssetManifest, X as CloudControlProvider, Xn as PartialFailureError, Xt as ensureAssetStorage, Y as findSilentDropProperties, Yn as NestedStackChildDirectDestroyError, Yt as BOOTSTRAP_MARKER_PREFIX, Z as slowCcOperationTimeoutMs, Zn as ProvisioningError, Zt as getBootstrapMarkerKey, _ as computeImplicitDeleteEdges, _n as resolveStateBucketWithDefault, _t as STATE_SOURCED_READBACK_RULES, a as DeploymentEventsStore, an as runDockerForeground, at as normalizeAwsTagsToCfn, b as buildFinalSnapshotIdentifier, bn as stateBucketExistenceConfirmed, bt as maskSecretsInText, c as replayFailedOperations, cn as getDockerImageBySourceHash, cr as isMarkedNonRetryable, ct as coerceCfnBoolean, d as updatePartialReason, dn as getDefaultStateBucketName, dr as markNonRetryable, dt as readConfigString, en as validateContainerRepoName, er as StackHasActiveImportsError, et as IntrinsicFunctionResolver, f as UNSPECIFIED_SKIP_REASON, fn as getLegacyStateBucketName, ft as replayWarn, g as IMPLICIT_DELETE_DEPENDENCIES, gn as resolveSkipPrefix, gt as STATE_SOURCED_CROSS_GENERATION_RULES, h as maskingRetryLogger, hn as resolveCaptureObservedState, ht as requireConfigString, i as DeploymentEventsReader, in as getDockerCmd, it as WAFv2WebACLProvider, j as formatResourceLine, jn as AssemblyReader, jt as describeTypeWithThrottleRetry, k as isStatefulRecreateTargetSync, kn as canonicalizeRegion, kt as DiffCalculator, l as replayRollback, ln as Synthesizer, lr as isRetryableTransientError, lt as configBooleanRefusal, m as withResourceDeadline, mn as resolveAutoAssetStorage, mt as requireConfigObject, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as buildDockerImage, nt as getAccountInfo, o as planFailedOps, on as runDockerStreaming, or as normalizeAwsError, ot as resolveExplicitPhysicalId, p as deleteSkipReason, pn as resolveApp, pt as requireConfigArray, q as ProviderRegistry, qt as stripControlChars, r as DeployEngine, rn as formatDockerLoginError, rt as refStateLookupFromResource, s as planRollback, sn as AssetManifestLoader, sr as withErrorHandling, st as assertRegionMatch, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as buildDenyExternalAccessPolicy, tr as StackTerminationProtectionError, tt as cfnRefValueFromPhysicalId, u as updatePartialMessage, un as synthesisStatusMessage, ur as isThrottlingError, ut as configStringRefusal, v as ATOMIC_FINAL_SNAPSHOT_TYPES, vn as resolveStateBucketWithDefaultAndSource, vt as TEMPLATE_SOURCED_RULES, w as refusesFinalSnapshot, wn as MIGRATE_TMP_PREFIX, wt as s3BucketDomainName, x as ccRoutedFinalSnapshotError, xn as warnDeprecatedNoPrefixCliFlag, xt as redactSecretsForState, y as PRE_DELETE_SNAPSHOT_TYPES, yn as resolveUseCdkBootstrapAssets, yt as createSecretMasker, z as collectPublishedOutputNames, zt as AssetPublisher } from "./deploy-engine-
|
|
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-
|
|
2
|
+
import { $ as isTerminationProtectionPropagationError, $n as ResourceUpdateNotSupportedError, $t as validateAssetBucketName, A as renderStatefulReason, An as derivePartitionAndUrlSuffix, At as INTRINSIC_KEYS, B as exportAliasCollisionScrubWarning, Bn as CdkdError, Bt as stringifyValue, C as isFinalSnapshotError, Cn as CFN_TEMPLATE_URL_LIMIT, Ct as s3BucketArn, D as extractDeploymentEventError, Dn as expectedOwnerParam, Dt as s3BucketWebsiteUrl, E as makeCanonicalizePropertiesFn, En as uploadCfnTemplate, Et as s3BucketRegionalDomainName, F as green, Fn as AwsClients, Ft as LockManager, G as collectInlinePolicyNamesManagedBySiblings, Gn as LocalMigrateError, Gt as rewriteTemplateAssetReferences, H as secretBearingStateKeyWarning, Ht as buildAssetRedirectMap, I as red, In as getAwsClients, It as S3StateBackend, J as findActionableSilentDrops, Jn as MissingCdkCliError, Jt as AssetModeResolver, K as clearOnUpdateRemoval, Kn as LocalStartServiceError, Kt as escapeRegExp, L as yellow, Ln as resetAwsClients, Lt as rebuildClientForBucketRegion, M as bold, Mn as processStackMessages, Mt as withRetry, N as cyan, Nt as DagBuilder, O as MULTI_REGION_RECREATE_BLOCKED_TYPES, On as PARTITION_TABLE, Ot as applyRoleArnIfSet, P as gray, Pn as resolveBucketRegion, Pt as TemplateParser, Q as disableInstanceApiTermination, Qn as ResourceTimeoutError, Qt as parseBootstrapMarker, R as collectDeclaredOutputNames, Rn as setAwsClients, Rt as shouldRetainResource, S as createPreDeleteFinalSnapshot, Sn as CFN_TEMPLATE_BODY_LIMIT, St as scrubResourceRecord, T as unsupportedFinalSnapshotError, Tn as findLargeInlineResources, Tt as s3BucketDualStackDomainName, U as stateKeySecretExposure, Un as DeployCancelledError, Ut as createAssetRedirectResolver, V as isExportAliasCollision, Vt as WorkGraph, W as IAMRoleProvider, Wn as LocalInvokeBuildError$1, Wt as loadPublishableAssetManifest, X as CloudControlProvider, Xn as PartialFailureError, Xt as ensureAssetStorage, Y as findSilentDropProperties, Yn as NestedStackChildDirectDestroyError, Yt as BOOTSTRAP_MARKER_PREFIX, Z as slowCcOperationTimeoutMs, Zn as ProvisioningError, Zt as getBootstrapMarkerKey, _ as computeImplicitDeleteEdges, _n as resolveStateBucketWithDefault, _t as STATE_SOURCED_READBACK_RULES, a as DeploymentEventsStore, an as runDockerForeground, at as normalizeAwsTagsToCfn, b as buildFinalSnapshotIdentifier, bn as stateBucketExistenceConfirmed, bt as maskSecretsInText, c as replayFailedOperations, cn as getDockerImageBySourceHash, cr as isMarkedNonRetryable, ct as coerceCfnBoolean, d as updatePartialReason, dn as getDefaultStateBucketName, dr as markNonRetryable, dt as readConfigString, en as validateContainerRepoName, er as StackHasActiveImportsError, et as IntrinsicFunctionResolver, f as UNSPECIFIED_SKIP_REASON, fn as getLegacyStateBucketName, ft as replayWarn, g as IMPLICIT_DELETE_DEPENDENCIES, gn as resolveSkipPrefix, gt as STATE_SOURCED_CROSS_GENERATION_RULES, h as maskingRetryLogger, hn as resolveCaptureObservedState, ht as requireConfigString, i as DeploymentEventsReader, in as getDockerCmd, it as WAFv2WebACLProvider, j as formatResourceLine, jn as AssemblyReader, jt as describeTypeWithThrottleRetry, k as isStatefulRecreateTargetSync, kn as canonicalizeRegion, kt as DiffCalculator, l as replayRollback, ln as Synthesizer, lr as isRetryableTransientError, lt as configBooleanRefusal, m as withResourceDeadline, mn as resolveAutoAssetStorage, mt as requireConfigObject, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as buildDockerImage, nt as getAccountInfo, o as planFailedOps, on as runDockerStreaming, or as normalizeAwsError, ot as resolveExplicitPhysicalId, p as deleteSkipReason, pn as resolveApp, pt as requireConfigArray, q as ProviderRegistry, qt as stripControlChars, r as DeployEngine, rn as formatDockerLoginError, rt as refStateLookupFromResource, s as planRollback, sn as AssetManifestLoader, sr as withErrorHandling, st as assertRegionMatch, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as buildDenyExternalAccessPolicy, tr as StackTerminationProtectionError, tt as cfnRefValueFromPhysicalId, u as updatePartialMessage, un as synthesisStatusMessage, ur as isThrottlingError, ut as configStringRefusal, v as ATOMIC_FINAL_SNAPSHOT_TYPES, vn as resolveStateBucketWithDefaultAndSource, vt as TEMPLATE_SOURCED_RULES, w as refusesFinalSnapshot, wn as MIGRATE_TMP_PREFIX, wt as s3BucketDomainName, x as ccRoutedFinalSnapshotError, xn as warnDeprecatedNoPrefixCliFlag, xt as redactSecretsForState, y as PRE_DELETE_SNAPSHOT_TYPES, yn as resolveUseCdkBootstrapAssets, yt as createSecretMasker, z as collectPublishedOutputNames, zt as AssetPublisher } from "./deploy-engine-Bnu1WuRI.js";
|
|
3
|
+
import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, f as getCurrentStackName, h as withStackName, i as runStackBuffered, m as withSkipPrefix, n as getLogger, o as PATTERN_B_NAME_OPTIONS, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-zRrlbaQt.js";
|
|
4
|
+
import { t as ASGProvider } from "./asg-provider-BbLu5LGL.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";
|
|
@@ -30,7 +30,7 @@ import * as path from "node:path";
|
|
|
30
30
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
31
31
|
import { execFile, spawn } from "node:child_process";
|
|
32
32
|
import { tmpdir } from "node:os";
|
|
33
|
-
import { AssociateVPCWithHostedZoneCommand, ChangeResourceRecordSetsCommand, ChangeTagsForResourceCommand, CreateHostedZoneCommand, CreateQueryLoggingConfigCommand, DeleteHostedZoneCommand, DeleteQueryLoggingConfigCommand, DisassociateVPCFromHostedZoneCommand, GetHostedZoneCommand, ListHostedZonesByNameCommand, ListQueryLoggingConfigsCommand, ListResourceRecordSetsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$8, Route53Client, UpdateHostedZoneCommentCommand, UpdateHostedZoneFeaturesCommand } from "@aws-sdk/client-route-53";
|
|
33
|
+
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";
|
|
34
34
|
import { AddTagsCommand, CreateListenerCommand, CreateLoadBalancerCommand, CreateTargetGroupCommand, DeleteListenerCommand, DeleteLoadBalancerCommand, DeleteTargetGroupCommand, DeregisterTargetsCommand, DescribeCapacityReservationCommand, DescribeListenerAttributesCommand, DescribeListenersCommand, DescribeLoadBalancerAttributesCommand, DescribeLoadBalancersCommand, DescribeTagsCommand, DescribeTargetGroupAttributesCommand, DescribeTargetGroupsCommand, DescribeTargetHealthCommand, ElasticLoadBalancingV2Client, ModifyCapacityReservationCommand, ModifyIpPoolsCommand, ModifyListenerAttributesCommand, ModifyListenerCommand, ModifyLoadBalancerAttributesCommand, ModifyTargetGroupAttributesCommand, ModifyTargetGroupCommand, RegisterTargetsCommand, RemoveTagsCommand, SetIpAddressTypeCommand, SetSecurityGroupsCommand, SetSubnetsCommand, waitUntilLoadBalancerAvailable } from "@aws-sdk/client-elastic-load-balancing-v2";
|
|
35
35
|
import { CreateAliasCommand, CreateKeyCommand, DeleteAliasCommand, DescribeKeyCommand, DisableKeyCommand, DisableKeyRotationCommand, EnableKeyCommand, EnableKeyRotationCommand, GetKeyPolicyCommand, GetKeyRotationStatusCommand, KMSClient, ListAliasesCommand, ListResourceTagsCommand, NotFoundException as NotFoundException$2, PutKeyPolicyCommand, ScheduleKeyDeletionCommand, TagResourceCommand as TagResourceCommand$11, UntagResourceCommand as UntagResourceCommand$11, UpdateAliasCommand, UpdateKeyDescriptionCommand } from "@aws-sdk/client-kms";
|
|
36
36
|
import { join as join$1 } from "path";
|
|
@@ -3845,6 +3845,16 @@ var IAMInstanceProfileProvider = class {
|
|
|
3845
3845
|
//#endregion
|
|
3846
3846
|
//#region src/provisioning/providers/iam-access-key-provider.ts
|
|
3847
3847
|
/**
|
|
3848
|
+
* How far before an attempt's start a key's `CreateDate` may fall and still be
|
|
3849
|
+
* treated as minted by that attempt (issue #2039).
|
|
3850
|
+
*
|
|
3851
|
+
* IAM stamps `CreateDate` from its own clock while the floor comes from ours,
|
|
3852
|
+
* so some tolerance is needed or ordinary skew disarms the check entirely. It
|
|
3853
|
+
* is deliberately small: every millisecond of it is a millisecond in which a
|
|
3854
|
+
* key created by something else becomes deletable.
|
|
3855
|
+
*/
|
|
3856
|
+
const CREATE_DATE_SKEW_MARGIN_MS = 5e3;
|
|
3857
|
+
/**
|
|
3848
3858
|
* AWS IAM AccessKey Provider (issue #1323)
|
|
3849
3859
|
*
|
|
3850
3860
|
* Implements resource provisioning for AWS::IAM::AccessKey using the IAM SDK.
|
|
@@ -3894,6 +3904,38 @@ var IAMAccessKeyProvider = class {
|
|
|
3894
3904
|
"Serial",
|
|
3895
3905
|
"Status"
|
|
3896
3906
|
])]]);
|
|
3907
|
+
/**
|
|
3908
|
+
* Serializes create attempts per IAM user, in this process (issue
|
|
3909
|
+
* [#2039](https://github.com/go-to-k/cdkd/issues/2039)).
|
|
3910
|
+
*
|
|
3911
|
+
* The orphan reconcile below identifies "a key that appeared since my
|
|
3912
|
+
* baseline". With two `AWS::IAM::AccessKey` resources on ONE user — the
|
|
3913
|
+
* ordinary two-key rotation shape — the deploy engine dispatches both
|
|
3914
|
+
* concurrently at the default `--concurrency 10`, and a key the SIBLING
|
|
3915
|
+
* legitimately created lands inside that window and matches the same
|
|
3916
|
+
* description. Deleting it destroys a live credential that is already in
|
|
3917
|
+
* cdkd state and may already be piped into a downstream Secrets Manager
|
|
3918
|
+
* secret via `Fn::GetAtt`, and `readCurrentState` returns `undefined` for a
|
|
3919
|
+
* deleted key, so `cdkd drift` reports UNKNOWN rather than drift and the
|
|
3920
|
+
* loss is invisible. That is strictly worse than the untracked-but-alive
|
|
3921
|
+
* credential this whole mechanism exists to prevent.
|
|
3922
|
+
*
|
|
3923
|
+
* This lock removes that race rather than narrowing it: the baseline read,
|
|
3924
|
+
* the create attempt, and the reconcile all happen under it, so no sibling
|
|
3925
|
+
* create on the same user can interleave with them.
|
|
3926
|
+
*/
|
|
3927
|
+
userCreateLocks = /* @__PURE__ */ new Map();
|
|
3928
|
+
/**
|
|
3929
|
+
* Access key ids THIS process created successfully.
|
|
3930
|
+
*
|
|
3931
|
+
* The complement of the baseline: a baseline can only say "this key is
|
|
3932
|
+
* newer than my snapshot", which is true both of the orphan left by my own
|
|
3933
|
+
* failed attempt AND of a key someone else minted in the same window. This
|
|
3934
|
+
* set names the keys cdkd itself owns and has already recorded, so they are
|
|
3935
|
+
* never deletable however the timing falls — including the case the lock
|
|
3936
|
+
* cannot cover, a second `cdkd` process sharing the user.
|
|
3937
|
+
*/
|
|
3938
|
+
accessKeyIdsCreatedByThisProcess = /* @__PURE__ */ new Set();
|
|
3897
3939
|
constructor() {
|
|
3898
3940
|
const awsClients = getAwsClients();
|
|
3899
3941
|
this.iamClient = awsClients.iam;
|
|
@@ -3912,6 +3954,16 @@ var IAMAccessKeyProvider = class {
|
|
|
3912
3954
|
const userName = properties["UserName"];
|
|
3913
3955
|
if (!userName) throw new ProvisioningError(`AccessKey ${logicalId} requires UserName`, resourceType, logicalId);
|
|
3914
3956
|
const status = requireConfigString(properties["Status"], "Active", "AWS::IAM::AccessKey Status", replayWarn(this.logger, context));
|
|
3957
|
+
return this.withUserCreateLock(userName, () => this.createAccessKeyUnderUserLock(logicalId, resourceType, userName, status));
|
|
3958
|
+
}
|
|
3959
|
+
/**
|
|
3960
|
+
* The body of {@link IAMAccessKeyProvider.create}, running with this user's
|
|
3961
|
+
* create lock held so the baseline, the create, and the reconcile are atomic
|
|
3962
|
+
* against any sibling create on the same user.
|
|
3963
|
+
*/
|
|
3964
|
+
async createAccessKeyUnderUserLock(logicalId, resourceType, userName, status) {
|
|
3965
|
+
const attemptStartMs = Date.now() - CREATE_DATE_SKEW_MARGIN_MS;
|
|
3966
|
+
const baseline = await this.tryListAccessKeyIds(userName, logicalId);
|
|
3915
3967
|
try {
|
|
3916
3968
|
const response = await this.iamClient.send(new CreateAccessKeyCommand({ UserName: userName }));
|
|
3917
3969
|
const accessKeyId = response.AccessKey?.AccessKeyId;
|
|
@@ -3946,6 +3998,7 @@ var IAMAccessKeyProvider = class {
|
|
|
3946
3998
|
throw innerError;
|
|
3947
3999
|
}
|
|
3948
4000
|
this.logger.debug(`Successfully created IAM access key ${logicalId}: ${accessKeyId}`);
|
|
4001
|
+
this.accessKeyIdsCreatedByThisProcess.add(accessKeyId);
|
|
3949
4002
|
return {
|
|
3950
4003
|
physicalId: accessKeyId,
|
|
3951
4004
|
attributes: {
|
|
@@ -3954,11 +4007,135 @@ var IAMAccessKeyProvider = class {
|
|
|
3954
4007
|
}
|
|
3955
4008
|
};
|
|
3956
4009
|
} catch (error) {
|
|
4010
|
+
await this.deleteOrphanFromFailedAttempt(userName, logicalId, baseline, attemptStartMs);
|
|
3957
4011
|
const cause = error instanceof Error ? error : void 0;
|
|
3958
4012
|
throw new ProvisioningError(`Failed to create IAM access key ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
|
|
3959
4013
|
}
|
|
3960
4014
|
}
|
|
3961
4015
|
/**
|
|
4016
|
+
* Run `fn` with this IAM user's create lock held.
|
|
4017
|
+
*
|
|
4018
|
+
* Queue rather than mutual exclusion: each caller waits on its predecessor
|
|
4019
|
+
* and publishes its own completion for the next one, so N concurrent creates
|
|
4020
|
+
* on one user run strictly in arrival order. The stored promise only ever
|
|
4021
|
+
* RESOLVES (the release is in a `finally`), so one create failing cannot
|
|
4022
|
+
* wedge the queue behind it.
|
|
4023
|
+
*
|
|
4024
|
+
* Process-scoped, like every other guard here. A second `cdkd` process on the
|
|
4025
|
+
* same user is out of its reach, which is why
|
|
4026
|
+
* {@link IAMAccessKeyProvider.accessKeyIdsCreatedByThisProcess} and the
|
|
4027
|
+
* `CreateDate` floor exist as well.
|
|
4028
|
+
*/
|
|
4029
|
+
async withUserCreateLock(userName, fn) {
|
|
4030
|
+
const predecessor = this.userCreateLocks.get(userName) ?? Promise.resolve();
|
|
4031
|
+
let release;
|
|
4032
|
+
const held = new Promise((resolve) => {
|
|
4033
|
+
release = resolve;
|
|
4034
|
+
});
|
|
4035
|
+
this.userCreateLocks.set(userName, predecessor.then(() => held));
|
|
4036
|
+
await predecessor;
|
|
4037
|
+
try {
|
|
4038
|
+
return await fn();
|
|
4039
|
+
} finally {
|
|
4040
|
+
release();
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
/**
|
|
4044
|
+
* Delete the access key THIS attempt minted before losing its response
|
|
4045
|
+
* (issue [#2039](https://github.com/go-to-k/cdkd/issues/2039)).
|
|
4046
|
+
*
|
|
4047
|
+
* ## Why a reconcile rather than a token or an opt-out
|
|
4048
|
+
*
|
|
4049
|
+
* `CreateAccessKey` has no `ClientToken` / `ClientRequestToken` member, so
|
|
4050
|
+
* there is nothing to make the call itself idempotent. The other candidate
|
|
4051
|
+
* was `disableOuterRetry`, and it costs too much here: it would make the
|
|
4052
|
+
* provider single-shot for EVERY transient error, including the IAM
|
|
4053
|
+
* propagation window between a sibling `AWS::IAM::User` being created and
|
|
4054
|
+
* being visible to `CreateAccessKey`.
|
|
4055
|
+
*
|
|
4056
|
+
* The orphan cannot be ADOPTED — its secret was returned once, in the
|
|
4057
|
+
* response that was lost — so deleting it is the only remedy available.
|
|
4058
|
+
*
|
|
4059
|
+
* ## Three independent conditions, because deleting the wrong key is worse
|
|
4060
|
+
* than leaving an orphan
|
|
4061
|
+
*
|
|
4062
|
+
* A key is deleted only when ALL of these hold:
|
|
4063
|
+
*
|
|
4064
|
+
* 1. It is absent from the baseline taken moments earlier, under the lock.
|
|
4065
|
+
* 2. Its `CreateDate` is at or after this attempt started. IAM stamps that
|
|
4066
|
+
* date from ITS clock and the floor comes from ours, so a margin absorbs
|
|
4067
|
+
* ordinary skew; the residual risk is bounded by the length of ONE
|
|
4068
|
+
* `CreateAccessKey` call rather than by the retry schedule.
|
|
4069
|
+
* 3. cdkd did not create it successfully in this process. This is the one
|
|
4070
|
+
* the baseline structurally cannot express — "newer than my snapshot" is
|
|
4071
|
+
* equally true of my orphan and of somebody else's key.
|
|
4072
|
+
*
|
|
4073
|
+
* Anything that fails 2 or 3 is reported and LEFT ALONE. That is the safe
|
|
4074
|
+
* direction: an untracked key costs a quota slot and a manual cleanup, while
|
|
4075
|
+
* a wrongly deleted one destroys a live credential that cdkd state still
|
|
4076
|
+
* advertises — and because `readCurrentState` returns `undefined` for a
|
|
4077
|
+
* deleted key, `cdkd drift` would report UNKNOWN rather than drift, so the
|
|
4078
|
+
* loss would not even be visible.
|
|
4079
|
+
*
|
|
4080
|
+
* @param baseline `undefined` when the pre-create read failed, which disarms
|
|
4081
|
+
* the reconcile entirely — see {@link IAMAccessKeyProvider.tryListAccessKeyIds}.
|
|
4082
|
+
*/
|
|
4083
|
+
async deleteOrphanFromFailedAttempt(userName, logicalId, baseline, attemptStartMs) {
|
|
4084
|
+
if (baseline === void 0) return;
|
|
4085
|
+
const current = await this.tryListAccessKeyMetadata(userName, logicalId);
|
|
4086
|
+
if (!current) return;
|
|
4087
|
+
for (const key of current) {
|
|
4088
|
+
const accessKeyId = key.accessKeyId;
|
|
4089
|
+
if (baseline.has(accessKeyId) || this.accessKeyIdsCreatedByThisProcess.has(accessKeyId)) continue;
|
|
4090
|
+
if (key.createDateMs === void 0 || key.createDateMs < attemptStartMs) {
|
|
4091
|
+
this.logger.warn(`IAM access key ${accessKeyId} appeared on user ${userName} while creating ${logicalId}, but cdkd cannot attribute it to this attempt (created ${key.createDateMs === void 0 ? "at an unknown time" : new Date(key.createDateMs).toISOString()}, attempt started ${new Date(attemptStartMs).toISOString()}). Leaving it in place. If it is an orphan of a failed cdkd deploy, delete it with: aws iam delete-access-key --user-name ${userName} --access-key-id ${accessKeyId}`);
|
|
4092
|
+
continue;
|
|
4093
|
+
}
|
|
4094
|
+
this.logger.warn(`IAM access key ${accessKeyId} was minted on user ${userName} by this failed attempt at ${logicalId} and its secret was lost with the response, so it is unusable and unrecorded; deleting it before the retry`);
|
|
4095
|
+
try {
|
|
4096
|
+
await this.iamClient.send(new DeleteAccessKeyCommand({
|
|
4097
|
+
UserName: userName,
|
|
4098
|
+
AccessKeyId: accessKeyId
|
|
4099
|
+
}));
|
|
4100
|
+
} catch (error) {
|
|
4101
|
+
this.logger.warn(`Failed to delete the orphaned IAM access key ${accessKeyId} for user ${userName}: ${error instanceof Error ? error.message : String(error)}. Manual deletion may be required: aws iam delete-access-key --user-name ${userName} --access-key-id ${accessKeyId}`);
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
/**
|
|
4106
|
+
* The user's current access key ids, or `undefined` when the read failed.
|
|
4107
|
+
*
|
|
4108
|
+
* `undefined` disarms the reconcile rather than failing the create: a missing
|
|
4109
|
+
* `iam:ListAccessKeys` permission must not turn a working deploy into a
|
|
4110
|
+
* broken one over a safety net.
|
|
4111
|
+
*/
|
|
4112
|
+
async tryListAccessKeyIds(userName, logicalId) {
|
|
4113
|
+
const metadata = await this.tryListAccessKeyMetadata(userName, logicalId);
|
|
4114
|
+
return metadata && new Set(metadata.map((key) => key.accessKeyId));
|
|
4115
|
+
}
|
|
4116
|
+
/** `ListAccessKeys` with its `CreateDate`s, or `undefined` when the read failed. */
|
|
4117
|
+
async tryListAccessKeyMetadata(userName, logicalId) {
|
|
4118
|
+
const keys = [];
|
|
4119
|
+
let marker;
|
|
4120
|
+
try {
|
|
4121
|
+
do {
|
|
4122
|
+
const page = await this.iamClient.send(new ListAccessKeysCommand({
|
|
4123
|
+
UserName: userName,
|
|
4124
|
+
...marker && { Marker: marker }
|
|
4125
|
+
}));
|
|
4126
|
+
for (const key of page.AccessKeyMetadata ?? []) if (key.AccessKeyId) keys.push({
|
|
4127
|
+
accessKeyId: key.AccessKeyId,
|
|
4128
|
+
createDateMs: key.CreateDate ? key.CreateDate.getTime() : void 0
|
|
4129
|
+
});
|
|
4130
|
+
marker = page.IsTruncated ? page.Marker : void 0;
|
|
4131
|
+
} while (marker);
|
|
4132
|
+
} catch (error) {
|
|
4133
|
+
this.logger.warn(`Could not list existing access keys for user ${userName} while creating ${logicalId}: ${error instanceof Error ? error.message : String(error)}. Orphan detection is DISABLED for this create, so a retried attempt may leave an unusable but ACTIVE access key on the user. Grant iam:ListAccessKeys, or check the user's keys after the deploy: aws iam list-access-keys --user-name ${userName}`);
|
|
4134
|
+
return;
|
|
4135
|
+
}
|
|
4136
|
+
return keys;
|
|
4137
|
+
}
|
|
4138
|
+
/**
|
|
3962
4139
|
* Update an IAM access key.
|
|
3963
4140
|
*
|
|
3964
4141
|
* `UserName` / `Serial` are createOnly (replacement — this method never
|
|
@@ -23206,6 +23383,118 @@ var EventBridgeBusProvider = class {
|
|
|
23206
23383
|
}
|
|
23207
23384
|
};
|
|
23208
23385
|
|
|
23386
|
+
//#endregion
|
|
23387
|
+
//#region src/provisioning/providers/idempotency-token.ts
|
|
23388
|
+
/**
|
|
23389
|
+
* Retry-stable idempotency tokens for provider CREATE calls (issue
|
|
23390
|
+
* [#2039](https://github.com/go-to-k/cdkd/issues/2039)).
|
|
23391
|
+
*
|
|
23392
|
+
* ## The failure this exists to prevent
|
|
23393
|
+
*
|
|
23394
|
+
* The deploy engine wraps every `provider.create()` in an outer transient-error
|
|
23395
|
+
* retry (`src/deployment/retry.ts`), and issue #2026 made HTTP 500 / 502 / 504
|
|
23396
|
+
* retryable for the default classifier. A 500 whose request actually SUCCEEDED
|
|
23397
|
+
* server-side therefore re-invokes `create()` from the top. For a CREATE that
|
|
23398
|
+
* carries no name and no idempotency token, the replay mints a SECOND resource:
|
|
23399
|
+
* the first has no state entry, so `cdkd destroy` never reaches it and it bills
|
|
23400
|
+
* indefinitely. `RunInstances` is the worst instance of the class.
|
|
23401
|
+
*
|
|
23402
|
+
* ## What "stable" has to mean, precisely
|
|
23403
|
+
*
|
|
23404
|
+
* A token regenerated per attempt is not a fix — it is worse than none, because
|
|
23405
|
+
* the call site now LOOKS idempotent while behaving exactly as before. So the
|
|
23406
|
+
* value must be:
|
|
23407
|
+
*
|
|
23408
|
+
* - **Identical across every attempt of one logical create.** Guaranteed here
|
|
23409
|
+
* by memoising on `(scope, region, stackName, logicalId)` — a tuple the retry
|
|
23410
|
+
* loop reproduces byte-for-byte, because it re-invokes the same closure with
|
|
23411
|
+
* the same arguments. Deliberately NOT keyed on the resolved properties.
|
|
23412
|
+
* (The earlier version of this note justified that by saying the properties
|
|
23413
|
+
* are re-derived per attempt. They are NOT: `deploy-engine.ts` computes them
|
|
23414
|
+
* once OUTSIDE the retry closure. The reason to keep them out of the key is
|
|
23415
|
+
* that it makes the guarantee STRUCTURAL rather than contingent — an
|
|
23416
|
+
* identity built from a logical id cannot be perturbed by a future change to
|
|
23417
|
+
* how properties are resolved, whereas one built from their contents can.)
|
|
23418
|
+
* - **Different for a genuinely NEW create of the same logical resource.**
|
|
23419
|
+
* Reusing a token after a create SUCCEEDED is its own hazard: EC2 keeps a
|
|
23420
|
+
* `RunInstances` token for ~24h and answers a repeat with the ORIGINAL
|
|
23421
|
+
* instance, so a `--replace` re-create (delete old, then create with
|
|
23422
|
+
* unchanged properties, all in one process) would be handed back the
|
|
23423
|
+
* instance it had just terminated. {@link IdempotencyToken.release} is what
|
|
23424
|
+
* prevents that: it is called only on the success path, and it bumps a
|
|
23425
|
+
* per-key generation counter so the next acquire derives a fresh value.
|
|
23426
|
+
* - **Never reused by a LATER cdkd run.** A per-process nonce is mixed into
|
|
23427
|
+
* the digest, so two `cdkd deploy` invocations of the same template never
|
|
23428
|
+
* collide even though every other input is identical.
|
|
23429
|
+
*
|
|
23430
|
+
* ## Why not a purely deterministic hash of the create inputs
|
|
23431
|
+
*
|
|
23432
|
+
* That is what `EFSProvider`'s `CreationToken` and `FSxFileSystemProvider`'s
|
|
23433
|
+
* `ClientRequestToken` do, and it is right FOR THEM: those APIs enforce token
|
|
23434
|
+
* uniqueness only among LIVE file systems, so a re-deploy after a destroy is
|
|
23435
|
+
* unaffected, and hashing the immutable inputs is what lets the new file system
|
|
23436
|
+
* coexist with the old one during a replacement. EC2 is the opposite shape —
|
|
23437
|
+
* the token survives the resource's own termination — so a value that is stable
|
|
23438
|
+
* across RUNS is a liability there rather than an asset. Both providers keep
|
|
23439
|
+
* their existing derivation; this module is for the APIs where the token must
|
|
23440
|
+
* be scoped to one process and one create.
|
|
23441
|
+
*/
|
|
23442
|
+
/**
|
|
23443
|
+
* Mixed into every digest so no two `cdkd` processes can derive the same token
|
|
23444
|
+
* from the same template. Evaluated once at module load; ESM guarantees a
|
|
23445
|
+
* single module instance per process, which is exactly the scope wanted.
|
|
23446
|
+
*/
|
|
23447
|
+
const PROCESS_NONCE = randomUUID();
|
|
23448
|
+
/** Tokens currently in flight, keyed by {@link tokenKey}. */
|
|
23449
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
23450
|
+
/**
|
|
23451
|
+
* How many times each key has been RELEASED. Folded into the digest so a
|
|
23452
|
+
* post-success re-acquire (the `--replace` delete-then-create path) yields a
|
|
23453
|
+
* different token than the create that already succeeded.
|
|
23454
|
+
*/
|
|
23455
|
+
const generations = /* @__PURE__ */ new Map();
|
|
23456
|
+
/**
|
|
23457
|
+
* Ceiling on {@link generations}. Chosen far above any realistic stack size so
|
|
23458
|
+
* eviction is unreachable in a normal `cdkd` run, and present only so a
|
|
23459
|
+
* long-lived process cannot grow the map without bound.
|
|
23460
|
+
*/
|
|
23461
|
+
const MAX_TRACKED_GENERATIONS = 1e5;
|
|
23462
|
+
const tokenKey = (scope, logicalId) => `${scope}\u0000${process.env["AWS_REGION"] ?? ""}\u0000${getCurrentStackName() ?? ""}\u0000${logicalId}`;
|
|
23463
|
+
const derive = (key, generation, maxLength) => {
|
|
23464
|
+
return `cdkd-${createHash("sha256").update(`${PROCESS_NONCE}\u0000${key}\u0000${generation}`).digest("hex")}`.slice(0, maxLength);
|
|
23465
|
+
};
|
|
23466
|
+
/**
|
|
23467
|
+
* Get the idempotency token for one logical create, minting it on first use and
|
|
23468
|
+
* returning the SAME value for every retry of that create.
|
|
23469
|
+
*
|
|
23470
|
+
* @example
|
|
23471
|
+
* ```ts
|
|
23472
|
+
* const token = acquireIdempotencyToken({ scope: 'RunInstances', logicalId });
|
|
23473
|
+
* const response = await client.send(new RunInstancesCommand({ ...input, ClientToken: token.value }));
|
|
23474
|
+
* token.release(); // success path only
|
|
23475
|
+
* ```
|
|
23476
|
+
*/
|
|
23477
|
+
function acquireIdempotencyToken(options) {
|
|
23478
|
+
const { scope, logicalId, maxLength = 64 } = options;
|
|
23479
|
+
const key = tokenKey(scope, logicalId);
|
|
23480
|
+
const existing = inFlight.get(key);
|
|
23481
|
+
const value = existing ?? derive(key, generations.get(key) ?? 0, maxLength);
|
|
23482
|
+
if (existing === void 0) inFlight.set(key, value);
|
|
23483
|
+
return {
|
|
23484
|
+
value,
|
|
23485
|
+
release: () => {
|
|
23486
|
+
if (!inFlight.has(key)) return;
|
|
23487
|
+
inFlight.delete(key);
|
|
23488
|
+
const generation = (generations.get(key) ?? 0) + 1;
|
|
23489
|
+
if (!generations.has(key) && generations.size >= MAX_TRACKED_GENERATIONS) {
|
|
23490
|
+
const oldest = generations.keys().next();
|
|
23491
|
+
if (!oldest.done) generations.delete(oldest.value);
|
|
23492
|
+
}
|
|
23493
|
+
generations.set(key, generation);
|
|
23494
|
+
}
|
|
23495
|
+
};
|
|
23496
|
+
}
|
|
23497
|
+
|
|
23209
23498
|
//#endregion
|
|
23210
23499
|
//#region src/utils/ip-protocol.ts
|
|
23211
23500
|
/**
|
|
@@ -24274,8 +24563,13 @@ var EC2Provider = class {
|
|
|
24274
24563
|
this.logger.debug(`Creating NatGateway ${logicalId}`);
|
|
24275
24564
|
const subnetId = properties["SubnetId"];
|
|
24276
24565
|
if (!subnetId) throw new ProvisioningError(`SubnetId is required for NatGateway ${logicalId}`, resourceType, logicalId);
|
|
24566
|
+
const natGatewayToken = acquireIdempotencyToken({
|
|
24567
|
+
scope: "CreateNatGateway",
|
|
24568
|
+
logicalId
|
|
24569
|
+
});
|
|
24277
24570
|
try {
|
|
24278
24571
|
const natGatewayId = (await this.ec2Client.send(new CreateNatGatewayCommand({
|
|
24572
|
+
ClientToken: natGatewayToken.value,
|
|
24279
24573
|
SubnetId: subnetId,
|
|
24280
24574
|
AllocationId: properties["AllocationId"],
|
|
24281
24575
|
ConnectivityType: properties["ConnectivityType"] ?? void 0,
|
|
@@ -24296,6 +24590,7 @@ var EC2Provider = class {
|
|
|
24296
24590
|
this.logger.debug(`NatGateway ${natGatewayId} is available`);
|
|
24297
24591
|
} else this.logger.debug(`NatGateway ${natGatewayId} created (skipping available-state wait per --no-wait)`);
|
|
24298
24592
|
this.logger.debug(`Successfully created NatGateway ${logicalId}: ${natGatewayId}`);
|
|
24593
|
+
natGatewayToken.release();
|
|
24299
24594
|
return {
|
|
24300
24595
|
physicalId: natGatewayId,
|
|
24301
24596
|
attributes: { NatGatewayId: natGatewayId }
|
|
@@ -24338,10 +24633,18 @@ var EC2Provider = class {
|
|
|
24338
24633
|
this.logger.debug(`Creating RouteTable ${logicalId}`);
|
|
24339
24634
|
const vpcId = properties["VpcId"];
|
|
24340
24635
|
if (!vpcId) throw new ProvisioningError(`VpcId is required for RouteTable ${logicalId}`, resourceType, logicalId);
|
|
24636
|
+
const routeTableToken = acquireIdempotencyToken({
|
|
24637
|
+
scope: "CreateRouteTable",
|
|
24638
|
+
logicalId
|
|
24639
|
+
});
|
|
24341
24640
|
try {
|
|
24342
|
-
const routeTableId = (await this.ec2Client.send(new CreateRouteTableCommand({
|
|
24641
|
+
const routeTableId = (await this.ec2Client.send(new CreateRouteTableCommand({
|
|
24642
|
+
VpcId: vpcId,
|
|
24643
|
+
ClientToken: routeTableToken.value
|
|
24644
|
+
}))).RouteTable.RouteTableId;
|
|
24343
24645
|
await this.applyTags(routeTableId, properties, logicalId);
|
|
24344
24646
|
this.logger.debug(`Successfully created RouteTable ${logicalId}: ${routeTableId}`);
|
|
24647
|
+
routeTableToken.release();
|
|
24345
24648
|
return {
|
|
24346
24649
|
physicalId: routeTableId,
|
|
24347
24650
|
attributes: { RouteTableId: routeTableId }
|
|
@@ -24890,7 +25193,12 @@ var EC2Provider = class {
|
|
|
24890
25193
|
const securityGroups = properties["SecurityGroups"];
|
|
24891
25194
|
const iamInstanceProfile = this.normalizeIamInstanceProfile(properties["IamInstanceProfile"]);
|
|
24892
25195
|
const networkInterfaces = this.buildNetworkInterfaces(properties);
|
|
25196
|
+
const runInstancesToken = acquireIdempotencyToken({
|
|
25197
|
+
scope: "RunInstances",
|
|
25198
|
+
logicalId
|
|
25199
|
+
});
|
|
24893
25200
|
const instance = (await this.ec2Client.send(new RunInstancesCommand({
|
|
25201
|
+
ClientToken: runInstancesToken.value,
|
|
24894
25202
|
ImageId: imageId,
|
|
24895
25203
|
InstanceType: instanceType,
|
|
24896
25204
|
KeyName: properties["KeyName"] ?? void 0,
|
|
@@ -24940,6 +25248,7 @@ var EC2Provider = class {
|
|
|
24940
25248
|
AvailabilityZone: runningInstance?.Placement?.AvailabilityZone ?? ""
|
|
24941
25249
|
};
|
|
24942
25250
|
this.logger.debug(`Successfully created EC2 Instance ${logicalId}: ${instanceId}`);
|
|
25251
|
+
runInstancesToken.release();
|
|
24943
25252
|
return {
|
|
24944
25253
|
physicalId: instanceId,
|
|
24945
25254
|
attributes
|
|
@@ -24947,6 +25256,7 @@ var EC2Provider = class {
|
|
|
24947
25256
|
} catch (innerError) {
|
|
24948
25257
|
try {
|
|
24949
25258
|
await this.ec2Client.send(new TerminateInstancesCommand({ InstanceIds: [instanceId] }));
|
|
25259
|
+
runInstancesToken.release();
|
|
24950
25260
|
this.logger.debug(`Terminate requested for partially-created EC2 Instance ${logicalId} (${instanceId}) after wiring failure (not waiting for terminated state)`);
|
|
24951
25261
|
} catch (cleanupError) {
|
|
24952
25262
|
this.logger.warn(`Failed to terminate partially-created EC2 Instance ${logicalId} (${instanceId}): ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}. THE INSTANCE IS STILL RUNNING AND BILLING. Manual termination required: aws ec2 terminate-instances --instance-ids ${instanceId}`);
|
|
@@ -25461,10 +25771,18 @@ var EC2Provider = class {
|
|
|
25461
25771
|
this.logger.debug(`Creating NetworkAcl ${logicalId}`);
|
|
25462
25772
|
const vpcId = properties["VpcId"];
|
|
25463
25773
|
if (!vpcId) throw new ProvisioningError(`VpcId is required for NetworkAcl ${logicalId}`, resourceType, logicalId);
|
|
25774
|
+
const networkAclToken = acquireIdempotencyToken({
|
|
25775
|
+
scope: "CreateNetworkAcl",
|
|
25776
|
+
logicalId
|
|
25777
|
+
});
|
|
25464
25778
|
try {
|
|
25465
|
-
const networkAclId = (await this.ec2Client.send(new CreateNetworkAclCommand({
|
|
25779
|
+
const networkAclId = (await this.ec2Client.send(new CreateNetworkAclCommand({
|
|
25780
|
+
VpcId: vpcId,
|
|
25781
|
+
ClientToken: networkAclToken.value
|
|
25782
|
+
}))).NetworkAcl.NetworkAclId;
|
|
25466
25783
|
await this.applyTags(networkAclId, properties, logicalId);
|
|
25467
25784
|
this.logger.debug(`Successfully created NetworkAcl ${logicalId}: ${networkAclId}`);
|
|
25785
|
+
networkAclToken.release();
|
|
25468
25786
|
return {
|
|
25469
25787
|
physicalId: networkAclId,
|
|
25470
25788
|
attributes: { Id: networkAclId }
|
|
@@ -38735,9 +39053,14 @@ var Route53Provider = class {
|
|
|
38735
39053
|
const createComment = readConfigString(hostedZoneConfig, "Comment", "", "AWS::Route53::HostedZone HostedZoneConfig");
|
|
38736
39054
|
const vpcs = properties["VPCs"];
|
|
38737
39055
|
const firstVpc = vpcs && vpcs.length > 0 ? vpcs[0] : void 0;
|
|
38738
|
-
const
|
|
39056
|
+
const callerReferenceToken = acquireIdempotencyToken({
|
|
39057
|
+
scope: "CreateHostedZone",
|
|
39058
|
+
logicalId,
|
|
39059
|
+
maxLength: 128
|
|
39060
|
+
});
|
|
39061
|
+
const response = await this.createOrAdoptHostedZone({
|
|
38739
39062
|
Name: name,
|
|
38740
|
-
CallerReference:
|
|
39063
|
+
CallerReference: callerReferenceToken.value,
|
|
38741
39064
|
...createComment !== "" ? { HostedZoneConfig: {
|
|
38742
39065
|
Comment: createComment,
|
|
38743
39066
|
...firstVpc ? { PrivateZone: true } : {}
|
|
@@ -38746,7 +39069,7 @@ var Route53Provider = class {
|
|
|
38746
39069
|
VPCId: firstVpc["VPCId"],
|
|
38747
39070
|
VPCRegion: firstVpc["VPCRegion"]
|
|
38748
39071
|
} } : {}
|
|
38749
|
-
})
|
|
39072
|
+
});
|
|
38750
39073
|
const hostedZone = response.HostedZone;
|
|
38751
39074
|
if (!hostedZone?.Id) throw new Error("CreateHostedZone did not return HostedZone.Id");
|
|
38752
39075
|
const zoneId = hostedZone.Id.replace("/hostedzone/", "");
|
|
@@ -38773,6 +39096,7 @@ var Route53Provider = class {
|
|
|
38773
39096
|
try {
|
|
38774
39097
|
await this.deleteQueryLoggingConfigForZone(zoneId, logicalId);
|
|
38775
39098
|
await this.getClient().send(new DeleteHostedZoneCommand({ Id: zoneId }));
|
|
39099
|
+
callerReferenceToken.release();
|
|
38776
39100
|
} catch (deleteError) {
|
|
38777
39101
|
this.logger.warn(`Best-effort rollback DeleteHostedZone for ${zoneId} also failed: ${deleteError instanceof Error ? deleteError.message : String(deleteError)} — operator may need to clean up the orphan zone`);
|
|
38778
39102
|
}
|
|
@@ -38781,6 +39105,7 @@ var Route53Provider = class {
|
|
|
38781
39105
|
}
|
|
38782
39106
|
const nameServers = response.DelegationSet?.NameServers ?? [];
|
|
38783
39107
|
this.logger.debug(`Successfully created hosted zone ${logicalId}: ${zoneId}`);
|
|
39108
|
+
callerReferenceToken.release();
|
|
38784
39109
|
return {
|
|
38785
39110
|
physicalId: zoneId,
|
|
38786
39111
|
attributes: {
|
|
@@ -38794,6 +39119,77 @@ var Route53Provider = class {
|
|
|
38794
39119
|
throw new ProvisioningError(`Failed to create hosted zone ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
|
|
38795
39120
|
}
|
|
38796
39121
|
}
|
|
39122
|
+
/**
|
|
39123
|
+
* `CreateHostedZone`, with the retry-replay case ADOPTED rather than failed
|
|
39124
|
+
* (issue [#2039](https://github.com/go-to-k/cdkd/issues/2039)).
|
|
39125
|
+
*
|
|
39126
|
+
* A stable `CallerReference` stops a replayed create from minting a second
|
|
39127
|
+
* zone, but on its own it converts the replay into a hard
|
|
39128
|
+
* `HostedZoneAlreadyExists` — so the first attempt's zone would still be live
|
|
39129
|
+
* and still absent from state, only now the deploy fails as well. Route 53
|
|
39130
|
+
* reports the caller reference back on every `HostedZone`, which is what lets
|
|
39131
|
+
* the replay find the zone the lost response described and continue with it.
|
|
39132
|
+
*
|
|
39133
|
+
* The lookup is `ListHostedZonesByName`, which starts at the requested DNS
|
|
39134
|
+
* name and returns zones in name order, so the walk stops as soon as the name
|
|
39135
|
+
* changes rather than paging the whole account. A `GetHostedZone` follows,
|
|
39136
|
+
* because only that call returns the `DelegationSet` whose `NameServers` are
|
|
39137
|
+
* the resource's `Fn::GetAtt` value — an adopted zone must return the same
|
|
39138
|
+
* attributes a first-attempt create would have.
|
|
39139
|
+
*
|
|
39140
|
+
* Finding nothing rethrows the original error: a caller reference AWS says is
|
|
39141
|
+
* taken but that names no zone we can see is not something to paper over.
|
|
39142
|
+
*/
|
|
39143
|
+
async createOrAdoptHostedZone(input) {
|
|
39144
|
+
try {
|
|
39145
|
+
return await this.getClient().send(new CreateHostedZoneCommand(input));
|
|
39146
|
+
} catch (error) {
|
|
39147
|
+
if (!(error instanceof HostedZoneAlreadyExists || error?.name === "HostedZoneAlreadyExists")) throw error;
|
|
39148
|
+
let adopted;
|
|
39149
|
+
try {
|
|
39150
|
+
adopted = await this.findHostedZoneByCallerReference(input.Name, input.CallerReference);
|
|
39151
|
+
} catch (lookupError) {
|
|
39152
|
+
this.logger.warn(`CreateHostedZone for ${input.Name} was refused as already-existing, and the lookup that would have adopted the existing zone failed: ${lookupError instanceof Error ? lookupError.message : String(lookupError)}. Reporting the original error.`);
|
|
39153
|
+
throw error;
|
|
39154
|
+
}
|
|
39155
|
+
if (!adopted?.Id) throw error;
|
|
39156
|
+
this.logger.warn(`CreateHostedZone for ${input.Name} was replayed after a lost response; adopting the hosted zone ${adopted.Id} the previous attempt already created instead of creating a second one`);
|
|
39157
|
+
try {
|
|
39158
|
+
const described = await this.getClient().send(new GetHostedZoneCommand({ Id: adopted.Id }));
|
|
39159
|
+
return {
|
|
39160
|
+
HostedZone: described.HostedZone ?? adopted,
|
|
39161
|
+
...described.DelegationSet ? { DelegationSet: described.DelegationSet } : {}
|
|
39162
|
+
};
|
|
39163
|
+
} catch (describeError) {
|
|
39164
|
+
this.logger.warn(`Adopted hosted zone ${adopted.Id} but could not read it back: ${describeError instanceof Error ? describeError.message : String(describeError)}. Reporting the original error rather than returning a zone with no NameServers.`);
|
|
39165
|
+
throw error;
|
|
39166
|
+
}
|
|
39167
|
+
}
|
|
39168
|
+
}
|
|
39169
|
+
/**
|
|
39170
|
+
* The hosted zone carrying `callerReference`, or `undefined`.
|
|
39171
|
+
*
|
|
39172
|
+
* Scoped to zones whose name matches `dnsName`: `ListHostedZonesByName` is
|
|
39173
|
+
* ordered by name, so once the name differs no later page can hold ours.
|
|
39174
|
+
*/
|
|
39175
|
+
async findHostedZoneByCallerReference(dnsName, callerReference) {
|
|
39176
|
+
const wanted = canonicalizeQueryName(dnsName);
|
|
39177
|
+
let marker = { DNSName: wanted };
|
|
39178
|
+
while (marker) {
|
|
39179
|
+
const page = await this.getClient().send(new ListHostedZonesByNameCommand({
|
|
39180
|
+
DNSName: marker.DNSName,
|
|
39181
|
+
...marker.HostedZoneId ? { HostedZoneId: marker.HostedZoneId } : {}
|
|
39182
|
+
}));
|
|
39183
|
+
for (const zone of page.HostedZones ?? []) {
|
|
39184
|
+
if (canonicalizeQueryName(zone.Name ?? "") !== wanted) return;
|
|
39185
|
+
if (zone.CallerReference === callerReference) return zone;
|
|
39186
|
+
}
|
|
39187
|
+
marker = page.IsTruncated && page.NextDNSName ? {
|
|
39188
|
+
DNSName: page.NextDNSName,
|
|
39189
|
+
...page.NextHostedZoneId ? { HostedZoneId: page.NextHostedZoneId } : {}
|
|
39190
|
+
} : void 0;
|
|
39191
|
+
}
|
|
39192
|
+
}
|
|
38797
39193
|
async updateHostedZone(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
38798
39194
|
this.logger.debug(`Updating Route 53 hosted zone ${logicalId}: ${physicalId}`);
|
|
38799
39195
|
try {
|
|
@@ -83294,7 +83690,7 @@ function createMigrateCommand() {
|
|
|
83294
83690
|
*/
|
|
83295
83691
|
function buildProgram() {
|
|
83296
83692
|
const program = new Command();
|
|
83297
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.
|
|
83693
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.17");
|
|
83298
83694
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
83299
83695
|
const { profile } = actionCommand.optsWithGlobals();
|
|
83300
83696
|
if (profile !== void 0) process.env["AWS_PROFILE"] = profile;
|