@go-to-k/cdkd 0.284.16 → 0.284.18
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-IPF2MfAC.js} +2 -2
- package/dist/{asg-provider-D7DqNvVo.js.map → asg-provider-IPF2MfAC.js.map} +1 -1
- package/dist/cli.js +525 -49
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-CI0a6yLX.js → deploy-engine-HK-oROJf.js} +24 -13
- package/dist/deploy-engine-HK-oROJf.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-HK-oROJf.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-IPF2MfAC.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 }
|
|
@@ -34435,10 +34753,18 @@ var ELBv2Provider = class {
|
|
|
34435
34753
|
* `withRetry`, whose per-attempt `debug` line and give-up `warn` summary
|
|
34436
34754
|
* interpolate the AWS message verbatim — and that payload is built from
|
|
34437
34755
|
* RESOLVED template properties. See {@link maskedRetryLogger}.
|
|
34756
|
+
*
|
|
34757
|
+
* The LoadBalancer arm receives it too (issue #2063). It runs no `withRetry`,
|
|
34758
|
+
* but it has the two surfaces that do not need one: the `ProvisioningError`
|
|
34759
|
+
* its catch throws (printed at ERROR — DEFAULT verbosity — by
|
|
34760
|
+
* `deploy-engine.ts`) and its partial-create cleanup `warn`. Its payload is
|
|
34761
|
+
* `Name` / `Subnets` / `SecurityGroups` / `LoadBalancerAttributes` values /
|
|
34762
|
+
* `Tags` values straight out of the resolved bag, and AWS quotes a rejected
|
|
34763
|
+
* value back.
|
|
34438
34764
|
*/
|
|
34439
34765
|
async create(logicalId, resourceType, properties, context) {
|
|
34440
34766
|
switch (resourceType) {
|
|
34441
|
-
case "AWS::ElasticLoadBalancingV2::LoadBalancer": return this.createLoadBalancer(logicalId, resourceType, properties);
|
|
34767
|
+
case "AWS::ElasticLoadBalancingV2::LoadBalancer": return this.createLoadBalancer(logicalId, resourceType, properties, context?.maskSecrets);
|
|
34442
34768
|
case "AWS::ElasticLoadBalancingV2::TargetGroup": return this.createTargetGroup(logicalId, resourceType, properties, context?.maskSecrets);
|
|
34443
34769
|
case "AWS::ElasticLoadBalancingV2::Listener": return this.createListener(logicalId, resourceType, properties, context?.maskSecrets);
|
|
34444
34770
|
default: throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId);
|
|
@@ -34447,6 +34773,15 @@ var ELBv2Provider = class {
|
|
|
34447
34773
|
/**
|
|
34448
34774
|
* `context` is read for ONE thing today: `maskSecrets` (issue #2050) — the
|
|
34449
34775
|
* update-path twin of the `create()` note above.
|
|
34776
|
+
*
|
|
34777
|
+
* The catch below is the LoadBalancer arm's masking site (issue #2063), and
|
|
34778
|
+
* the reason that arm needs no `maskSecrets` parameter of its own.
|
|
34779
|
+
* `updateLoadBalancer` has no try/catch: its `ModifyLoadBalancerAttributes` /
|
|
34780
|
+
* `SetSubnets` / `SetSecurityGroups` / `AddTags` rejections — every one of
|
|
34781
|
+
* them built from the resolved `properties` bag — propagate RAW to here.
|
|
34782
|
+
* The sibling arms wrap their own errors into a `ProvisioningError` first,
|
|
34783
|
+
* so the `CdkdError` passthrough short-circuits them and this frame masks
|
|
34784
|
+
* only what actually escapes unwrapped.
|
|
34450
34785
|
*/
|
|
34451
34786
|
async update(logicalId, physicalId, resourceType, properties, previousProperties, context) {
|
|
34452
34787
|
try {
|
|
@@ -34454,12 +34789,12 @@ var ELBv2Provider = class {
|
|
|
34454
34789
|
} catch (error) {
|
|
34455
34790
|
if (error instanceof CdkdError) throw error;
|
|
34456
34791
|
const cause = error instanceof Error ? error : void 0;
|
|
34457
|
-
throw new ProvisioningError(`Failed to update ELBv2 resource ${logicalId}: ${
|
|
34792
|
+
throw new ProvisioningError(`Failed to update ELBv2 resource ${logicalId}: ${this.maskErrorMessage(error, context?.maskSecrets)}`, resourceType, logicalId, physicalId, cause);
|
|
34458
34793
|
}
|
|
34459
34794
|
}
|
|
34460
34795
|
async applyUpdate(logicalId, physicalId, resourceType, properties, previousProperties, context) {
|
|
34461
34796
|
switch (resourceType) {
|
|
34462
|
-
case "AWS::ElasticLoadBalancingV2::LoadBalancer": return this.updateLoadBalancer(logicalId, physicalId, resourceType, properties, previousProperties);
|
|
34797
|
+
case "AWS::ElasticLoadBalancingV2::LoadBalancer": return this.updateLoadBalancer(logicalId, physicalId, resourceType, properties, previousProperties, context?.maskSecrets);
|
|
34463
34798
|
case "AWS::ElasticLoadBalancingV2::TargetGroup": return this.updateTargetGroup(logicalId, physicalId, resourceType, properties, previousProperties, context?.maskSecrets);
|
|
34464
34799
|
case "AWS::ElasticLoadBalancingV2::Listener": return this.updateListener(logicalId, physicalId, resourceType, properties, previousProperties, context?.maskSecrets);
|
|
34465
34800
|
default: throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId, physicalId);
|
|
@@ -34473,7 +34808,7 @@ var ELBv2Provider = class {
|
|
|
34473
34808
|
default: throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId, physicalId);
|
|
34474
34809
|
}
|
|
34475
34810
|
}
|
|
34476
|
-
async createLoadBalancer(logicalId, resourceType, properties) {
|
|
34811
|
+
async createLoadBalancer(logicalId, resourceType, properties, maskSecrets) {
|
|
34477
34812
|
this.logger.debug(`Creating LoadBalancer ${logicalId}`);
|
|
34478
34813
|
try {
|
|
34479
34814
|
const tags = this.extractTags(properties);
|
|
@@ -34520,7 +34855,7 @@ var ELBv2Provider = class {
|
|
|
34520
34855
|
SecurityGroups: properties["SecurityGroups"] ?? [],
|
|
34521
34856
|
EnforceSecurityGroupInboundRulesOnPrivateLinkTraffic: enforcePrivateLink
|
|
34522
34857
|
}));
|
|
34523
|
-
this.logger.debug(`Applied EnforceSecurityGroupInboundRulesOnPrivateLinkTraffic=${enforcePrivateLink} for ${logicalId}`);
|
|
34858
|
+
this.logger.debug(`Applied EnforceSecurityGroupInboundRulesOnPrivateLinkTraffic=${maskerOrIdentity(maskSecrets)(String(enforcePrivateLink))} for ${logicalId}`);
|
|
34524
34859
|
}
|
|
34525
34860
|
const minCapacity = properties["MinimumLoadBalancerCapacity"];
|
|
34526
34861
|
if (minCapacity?.CapacityUnits !== void 0) {
|
|
@@ -34528,7 +34863,7 @@ var ELBv2Provider = class {
|
|
|
34528
34863
|
LoadBalancerArn: lbArn,
|
|
34529
34864
|
MinimumLoadBalancerCapacity: { CapacityUnits: Number(minCapacity.CapacityUnits) }
|
|
34530
34865
|
}));
|
|
34531
|
-
this.logger.debug(`Requested capacity reservation of ${minCapacity.CapacityUnits} LCU for ${logicalId}`);
|
|
34866
|
+
this.logger.debug(`Requested capacity reservation of ${maskerOrIdentity(maskSecrets)(String(minCapacity.CapacityUnits))} LCU for ${logicalId}`);
|
|
34532
34867
|
if (isTruthyCfnBoolean(properties["EnableCapacityReservationProvisionStabilize"]) && process.env["CDKD_NO_WAIT"] !== "true") await this.waitForCapacityReservationProvisioned(lbArn, logicalId);
|
|
34533
34868
|
}
|
|
34534
34869
|
} catch (innerError) {
|
|
@@ -34536,7 +34871,7 @@ var ELBv2Provider = class {
|
|
|
34536
34871
|
await this.getClient().send(new DeleteLoadBalancerCommand({ LoadBalancerArn: lbArn }));
|
|
34537
34872
|
this.logger.debug(`Cleaned up partially-created LoadBalancer ${logicalId} (${lbArn}) after wiring failure`);
|
|
34538
34873
|
} catch (cleanupError) {
|
|
34539
|
-
this.logger.warn(`Failed to clean up partially-created LoadBalancer ${logicalId} (${lbArn}): ${
|
|
34874
|
+
this.logger.warn(`Failed to clean up partially-created LoadBalancer ${logicalId} (${lbArn}): ${this.maskErrorMessage(cleanupError, maskSecrets)}. Manual deletion may be required before the next deploy: aws elbv2 delete-load-balancer --load-balancer-arn ${lbArn}`);
|
|
34540
34875
|
}
|
|
34541
34876
|
throw innerError;
|
|
34542
34877
|
}
|
|
@@ -34552,10 +34887,10 @@ var ELBv2Provider = class {
|
|
|
34552
34887
|
};
|
|
34553
34888
|
} catch (error) {
|
|
34554
34889
|
const cause = error instanceof Error ? error : void 0;
|
|
34555
|
-
throw new ProvisioningError(`Failed to create LoadBalancer ${logicalId}: ${
|
|
34890
|
+
throw new ProvisioningError(`Failed to create LoadBalancer ${logicalId}: ${this.maskErrorMessage(error, maskSecrets)}`, resourceType, logicalId, void 0, cause);
|
|
34556
34891
|
}
|
|
34557
34892
|
}
|
|
34558
|
-
async updateLoadBalancer(logicalId, physicalId, _resourceType, properties, previousProperties) {
|
|
34893
|
+
async updateLoadBalancer(logicalId, physicalId, _resourceType, properties, previousProperties, maskSecrets) {
|
|
34559
34894
|
const handledKeys = /* @__PURE__ */ new Set([
|
|
34560
34895
|
"LoadBalancerAttributes",
|
|
34561
34896
|
"Subnets",
|
|
@@ -34637,7 +34972,7 @@ var ELBv2Provider = class {
|
|
|
34637
34972
|
LoadBalancerArn: physicalId,
|
|
34638
34973
|
...newCapacityUnits !== void 0 ? { MinimumLoadBalancerCapacity: { CapacityUnits: newCapacityUnits } } : { ResetCapacityReservation: true }
|
|
34639
34974
|
}));
|
|
34640
|
-
this.logger.debug(newCapacityUnits !== void 0 ? `Requested capacity reservation of ${
|
|
34975
|
+
this.logger.debug(newCapacityUnits !== void 0 ? `Requested capacity reservation of ${maskerOrIdentity(maskSecrets)(String(newCapacity?.CapacityUnits))} LCU for ${logicalId}` : `Reset capacity reservation for ${logicalId}`);
|
|
34641
34976
|
if (newCapacityUnits !== void 0 && isTruthyCfnBoolean(properties["EnableCapacityReservationProvisionStabilize"]) && process.env["CDKD_NO_WAIT"] !== "true") await this.waitForCapacityReservationProvisioned(physicalId, logicalId);
|
|
34642
34977
|
}
|
|
34643
34978
|
await this.applyTagDiff(physicalId, previousProperties["Tags"], properties["Tags"]);
|
|
@@ -34655,6 +34990,16 @@ var ELBv2Provider = class {
|
|
|
34655
34990
|
* a timeout WARNS and continues (the reservation keeps provisioning
|
|
34656
34991
|
* asynchronously and the ModifyCapacityReservation was accepted), while a
|
|
34657
34992
|
* `failed` zonal state throws — that reservation will never provision.
|
|
34993
|
+
*
|
|
34994
|
+
* Takes NO masker, deliberately (issue #2063 audit). Its only request is
|
|
34995
|
+
* `DescribeCapacityReservation({ LoadBalancerArn })` with an AWS-issued ARN,
|
|
34996
|
+
* so no resolved template value is in the payload for AWS to quote back into
|
|
34997
|
+
* the transient-failure `debug` line; the timeout `warn` and the `failed`
|
|
34998
|
+
* throw are assembled from a logical id, a count and an AWS-supplied zonal
|
|
34999
|
+
* reason. A structural reason, not a likelihood judgement — add the
|
|
35000
|
+
* parameter if this ever sends a template-derived member. The `failed` throw
|
|
35001
|
+
* is an ordinary `Error`, so it lands in `createLoadBalancer`'s masked catch
|
|
35002
|
+
* on the create path and in `update()`'s on the update path either way.
|
|
34658
35003
|
*/
|
|
34659
35004
|
async waitForCapacityReservationProvisioned(lbArn, logicalId) {
|
|
34660
35005
|
const maxAttempts = 40;
|
|
@@ -38735,9 +39080,14 @@ var Route53Provider = class {
|
|
|
38735
39080
|
const createComment = readConfigString(hostedZoneConfig, "Comment", "", "AWS::Route53::HostedZone HostedZoneConfig");
|
|
38736
39081
|
const vpcs = properties["VPCs"];
|
|
38737
39082
|
const firstVpc = vpcs && vpcs.length > 0 ? vpcs[0] : void 0;
|
|
38738
|
-
const
|
|
39083
|
+
const callerReferenceToken = acquireIdempotencyToken({
|
|
39084
|
+
scope: "CreateHostedZone",
|
|
39085
|
+
logicalId,
|
|
39086
|
+
maxLength: 128
|
|
39087
|
+
});
|
|
39088
|
+
const response = await this.createOrAdoptHostedZone({
|
|
38739
39089
|
Name: name,
|
|
38740
|
-
CallerReference:
|
|
39090
|
+
CallerReference: callerReferenceToken.value,
|
|
38741
39091
|
...createComment !== "" ? { HostedZoneConfig: {
|
|
38742
39092
|
Comment: createComment,
|
|
38743
39093
|
...firstVpc ? { PrivateZone: true } : {}
|
|
@@ -38746,7 +39096,7 @@ var Route53Provider = class {
|
|
|
38746
39096
|
VPCId: firstVpc["VPCId"],
|
|
38747
39097
|
VPCRegion: firstVpc["VPCRegion"]
|
|
38748
39098
|
} } : {}
|
|
38749
|
-
})
|
|
39099
|
+
});
|
|
38750
39100
|
const hostedZone = response.HostedZone;
|
|
38751
39101
|
if (!hostedZone?.Id) throw new Error("CreateHostedZone did not return HostedZone.Id");
|
|
38752
39102
|
const zoneId = hostedZone.Id.replace("/hostedzone/", "");
|
|
@@ -38773,6 +39123,7 @@ var Route53Provider = class {
|
|
|
38773
39123
|
try {
|
|
38774
39124
|
await this.deleteQueryLoggingConfigForZone(zoneId, logicalId);
|
|
38775
39125
|
await this.getClient().send(new DeleteHostedZoneCommand({ Id: zoneId }));
|
|
39126
|
+
callerReferenceToken.release();
|
|
38776
39127
|
} catch (deleteError) {
|
|
38777
39128
|
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
39129
|
}
|
|
@@ -38781,6 +39132,7 @@ var Route53Provider = class {
|
|
|
38781
39132
|
}
|
|
38782
39133
|
const nameServers = response.DelegationSet?.NameServers ?? [];
|
|
38783
39134
|
this.logger.debug(`Successfully created hosted zone ${logicalId}: ${zoneId}`);
|
|
39135
|
+
callerReferenceToken.release();
|
|
38784
39136
|
return {
|
|
38785
39137
|
physicalId: zoneId,
|
|
38786
39138
|
attributes: {
|
|
@@ -38794,6 +39146,77 @@ var Route53Provider = class {
|
|
|
38794
39146
|
throw new ProvisioningError(`Failed to create hosted zone ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
|
|
38795
39147
|
}
|
|
38796
39148
|
}
|
|
39149
|
+
/**
|
|
39150
|
+
* `CreateHostedZone`, with the retry-replay case ADOPTED rather than failed
|
|
39151
|
+
* (issue [#2039](https://github.com/go-to-k/cdkd/issues/2039)).
|
|
39152
|
+
*
|
|
39153
|
+
* A stable `CallerReference` stops a replayed create from minting a second
|
|
39154
|
+
* zone, but on its own it converts the replay into a hard
|
|
39155
|
+
* `HostedZoneAlreadyExists` — so the first attempt's zone would still be live
|
|
39156
|
+
* and still absent from state, only now the deploy fails as well. Route 53
|
|
39157
|
+
* reports the caller reference back on every `HostedZone`, which is what lets
|
|
39158
|
+
* the replay find the zone the lost response described and continue with it.
|
|
39159
|
+
*
|
|
39160
|
+
* The lookup is `ListHostedZonesByName`, which starts at the requested DNS
|
|
39161
|
+
* name and returns zones in name order, so the walk stops as soon as the name
|
|
39162
|
+
* changes rather than paging the whole account. A `GetHostedZone` follows,
|
|
39163
|
+
* because only that call returns the `DelegationSet` whose `NameServers` are
|
|
39164
|
+
* the resource's `Fn::GetAtt` value — an adopted zone must return the same
|
|
39165
|
+
* attributes a first-attempt create would have.
|
|
39166
|
+
*
|
|
39167
|
+
* Finding nothing rethrows the original error: a caller reference AWS says is
|
|
39168
|
+
* taken but that names no zone we can see is not something to paper over.
|
|
39169
|
+
*/
|
|
39170
|
+
async createOrAdoptHostedZone(input) {
|
|
39171
|
+
try {
|
|
39172
|
+
return await this.getClient().send(new CreateHostedZoneCommand(input));
|
|
39173
|
+
} catch (error) {
|
|
39174
|
+
if (!(error instanceof HostedZoneAlreadyExists || error?.name === "HostedZoneAlreadyExists")) throw error;
|
|
39175
|
+
let adopted;
|
|
39176
|
+
try {
|
|
39177
|
+
adopted = await this.findHostedZoneByCallerReference(input.Name, input.CallerReference);
|
|
39178
|
+
} catch (lookupError) {
|
|
39179
|
+
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.`);
|
|
39180
|
+
throw error;
|
|
39181
|
+
}
|
|
39182
|
+
if (!adopted?.Id) throw error;
|
|
39183
|
+
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`);
|
|
39184
|
+
try {
|
|
39185
|
+
const described = await this.getClient().send(new GetHostedZoneCommand({ Id: adopted.Id }));
|
|
39186
|
+
return {
|
|
39187
|
+
HostedZone: described.HostedZone ?? adopted,
|
|
39188
|
+
...described.DelegationSet ? { DelegationSet: described.DelegationSet } : {}
|
|
39189
|
+
};
|
|
39190
|
+
} catch (describeError) {
|
|
39191
|
+
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.`);
|
|
39192
|
+
throw error;
|
|
39193
|
+
}
|
|
39194
|
+
}
|
|
39195
|
+
}
|
|
39196
|
+
/**
|
|
39197
|
+
* The hosted zone carrying `callerReference`, or `undefined`.
|
|
39198
|
+
*
|
|
39199
|
+
* Scoped to zones whose name matches `dnsName`: `ListHostedZonesByName` is
|
|
39200
|
+
* ordered by name, so once the name differs no later page can hold ours.
|
|
39201
|
+
*/
|
|
39202
|
+
async findHostedZoneByCallerReference(dnsName, callerReference) {
|
|
39203
|
+
const wanted = canonicalizeQueryName(dnsName);
|
|
39204
|
+
let marker = { DNSName: wanted };
|
|
39205
|
+
while (marker) {
|
|
39206
|
+
const page = await this.getClient().send(new ListHostedZonesByNameCommand({
|
|
39207
|
+
DNSName: marker.DNSName,
|
|
39208
|
+
...marker.HostedZoneId ? { HostedZoneId: marker.HostedZoneId } : {}
|
|
39209
|
+
}));
|
|
39210
|
+
for (const zone of page.HostedZones ?? []) {
|
|
39211
|
+
if (canonicalizeQueryName(zone.Name ?? "") !== wanted) return;
|
|
39212
|
+
if (zone.CallerReference === callerReference) return zone;
|
|
39213
|
+
}
|
|
39214
|
+
marker = page.IsTruncated && page.NextDNSName ? {
|
|
39215
|
+
DNSName: page.NextDNSName,
|
|
39216
|
+
...page.NextHostedZoneId ? { HostedZoneId: page.NextHostedZoneId } : {}
|
|
39217
|
+
} : void 0;
|
|
39218
|
+
}
|
|
39219
|
+
}
|
|
38797
39220
|
async updateHostedZone(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
38798
39221
|
this.logger.debug(`Updating Route 53 hosted zone ${logicalId}: ${physicalId}`);
|
|
38799
39222
|
try {
|
|
@@ -41818,25 +42241,38 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
|
|
|
41818
42241
|
* `context` is read for ONE thing today: `maskSecrets` (issue #2050). The
|
|
41819
42242
|
* Service create path runs a post-create `UpdateServiceAttributes` through
|
|
41820
42243
|
* `withRetry` — see {@link maskedRetryLogger}.
|
|
42244
|
+
*
|
|
42245
|
+
* EVERY arm receives it, not just the Service one (issue #2063). The three
|
|
42246
|
+
* namespace arms build their `Create*NamespaceCommand` payload out of the
|
|
42247
|
+
* RESOLVED `properties` bag — `Name`, `Description`, `Tags` — and each has
|
|
42248
|
+
* two disclosure surfaces for it: the `ProvisioningError` its catch throws
|
|
42249
|
+
* (printed at ERROR, i.e. DEFAULT verbosity, by `deploy-engine.ts`) and the
|
|
42250
|
+
* AWS `ErrorMessage` that {@link pollOperation} raises for a FAILED
|
|
42251
|
+
* operation. Cloud Map's create is operation-based, so the second one is the
|
|
42252
|
+
* NORMAL rejection path for those arms, not an edge case.
|
|
41821
42253
|
*/
|
|
41822
42254
|
async create(logicalId, resourceType, properties, context) {
|
|
41823
42255
|
switch (resourceType) {
|
|
41824
|
-
case "AWS::ServiceDiscovery::PrivateDnsNamespace": return this.createNamespace(logicalId, resourceType, properties);
|
|
41825
|
-
case "AWS::ServiceDiscovery::HttpNamespace": return this.createHttpNamespace(logicalId, resourceType, properties);
|
|
41826
|
-
case "AWS::ServiceDiscovery::PublicDnsNamespace": return this.createPublicDnsNamespace(logicalId, resourceType, properties);
|
|
42256
|
+
case "AWS::ServiceDiscovery::PrivateDnsNamespace": return this.createNamespace(logicalId, resourceType, properties, context?.maskSecrets);
|
|
42257
|
+
case "AWS::ServiceDiscovery::HttpNamespace": return this.createHttpNamespace(logicalId, resourceType, properties, context?.maskSecrets);
|
|
42258
|
+
case "AWS::ServiceDiscovery::PublicDnsNamespace": return this.createPublicDnsNamespace(logicalId, resourceType, properties, context?.maskSecrets);
|
|
41827
42259
|
case "AWS::ServiceDiscovery::Service": return this.createService(logicalId, resourceType, properties, context?.maskSecrets);
|
|
41828
42260
|
default: throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId);
|
|
41829
42261
|
}
|
|
41830
42262
|
}
|
|
41831
42263
|
/**
|
|
41832
42264
|
* `context` is read for ONE thing today: `maskSecrets` (issue #2050) — the
|
|
41833
|
-
* update-path twin of the `create()` note above
|
|
42265
|
+
* update-path twin of the `create()` note above, and threaded into every arm
|
|
42266
|
+
* for the same reason (issue #2063). The namespace update arms carry one
|
|
42267
|
+
* payload surface the create arms do not: {@link syncNamespaceTags} sends
|
|
42268
|
+
* `TagResource` with the resolved `Tags` VALUES, whose rejection lands in
|
|
42269
|
+
* the arm's own catch.
|
|
41834
42270
|
*/
|
|
41835
42271
|
update(logicalId, physicalId, resourceType, properties, previousProperties, context) {
|
|
41836
42272
|
switch (resourceType) {
|
|
41837
|
-
case "AWS::ServiceDiscovery::PrivateDnsNamespace": return this.updateNamespace(logicalId, physicalId, resourceType, properties, previousProperties);
|
|
41838
|
-
case "AWS::ServiceDiscovery::HttpNamespace": return this.updateHttpNamespace(logicalId, physicalId, resourceType, properties, previousProperties);
|
|
41839
|
-
case "AWS::ServiceDiscovery::PublicDnsNamespace": return this.updatePublicDnsNamespace(logicalId, physicalId, resourceType, properties, previousProperties);
|
|
42273
|
+
case "AWS::ServiceDiscovery::PrivateDnsNamespace": return this.updateNamespace(logicalId, physicalId, resourceType, properties, previousProperties, context?.maskSecrets);
|
|
42274
|
+
case "AWS::ServiceDiscovery::HttpNamespace": return this.updateHttpNamespace(logicalId, physicalId, resourceType, properties, previousProperties, context?.maskSecrets);
|
|
42275
|
+
case "AWS::ServiceDiscovery::PublicDnsNamespace": return this.updatePublicDnsNamespace(logicalId, physicalId, resourceType, properties, previousProperties, context?.maskSecrets);
|
|
41840
42276
|
case "AWS::ServiceDiscovery::Service": return this.updateService(logicalId, physicalId, resourceType, properties, previousProperties, context?.maskSecrets);
|
|
41841
42277
|
default: throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId, physicalId);
|
|
41842
42278
|
}
|
|
@@ -41850,7 +42286,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
|
|
|
41850
42286
|
default: throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId, physicalId);
|
|
41851
42287
|
}
|
|
41852
42288
|
}
|
|
41853
|
-
async createNamespace(logicalId, resourceType, properties) {
|
|
42289
|
+
async createNamespace(logicalId, resourceType, properties, maskSecrets) {
|
|
41854
42290
|
this.logger.debug(`Creating private DNS namespace ${logicalId}`);
|
|
41855
42291
|
const client = this.getClient();
|
|
41856
42292
|
const name = properties["Name"];
|
|
@@ -41869,7 +42305,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
|
|
|
41869
42305
|
...inputProperties && { Properties: inputProperties }
|
|
41870
42306
|
}))).OperationId;
|
|
41871
42307
|
if (!operationId) throw new Error("CreatePrivateDnsNamespace did not return OperationId");
|
|
41872
|
-
const namespaceId = await this.pollOperation(operationId, logicalId, resourceType);
|
|
42308
|
+
const namespaceId = await this.pollOperation(operationId, logicalId, resourceType, maskSecrets);
|
|
41873
42309
|
const arn = await this.buildNamespaceArn(namespaceId);
|
|
41874
42310
|
this.logger.debug(`Successfully created private DNS namespace ${logicalId}: ${namespaceId}`);
|
|
41875
42311
|
return {
|
|
@@ -41882,7 +42318,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
|
|
|
41882
42318
|
} catch (error) {
|
|
41883
42319
|
if (error instanceof ProvisioningError) throw error;
|
|
41884
42320
|
const cause = error instanceof Error ? error : void 0;
|
|
41885
|
-
throw new ProvisioningError(`Failed to create private DNS namespace ${logicalId}: ${
|
|
42321
|
+
throw new ProvisioningError(`Failed to create private DNS namespace ${logicalId}: ${this.maskErrorMessage(error, maskSecrets)}`, resourceType, logicalId, void 0, cause);
|
|
41886
42322
|
}
|
|
41887
42323
|
}
|
|
41888
42324
|
/**
|
|
@@ -41908,7 +42344,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
|
|
|
41908
42344
|
* kind's Cloud Map default ({@link PRIVATE_DNS_NAMESPACE_DEFAULT_SOA_TTL}) —
|
|
41909
42345
|
* the CFn-parity shape, live A/B'd 2026-08-11 on the private kind itself.
|
|
41910
42346
|
*/
|
|
41911
|
-
async updateNamespace(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
42347
|
+
async updateNamespace(logicalId, physicalId, resourceType, properties, previousProperties, maskSecrets) {
|
|
41912
42348
|
this.logger.debug(`Updating private DNS namespace ${logicalId}: ${physicalId}`);
|
|
41913
42349
|
const client = this.getClient();
|
|
41914
42350
|
const namespaceChange = {};
|
|
@@ -41922,7 +42358,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
|
|
|
41922
42358
|
Id: physicalId,
|
|
41923
42359
|
Namespace: namespaceChange
|
|
41924
42360
|
}))).OperationId;
|
|
41925
|
-
if (operationId) await this.pollOperation(operationId, logicalId, resourceType);
|
|
42361
|
+
if (operationId) await this.pollOperation(operationId, logicalId, resourceType, maskSecrets);
|
|
41926
42362
|
} else this.logger.debug(`No mutable namespace-body diff for PrivateDnsNamespace ${logicalId}`);
|
|
41927
42363
|
await this.syncNamespaceTags(logicalId, physicalId, properties, previousProperties);
|
|
41928
42364
|
this.logger.debug(`Successfully updated private DNS namespace ${logicalId}`);
|
|
@@ -41933,7 +42369,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
|
|
|
41933
42369
|
} catch (error) {
|
|
41934
42370
|
if (error instanceof ProvisioningError) throw error;
|
|
41935
42371
|
const cause = error instanceof Error ? error : void 0;
|
|
41936
|
-
throw new ProvisioningError(`Failed to update private DNS namespace ${logicalId}: ${
|
|
42372
|
+
throw new ProvisioningError(`Failed to update private DNS namespace ${logicalId}: ${this.maskErrorMessage(error, maskSecrets)}`, resourceType, logicalId, physicalId, cause);
|
|
41937
42373
|
}
|
|
41938
42374
|
}
|
|
41939
42375
|
/**
|
|
@@ -41967,7 +42403,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
|
|
|
41967
42403
|
throw new ProvisioningError(`Failed to delete Cloud Map namespace ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
|
|
41968
42404
|
}
|
|
41969
42405
|
}
|
|
41970
|
-
async createHttpNamespace(logicalId, resourceType, properties) {
|
|
42406
|
+
async createHttpNamespace(logicalId, resourceType, properties, maskSecrets) {
|
|
41971
42407
|
this.logger.debug(`Creating HTTP namespace ${logicalId}`);
|
|
41972
42408
|
const client = this.getClient();
|
|
41973
42409
|
const name = properties["Name"];
|
|
@@ -41981,7 +42417,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
|
|
|
41981
42417
|
...tags && tags.length > 0 && { Tags: tags }
|
|
41982
42418
|
}))).OperationId;
|
|
41983
42419
|
if (!operationId) throw new Error("CreateHttpNamespace did not return OperationId");
|
|
41984
|
-
const namespaceId = await this.pollOperation(operationId, logicalId, resourceType);
|
|
42420
|
+
const namespaceId = await this.pollOperation(operationId, logicalId, resourceType, maskSecrets);
|
|
41985
42421
|
const arn = await this.resolveNamespaceArn(namespaceId);
|
|
41986
42422
|
this.logger.debug(`Successfully created HTTP namespace ${logicalId}: ${namespaceId}`);
|
|
41987
42423
|
return {
|
|
@@ -41994,7 +42430,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
|
|
|
41994
42430
|
} catch (error) {
|
|
41995
42431
|
if (error instanceof ProvisioningError) throw error;
|
|
41996
42432
|
const cause = error instanceof Error ? error : void 0;
|
|
41997
|
-
throw new ProvisioningError(`Failed to create HTTP namespace ${logicalId}: ${
|
|
42433
|
+
throw new ProvisioningError(`Failed to create HTTP namespace ${logicalId}: ${this.maskErrorMessage(error, maskSecrets)}`, resourceType, logicalId, void 0, cause);
|
|
41998
42434
|
}
|
|
41999
42435
|
}
|
|
42000
42436
|
/**
|
|
@@ -42015,7 +42451,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
|
|
|
42015
42451
|
* the template must be cleared explicitly via the `''` sentinel — the
|
|
42016
42452
|
* CFn-parity shape, live A/B'd 2026-08-11 on this exact kind.
|
|
42017
42453
|
*/
|
|
42018
|
-
async updateHttpNamespace(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
42454
|
+
async updateHttpNamespace(logicalId, physicalId, resourceType, properties, previousProperties, maskSecrets) {
|
|
42019
42455
|
this.logger.debug(`Updating HTTP namespace ${logicalId}: ${physicalId}`);
|
|
42020
42456
|
const client = this.getClient();
|
|
42021
42457
|
const description = clearOnUpdateRemoval(properties["Description"], previousProperties["Description"], "");
|
|
@@ -42026,7 +42462,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
|
|
|
42026
42462
|
Id: physicalId,
|
|
42027
42463
|
Namespace: namespaceChange
|
|
42028
42464
|
}))).OperationId;
|
|
42029
|
-
if (operationId) await this.pollOperation(operationId, logicalId, resourceType);
|
|
42465
|
+
if (operationId) await this.pollOperation(operationId, logicalId, resourceType, maskSecrets);
|
|
42030
42466
|
}
|
|
42031
42467
|
await this.syncNamespaceTags(logicalId, physicalId, properties, previousProperties);
|
|
42032
42468
|
this.logger.debug(`Successfully updated HTTP namespace ${logicalId}`);
|
|
@@ -42037,10 +42473,10 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
|
|
|
42037
42473
|
} catch (error) {
|
|
42038
42474
|
if (error instanceof ProvisioningError) throw error;
|
|
42039
42475
|
const cause = error instanceof Error ? error : void 0;
|
|
42040
|
-
throw new ProvisioningError(`Failed to update HTTP namespace ${logicalId}: ${
|
|
42476
|
+
throw new ProvisioningError(`Failed to update HTTP namespace ${logicalId}: ${this.maskErrorMessage(error, maskSecrets)}`, resourceType, logicalId, physicalId, cause);
|
|
42041
42477
|
}
|
|
42042
42478
|
}
|
|
42043
|
-
async createPublicDnsNamespace(logicalId, resourceType, properties) {
|
|
42479
|
+
async createPublicDnsNamespace(logicalId, resourceType, properties, maskSecrets) {
|
|
42044
42480
|
this.logger.debug(`Creating public DNS namespace ${logicalId}`);
|
|
42045
42481
|
const client = this.getClient();
|
|
42046
42482
|
const name = properties["Name"];
|
|
@@ -42056,7 +42492,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
|
|
|
42056
42492
|
...inputProperties && { Properties: inputProperties }
|
|
42057
42493
|
}))).OperationId;
|
|
42058
42494
|
if (!operationId) throw new Error("CreatePublicDnsNamespace did not return OperationId");
|
|
42059
|
-
const namespaceId = await this.pollOperation(operationId, logicalId, resourceType);
|
|
42495
|
+
const namespaceId = await this.pollOperation(operationId, logicalId, resourceType, maskSecrets);
|
|
42060
42496
|
let arn;
|
|
42061
42497
|
let hostedZoneId;
|
|
42062
42498
|
try {
|
|
@@ -42079,7 +42515,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
|
|
|
42079
42515
|
} catch (error) {
|
|
42080
42516
|
if (error instanceof ProvisioningError) throw error;
|
|
42081
42517
|
const cause = error instanceof Error ? error : void 0;
|
|
42082
|
-
throw new ProvisioningError(`Failed to create public DNS namespace ${logicalId}: ${
|
|
42518
|
+
throw new ProvisioningError(`Failed to create public DNS namespace ${logicalId}: ${this.maskErrorMessage(error, maskSecrets)}`, resourceType, logicalId, void 0, cause);
|
|
42083
42519
|
}
|
|
42084
42520
|
}
|
|
42085
42521
|
/**
|
|
@@ -42097,7 +42533,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
|
|
|
42097
42533
|
* kind's Cloud Map default ({@link PUBLIC_DNS_NAMESPACE_DEFAULT_SOA_TTL}) —
|
|
42098
42534
|
* the CFn-parity shape, live A/B'd 2026-08-11 on this exact kind.
|
|
42099
42535
|
*/
|
|
42100
|
-
async updatePublicDnsNamespace(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
42536
|
+
async updatePublicDnsNamespace(logicalId, physicalId, resourceType, properties, previousProperties, maskSecrets) {
|
|
42101
42537
|
this.logger.debug(`Updating public DNS namespace ${logicalId}: ${physicalId}`);
|
|
42102
42538
|
const client = this.getClient();
|
|
42103
42539
|
const namespaceChange = {};
|
|
@@ -42111,7 +42547,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
|
|
|
42111
42547
|
Id: physicalId,
|
|
42112
42548
|
Namespace: namespaceChange
|
|
42113
42549
|
}))).OperationId;
|
|
42114
|
-
if (operationId) await this.pollOperation(operationId, logicalId, resourceType);
|
|
42550
|
+
if (operationId) await this.pollOperation(operationId, logicalId, resourceType, maskSecrets);
|
|
42115
42551
|
}
|
|
42116
42552
|
await this.syncNamespaceTags(logicalId, physicalId, properties, previousProperties);
|
|
42117
42553
|
this.logger.debug(`Successfully updated public DNS namespace ${logicalId}`);
|
|
@@ -42122,7 +42558,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
|
|
|
42122
42558
|
} catch (error) {
|
|
42123
42559
|
if (error instanceof ProvisioningError) throw error;
|
|
42124
42560
|
const cause = error instanceof Error ? error : void 0;
|
|
42125
|
-
throw new ProvisioningError(`Failed to update public DNS namespace ${logicalId}: ${
|
|
42561
|
+
throw new ProvisioningError(`Failed to update public DNS namespace ${logicalId}: ${this.maskErrorMessage(error, maskSecrets)}`, resourceType, logicalId, physicalId, cause);
|
|
42126
42562
|
}
|
|
42127
42563
|
}
|
|
42128
42564
|
async createService(logicalId, resourceType, properties, maskSecrets) {
|
|
@@ -42228,7 +42664,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
|
|
|
42228
42664
|
Id: physicalId,
|
|
42229
42665
|
Service: serviceChange
|
|
42230
42666
|
}))).OperationId;
|
|
42231
|
-
if (operationId) await this.pollOperation(operationId, logicalId, resourceType);
|
|
42667
|
+
if (operationId) await this.pollOperation(operationId, logicalId, resourceType, maskSecrets);
|
|
42232
42668
|
}
|
|
42233
42669
|
if (hasAttrUpsert) {
|
|
42234
42670
|
await withRetry(() => client.send(new UpdateServiceAttributesCommand({
|
|
@@ -42299,6 +42735,15 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
|
|
|
42299
42735
|
* Resolve a namespace's ARN — authoritative via `GetNamespace`, with a
|
|
42300
42736
|
* deterministic STS-based construction as fallback so a transient read
|
|
42301
42737
|
* failure right after create does not fail the whole resource.
|
|
42738
|
+
*
|
|
42739
|
+
* Takes NO masker, deliberately (issue #2063 audit). Its request is
|
|
42740
|
+
* `GetNamespace({ Id })` with an AWS-issued namespace id — no resolved
|
|
42741
|
+
* template value is in the payload, so the AWS error text it logs cannot
|
|
42742
|
+
* echo one back. That is a structural reason, not an assessment of
|
|
42743
|
+
* likelihood; add the parameter if this call ever grows a template-derived
|
|
42744
|
+
* member. The `TagResource` / `UntagResource` calls in
|
|
42745
|
+
* {@link syncNamespaceTags} DO carry resolved values, but they throw rather
|
|
42746
|
+
* than log, and the throw lands in the calling arm's masked catch.
|
|
42302
42747
|
*/
|
|
42303
42748
|
async resolveNamespaceArn(namespaceId) {
|
|
42304
42749
|
try {
|
|
@@ -42341,8 +42786,39 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
|
|
|
42341
42786
|
/**
|
|
42342
42787
|
* Poll a Service Discovery operation until it completes.
|
|
42343
42788
|
* Returns the target resource ID from the operation result.
|
|
42344
|
-
|
|
42345
|
-
|
|
42789
|
+
*
|
|
42790
|
+
* MASKS `Operation.ErrorMessage` (issue #2063), and this is the only place
|
|
42791
|
+
* that can. Cloud Map's create / update are operation-based: the caller's
|
|
42792
|
+
* `Create*Namespace` / `Update*` request carried the RESOLVED `properties`
|
|
42793
|
+
* bag — `Name`, `Description`, `Tags`, `DnsConfig` — and AWS reports its
|
|
42794
|
+
* rejection here rather than by rejecting the submit call, quoting the
|
|
42795
|
+
* offending value back in `ErrorMessage`. A FAILED operation is therefore
|
|
42796
|
+
* the NORMAL rejection path for these arms, not an edge case.
|
|
42797
|
+
*
|
|
42798
|
+
* Why not leave it to the arms' own catch: every namespace arm and
|
|
42799
|
+
* `updateService` open with `if (error instanceof ProvisioningError) throw
|
|
42800
|
+
* error;`, so a `ProvisioningError` raised here is re-thrown VERBATIM and
|
|
42801
|
+
* never reaches their `maskErrorMessage`. `deploy-engine.ts` then prints it
|
|
42802
|
+
* at ERROR — DEFAULT verbosity. That passthrough is correct (it preserves
|
|
42803
|
+
* the resource/logical-id context this frame added), which is exactly why
|
|
42804
|
+
* the mask has to be applied at the point of construction.
|
|
42805
|
+
*
|
|
42806
|
+
* `errorMessage` is masked RAW, before any interpolation. Stating precisely
|
|
42807
|
+
* what that buys, because the obvious phrasing over-claims (issue #2063
|
|
42808
|
+
* review): `maskSecretsInText`'s WHOLE-VALUE arm fires only when
|
|
42809
|
+
* `secrets.has(text)` — i.e. when the text IS the secret in its entirety —
|
|
42810
|
+
* so masking raw covers the case where AWS returns the offending value as
|
|
42811
|
+
* the whole `ErrorMessage`, at ANY length, including below the substring
|
|
42812
|
+
* arm's 4-character `MIN_NEEDLE_LENGTH` floor. When AWS instead quotes the
|
|
42813
|
+
* value INSIDE a sentence, the substring arm handles it and that floor
|
|
42814
|
+
* applies either way, raw or not. So masking here is strictly better than
|
|
42815
|
+
* masking the assembled message, but it is not general sub-4-character
|
|
42816
|
+
* coverage. See `SecretMaskingContext` in `src/types/resource.ts`.
|
|
42817
|
+
*
|
|
42818
|
+
* The `operationId` / timeout message below carries no template-derived
|
|
42819
|
+
* value (an AWS-issued opaque id), so it is left as-is.
|
|
42820
|
+
*/
|
|
42821
|
+
async pollOperation(operationId, logicalId, resourceType, maskSecrets) {
|
|
42346
42822
|
const client = this.getClient();
|
|
42347
42823
|
const maxAttempts = 60;
|
|
42348
42824
|
let delay = 1e3;
|
|
@@ -42354,7 +42830,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
|
|
|
42354
42830
|
if (targets) return targets["NAMESPACE"] || targets["SERVICE"] || operationId;
|
|
42355
42831
|
return operationId;
|
|
42356
42832
|
}
|
|
42357
|
-
if (status === "FAIL") throw new ProvisioningError(`Operation failed for ${logicalId}: ${result.Operation?.ErrorMessage || "Unknown error"}`, resourceType, logicalId);
|
|
42833
|
+
if (status === "FAIL") throw new ProvisioningError(`Operation failed for ${logicalId}: ${maskerOrIdentity(maskSecrets)(result.Operation?.ErrorMessage || "Unknown error")}`, resourceType, logicalId);
|
|
42358
42834
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
42359
42835
|
delay = Math.min(delay * 2, 1e4);
|
|
42360
42836
|
}
|
|
@@ -83294,7 +83770,7 @@ function createMigrateCommand() {
|
|
|
83294
83770
|
*/
|
|
83295
83771
|
function buildProgram() {
|
|
83296
83772
|
const program = new Command();
|
|
83297
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.
|
|
83773
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.18");
|
|
83298
83774
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
83299
83775
|
const { profile } = actionCommand.optsWithGlobals();
|
|
83300
83776
|
if (profile !== void 0) process.env["AWS_PROFILE"] = profile;
|