@go-to-k/cdkd 0.193.0 → 0.195.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -242,6 +242,7 @@ maintain, no `cdk synth | sam ...` round-trip.
|
|
|
242
242
|
| `cdkd local run-task <target>` | ECS RunTask — every container in a task definition started on a per-task docker network |
|
|
243
243
|
| `cdkd local start-service <target>` | Long-running ECS Service emulator — `DesiredCount` replicas with restart-on-exit (no local load balancer in v1) |
|
|
244
244
|
| `cdkd local invoke-agentcore <target>` | One-shot Bedrock AgentCore Runtime invoke (HTTP `/invocations` / MCP `/mcp` / A2A `/a2a` / AGUI / WebSocket `--ws`) |
|
|
245
|
+
| `cdkd local start-alb <targets...>` | Long-running local ALB front-door (HTTP + HTTPS listeners, path / host / header / weighted / redirect / fixed-response routing, authenticate-cognito / authenticate-oidc) for ECS / Lambda backing services |
|
|
245
246
|
|
|
246
247
|
Requires Docker. Pass `--from-state` (cdkd-deployed) or
|
|
247
248
|
`--from-cfn-stack` (cdk-deployed / CFn-managed) to substitute deployed
|
|
@@ -302,6 +303,21 @@ restart-on-exit, cross-service Service Connect / Cloud Map DNS
|
|
|
302
303
|
discovery (peer containers reach each other by `<discoveryName>.<namespace>`).
|
|
303
304
|
No local load-balancer in v1.
|
|
304
305
|
|
|
306
|
+
### `local start-alb`
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
cdkd local start-alb MyStack/MyAlb --lb-port 80=8080 # remap privileged listener port
|
|
310
|
+
cdkd local start-alb MyStack/MyAlb --from-state # OR --from-cfn-stack
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Long-running local ALB front-door: names an `AWS::ElasticLoadBalancingV2::LoadBalancer`,
|
|
314
|
+
boots every ECS service behind its listeners, and stands up a local
|
|
315
|
+
HTTP / HTTPS front-door on each listener port that round-robins across
|
|
316
|
+
the running replicas and routes its listener rules across the backing
|
|
317
|
+
services. Forward / redirect / fixed-response actions; ECS or Lambda
|
|
318
|
+
targets; authenticate-cognito / authenticate-oidc via a local Bearer-JWT
|
|
319
|
+
check.
|
|
320
|
+
|
|
305
321
|
See **[docs/local-emulation.md](docs/local-emulation.md)** for the
|
|
306
322
|
full reference — runtimes, target resolution, every flag, integration
|
|
307
323
|
and authorizer detail, route precedence, container pool, networking,
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { _ as withSkipPrefix, a as runDockerStreaming, c as getLogger, d as getLiveRenderer, f as PATTERN_B_NAME_PROPERTIES, g as generateResourceNameWithFallback, h as generateResourceName, i as runDockerForeground, n as formatDockerLoginError, p as PATTERN_B_RESOURCE_TYPES, r as getDockerCmd, u as runStackBuffered, v as withStackName } from "./docker-cmd-iDMcWcre.js";
|
|
3
|
-
import { A as S3StateBackend, B as resolveCaptureObservedState, C as assertRegionMatch, D as DagBuilder, E as DiffCalculator, F as buildDockerImage, G as CFN_TEMPLATE_BODY_LIMIT, H as resolveStateBucketWithDefault, I as Synthesizer, J as findLargeInlineResources, K as CFN_TEMPLATE_URL_LIMIT, L as getDefaultStateBucketName, M as AssetPublisher, N as stringifyValue, O as TemplateParser, P as WorkGraph, Q as resolveBucketRegion, R as getLegacyStateBucketName, S as CloudControlProvider, T as applyRoleArnIfSet, U as resolveStateBucketWithDefaultAndSource, V as resolveSkipPrefix, W as warnDeprecatedNoPrefixCliFlag, X as AssemblyReader, Y as uploadCfnTemplate, _ as matchesCdkPath, a as withRetry, at as LocalStartServiceError, b as ProviderRegistry, bt as withErrorHandling, c as bold, ct as NestedStackChildDirectDestroyError, d as green, dt as ResourceTimeoutError, et as CdkdError, f as red, ft as ResourceUpdateNotSupportedError, g as CDK_PATH_TAG, h as collectInlinePolicyNamesManagedBySiblings, i as withResourceDeadline, it as LocalMigrateError, j as shouldRetainResource, k as LockManager, l as cyan, lt as PartialFailureError, m as IAMRoleProvider, mt as StackTerminationProtectionError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, o as IMPLICIT_DELETE_DEPENDENCIES, p as yellow, pt as StackHasActiveImportsError, q as MIGRATE_TMP_PREFIX, r as DeployEngine, rt as LocalInvokeBuildError$1, s as formatResourceLine, st as MissingCdkCliError, t as DEFAULT_RESOURCE_TIMEOUT_MS, u as gray, ut as ProvisioningError, v as normalizeAwsTagsToCfn, w as IntrinsicFunctionResolver, x as findActionableSilentDrops, y as resolveExplicitPhysicalId, yt as normalizeAwsError, z as resolveApp } from "./deploy-engine-
|
|
3
|
+
import { A as S3StateBackend, B as resolveCaptureObservedState, C as assertRegionMatch, D as DagBuilder, E as DiffCalculator, F as buildDockerImage, G as CFN_TEMPLATE_BODY_LIMIT, H as resolveStateBucketWithDefault, I as Synthesizer, J as findLargeInlineResources, K as CFN_TEMPLATE_URL_LIMIT, L as getDefaultStateBucketName, M as AssetPublisher, N as stringifyValue, O as TemplateParser, P as WorkGraph, Q as resolveBucketRegion, R as getLegacyStateBucketName, S as CloudControlProvider, T as applyRoleArnIfSet, U as resolveStateBucketWithDefaultAndSource, V as resolveSkipPrefix, W as warnDeprecatedNoPrefixCliFlag, X as AssemblyReader, Y as uploadCfnTemplate, _ as matchesCdkPath, a as withRetry, at as LocalStartServiceError, b as ProviderRegistry, bt as withErrorHandling, c as bold, ct as NestedStackChildDirectDestroyError, d as green, dt as ResourceTimeoutError, et as CdkdError, f as red, ft as ResourceUpdateNotSupportedError, g as CDK_PATH_TAG, h as collectInlinePolicyNamesManagedBySiblings, i as withResourceDeadline, it as LocalMigrateError, j as shouldRetainResource, k as LockManager, l as cyan, lt as PartialFailureError, m as IAMRoleProvider, mt as StackTerminationProtectionError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, o as IMPLICIT_DELETE_DEPENDENCIES, p as yellow, pt as StackHasActiveImportsError, q as MIGRATE_TMP_PREFIX, r as DeployEngine, rt as LocalInvokeBuildError$1, s as formatResourceLine, st as MissingCdkCliError, t as DEFAULT_RESOURCE_TIMEOUT_MS, u as gray, ut as ProvisioningError, v as normalizeAwsTagsToCfn, w as IntrinsicFunctionResolver, x as findActionableSilentDrops, y as resolveExplicitPhysicalId, yt as normalizeAwsError, z as resolveApp } from "./deploy-engine-BDmJX4ss.js";
|
|
4
4
|
import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-B15NAPbL.js";
|
|
5
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
6
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
@@ -28,7 +28,7 @@ import * as path from "node:path";
|
|
|
28
28
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
29
29
|
import { execFile, spawn } from "node:child_process";
|
|
30
30
|
import { tmpdir } from "node:os";
|
|
31
|
-
import { AssociateVPCWithHostedZoneCommand, ChangeResourceRecordSetsCommand, ChangeTagsForResourceCommand, CreateHostedZoneCommand, CreateQueryLoggingConfigCommand, DeleteHostedZoneCommand, DeleteQueryLoggingConfigCommand, DisassociateVPCFromHostedZoneCommand, GetHostedZoneCommand, ListHostedZonesByNameCommand, ListHostedZonesCommand, ListQueryLoggingConfigsCommand, ListResourceRecordSetsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$6, Route53Client, UpdateHostedZoneCommentCommand } from "@aws-sdk/client-route-53";
|
|
31
|
+
import { AssociateVPCWithHostedZoneCommand, ChangeResourceRecordSetsCommand, ChangeTagsForResourceCommand, CreateHostedZoneCommand, CreateQueryLoggingConfigCommand, DeleteHostedZoneCommand, DeleteQueryLoggingConfigCommand, DisassociateVPCFromHostedZoneCommand, GetHostedZoneCommand, ListHostedZonesByNameCommand, ListHostedZonesCommand, ListQueryLoggingConfigsCommand, ListResourceRecordSetsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$6, Route53Client, UpdateHostedZoneCommentCommand, UpdateHostedZoneFeaturesCommand } from "@aws-sdk/client-route-53";
|
|
32
32
|
import { AddTagsCommand, CreateListenerCommand, CreateLoadBalancerCommand, CreateTargetGroupCommand, DeleteListenerCommand, DeleteLoadBalancerCommand, DeleteTargetGroupCommand, DescribeListenersCommand, DescribeLoadBalancerAttributesCommand, DescribeLoadBalancersCommand, DescribeTagsCommand, DescribeTargetGroupsCommand, ElasticLoadBalancingV2Client, ModifyListenerCommand, ModifyLoadBalancerAttributesCommand, ModifyTargetGroupCommand, RemoveTagsCommand, SetIpAddressTypeCommand, SetSecurityGroupsCommand, SetSubnetsCommand } from "@aws-sdk/client-elastic-load-balancing-v2";
|
|
33
33
|
import { CreateAliasCommand, CreateKeyCommand, DeleteAliasCommand, DescribeKeyCommand, DisableKeyCommand, DisableKeyRotationCommand, EnableKeyCommand, EnableKeyRotationCommand, GetKeyPolicyCommand, GetKeyRotationStatusCommand, KMSClient, ListAliasesCommand, ListKeysCommand, ListResourceTagsCommand, NotFoundException as NotFoundException$2, PutKeyPolicyCommand, ScheduleKeyDeletionCommand, TagResourceCommand as TagResourceCommand$8, UntagResourceCommand as UntagResourceCommand$8, UpdateAliasCommand, UpdateKeyDescriptionCommand } from "@aws-sdk/client-kms";
|
|
34
34
|
import { CreateRepositoryCommand, DeleteLifecyclePolicyCommand, DeleteRepositoryCommand, DeleteRepositoryPolicyCommand, DescribeRepositoriesCommand, ECRClient, GetAuthorizationTokenCommand, GetLifecyclePolicyCommand, LifecyclePolicyNotFoundException, ListTagsForResourceCommand as ListTagsForResourceCommand$7, PutImageScanningConfigurationCommand, PutImageTagMutabilityCommand, PutLifecyclePolicyCommand, RepositoryNotFoundException, SetRepositoryPolicyCommand, TagResourceCommand as TagResourceCommand$9 } from "@aws-sdk/client-ecr";
|
|
@@ -63,7 +63,7 @@ import { CreateNamespaceCommand, CreateTableBucketCommand, CreateTableCommand as
|
|
|
63
63
|
import { AttachLoadBalancerTargetGroupsCommand, AttachLoadBalancersCommand, AttachTrafficSourcesCommand, AutoScalingClient, CreateAutoScalingGroupCommand, CreateOrUpdateTagsCommand, DeleteAutoScalingGroupCommand, DeleteLifecycleHookCommand, DeleteNotificationConfigurationCommand, DeleteTagsCommand as DeleteTagsCommand$1, DescribeAutoScalingGroupsCommand, DescribeLifecycleHooksCommand, DescribeNotificationConfigurationsCommand, DescribeTrafficSourcesCommand, DetachLoadBalancerTargetGroupsCommand, DetachLoadBalancersCommand, DetachTrafficSourcesCommand, DisableMetricsCollectionCommand, EnableMetricsCollectionCommand, PutLifecycleHookCommand, PutNotificationConfigurationCommand, UpdateAutoScalingGroupCommand } from "@aws-sdk/client-auto-scaling";
|
|
64
64
|
import { Document, Pair, Scalar, YAMLMap, YAMLSeq, parse as parse$1, stringify } from "yaml";
|
|
65
65
|
import { createLocalStateProvider, getEmbedConfig, isCfnFlagPresent, listTargets, rejectExplicitCfnStackWithMultipleStacks, resolveCfnFallbackRegion, setEmbedConfig, substituteAgainstState, substituteAgainstStateAsync, substituteEnvVarsFromState, substituteEnvVarsFromStateAsync } from "cdk-local";
|
|
66
|
-
import { A2A_CONTAINER_PORT, A2A_PATH, AGENTCORE_A2A_PROTOCOL, AGENTCORE_AGUI_PROTOCOL, AGENTCORE_MCP_PROTOCOL, CloudMapRegistry, ConnectionRegistry, EcsTaskResolutionError, HOST_GATEWAY_MIN_VERSION, LocalInvokeBuildError, MCP_CONTAINER_PORT, MCP_PATH, a2aInvokeOnce, architectureToPlatform, attachAuthorizers, attachStageContext, availableApiIdentifiers, bufferToBody, buildAgentCoreCodeImage, buildCloudMapIndex, buildCognitoJwksUrl, buildConnectEvent, buildContainerImage, buildCorsConfigByApiId, buildCorsConfigFromCloudFrontChain, buildDisconnectEvent, buildJwksUrlFromIssuer, buildMessageEvent, buildMgmtEndpointEnvUrl, buildStageMap, createAuthorizerCache, createFileWatcher, createJwksCache, createWatchPredicates, defaultCredentialsLoader, derivePseudoParametersFromRegion, discoverRoutes, discoverWebSocketApis, downloadAndExtractS3Bundle, filterRoutesByApiIdentifier, getContainerNetworkIp, groupRoutesByServer, handleConnectionsRequest, invokeAgentCore, invokeAgentCoreWs, materializeLayerFromArn, mcpInvokeOnce, parseConnectionsPath, parseSelectionExpressionPath, pickAgentCoreCandidateStack, probeHostGatewaySupport, readMtlsMaterialsFromDisk, resolveAgentCoreTarget, resolveEnvVars, resolveRuntimeCodeMountPath, resolveRuntimeFileExtension, resolveRuntimeImage, resolveSingleTarget, resolveWatchConfig, signAgentCoreInvocation, startApiServer, substituteImagePlaceholders, tryResolveImageFnJoin, verifyJwtViaDiscovery, waitForAgentCorePing } from "cdk-local/internal";
|
|
66
|
+
import { A2A_CONTAINER_PORT, A2A_PATH, AGENTCORE_A2A_PROTOCOL, AGENTCORE_AGUI_PROTOCOL, AGENTCORE_MCP_PROTOCOL, CloudMapRegistry, ConnectionRegistry, EcsTaskResolutionError, HOST_GATEWAY_MIN_VERSION, LocalInvokeBuildError, MCP_CONTAINER_PORT, MCP_PATH, a2aInvokeOnce, addCommonEcsServiceOptions, architectureToPlatform, attachAuthorizers, attachStageContext, availableApiIdentifiers, bufferToBody, buildAgentCoreCodeImage, buildCloudMapIndex, buildCognitoJwksUrl, buildConnectEvent, buildContainerImage, buildCorsConfigByApiId, buildCorsConfigFromCloudFrontChain, buildDisconnectEvent, buildJwksUrlFromIssuer, buildMessageEvent, buildMgmtEndpointEnvUrl, buildStageMap, createAuthorizerCache, createFileWatcher, createJwksCache, createWatchPredicates, defaultCredentialsLoader, derivePseudoParametersFromRegion, discoverRoutes, discoverWebSocketApis, downloadAndExtractS3Bundle, filterRoutesByApiIdentifier, getContainerNetworkIp, groupRoutesByServer, handleConnectionsRequest, invokeAgentCore, invokeAgentCoreWs, isApplicationLoadBalancer, materializeLayerFromArn, mcpInvokeOnce, parseConnectionsPath, parseSelectionExpressionPath, pickAgentCoreCandidateStack, probeHostGatewaySupport, readMtlsMaterialsFromDisk, resolveAgentCoreTarget, resolveAlbFrontDoor, resolveEnvVars, resolveRuntimeCodeMountPath, resolveRuntimeFileExtension, resolveRuntimeImage, resolveSingleTarget, resolveWatchConfig, runEcsServiceEmulator, signAgentCoreInvocation, startApiServer, substituteImagePlaceholders, tryResolveImageFnJoin, verifyJwtViaDiscovery, waitForAgentCorePing } from "cdk-local/internal";
|
|
67
67
|
import { createServer } from "node:net";
|
|
68
68
|
import { promisify } from "node:util";
|
|
69
69
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
@@ -22650,7 +22650,8 @@ var Route53Provider = class {
|
|
|
22650
22650
|
"HostedZoneConfig",
|
|
22651
22651
|
"HostedZoneTags",
|
|
22652
22652
|
"VPCs",
|
|
22653
|
-
"QueryLoggingConfig"
|
|
22653
|
+
"QueryLoggingConfig",
|
|
22654
|
+
"HostedZoneFeatures"
|
|
22654
22655
|
])], ["AWS::Route53::RecordSet", new Set([
|
|
22655
22656
|
"HostedZoneId",
|
|
22656
22657
|
"HostedZoneName",
|
|
@@ -22681,9 +22682,9 @@ var Route53Provider = class {
|
|
|
22681
22682
|
default: throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId);
|
|
22682
22683
|
}
|
|
22683
22684
|
}
|
|
22684
|
-
async update(logicalId, physicalId, resourceType, properties,
|
|
22685
|
+
async update(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
22685
22686
|
switch (resourceType) {
|
|
22686
|
-
case "AWS::Route53::HostedZone": return this.updateHostedZone(logicalId, physicalId, resourceType, properties);
|
|
22687
|
+
case "AWS::Route53::HostedZone": return this.updateHostedZone(logicalId, physicalId, resourceType, properties, previousProperties);
|
|
22687
22688
|
case "AWS::Route53::RecordSet": return this.updateRecordSet(logicalId, physicalId, resourceType, properties);
|
|
22688
22689
|
default: throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId, physicalId);
|
|
22689
22690
|
}
|
|
@@ -22737,6 +22738,22 @@ var Route53Provider = class {
|
|
|
22737
22738
|
}
|
|
22738
22739
|
await this.applyHostedZoneTags(zoneId, properties, logicalId);
|
|
22739
22740
|
await this.applyQueryLoggingConfig(zoneId, properties, logicalId);
|
|
22741
|
+
if (properties["HostedZoneFeatures"]?.["AcceleratedRecoveryStatus"] === "ENABLED") try {
|
|
22742
|
+
await this.getClient().send(new UpdateHostedZoneFeaturesCommand({
|
|
22743
|
+
HostedZoneId: zoneId,
|
|
22744
|
+
EnableAcceleratedRecovery: true
|
|
22745
|
+
}));
|
|
22746
|
+
} catch (uhfError) {
|
|
22747
|
+
this.logger.error(`UpdateHostedZoneFeatures failed for ${zoneId} after CreateHostedZone — rolling back the hosted zone to keep deploy retry idempotent`);
|
|
22748
|
+
try {
|
|
22749
|
+
await this.deleteQueryLoggingConfigForZone(zoneId, logicalId);
|
|
22750
|
+
await this.getClient().send(new DeleteHostedZoneCommand({ Id: zoneId }));
|
|
22751
|
+
} catch (deleteError) {
|
|
22752
|
+
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`);
|
|
22753
|
+
}
|
|
22754
|
+
const cause = uhfError instanceof Error ? uhfError : void 0;
|
|
22755
|
+
throw new ProvisioningError(`Failed to enable Accelerated Recovery on hosted zone ${logicalId} (${zoneId}): ${uhfError instanceof Error ? uhfError.message : String(uhfError)}`, resourceType, logicalId, zoneId, cause);
|
|
22756
|
+
}
|
|
22740
22757
|
const nameServers = response.DelegationSet?.NameServers ?? [];
|
|
22741
22758
|
this.logger.debug(`Successfully created hosted zone ${logicalId}: ${zoneId}`);
|
|
22742
22759
|
return {
|
|
@@ -22752,7 +22769,7 @@ var Route53Provider = class {
|
|
|
22752
22769
|
throw new ProvisioningError(`Failed to create hosted zone ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
|
|
22753
22770
|
}
|
|
22754
22771
|
}
|
|
22755
|
-
async updateHostedZone(logicalId, physicalId, resourceType, properties) {
|
|
22772
|
+
async updateHostedZone(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
22756
22773
|
this.logger.debug(`Updating Route 53 hosted zone ${logicalId}: ${physicalId}`);
|
|
22757
22774
|
try {
|
|
22758
22775
|
const comment = properties["HostedZoneConfig"]?.["Comment"] ?? "";
|
|
@@ -22763,6 +22780,12 @@ var Route53Provider = class {
|
|
|
22763
22780
|
await this.applyHostedZoneTags(physicalId, properties, logicalId);
|
|
22764
22781
|
await this.applyQueryLoggingConfig(physicalId, properties, logicalId);
|
|
22765
22782
|
await this.syncVPCAssociations(physicalId, properties, logicalId);
|
|
22783
|
+
const prevEnabled = previousProperties["HostedZoneFeatures"]?.["AcceleratedRecoveryStatus"] === "ENABLED";
|
|
22784
|
+
const nextEnabled = properties["HostedZoneFeatures"]?.["AcceleratedRecoveryStatus"] === "ENABLED";
|
|
22785
|
+
if (prevEnabled !== nextEnabled) await this.getClient().send(new UpdateHostedZoneFeaturesCommand({
|
|
22786
|
+
HostedZoneId: physicalId,
|
|
22787
|
+
EnableAcceleratedRecovery: nextEnabled
|
|
22788
|
+
}));
|
|
22766
22789
|
const nameServers = (await this.getClient().send(new GetHostedZoneCommand({ Id: physicalId }))).DelegationSet?.NameServers ?? [];
|
|
22767
22790
|
this.logger.debug(`Successfully updated hosted zone ${logicalId}`);
|
|
22768
22791
|
return {
|
|
@@ -22782,6 +22805,7 @@ var Route53Provider = class {
|
|
|
22782
22805
|
this.logger.debug(`Deleting Route 53 hosted zone ${logicalId}: ${physicalId}`);
|
|
22783
22806
|
try {
|
|
22784
22807
|
await this.deleteQueryLoggingConfigForZone(physicalId, logicalId);
|
|
22808
|
+
await this.ensureAcceleratedRecoveryDisabledForDelete(physicalId, logicalId);
|
|
22785
22809
|
await this.getClient().send(new DeleteHostedZoneCommand({ Id: physicalId }));
|
|
22786
22810
|
this.logger.debug(`Successfully deleted hosted zone ${logicalId}`);
|
|
22787
22811
|
} catch (error) {
|
|
@@ -22790,10 +22814,86 @@ var Route53Provider = class {
|
|
|
22790
22814
|
this.logger.debug(`Hosted zone ${physicalId} does not exist, skipping deletion`);
|
|
22791
22815
|
return;
|
|
22792
22816
|
}
|
|
22817
|
+
if (error instanceof ProvisioningError) throw error;
|
|
22793
22818
|
const cause = error instanceof Error ? error : void 0;
|
|
22794
22819
|
throw new ProvisioningError(`Failed to delete hosted zone ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
|
|
22795
22820
|
}
|
|
22796
22821
|
}
|
|
22822
|
+
/**
|
|
22823
|
+
* Pre-delete guard for `AWS::Route53::HostedZone:HostedZoneFeatures`.
|
|
22824
|
+
*
|
|
22825
|
+
* AWS rejects `DeleteHostedZone` while AcceleratedRecovery is anything
|
|
22826
|
+
* other than DISABLED (the feature uses an async control plane API —
|
|
22827
|
+
* `UpdateHostedZoneFeatures` — and AWS won't tear down a zone whose
|
|
22828
|
+
* feature state is still in flight). This helper:
|
|
22829
|
+
*
|
|
22830
|
+
* 1. Reads the current `HostedZone.Features.AcceleratedRecoveryStatus`.
|
|
22831
|
+
* DISABLED / undefined / NoSuchHostedZone → return early (nothing
|
|
22832
|
+
* to do, the delete will proceed normally or hit the existing
|
|
22833
|
+
* NoSuchHostedZone short-circuit).
|
|
22834
|
+
* 2. ENABLING / ENABLED → issue `UpdateHostedZoneFeatures(false)` then
|
|
22835
|
+
* poll `GetHostedZone` until the status settles to DISABLED.
|
|
22836
|
+
* 3. DISABLING → poll without re-issuing the toggle.
|
|
22837
|
+
* 4. *_FAILED / *_HOSTED_ZONE_LOCKED → surface the AWS error to the
|
|
22838
|
+
* operator (out of scope for cdkd to recover automatically).
|
|
22839
|
+
*
|
|
22840
|
+
* Poll budget: env-overridable timeout (default 10 min) + interval
|
|
22841
|
+
* (default 15s; tests override via env to keep runs fast). Failure to
|
|
22842
|
+
* settle within the budget throws — the operator can either re-run
|
|
22843
|
+
* `cdkd state destroy` (poll resets) or disable manually.
|
|
22844
|
+
*/
|
|
22845
|
+
async ensureAcceleratedRecoveryDisabledForDelete(physicalId, logicalId) {
|
|
22846
|
+
const client = this.getClient();
|
|
22847
|
+
const pollIntervalMs = Number.parseInt(process.env["CDKD_R53_ACCEL_RECOVERY_POLL_INTERVAL_MS"] ?? "15000", 10);
|
|
22848
|
+
const timeoutMs = Number.parseInt(process.env["CDKD_R53_ACCEL_RECOVERY_POLL_TIMEOUT_MS"] ?? "600000", 10);
|
|
22849
|
+
const readStatus = async () => {
|
|
22850
|
+
try {
|
|
22851
|
+
return (await client.send(new GetHostedZoneCommand({ Id: physicalId }))).HostedZone?.Features?.AcceleratedRecoveryStatus;
|
|
22852
|
+
} catch (err) {
|
|
22853
|
+
if (err instanceof Error && err.name === "NoSuchHostedZone") return void 0;
|
|
22854
|
+
throw err;
|
|
22855
|
+
}
|
|
22856
|
+
};
|
|
22857
|
+
const deadline = Date.now() + timeoutMs;
|
|
22858
|
+
const TERMINAL_FAILED = new Set([
|
|
22859
|
+
"ENABLE_FAILED",
|
|
22860
|
+
"DISABLE_FAILED",
|
|
22861
|
+
"ENABLING_HOSTED_ZONE_LOCKED",
|
|
22862
|
+
"DISABLING_HOSTED_ZONE_LOCKED"
|
|
22863
|
+
]);
|
|
22864
|
+
const waitFor = async (targets, label) => {
|
|
22865
|
+
while (Date.now() < deadline) {
|
|
22866
|
+
const status = await readStatus();
|
|
22867
|
+
if (status === void 0 || targets.has(status)) return status;
|
|
22868
|
+
if (TERMINAL_FAILED.has(status)) throw new ProvisioningError(`Cannot delete hosted zone ${logicalId} (${physicalId}): AcceleratedRecoveryStatus transitioned to '${status}' while waiting for ${label} — operator must resolve before destroy can proceed`, "AWS::Route53::HostedZone", logicalId, physicalId);
|
|
22869
|
+
this.logger.debug(`Polling AcceleratedRecoveryStatus for ${physicalId}: ${status} (waiting for ${label}, will re-poll in ~${pollIntervalMs}ms)`);
|
|
22870
|
+
const sliceEnd = Math.min(Date.now() + pollIntervalMs, deadline);
|
|
22871
|
+
while (Date.now() < sliceEnd) {
|
|
22872
|
+
const tick = Math.min(1e3, sliceEnd - Date.now());
|
|
22873
|
+
if (tick <= 0) break;
|
|
22874
|
+
await new Promise((resolve) => setTimeout(resolve, tick));
|
|
22875
|
+
}
|
|
22876
|
+
}
|
|
22877
|
+
throw new ProvisioningError(`Timed out after ${timeoutMs}ms waiting for AcceleratedRecoveryStatus to reach ${label} on hosted zone ${logicalId} (${physicalId}); re-run \`cdkd state destroy\` or disable manually via \`aws route53 update-hosted-zone-features --hosted-zone-id ${physicalId} --no-enable-accelerated-recovery\``, "AWS::Route53::HostedZone", logicalId, physicalId);
|
|
22878
|
+
};
|
|
22879
|
+
let current = await readStatus();
|
|
22880
|
+
if (current === void 0 || current === "DISABLED") return;
|
|
22881
|
+
if (TERMINAL_FAILED.has(current)) throw new ProvisioningError(`Cannot delete hosted zone ${logicalId} (${physicalId}): AcceleratedRecoveryStatus is '${current}' — operator must resolve before destroy can proceed`, "AWS::Route53::HostedZone", logicalId, physicalId);
|
|
22882
|
+
if (current === "ENABLING") {
|
|
22883
|
+
this.logger.debug(`Hosted zone ${physicalId} is ENABLING; waiting for it to settle before issuing disable`);
|
|
22884
|
+
current = await waitFor(new Set(["ENABLED", "DISABLED"]), "ENABLED or DISABLED");
|
|
22885
|
+
if (current === void 0 || current === "DISABLED") return;
|
|
22886
|
+
}
|
|
22887
|
+
if (current === "ENABLED") {
|
|
22888
|
+
this.logger.debug(`Hosted zone ${physicalId} is ENABLED; issuing UpdateHostedZoneFeatures(false) before delete`);
|
|
22889
|
+
await client.send(new UpdateHostedZoneFeaturesCommand({
|
|
22890
|
+
HostedZoneId: physicalId,
|
|
22891
|
+
EnableAcceleratedRecovery: false
|
|
22892
|
+
}));
|
|
22893
|
+
} else if (current === "DISABLING") this.logger.debug(`Hosted zone ${physicalId} AcceleratedRecovery is already DISABLING; waiting for settle`);
|
|
22894
|
+
const settled = await waitFor(new Set(["DISABLED"]), "DISABLED");
|
|
22895
|
+
this.logger.debug(`Hosted zone ${physicalId} AcceleratedRecoveryStatus settled to ${settled ?? "undefined (zone gone)"}`);
|
|
22896
|
+
}
|
|
22797
22897
|
async getHostedZoneAttribute(physicalId, attributeName) {
|
|
22798
22898
|
switch (attributeName) {
|
|
22799
22899
|
case "Id": return physicalId;
|
|
@@ -23131,6 +23231,7 @@ var Route53Provider = class {
|
|
|
23131
23231
|
if (resp.HostedZone.Config?.PrivateZone !== void 0) cfg["PrivateZone"] = resp.HostedZone.Config.PrivateZone;
|
|
23132
23232
|
result["HostedZoneConfig"] = cfg;
|
|
23133
23233
|
}
|
|
23234
|
+
if (resp.HostedZone.Features?.AcceleratedRecoveryStatus !== void 0) result["HostedZoneFeatures"] = { AcceleratedRecoveryStatus: resp.HostedZone.Features.AcceleratedRecoveryStatus };
|
|
23134
23235
|
if (resp.HostedZone.Config?.PrivateZone === true) result["VPCs"] = (resp.VPCs ?? []).map((v) => {
|
|
23135
23236
|
const out = {};
|
|
23136
23237
|
if (v.VPCId !== void 0) out["VPCId"] = v.VPCId;
|
|
@@ -42947,6 +43048,14 @@ const fromStateFactory = (options) => {
|
|
|
42947
43048
|
});
|
|
42948
43049
|
};
|
|
42949
43050
|
/**
|
|
43051
|
+
* Cdkd's `extraStateProviders` map for cdk-local's engine entry points
|
|
43052
|
+
* (e.g. `runEcsServiceEmulator`) that accept a state-source factory
|
|
43053
|
+
* registry directly instead of going through `createLocalStateProvider`.
|
|
43054
|
+
* The engine calls `createLocalStateProvider` internally with this map,
|
|
43055
|
+
* so cdkd's `--from-state` flow is wired in transparently.
|
|
43056
|
+
*/
|
|
43057
|
+
const cdkdExtraStateProviders = { fromState: fromStateFactory };
|
|
43058
|
+
/**
|
|
42950
43059
|
* Pick and construct the right `LocalStateProvider` for the supplied
|
|
42951
43060
|
* flag set. Delegates to cdk-local's dispatcher with cdkd's
|
|
42952
43061
|
* `--from-state` factory wired in. Returns `undefined` when neither
|
|
@@ -42965,7 +43074,7 @@ const fromStateFactory = (options) => {
|
|
|
42965
43074
|
* per-stack loop — see that helper's docstring for the rationale.
|
|
42966
43075
|
*/
|
|
42967
43076
|
function createLocalStateProvider$1(options, cdkdStackName, synthRegion) {
|
|
42968
|
-
return createLocalStateProvider(options, cdkdStackName, synthRegion,
|
|
43077
|
+
return createLocalStateProvider(options, cdkdStackName, synthRegion, cdkdExtraStateProviders);
|
|
42969
43078
|
}
|
|
42970
43079
|
|
|
42971
43080
|
//#endregion
|
|
@@ -43010,7 +43119,7 @@ function parseTarget(target) {
|
|
|
43010
43119
|
function resolveLambdaTarget(target, stacks) {
|
|
43011
43120
|
if (stacks.length === 0) throw new LocalInvokeResolutionError("No stacks found in the synthesized assembly.");
|
|
43012
43121
|
const parsed = parseTarget(target);
|
|
43013
|
-
const stack = pickStack$
|
|
43122
|
+
const stack = pickStack$3(parsed, stacks);
|
|
43014
43123
|
const template = stack.template;
|
|
43015
43124
|
const resources = template.Resources ?? {};
|
|
43016
43125
|
let match;
|
|
@@ -43044,7 +43153,7 @@ function resolveLambdaTarget(target, stacks) {
|
|
|
43044
43153
|
* user may omit the stack prefix. Otherwise an explicit stack pattern is
|
|
43045
43154
|
* required.
|
|
43046
43155
|
*/
|
|
43047
|
-
function pickStack$
|
|
43156
|
+
function pickStack$3(parsed, stacks) {
|
|
43048
43157
|
if (parsed.stackPattern === null) {
|
|
43049
43158
|
if (stacks.length === 1) return stacks[0];
|
|
43050
43159
|
throw new LocalInvokeResolutionError(`Multiple stacks in app, target '${parsed.pathOrId}' is missing a stack prefix. Use 'StackName:${parsed.pathOrId}' or 'StackName/...' (path form). Available stacks: ${stacks.map((s) => s.stackName).join(", ")}.`);
|
|
@@ -43667,7 +43776,7 @@ function parseEcsTarget(target) {
|
|
|
43667
43776
|
function resolveEcsTaskTarget(target, stacks, context) {
|
|
43668
43777
|
if (stacks.length === 0) throw new EcsTaskResolutionError("No stacks found in the synthesized assembly.");
|
|
43669
43778
|
const parsed = parseEcsTarget(target);
|
|
43670
|
-
const stack = pickStack$
|
|
43779
|
+
const stack = pickStack$2(parsed, stacks);
|
|
43671
43780
|
const resources = stack.template.Resources ?? {};
|
|
43672
43781
|
let logicalId;
|
|
43673
43782
|
let resource;
|
|
@@ -43688,7 +43797,7 @@ function resolveEcsTaskTarget(target, stacks, context) {
|
|
|
43688
43797
|
if (resource.Type !== "AWS::ECS::TaskDefinition") throw new EcsTaskResolutionError(`Resource '${logicalId}' in ${stack.stackName} is ${resource.Type}, not an AWS::ECS::TaskDefinition.`);
|
|
43689
43798
|
return extractTaskDefinitionProperties(stack, logicalId, resource, context);
|
|
43690
43799
|
}
|
|
43691
|
-
function pickStack$
|
|
43800
|
+
function pickStack$2(parsed, stacks) {
|
|
43692
43801
|
if (parsed.stackPattern === null) {
|
|
43693
43802
|
if (stacks.length === 1) return stacks[0];
|
|
43694
43803
|
throw new EcsTaskResolutionError(`Multiple stacks in app, target '${parsed.pathOrId}' is missing a stack prefix. Use 'StackName:${parsed.pathOrId}' or 'StackName/...' (path form). Available stacks: ${stacks.map((s) => s.stackName).join(", ")}.`);
|
|
@@ -48430,7 +48539,7 @@ async function localRunTaskCommand(target, options) {
|
|
|
48430
48539
|
const { stacks } = await synthesizer.synthesize(synthOpts);
|
|
48431
48540
|
const candidate = pickCandidateStack$1(parseEcsTarget(target).stackPattern, stacks);
|
|
48432
48541
|
stateProvider = createLocalStateProvider$1(options, candidate?.stackName ?? "", candidate?.region);
|
|
48433
|
-
const imageContext = await buildEcsImageResolutionContext$
|
|
48542
|
+
const imageContext = await buildEcsImageResolutionContext$2(candidate, stateProvider, options);
|
|
48434
48543
|
const task = resolveEcsTaskTarget(target, stacks, imageContext);
|
|
48435
48544
|
logger.info(`Target: ${task.stack.stackName}/${task.taskDefinitionLogicalId} (family=${task.family}, containers=${task.containers.length})`);
|
|
48436
48545
|
const taskNeeds = detectEcsImageResolutionNeeds(stacks.find((s) => s.stackName === task.stack.stackName) ?? task.stack);
|
|
@@ -48554,7 +48663,7 @@ async function assumeTaskRole$1(roleArn, region) {
|
|
|
48554
48663
|
* `--from-state` and `--from-cfn-stack` produce the same downstream
|
|
48555
48664
|
* context shape (issue #606).
|
|
48556
48665
|
*/
|
|
48557
|
-
async function buildEcsImageResolutionContext$
|
|
48666
|
+
async function buildEcsImageResolutionContext$2(candidate, stateProvider, options) {
|
|
48558
48667
|
const logger = getLogger();
|
|
48559
48668
|
if (!candidate) return void 0;
|
|
48560
48669
|
const needs = detectEcsImageResolutionNeeds(candidate);
|
|
@@ -48682,7 +48791,7 @@ function createLocalRunTaskCommand() {
|
|
|
48682
48791
|
function resolveEcsServiceTarget(target, stacks, context) {
|
|
48683
48792
|
if (stacks.length === 0) throw new EcsTaskResolutionError("No stacks found in the synthesized assembly.");
|
|
48684
48793
|
const parsed = parseEcsTarget(target);
|
|
48685
|
-
const stack = pickStack(parsed, stacks);
|
|
48794
|
+
const stack = pickStack$1(parsed, stacks);
|
|
48686
48795
|
const resources = stack.template.Resources ?? {};
|
|
48687
48796
|
let serviceLogicalId;
|
|
48688
48797
|
let serviceResource;
|
|
@@ -48884,7 +48993,7 @@ function parseServiceName(raw, serviceLogicalId) {
|
|
|
48884
48993
|
* service-specific extensions (e.g. cross-stack service-to-task refs)
|
|
48885
48994
|
* can diverge without breaking the run-task code path.
|
|
48886
48995
|
*/
|
|
48887
|
-
function pickStack(parsed, stacks) {
|
|
48996
|
+
function pickStack$1(parsed, stacks) {
|
|
48888
48997
|
if (parsed.stackPattern === null) {
|
|
48889
48998
|
if (stacks.length === 1) return stacks[0];
|
|
48890
48999
|
throw new EcsTaskResolutionError(`Target has no stack prefix, and the assembly contains ${stacks.length} stacks: ${stacks.map((s) => s.stackName).join(", ")}. Pass the target as 'Stack/Path' or 'Stack:LogicalId'.`);
|
|
@@ -49438,7 +49547,7 @@ async function localStartServiceCommand(targets, options) {
|
|
|
49438
49547
|
for (const w of index.warnings) logger.warn(w);
|
|
49439
49548
|
}
|
|
49440
49549
|
const registry = new CloudMapRegistry();
|
|
49441
|
-
const sidecarCredentials = await resolveSharedSidecarCredentials(options);
|
|
49550
|
+
const sidecarCredentials = await resolveSharedSidecarCredentials$1(options);
|
|
49442
49551
|
try {
|
|
49443
49552
|
sharedNetwork = await createSharedSvcNetwork({
|
|
49444
49553
|
prefix: options.cluster,
|
|
@@ -49495,7 +49604,7 @@ async function bootOneTarget(target, runState, stacks, options, discovery, skipP
|
|
|
49495
49604
|
}
|
|
49496
49605
|
async function runOneTarget(target, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile) {
|
|
49497
49606
|
const logger = getLogger();
|
|
49498
|
-
const imageContext = await buildEcsImageResolutionContext(target, stacks, options, stateProvider);
|
|
49607
|
+
const imageContext = await buildEcsImageResolutionContext$1(target, stacks, options, stateProvider);
|
|
49499
49608
|
const service = resolveEcsServiceTarget(target, stacks, imageContext);
|
|
49500
49609
|
logger.info(`Target: ${service.stack.stackName}/${service.serviceLogicalId} (service=${service.serviceName}, desiredCount=${service.desiredCount}, task=${service.task.taskDefinitionLogicalId})`);
|
|
49501
49610
|
for (const w of service.warnings) logger.warn(w);
|
|
@@ -49588,7 +49697,7 @@ async function assumeTaskRole(roleArn, region) {
|
|
|
49588
49697
|
* the candidate stack picker differs because services and tasks share
|
|
49589
49698
|
* the same stack-pattern grammar.
|
|
49590
49699
|
*/
|
|
49591
|
-
async function buildEcsImageResolutionContext(target, stacks, options, stateProvider) {
|
|
49700
|
+
async function buildEcsImageResolutionContext$1(target, stacks, options, stateProvider) {
|
|
49592
49701
|
const logger = getLogger();
|
|
49593
49702
|
const candidate = pickCandidateStack(parseEcsTarget(target).stackPattern, stacks);
|
|
49594
49703
|
if (!candidate) return void 0;
|
|
@@ -49680,13 +49789,13 @@ function parsePositiveInt(raw, flagName) {
|
|
|
49680
49789
|
* Raising this requires extending the allocator to walk a different
|
|
49681
49790
|
* IP range.
|
|
49682
49791
|
*/
|
|
49683
|
-
const MAX_TASKS_SUBNET_RANGE_CAP = 83;
|
|
49684
|
-
function parseMaxTasks(raw) {
|
|
49792
|
+
const MAX_TASKS_SUBNET_RANGE_CAP$1 = 83;
|
|
49793
|
+
function parseMaxTasks$1(raw) {
|
|
49685
49794
|
const parsed = parsePositiveInt(raw, "--max-tasks");
|
|
49686
49795
|
if (parsed > 83) throw new LocalStartServiceError(`--max-tasks ${parsed} exceeds the per-replica link-local /24 subnet allocator's range (${83}). The allocator in ecs-service-runner.ts assigns each replica its own 169.254.x.0/24 from the range 169.254.170.0..169.254.253.0; replica indices >= ${83} would collide with earlier replicas via modulo wrap. Lower --max-tasks to <= ${83}, or accept reduced local concurrency for high-DesiredCount services.`);
|
|
49687
49796
|
return parsed;
|
|
49688
49797
|
}
|
|
49689
|
-
function parseRestartPolicy(raw) {
|
|
49798
|
+
function parseRestartPolicy$1(raw) {
|
|
49690
49799
|
if (raw === "on-failure" || raw === "always" || raw === "none") return raw;
|
|
49691
49800
|
throw new LocalStartServiceError(`--restart-policy must be one of 'on-failure', 'always', or 'none' (got '${raw}').`);
|
|
49692
49801
|
}
|
|
@@ -49715,11 +49824,11 @@ function parseRestartPolicy(raw) {
|
|
|
49715
49824
|
* branches without having to mock the full Synth + Docker + AWS
|
|
49716
49825
|
* pipeline (the strategy PR #655 used for the Lambda container path).
|
|
49717
49826
|
*/
|
|
49718
|
-
async function resolveSharedSidecarCredentials(options) {
|
|
49827
|
+
async function resolveSharedSidecarCredentials$1(options) {
|
|
49719
49828
|
if (options.profile) return resolveProfileCredentials(options.profile);
|
|
49720
49829
|
}
|
|
49721
49830
|
function createLocalStartServiceCommand() {
|
|
49722
|
-
const cmd = new Command("start-service").description("Run one or more AWS::ECS::Service resources locally as a long-running emulator. Spins up DesiredCount task replicas per service (clamped by --max-tasks) using the same per-task docker network + metadata sidecar pattern as `cdkd local run-task`, then keeps each replica running and restarts it on exit per --restart-policy. ^C tears every replica + sidecar + network down. Each <target> accepts a CDK display path (MyStack/MyService) or stack-qualified logical ID (MyStack:MyServiceXYZ); single-stack apps may omit the stack prefix. When two or more <target>s are supplied, every service is booted into a shared Cloud Map / Service Connect registry so peer services discover each other via docker --add-host overlay (Issue #460).").argument("<targets...>", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ECS::Service resources to run").addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default("cdkd-local")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"ContainerName\":{\"KEY\":\"VALUE\"}, \"Parameters\":{}})")).addOption(new Option("--container-host <ip>", "Host IP to bind published container ports to. Must be a numeric IP (Docker rejects hostnames here)").default("127.0.0.1")).addOption(new Option("--assume-task-role [arn]", "Assume the task definition's TaskRoleArn (or the supplied ARN) and forward STS-issued temp credentials via the metadata sidecar so containers run with the deployed task role. Bare flag uses the template's TaskRoleArn; pass an explicit ARN to override.")).addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries.")).addOption(new Option("--platform <platform>", "Force docker --platform (linux/amd64 or linux/arm64). Default: inferred from task RuntimePlatform.CpuArchitecture")).addOption(new Option("--max-tasks <n>", `Hard cap on local replica count. Caps the template DesiredCount so local dev machines don't run an unbounded number of containers. Cannot exceed ${83} due to the per-replica link-local /24 subnet allocator's range.`).default(3).argParser(parseMaxTasks)).addOption(new Option("--restart-policy <policy>", "How to react when an essential container exits. 'on-failure' (default) restarts only on non-zero exit; 'always' restarts on every exit; 'none' shuts the replica down and runs the service degraded.").default("on-failure").argParser(parseRestartPolicy)).addOption(new Option("--from-state", "Read cdkd S3 state for the target stack and substitute Fn::Sub / Fn::GetAtt / Fn::ImportValue / Fn::GetStackOutput intrinsics in container images, environment variables, secrets, role ARNs, and volumes.").default(false)).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via DescribeStackResources and substitute Ref / Fn::ImportValue in container env vars / secrets / image URIs with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (`cdk deploy`). Bare form uses the cdkd stack name; pass an explicit value when the CFn stack name differs. Mutually exclusive with --from-state. Fn::GetAtt is warn-and-dropped in v1 (CFn DescribeStackResources does not return per-attribute values).")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-state when the same stack name has state in multiple regions, and with --from-cfn-stack as the CFn client region (cdkd does not have a separate --cfn-stack-region flag).")).action(withErrorHandling(localStartServiceCommand));
|
|
49831
|
+
const cmd = new Command("start-service").description("Run one or more AWS::ECS::Service resources locally as a long-running emulator. Spins up DesiredCount task replicas per service (clamped by --max-tasks) using the same per-task docker network + metadata sidecar pattern as `cdkd local run-task`, then keeps each replica running and restarts it on exit per --restart-policy. ^C tears every replica + sidecar + network down. Each <target> accepts a CDK display path (MyStack/MyService) or stack-qualified logical ID (MyStack:MyServiceXYZ); single-stack apps may omit the stack prefix. When two or more <target>s are supplied, every service is booted into a shared Cloud Map / Service Connect registry so peer services discover each other via docker --add-host overlay (Issue #460).").argument("<targets...>", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ECS::Service resources to run").addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default("cdkd-local")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"ContainerName\":{\"KEY\":\"VALUE\"}, \"Parameters\":{}})")).addOption(new Option("--container-host <ip>", "Host IP to bind published container ports to. Must be a numeric IP (Docker rejects hostnames here)").default("127.0.0.1")).addOption(new Option("--assume-task-role [arn]", "Assume the task definition's TaskRoleArn (or the supplied ARN) and forward STS-issued temp credentials via the metadata sidecar so containers run with the deployed task role. Bare flag uses the template's TaskRoleArn; pass an explicit ARN to override.")).addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries.")).addOption(new Option("--platform <platform>", "Force docker --platform (linux/amd64 or linux/arm64). Default: inferred from task RuntimePlatform.CpuArchitecture")).addOption(new Option("--max-tasks <n>", `Hard cap on local replica count. Caps the template DesiredCount so local dev machines don't run an unbounded number of containers. Cannot exceed ${83} due to the per-replica link-local /24 subnet allocator's range.`).default(3).argParser(parseMaxTasks$1)).addOption(new Option("--restart-policy <policy>", "How to react when an essential container exits. 'on-failure' (default) restarts only on non-zero exit; 'always' restarts on every exit; 'none' shuts the replica down and runs the service degraded.").default("on-failure").argParser(parseRestartPolicy$1)).addOption(new Option("--from-state", "Read cdkd S3 state for the target stack and substitute Fn::Sub / Fn::GetAtt / Fn::ImportValue / Fn::GetStackOutput intrinsics in container images, environment variables, secrets, role ARNs, and volumes.").default(false)).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via DescribeStackResources and substitute Ref / Fn::ImportValue in container env vars / secrets / image URIs with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (`cdk deploy`). Bare form uses the cdkd stack name; pass an explicit value when the CFn stack name differs. Mutually exclusive with --from-state. Fn::GetAtt is warn-and-dropped in v1 (CFn DescribeStackResources does not return per-attribute values).")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-state when the same stack name has state in multiple regions, and with --from-cfn-stack as the CFn client region (cdkd does not have a separate --cfn-stack-region flag).")).action(withErrorHandling(localStartServiceCommand));
|
|
49723
49832
|
[
|
|
49724
49833
|
...commonOptions,
|
|
49725
49834
|
...appOptions,
|
|
@@ -50536,6 +50645,189 @@ function createLocalInvokeAgentCoreCommand() {
|
|
|
50536
50645
|
return cmd;
|
|
50537
50646
|
}
|
|
50538
50647
|
|
|
50648
|
+
//#endregion
|
|
50649
|
+
//#region src/cli/commands/local-start-alb.ts
|
|
50650
|
+
/**
|
|
50651
|
+
* Issue #86 v1 — parse `--lb-port <listenerPort>=<hostPort>` overrides into a
|
|
50652
|
+
* `listenerPort -> hostPort` map. The local ALB front-door binds the listener
|
|
50653
|
+
* port on the host by default, but a privileged listener port (e.g. 80 / 443)
|
|
50654
|
+
* fails to bind as non-root on macOS, so the user opts in to a non-privileged
|
|
50655
|
+
* host port (e.g. `--lb-port 80=8080`). Repeatable; each value is
|
|
50656
|
+
* `<listenerPort>=<hostPort>` with both in 1-65535.
|
|
50657
|
+
*/
|
|
50658
|
+
function parseLbPortOverrides(values) {
|
|
50659
|
+
const out = {};
|
|
50660
|
+
for (const raw of values ?? []) {
|
|
50661
|
+
const m = /^(\d+)=(\d+)$/.exec(raw.trim());
|
|
50662
|
+
if (!m) throw new LocalStartServiceError(`Invalid --lb-port '${raw}'. Expected <listenerPort>=<hostPort> (e.g. 80=8080).`);
|
|
50663
|
+
const listenerPort = Number(m[1]);
|
|
50664
|
+
const hostPort = Number(m[2]);
|
|
50665
|
+
for (const [label, p] of [["listener", listenerPort], ["host", hostPort]]) if (p < 1 || p > 65535) throw new LocalStartServiceError(`Invalid --lb-port '${raw}': ${label} port must be 1-65535.`);
|
|
50666
|
+
out[listenerPort] = hostPort;
|
|
50667
|
+
}
|
|
50668
|
+
return out;
|
|
50669
|
+
}
|
|
50670
|
+
/**
|
|
50671
|
+
* Resolve an ALB target string (`Stack/Path` display path or `Stack:LogicalId`)
|
|
50672
|
+
* to its stack + `AWS::ElasticLoadBalancingV2::LoadBalancer` logical id. Mirrors
|
|
50673
|
+
* the ECS service resolver's target grammar.
|
|
50674
|
+
*/
|
|
50675
|
+
function resolveAlbTarget(target, stacks) {
|
|
50676
|
+
if (stacks.length === 0) throw new LocalStartServiceError("No stacks found in the synthesized assembly.");
|
|
50677
|
+
const parsed = parseEcsTarget(target);
|
|
50678
|
+
const stack = pickStack(parsed.stackPattern, stacks, target);
|
|
50679
|
+
const resources = stack.template.Resources ?? {};
|
|
50680
|
+
if (parsed.isPath) {
|
|
50681
|
+
const index = buildCdkPathIndex(stack.template);
|
|
50682
|
+
const albs = resolveCdkPathToLogicalIds(parsed.pathOrId, index).filter(({ logicalId }) => {
|
|
50683
|
+
const r = resources[logicalId];
|
|
50684
|
+
return r !== void 0 && isApplicationLoadBalancer(r);
|
|
50685
|
+
});
|
|
50686
|
+
if (albs.length === 0) throw notFound(target, stack, resources);
|
|
50687
|
+
if (albs.length > 1) throw new LocalStartServiceError(`Target '${target}' matches ${albs.length} load balancers in ${stack.stackName}: ${albs.map((a) => a.logicalId).join(", ")}. Refine the path or use the stack:LogicalId form.`);
|
|
50688
|
+
return {
|
|
50689
|
+
stack,
|
|
50690
|
+
albLogicalId: albs[0].logicalId
|
|
50691
|
+
};
|
|
50692
|
+
}
|
|
50693
|
+
const res = resources[parsed.pathOrId];
|
|
50694
|
+
if (!res || !isApplicationLoadBalancer(res)) throw notFound(target, stack, resources);
|
|
50695
|
+
return {
|
|
50696
|
+
stack,
|
|
50697
|
+
albLogicalId: parsed.pathOrId
|
|
50698
|
+
};
|
|
50699
|
+
}
|
|
50700
|
+
function pickStack(stackPattern, stacks, target) {
|
|
50701
|
+
if (stackPattern === null) {
|
|
50702
|
+
if (stacks.length === 1) return stacks[0];
|
|
50703
|
+
throw new LocalStartServiceError(`Target '${target}' has no stack prefix, and the assembly contains ${stacks.length} stacks: ${stacks.map((s) => s.stackName).join(", ")}. Pass it as 'Stack/Path' or 'Stack:LogicalId'.`);
|
|
50704
|
+
}
|
|
50705
|
+
const matched = matchStacks(stacks, [stackPattern]);
|
|
50706
|
+
if (matched.length === 0) throw new LocalStartServiceError(`No stack matches '${stackPattern}'. Available stacks: ${stacks.map((s) => s.stackName).join(", ")}.`);
|
|
50707
|
+
if (matched.length > 1) throw new LocalStartServiceError(`Multiple stacks match '${stackPattern}': ${matched.map((s) => s.stackName).join(", ")}. Refine the pattern.`);
|
|
50708
|
+
return matched[0];
|
|
50709
|
+
}
|
|
50710
|
+
function notFound(target, stack, resources) {
|
|
50711
|
+
const albs = Object.entries(resources).filter(([, r]) => r.Type === "AWS::ElasticLoadBalancingV2::LoadBalancer").map(([logicalId]) => logicalId);
|
|
50712
|
+
const available = albs.length > 0 ? ` Available load balancers in ${stack.stackName}: ${albs.join(", ")}.` : ` ${stack.stackName} declares no AWS::ElasticLoadBalancingV2::LoadBalancer resources.`;
|
|
50713
|
+
return new LocalStartServiceError(`Target '${target}' did not match an application Load Balancer in ${stack.stackName}.${available}`);
|
|
50714
|
+
}
|
|
50715
|
+
/**
|
|
50716
|
+
* `cdkl start-alb` strategy — name the ALB, boot the ECS service(s) behind it,
|
|
50717
|
+
* and expose each listener via a local front-door. Mirrors how `start-api`
|
|
50718
|
+
* names the API and serves its backing Lambdas.
|
|
50719
|
+
*/
|
|
50720
|
+
function albStrategy(options) {
|
|
50721
|
+
const lbPortOverrides = parseLbPortOverrides(options.lbPort);
|
|
50722
|
+
return {
|
|
50723
|
+
pickEntries: (stacks) => listTargets(stacks).loadBalancers,
|
|
50724
|
+
pickerMessage: "Select one or more Application Load Balancers to run",
|
|
50725
|
+
pickerNoun: "Application Load Balancers",
|
|
50726
|
+
onMissing: () => new LocalStartServiceError(`${getEmbedConfig().cliName} start-alb requires at least one <target>. Pass one or more ALB paths like 'Stack/MyAlb', or run it in a TTY to pick interactively.`),
|
|
50727
|
+
resolveBoots: (stacks, chosenTargets) => {
|
|
50728
|
+
const warnings = [];
|
|
50729
|
+
const serviceTargets = /* @__PURE__ */ new Set();
|
|
50730
|
+
const listeners = [];
|
|
50731
|
+
const claimedHostPorts = /* @__PURE__ */ new Map();
|
|
50732
|
+
for (const albTarget of chosenTargets) {
|
|
50733
|
+
const { stack, albLogicalId } = resolveAlbTarget(albTarget, stacks);
|
|
50734
|
+
const resolution = resolveAlbFrontDoor(stack, albLogicalId);
|
|
50735
|
+
warnings.push(...resolution.warnings);
|
|
50736
|
+
const qualifyTarget = (t) => {
|
|
50737
|
+
if (t.kind === "lambda") return {
|
|
50738
|
+
kind: "lambda",
|
|
50739
|
+
lambda: resolveLambdaTarget(`${stack.stackName}:${t.lambdaLogicalId}`, stacks),
|
|
50740
|
+
targetGroupArn: `${stack.stackName}:${t.targetGroupLogicalId}`,
|
|
50741
|
+
multiValueHeaders: t.multiValueHeaders,
|
|
50742
|
+
weight: t.weight
|
|
50743
|
+
};
|
|
50744
|
+
const serviceTarget = `${stack.stackName}:${t.serviceLogicalId}`;
|
|
50745
|
+
serviceTargets.add(serviceTarget);
|
|
50746
|
+
return {
|
|
50747
|
+
kind: "ecs",
|
|
50748
|
+
serviceTarget,
|
|
50749
|
+
targetContainerName: t.targetContainerName,
|
|
50750
|
+
targetContainerPort: t.targetContainerPort,
|
|
50751
|
+
weight: t.weight
|
|
50752
|
+
};
|
|
50753
|
+
};
|
|
50754
|
+
const qualify = (action) => {
|
|
50755
|
+
if (action.kind === "forward") return {
|
|
50756
|
+
kind: "forward",
|
|
50757
|
+
targets: action.targets.map(qualifyTarget)
|
|
50758
|
+
};
|
|
50759
|
+
if (action.kind === "redirect") return {
|
|
50760
|
+
kind: "redirect",
|
|
50761
|
+
statusCode: action.statusCode,
|
|
50762
|
+
...action.protocol !== void 0 && { protocol: action.protocol },
|
|
50763
|
+
...action.host !== void 0 && { host: action.host },
|
|
50764
|
+
...action.port !== void 0 && { port: action.port },
|
|
50765
|
+
...action.path !== void 0 && { path: action.path },
|
|
50766
|
+
...action.query !== void 0 && { query: action.query }
|
|
50767
|
+
};
|
|
50768
|
+
return {
|
|
50769
|
+
kind: "fixed-response",
|
|
50770
|
+
statusCode: action.statusCode,
|
|
50771
|
+
...action.contentType !== void 0 && { contentType: action.contentType },
|
|
50772
|
+
...action.messageBody !== void 0 && { messageBody: action.messageBody }
|
|
50773
|
+
};
|
|
50774
|
+
};
|
|
50775
|
+
for (const listener of resolution.listeners) {
|
|
50776
|
+
const hostPort = lbPortOverrides[listener.listenerPort] ?? listener.listenerPort;
|
|
50777
|
+
const claimedBy = claimedHostPorts.get(hostPort);
|
|
50778
|
+
if (claimedBy !== void 0) {
|
|
50779
|
+
warnings.push(`Listener port ${listener.listenerPort} would bind host port ${hostPort}, already claimed by listener port ${claimedBy}; the local front-door fronts only the first. Use --lb-port to remap one of them.`);
|
|
50780
|
+
continue;
|
|
50781
|
+
}
|
|
50782
|
+
claimedHostPorts.set(hostPort, listener.listenerPort);
|
|
50783
|
+
listeners.push({
|
|
50784
|
+
listenerPort: listener.listenerPort,
|
|
50785
|
+
hostPort,
|
|
50786
|
+
protocol: listener.listenerProtocol,
|
|
50787
|
+
...listener.defaultAction ? { defaultAction: qualify(listener.defaultAction) } : {},
|
|
50788
|
+
...listener.defaultAuthGuard ? { defaultAuthGuard: listener.defaultAuthGuard } : {},
|
|
50789
|
+
rules: listener.rules.map((r) => ({
|
|
50790
|
+
priority: r.priority,
|
|
50791
|
+
pathPatterns: r.pathPatterns,
|
|
50792
|
+
hostPatterns: r.hostPatterns,
|
|
50793
|
+
httpHeaderConditions: r.httpHeaderConditions,
|
|
50794
|
+
httpRequestMethods: r.httpRequestMethods,
|
|
50795
|
+
queryStringConditions: r.queryStringConditions,
|
|
50796
|
+
sourceIpCidrs: r.sourceIpCidrs,
|
|
50797
|
+
action: qualify(r.action),
|
|
50798
|
+
...r.authGuard ? { authGuard: r.authGuard } : {}
|
|
50799
|
+
}))
|
|
50800
|
+
});
|
|
50801
|
+
}
|
|
50802
|
+
}
|
|
50803
|
+
const boots = [...serviceTargets].map((target) => ({ target }));
|
|
50804
|
+
const resolvedPorts = new Set(listeners.map((l) => l.listenerPort));
|
|
50805
|
+
for (const portStr of Object.keys(lbPortOverrides)) {
|
|
50806
|
+
const port = Number(portStr);
|
|
50807
|
+
if (!resolvedPorts.has(port)) warnings.push(`--lb-port override for listener port ${port} matched no ALB listener resolved for the named target(s); it was ignored.`);
|
|
50808
|
+
}
|
|
50809
|
+
return {
|
|
50810
|
+
boots,
|
|
50811
|
+
...listeners.length > 0 ? { frontDoor: { listeners } } : {},
|
|
50812
|
+
warnings
|
|
50813
|
+
};
|
|
50814
|
+
},
|
|
50815
|
+
lbPortOverrides
|
|
50816
|
+
};
|
|
50817
|
+
}
|
|
50818
|
+
/**
|
|
50819
|
+
* `cdkl start-alb <Stack/Alb>` — Issue #86 v1. Names an
|
|
50820
|
+
* `AWS::ElasticLoadBalancingV2::LoadBalancer`, discovers the ECS service(s)
|
|
50821
|
+
* behind its HTTP `forward` listeners, boots their replicas, and stands up a
|
|
50822
|
+
* local front-door on each listener port that round-robins across the replicas.
|
|
50823
|
+
* The symmetric ALB counterpart of `start-api`.
|
|
50824
|
+
*/
|
|
50825
|
+
function createLocalStartAlbCommand() {
|
|
50826
|
+
return addCommonEcsServiceOptions(new Command("start-alb").description("Run an Application Load Balancer locally: name the ALB, and cdk-local boots the ECS service(s) behind its listeners and stands up a local front-door on each listener port that round-robins across the running replicas and routes its listener rules across the backing services — a stable host endpoint, like behind a real load balancer. The symmetric ALB counterpart of `start-api`. Each <target> accepts a CDK display path (MyStack/MyAlb) or stack-qualified logical ID; single-stack apps may omit the stack prefix. Supports HTTP and HTTPS listeners (TLS terminated locally with --tls-cert/--tls-key or an auto-generated self-signed cert); all six ALB rule-condition fields (path-pattern / host-header / http-header / http-request-method / query-string / source-ip); forward (single and weighted), redirect, and fixed-response actions; and ECS or Lambda targets (a Lambda target group is invoked locally via the Lambda RIE). authenticate-cognito / authenticate-oidc actions enforce a local Bearer-JWT check (or AWSELBAuthSessionCookie pass-through) against the same JWKS / OIDC discovery URL the deployed ALB would; use --bearer-token <jwt> to inject a default token or --no-verify-auth to disable the guard. Omit <targets> in an interactive terminal to multi-select the load balancers from a list.").argument("[targets...]", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ElasticLoadBalancingV2::LoadBalancer resources to run (omit to multi-select interactively in a TTY)").addOption(new Option("--lb-port <listenerPort=hostPort...>", "Bind the local front-door on a specific host port (e.g. 80=8080); repeatable. Default: host port == ALB listener port. Use this on macOS to remap a privileged listener port (< 1024) to a non-privileged host port.")).addOption(new Option("--tls-cert <path>", "PEM-encoded server certificate for HTTPS front-door listeners. Must be set together with --tls-key. Omit both flags to auto-generate a self-signed cert (cached under $XDG_CACHE_HOME/cdk-local/alb-https/, default ~/.cache/cdk-local/alb-https/); requires openssl on PATH. The deployed Listener Certificates[] are NOT fetched (ACM private keys are not retrievable by design). The auto-generated cert lists DNS:localhost,IP:127.0.0.1 as SubjectAltName, so a client validating a non-loopback --container-host will fail the SAN check — pass --tls-cert / --tls-key with a SAN covering that host instead.")).addOption(new Option("--tls-key <path>", "PEM-encoded server private key matching --tls-cert. Must be set together with --tls-cert.")).addOption(new Option("--no-verify-auth", "Disable local enforcement of authenticate-cognito / authenticate-oidc actions. Every request is served as if the auth check passed. Useful for local dev where you do not want to mint a Bearer token at all.")).addOption(new Option("--bearer-token <jwt>", "Default Bearer JWT injected as Authorization: Bearer <jwt> when the inbound request has none. Verified against the same JWKS / OIDC discovery URL the deployed ALB would (signature + iss + aud + exp). Local-dev convenience; cookie pass-through (AWSELBAuthSessionCookie-*) also works.")).addOption(new Option("--from-state", "Read cdkd's S3 state for the target stack and substitute Ref / Fn::GetAtt / Fn::Sub / Fn::ImportValue / Fn::GetStackOutput intrinsics in container images, environment variables, secrets, role ARNs, and volumes of the ECS services behind the ALB. Mutually exclusive with --from-cfn-stack.").default(false)).addOption(new Option("--state-bucket <bucket>", "S3 bucket for --from-state. Falls back to CDKD_STATE_BUCKET env or cdk.json context.cdkd.stateBucket.")).addOption(new Option("--state-prefix <prefix>", "S3 key prefix for --from-state state files.").default("cdkd")).action(withErrorHandling(async (targets, options) => {
|
|
50827
|
+
await runEcsServiceEmulator(targets, options, albStrategy(options), cdkdExtraStateProviders);
|
|
50828
|
+
})));
|
|
50829
|
+
}
|
|
50830
|
+
|
|
50539
50831
|
//#endregion
|
|
50540
50832
|
//#region src/cli/commands/local-invoke.ts
|
|
50541
50833
|
/**
|
|
@@ -51339,6 +51631,7 @@ function createLocalCommand() {
|
|
|
51339
51631
|
local.addCommand(createLocalRunTaskCommand());
|
|
51340
51632
|
local.addCommand(createLocalStartServiceCommand());
|
|
51341
51633
|
local.addCommand(createLocalInvokeAgentCoreCommand());
|
|
51634
|
+
local.addCommand(createLocalStartAlbCommand());
|
|
51342
51635
|
return local;
|
|
51343
51636
|
}
|
|
51344
51637
|
|
|
@@ -52451,7 +52744,7 @@ function reorderArgs(argv) {
|
|
|
52451
52744
|
*/
|
|
52452
52745
|
async function main() {
|
|
52453
52746
|
const program = new Command();
|
|
52454
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
52747
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.195.0");
|
|
52455
52748
|
program.addCommand(createBootstrapCommand());
|
|
52456
52749
|
program.addCommand(createSynthCommand());
|
|
52457
52750
|
program.addCommand(createListCommand());
|