@go-to-k/cdkd 0.196.0 → 0.197.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/dist/cli.js CHANGED
@@ -1,14 +1,13 @@
1
1
  #!/usr/bin/env node
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-C6v_fcDw.js";
4
- import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-B15NAPbL.js";
2
+ import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-DWUnLza1.js";
3
+ import { $ as uploadCfnTemplate, A as S3StateBackend, At as PATTERN_B_NAME_PROPERTIES, B as Synthesizer, C as assertRegionMatch, Ct as normalizeAwsError, D as DagBuilder, E as DiffCalculator, Et as getLogger, F as buildDockerImage, Ft as withStackName, G as resolveSkipPrefix, H as getLegacyStateBucketName, I as formatDockerLoginError, J as warnDeprecatedNoPrefixCliFlag, K as resolveStateBucketWithDefault, L as getDockerCmd, M as AssetPublisher, Mt as generateResourceName, N as stringifyValue, Nt as generateResourceNameWithFallback, O as TemplateParser, Ot as runStackBuffered, P as WorkGraph, Pt as withSkipPrefix, Q as findLargeInlineResources, R as runDockerForeground, S as CloudControlProvider, T as applyRoleArnIfSet, U as resolveApp, V as getDefaultStateBucketName, W as resolveCaptureObservedState, X as CFN_TEMPLATE_URL_LIMIT, Y as CFN_TEMPLATE_BODY_LIMIT, Z as MIGRATE_TMP_PREFIX, _ as matchesCdkPath, _t as StackHasActiveImportsError, a as withRetry, b as ProviderRegistry, c as bold, ct as LocalMigrateError, d as green, dt as MissingCdkCliError, et as AssemblyReader, f as red, ft as NestedStackChildDirectDestroyError, g as CDK_PATH_TAG, gt as ResourceUpdateNotSupportedError, h as collectInlinePolicyNamesManagedBySiblings, ht as ResourceTimeoutError, i as withResourceDeadline, it as CdkdError, j as shouldRetainResource, jt as PATTERN_B_RESOURCE_TYPES, k as LockManager, kt as getLiveRenderer, l as cyan, lt as LocalStartServiceError, m as IAMRoleProvider, mt as ProvisioningError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as resolveBucketRegion, o as IMPLICIT_DELETE_DEPENDENCIES, p as yellow, pt as PartialFailureError, q as resolveStateBucketWithDefaultAndSource, r as DeployEngine, s as formatResourceLine, st as LocalInvokeBuildError$1, t as DEFAULT_RESOURCE_TIMEOUT_MS, u as gray, v as normalizeAwsTagsToCfn, vt as StackTerminationProtectionError, w as IntrinsicFunctionResolver, wt as withErrorHandling, x as findActionableSilentDrops, y as resolveExplicitPhysicalId, z as runDockerStreaming } from "./deploy-engine-z5g_l5ER.js";
5
4
  import { AsyncLocalStorage } from "node:async_hooks";
6
5
  import { randomBytes, randomUUID } from "node:crypto";
7
6
  import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListBucketsCommand, ListDirectoryBucketsCommand, 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";
8
7
  import { AddRoleToInstanceProfileCommand, AddUserToGroupCommand, AttachGroupPolicyCommand, AttachRolePolicyCommand, AttachUserPolicyCommand, CreateGroupCommand, CreateInstanceProfileCommand, CreateLoginProfileCommand, CreatePolicyCommand, CreatePolicyVersionCommand, CreateUserCommand, DeleteAccessKeyCommand, DeleteGroupCommand, DeleteGroupPolicyCommand, DeleteInstanceProfileCommand, DeleteLoginProfileCommand, DeletePolicyCommand, DeletePolicyVersionCommand, DeleteRolePolicyCommand, DeleteUserCommand, DeleteUserPermissionsBoundaryCommand, DeleteUserPolicyCommand, DetachGroupPolicyCommand, DetachRolePolicyCommand, DetachUserPolicyCommand, GetGroupCommand, GetGroupPolicyCommand, GetInstanceProfileCommand, GetPolicyCommand, GetPolicyVersionCommand, GetRolePolicyCommand, GetUserCommand, GetUserPolicyCommand, IAMClient, ListAccessKeysCommand, ListAttachedGroupPoliciesCommand, ListAttachedUserPoliciesCommand, ListEntitiesForPolicyCommand, ListGroupPoliciesCommand, ListGroupsForUserCommand, ListInstanceProfilesCommand, ListPoliciesCommand, ListPolicyTagsCommand, ListPolicyVersionsCommand, ListUserPoliciesCommand, ListUserTagsCommand, ListUsersCommand, NoSuchEntityException, PutGroupPolicyCommand, PutRolePolicyCommand, PutUserPermissionsBoundaryCommand, PutUserPolicyCommand, RemoveRoleFromInstanceProfileCommand, RemoveUserFromGroupCommand, TagPolicyCommand, TagUserCommand, UntagPolicyCommand, UntagUserCommand, UpdateLoginProfileCommand } from "@aws-sdk/client-iam";
9
8
  import { CreateQueueCommand, DeleteQueueCommand, GetQueueAttributesCommand, GetQueueUrlCommand, ListQueueTagsCommand, ListQueuesCommand, QueueDoesNotExist, SQSClient, SetQueueAttributesCommand, TagQueueCommand, UntagQueueCommand } from "@aws-sdk/client-sqs";
10
9
  import { CreateTopicCommand, DeleteTopicCommand, GetSubscriptionAttributesCommand, GetTopicAttributesCommand, ListTagsForResourceCommand, ListTopicsCommand, NotFoundException, SNSClient, SetTopicAttributesCommand, SubscribeCommand, TagResourceCommand, UnsubscribeCommand, UntagResourceCommand } from "@aws-sdk/client-sns";
11
- import { AddPermissionCommand, CreateEventSourceMappingCommand, CreateFunctionCommand, CreateFunctionUrlConfigCommand, DeleteEventSourceMappingCommand, DeleteFunctionCommand, DeleteFunctionUrlConfigCommand, DeleteLayerVersionCommand, GetEventSourceMappingCommand, GetFunctionCommand, GetFunctionRecursionConfigCommand, GetFunctionUrlConfigCommand, GetLayerVersionByArnCommand, GetPolicyCommand as GetPolicyCommand$1, LambdaClient, ListFunctionsCommand, ListLayersCommand, ListTagsCommand, PublishLayerVersionCommand, PutFunctionRecursionConfigCommand, RemovePermissionCommand, ResourceNotFoundException, TagResourceCommand as TagResourceCommand$1, UntagResourceCommand as UntagResourceCommand$1, UpdateEventSourceMappingCommand, UpdateFunctionCodeCommand, UpdateFunctionConfigurationCommand, UpdateFunctionUrlConfigCommand, waitUntilFunctionUpdatedV2 } from "@aws-sdk/client-lambda";
10
+ import { AddPermissionCommand, CreateEventSourceMappingCommand, CreateFunctionCommand, CreateFunctionUrlConfigCommand, DeleteEventSourceMappingCommand, DeleteFunctionCommand, DeleteFunctionConcurrencyCommand, DeleteFunctionUrlConfigCommand, DeleteLayerVersionCommand, GetEventSourceMappingCommand, GetFunctionCommand, GetFunctionConcurrencyCommand, GetFunctionRecursionConfigCommand, GetFunctionUrlConfigCommand, GetLayerVersionByArnCommand, GetPolicyCommand as GetPolicyCommand$1, LambdaClient, ListFunctionsCommand, ListLayersCommand, ListTagsCommand, PublishLayerVersionCommand, PutFunctionConcurrencyCommand, PutFunctionRecursionConfigCommand, RemovePermissionCommand, ResourceNotFoundException, TagResourceCommand as TagResourceCommand$1, UntagResourceCommand as UntagResourceCommand$1, UpdateEventSourceMappingCommand, UpdateFunctionCodeCommand, UpdateFunctionConfigurationCommand, UpdateFunctionUrlConfigCommand, waitUntilFunctionUpdatedV2 } from "@aws-sdk/client-lambda";
12
11
  import { AssumeRoleCommand, GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
13
12
  import { AssociateRouteTableCommand, AttachInternetGatewayCommand, AuthorizeSecurityGroupEgressCommand, AuthorizeSecurityGroupIngressCommand, CreateInternetGatewayCommand, CreateNatGatewayCommand, CreateNetworkAclCommand, CreateNetworkAclEntryCommand, CreateRouteCommand, CreateRouteTableCommand, CreateSecurityGroupCommand, CreateSubnetCommand, CreateTagsCommand, CreateVpcCommand, DeleteInternetGatewayCommand, DeleteNatGatewayCommand, DeleteNetworkAclCommand, DeleteNetworkAclEntryCommand, DeleteNetworkInterfaceCommand, DeleteRouteCommand, DeleteRouteTableCommand, DeleteSecurityGroupCommand, DeleteSubnetCommand, DeleteTagsCommand, DeleteVpcCommand, DescribeAvailabilityZonesCommand, DescribeInstanceAttributeCommand, DescribeInstancesCommand, DescribeInternetGatewaysCommand, DescribeNatGatewaysCommand, DescribeNetworkAclsCommand, DescribeNetworkInterfacesCommand, DescribeRouteTablesCommand, DescribeSecurityGroupsCommand, DescribeSubnetsCommand, DescribeVolumesCommand, DescribeVpcAttributeCommand, DescribeVpcsCommand, DetachInternetGatewayCommand, DisassociateRouteTableCommand, EC2Client, ModifyInstanceAttributeCommand, ModifySubnetAttributeCommand, ModifyVpcAttributeCommand, ReplaceNetworkAclAssociationCommand, RevokeSecurityGroupEgressCommand, RevokeSecurityGroupIngressCommand, RunInstancesCommand, TerminateInstancesCommand, waitUntilInstanceRunning, waitUntilInstanceTerminated, waitUntilNatGatewayAvailable, waitUntilNatGatewayDeleted } from "@aws-sdk/client-ec2";
14
13
  import { CreateTableCommand, DeleteTableCommand, DescribeContinuousBackupsCommand, DescribeContributorInsightsCommand, DescribeKinesisStreamingDestinationCommand, DescribeTableCommand, DescribeTimeToLiveCommand, DynamoDBClient, ListTablesCommand, ListTagsOfResourceCommand, ResourceNotFoundException as ResourceNotFoundException$1, TagResourceCommand as TagResourceCommand$2, UntagResourceCommand as UntagResourceCommand$2, UpdateContinuousBackupsCommand, UpdateTableCommand, UpdateTimeToLiveCommand } from "@aws-sdk/client-dynamodb";
@@ -63,7 +62,7 @@ import { CreateNamespaceCommand, CreateTableBucketCommand, CreateTableCommand as
63
62
  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
63
  import { Document, Pair, Scalar, YAMLMap, YAMLSeq, parse as parse$1, stringify } from "yaml";
65
64
  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, 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";
65
+ import { A2A_CONTAINER_PORT, A2A_PATH, AGENTCORE_A2A_PROTOCOL, AGENTCORE_AGUI_PROTOCOL, AGENTCORE_MCP_PROTOCOL, ConnectionRegistry, EcsTaskResolutionError, HOST_GATEWAY_MIN_VERSION, LocalInvokeBuildError, MCP_CONTAINER_PORT, MCP_PATH, a2aInvokeOnce, addCommonEcsServiceOptions, architectureToPlatform, attachAuthorizers, attachStageContext, availableApiIdentifiers, bufferToBody, buildAgentCoreCodeImage, buildCognitoJwksUrl, buildConnectEvent, buildContainerImage, buildCorsConfigByApiId, buildCorsConfigFromCloudFrontChain, buildDisconnectEvent, buildJwksUrlFromIssuer, buildMessageEvent, buildMgmtEndpointEnvUrl, buildStageMap, createAuthorizerCache, createFileWatcher, createJwksCache, createWatchPredicates, defaultCredentialsLoader, derivePseudoParametersFromRegion, discoverRoutes, discoverWebSocketApis, downloadAndExtractS3Bundle, filterRoutesByApiIdentifier, 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
66
  import { createServer } from "node:net";
68
67
  import { promisify } from "node:util";
69
68
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
@@ -7054,7 +7053,8 @@ var LambdaFunctionProvider = class {
7054
7053
  "ImageConfig",
7055
7054
  "SnapStart",
7056
7055
  "LoggingConfig",
7057
- "RecursiveLoop"
7056
+ "RecursiveLoop",
7057
+ "ReservedConcurrentExecutions"
7058
7058
  ])]]);
7059
7059
  eniWaitTimeoutMs = 600 * 1e3;
7060
7060
  eniWaitInitialDelayMs = 1e4;
@@ -7125,6 +7125,21 @@ var LambdaFunctionProvider = class {
7125
7125
  }
7126
7126
  throw new ProvisioningError(`Failed to set RecursiveLoop on Lambda function ${logicalId} (function was deleted to maintain atomicity): ${rlError instanceof Error ? rlError.message : String(rlError)}`, resourceType, logicalId, functionName, rlError instanceof Error ? rlError : void 0);
7127
7127
  }
7128
+ const reservedConcurrentExecutions = properties["ReservedConcurrentExecutions"];
7129
+ if (reservedConcurrentExecutions !== void 0) try {
7130
+ await this.lambdaClient.send(new PutFunctionConcurrencyCommand({
7131
+ FunctionName: functionName,
7132
+ ReservedConcurrentExecutions: reservedConcurrentExecutions
7133
+ }));
7134
+ } catch (pcError) {
7135
+ this.logger.warn(`PutFunctionConcurrency failed for ${logicalId}: ${pcError instanceof Error ? pcError.message : String(pcError)} — deleting partially-created function to maintain atomicity`);
7136
+ try {
7137
+ await this.lambdaClient.send(new DeleteFunctionCommand({ FunctionName: functionName }));
7138
+ } catch (deleteError) {
7139
+ this.logger.error(`Cleanup DeleteFunction failed for ${logicalId} after PutFunctionConcurrency failure — function may be orphaned: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`);
7140
+ }
7141
+ throw new ProvisioningError(`Failed to set ReservedConcurrentExecutions on Lambda function ${logicalId} (function was deleted to maintain atomicity): ${pcError instanceof Error ? pcError.message : String(pcError)}`, resourceType, logicalId, functionName, pcError instanceof Error ? pcError : void 0);
7142
+ }
7128
7143
  this.logger.debug(`Successfully created Lambda function ${logicalId}: ${functionName}`);
7129
7144
  return {
7130
7145
  physicalId: response.FunctionName || functionName,
@@ -7218,6 +7233,17 @@ var LambdaFunctionProvider = class {
7218
7233
  }));
7219
7234
  this.logger.debug(`Updated RecursiveLoop for Lambda function ${physicalId} to '${newRecursiveLoop}'`);
7220
7235
  }
7236
+ const newReservedConcurrentExecutions = properties["ReservedConcurrentExecutions"];
7237
+ if (newReservedConcurrentExecutions !== previousProperties["ReservedConcurrentExecutions"]) if (newReservedConcurrentExecutions === void 0) {
7238
+ await this.lambdaClient.send(new DeleteFunctionConcurrencyCommand({ FunctionName: physicalId }));
7239
+ this.logger.debug(`Cleared ReservedConcurrentExecutions for Lambda function ${physicalId} (template removed the property)`);
7240
+ } else {
7241
+ await this.lambdaClient.send(new PutFunctionConcurrencyCommand({
7242
+ FunctionName: physicalId,
7243
+ ReservedConcurrentExecutions: newReservedConcurrentExecutions
7244
+ }));
7245
+ this.logger.debug(`Updated ReservedConcurrentExecutions for Lambda function ${physicalId} to ${newReservedConcurrentExecutions}`);
7246
+ }
7221
7247
  const getResponse = await this.lambdaClient.send(new GetFunctionCommand({ FunctionName: physicalId }));
7222
7248
  const functionArn = getResponse.Configuration?.FunctionArn;
7223
7249
  await this.applyTagDiff(functionArn, previousProperties["Tags"], properties["Tags"]);
@@ -7743,6 +7769,12 @@ var LambdaFunctionProvider = class {
7743
7769
  } catch (rlErr) {
7744
7770
  if (!(rlErr instanceof ResourceNotFoundException)) this.logger.debug(`GetFunctionRecursionConfig failed for ${physicalId}: ${rlErr instanceof Error ? rlErr.message : String(rlErr)}`);
7745
7771
  }
7772
+ try {
7773
+ const pcResp = await this.lambdaClient.send(new GetFunctionConcurrencyCommand({ FunctionName: physicalId }));
7774
+ if (pcResp.ReservedConcurrentExecutions !== void 0) result["ReservedConcurrentExecutions"] = pcResp.ReservedConcurrentExecutions;
7775
+ } catch (pcErr) {
7776
+ if (!(pcErr instanceof ResourceNotFoundException)) this.logger.debug(`GetFunctionConcurrency failed for ${physicalId}: ${pcErr instanceof Error ? pcErr.message : String(pcErr)}`);
7777
+ }
7746
7778
  return result;
7747
7779
  } catch (err) {
7748
7780
  if (err instanceof ResourceNotFoundException) return void 0;
@@ -43136,14 +43168,14 @@ function parseTarget(target) {
43136
43168
  function resolveLambdaTarget(target, stacks) {
43137
43169
  if (stacks.length === 0) throw new LocalInvokeResolutionError("No stacks found in the synthesized assembly.");
43138
43170
  const parsed = parseTarget(target);
43139
- const stack = pickStack$3(parsed, stacks);
43171
+ const stack = pickStack$2(parsed, stacks);
43140
43172
  const template = stack.template;
43141
43173
  const resources = template.Resources ?? {};
43142
43174
  let match;
43143
43175
  if (parsed.isPath) {
43144
43176
  const index = buildCdkPathIndex(template);
43145
43177
  const lambdaMatches = resolveCdkPathToLogicalIds(parsed.pathOrId, index).filter(({ logicalId }) => resources[logicalId]?.Type === "AWS::Lambda::Function");
43146
- if (lambdaMatches.length === 0) throw notFoundError$2(target, stack, resources);
43178
+ if (lambdaMatches.length === 0) throw notFoundError$1(target, stack, resources);
43147
43179
  if (lambdaMatches.length > 1) throw new LocalInvokeResolutionError(`Target '${target}' matches ${lambdaMatches.length} Lambda functions in ${stack.stackName}: ` + lambdaMatches.map((m) => m.logicalId).join(", ") + ". Refine the path or use the stack:LogicalId form.");
43148
43180
  const m = lambdaMatches[0];
43149
43181
  match = {
@@ -43152,7 +43184,7 @@ function resolveLambdaTarget(target, stacks) {
43152
43184
  };
43153
43185
  } else {
43154
43186
  const resource = resources[parsed.pathOrId];
43155
- if (!resource) throw notFoundError$2(target, stack, resources);
43187
+ if (!resource) throw notFoundError$1(target, stack, resources);
43156
43188
  match = {
43157
43189
  logicalId: parsed.pathOrId,
43158
43190
  resource
@@ -43170,7 +43202,7 @@ function resolveLambdaTarget(target, stacks) {
43170
43202
  * user may omit the stack prefix. Otherwise an explicit stack pattern is
43171
43203
  * required.
43172
43204
  */
43173
- function pickStack$3(parsed, stacks) {
43205
+ function pickStack$2(parsed, stacks) {
43174
43206
  if (parsed.stackPattern === null) {
43175
43207
  if (stacks.length === 1) return stacks[0];
43176
43208
  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(", ")}.`);
@@ -43508,7 +43540,7 @@ function describeLayerEntry(entry) {
43508
43540
  * the resolved stack so the user can copy/paste a valid target. Mirrors
43509
43541
  * the format the issue spec calls out.
43510
43542
  */
43511
- function notFoundError$2(target, stack, resources) {
43543
+ function notFoundError$1(target, stack, resources) {
43512
43544
  const lambdas = [];
43513
43545
  for (const [logicalId, resource] of Object.entries(resources)) {
43514
43546
  if (resource.Type !== "AWS::Lambda::Function") continue;
@@ -43793,28 +43825,28 @@ function parseEcsTarget(target) {
43793
43825
  function resolveEcsTaskTarget(target, stacks, context) {
43794
43826
  if (stacks.length === 0) throw new EcsTaskResolutionError("No stacks found in the synthesized assembly.");
43795
43827
  const parsed = parseEcsTarget(target);
43796
- const stack = pickStack$2(parsed, stacks);
43828
+ const stack = pickStack$1(parsed, stacks);
43797
43829
  const resources = stack.template.Resources ?? {};
43798
43830
  let logicalId;
43799
43831
  let resource;
43800
43832
  if (parsed.isPath) {
43801
43833
  const index = buildCdkPathIndex(stack.template);
43802
43834
  const taskDefs = resolveCdkPathToLogicalIds(parsed.pathOrId, index).filter(({ logicalId: l }) => resources[l]?.Type === "AWS::ECS::TaskDefinition");
43803
- if (taskDefs.length === 0) throw notFoundError$1(target, stack, resources);
43835
+ if (taskDefs.length === 0) throw notFoundError(target, stack, resources);
43804
43836
  if (taskDefs.length > 1) throw new EcsTaskResolutionError(`Target '${target}' matches ${taskDefs.length} task definitions in ${stack.stackName}: ` + taskDefs.map((t) => t.logicalId).join(", ") + ". Refine the path or use the stack:LogicalId form.");
43805
43837
  logicalId = taskDefs[0].logicalId;
43806
43838
  resource = resources[logicalId];
43807
43839
  } else {
43808
43840
  resource = resources[parsed.pathOrId];
43809
- if (!resource) throw notFoundError$1(target, stack, resources);
43841
+ if (!resource) throw notFoundError(target, stack, resources);
43810
43842
  logicalId = parsed.pathOrId;
43811
43843
  }
43812
- if (!logicalId || !resource) throw notFoundError$1(target, stack, resources);
43844
+ if (!logicalId || !resource) throw notFoundError(target, stack, resources);
43813
43845
  if (resource.Type === "AWS::Lambda::Function") throw new EcsTaskResolutionError(`Resource '${logicalId}' in ${stack.stackName} is a Lambda function, not an ECS task definition. Use \`cdkd local invoke\` for Lambda; \`cdkd local run-task\` is ECS only.`);
43814
43846
  if (resource.Type !== "AWS::ECS::TaskDefinition") throw new EcsTaskResolutionError(`Resource '${logicalId}' in ${stack.stackName} is ${resource.Type}, not an AWS::ECS::TaskDefinition.`);
43815
43847
  return extractTaskDefinitionProperties(stack, logicalId, resource, context);
43816
43848
  }
43817
- function pickStack$2(parsed, stacks) {
43849
+ function pickStack$1(parsed, stacks) {
43818
43850
  if (parsed.stackPattern === null) {
43819
43851
  if (stacks.length === 1) return stacks[0];
43820
43852
  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(", ")}.`);
@@ -44340,7 +44372,7 @@ function pickStringArray(value) {
44340
44372
  for (const v of value) if (typeof v === "string") out.push(v);
44341
44373
  return out;
44342
44374
  }
44343
- function notFoundError$1(target, stack, resources) {
44375
+ function notFoundError(target, stack, resources) {
44344
44376
  const tasks = [];
44345
44377
  for (const [logicalId, resource] of Object.entries(resources)) {
44346
44378
  if (resource.Type !== "AWS::ECS::TaskDefinition") continue;
@@ -46233,7 +46265,7 @@ async function localStartApiCommand(target, options) {
46233
46265
  await ensureDockerAvailable();
46234
46266
  const appCmd = resolveApp(options.app);
46235
46267
  if (!appCmd) throw new Error("No CDK app specified. Pass --app, set CDKD_APP, or add \"app\" to cdk.json.");
46236
- const overrides = readEnvOverridesFile$4(options.envVars);
46268
+ const overrides = readEnvOverridesFile$3(options.envVars);
46237
46269
  const debugPortBase = options.debugPortBase ? parseDebugPort(options.debugPortBase) : void 0;
46238
46270
  const perLambdaConcurrency = parsePerLambdaConcurrency(options.perLambdaConcurrency);
46239
46271
  const inlineTmpDirs = /* @__PURE__ */ new Set();
@@ -47199,7 +47231,7 @@ function getTemplateEnv$1(resource) {
47199
47231
  return vars;
47200
47232
  }
47201
47233
  /** Read the SAM-shape `--env-vars` JSON file. */
47202
- function readEnvOverridesFile$4(filePath) {
47234
+ function readEnvOverridesFile$3(filePath) {
47203
47235
  if (!filePath) return void 0;
47204
47236
  let raw;
47205
47237
  try {
@@ -47637,7 +47669,9 @@ const execFileAsync$2 = promisify(execFile);
47637
47669
  * metadata AND `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=/role/<role-arn>`
47638
47670
  * for IAM task-role credentials. cdkd does NOT re-implement the sidecar
47639
47671
  * — pulling the AWS-published image keeps cdkd in lock-step with whatever
47640
- * ECS-Agent fidelity AWS chooses to provide.
47672
+ * ECS-Agent fidelity AWS chooses to provide. The `cdkd local start-service`
47673
+ * / `start-alb` shared-network shape lives in cdk-local's bundled ECS
47674
+ * service emulator engine (see `src/cli/commands/ecs-service-emulator.ts`).
47641
47675
  */
47642
47676
  /** AWS-published sidecar image (latest tag). amd64 is the only image AWS ships. */
47643
47677
  const METADATA_ENDPOINT_IMAGE = "amazon/amazon-ecs-local-container-endpoints:latest-amd64";
@@ -47645,29 +47679,18 @@ const METADATA_ENDPOINT_IMAGE = "amazon/amazon-ecs-local-container-endpoints:lat
47645
47679
  * Default well-known IP for the ECS local-container-endpoints sidecar —
47646
47680
  * matches the documented AWS task-metadata endpoint address. Containers
47647
47681
  * inject `ECS_CONTAINER_METADATA_URI_V4=http://169.254.170.2/v4/<id>`
47648
- * to reach it. `cdkd local run-task` keeps this verbatim; `cdkd local
47649
- * start-service` creates ONE shared network at CLI startup (design
47650
- * § 5 Option A) — the shared sidecar lives at `169.254.171.2` (see
47651
- * `SHARED_SVC_SUBNET_OCTET` below), one octet up so the two CLI
47652
- * variants can run on the same host without bridge-pool collision.
47682
+ * to reach it.
47653
47683
  */
47654
47684
  const METADATA_ENDPOINT_IP = "169.254.170.2";
47655
47685
  /** Default subnet — used when no `subnetOctet` override is supplied. */
47656
47686
  const DEFAULT_METADATA_ENDPOINT_SUBNET = "169.254.170.0/24";
47657
47687
  /**
47658
47688
  * Pure-functional subnet allocator. `cdkd local run-task` uses the
47659
- * default subnet; `cdkd local start-service` walks `subnetOctet=170,
47660
- * 171, 172, ...` (one per replica) to keep parallel docker networks
47661
- * from clashing. The link-local 169.254.0.0/16 space is reserved AWS-
47662
- * wide for cloud metadata so collisions with user workloads are
47663
- * unlikely, but each replica still gets its own /24 to ensure
47664
- * docker's `--subnet` allocator does not reject "Pool overlaps".
47665
- *
47666
- * `subnetOctet` is the second-from-last byte of the network: 170 →
47667
- * 169.254.170.0/24 (default), 171 → 169.254.171.0/24, etc. Valid
47668
- * range is 1..254; the runner clamps to `(170 + replicaIndex) % 84`
47669
- * + 170 in practice (rolling window) — exported here so the runner
47670
- * keeps the allocation logic in one place.
47689
+ * default subnet (`subnetOctet=170`). The link-local 169.254.0.0/16
47690
+ * space is reserved AWS-wide for cloud metadata so collisions with
47691
+ * user workloads are unlikely. `subnetOctet` is the second-from-last
47692
+ * byte of the network: 170 169.254.170.0/24 (default). Valid range
47693
+ * is 1..254.
47671
47694
  */
47672
47695
  function buildEndpointSubnet(subnetOctet) {
47673
47696
  if (subnetOctet < 1 || subnetOctet > 254 || !Number.isInteger(subnetOctet)) throw new Error(`buildEndpointSubnet: subnetOctet must be an integer in 1..254 (got ${subnetOctet}).`);
@@ -47677,50 +47700,12 @@ function buildEndpointSubnet(subnetOctet) {
47677
47700
  };
47678
47701
  }
47679
47702
  /**
47680
- * Subnet octet for the shared-service docker network used by
47681
- * `cdkd local start-service`. One octet up from `cdkd local run-task`'s
47682
- * default (170 171) so the two CLI variants can run on the same host
47683
- * without docker rejecting the second `--subnet`. The shared-service
47684
- * network reuses the same `createTaskNetwork` machinery; the sidecar at
47685
- * `169.254.171.2` serves the same metadata-endpoint API to every
47686
- * container that joins this one network.
47687
- */
47688
- const SHARED_SVC_SUBNET_OCTET = 171;
47689
- /**
47690
- * Create the one shared docker network + metadata-endpoints sidecar
47691
- * used by every service-replica boot in a single
47692
- * `cdkd local start-service` invocation. This is design doc § 5
47693
- * Option A — one network per CLI invocation instead of one network
47694
- * per task — so peer services can reach each other by IP / network
47695
- * alias without docker `--network connect` choreography (Option B,
47696
- * rejected in design § 5 as "unwieldy and racy"). The returned
47697
- * `TaskNetwork` carries `ownedByCaller: true` so `cleanupEcsRun()`
47698
- * (called per replica by the service runner) does NOT teardown — the
47699
- * CLI tears down ONCE at the end of the run.
47700
- */
47701
- async function createSharedSvcNetwork(options = {}) {
47702
- const networkName = `${options.prefix ?? "cdkd-local"}-svc-${randomBytes(4).toString("hex")}`;
47703
- const { cidr, sidecarIp } = buildEndpointSubnet(171);
47704
- return {
47705
- networkName,
47706
- sidecarContainerId: await createNetworkAndSidecar({
47707
- networkName,
47708
- cidr,
47709
- sidecarIp,
47710
- skipPull: options.skipPull ?? false,
47711
- ...options.credentials !== void 0 ? { credentials: options.credentials } : {},
47712
- ...options.cluster !== void 0 ? { cluster: options.cluster } : {}
47713
- }),
47714
- sidecarIp,
47715
- ownedByCaller: true
47716
- };
47717
- }
47718
- /**
47719
- * Internal helper shared by `createTaskNetwork` (per-task) and
47720
- * `createSharedSvcNetwork` (per-CLI-run). Creates the docker network,
47721
- * pulls the sidecar image, and starts the sidecar at the documented
47722
- * IP. Throws `DockerRunnerError` with a hint when the network already
47723
- * exists (the typical "leftover from previous run" path).
47703
+ * Internal helper that creates the docker network, pulls the sidecar image,
47704
+ * and starts the sidecar at the documented IP. Throws `DockerRunnerError`
47705
+ * with a hint when the network already exists (the typical "leftover from
47706
+ * previous run" path). Used by `createTaskNetwork` (per-task) only;
47707
+ * `cdkd local start-service` / `start-alb` share-network creation is owned
47708
+ * by cdk-local's bundled ECS service emulator engine.
47724
47709
  */
47725
47710
  async function createNetworkAndSidecar(args) {
47726
47711
  const logger = getLogger().child("ecs-network");
@@ -48226,7 +48211,7 @@ async function waitForContainerHealthy(containerId, displayName) {
48226
48211
  if (err instanceof EcsTaskRunnerError) throw err;
48227
48212
  logger.debug(`docker inspect on '${displayName}' failed: ${err instanceof Error ? err.message : String(err)}`);
48228
48213
  }
48229
- await sleep$1(1e3);
48214
+ await sleep(1e3);
48230
48215
  }
48231
48216
  throw new EcsTaskRunnerError(`Container '${displayName}' did not become healthy within 5 minutes.`);
48232
48217
  }
@@ -48250,7 +48235,7 @@ async function stopContainer(containerId, graceSeconds) {
48250
48235
  ]);
48251
48236
  } catch {}
48252
48237
  }
48253
- function sleep$1(ms) {
48238
+ function sleep(ms) {
48254
48239
  return new Promise((res) => setTimeout(res, ms));
48255
48240
  }
48256
48241
  /**
@@ -48554,9 +48539,9 @@ async function localRunTaskCommand(target, options) {
48554
48539
  ...options.profile && { macroExpandS3ClientOpts: { profile: options.profile } }
48555
48540
  };
48556
48541
  const { stacks } = await synthesizer.synthesize(synthOpts);
48557
- const candidate = pickCandidateStack$1(parseEcsTarget(target).stackPattern, stacks);
48542
+ const candidate = pickCandidateStack(parseEcsTarget(target).stackPattern, stacks);
48558
48543
  stateProvider = createLocalStateProvider$1(options, candidate?.stackName ?? "", candidate?.region);
48559
- const imageContext = await buildEcsImageResolutionContext$2(candidate, stateProvider, options);
48544
+ const imageContext = await buildEcsImageResolutionContext$1(candidate, stateProvider, options);
48560
48545
  const task = resolveEcsTaskTarget(target, stacks, imageContext);
48561
48546
  logger.info(`Target: ${task.stack.stackName}/${task.taskDefinitionLogicalId} (family=${task.family}, containers=${task.containers.length})`);
48562
48547
  const taskNeeds = detectEcsImageResolutionNeeds(stacks.find((s) => s.stackName === task.stack.stackName) ?? task.stack);
@@ -48584,15 +48569,15 @@ async function localRunTaskCommand(target, options) {
48584
48569
  let resolvedRoleArn;
48585
48570
  if (options.assumeTaskRole === true) {
48586
48571
  if (!task.taskRoleArn) throw new Error("--assume-task-role passed without an ARN but the task definition has no resolvable TaskRoleArn. Either the task definition does not set TaskRoleArn, or it points at a resource cdkd cannot resolve to an IAM Role at synth time. Pass the ARN explicitly: --assume-task-role <arn>");
48587
- resolvedRoleArn = await resolvePlaceholderAccount$1(task.taskRoleArn, options.region);
48588
- assumedCredentials = await assumeTaskRole$1(resolvedRoleArn, options.region);
48572
+ resolvedRoleArn = await resolvePlaceholderAccount(task.taskRoleArn, options.region);
48573
+ assumedCredentials = await assumeTaskRole(resolvedRoleArn, options.region);
48589
48574
  } else if (typeof options.assumeTaskRole === "string") {
48590
48575
  resolvedRoleArn = options.assumeTaskRole;
48591
- assumedCredentials = await assumeTaskRole$1(resolvedRoleArn, options.region);
48576
+ assumedCredentials = await assumeTaskRole(resolvedRoleArn, options.region);
48592
48577
  }
48593
48578
  const sidecarCredentials = await resolveSidecarCredentials(options, assumedCredentials);
48594
48579
  if (options.profile && sidecarCredentials && !assumedCredentials) profileCredsFile = await writeProfileCredentialsFile(options.profile, sidecarCredentials);
48595
- const envOverrides = readEnvOverridesFile$3(options.envVars);
48580
+ const envOverrides = readEnvOverridesFile$2(options.envVars);
48596
48581
  const runOpts = {
48597
48582
  cluster: options.cluster,
48598
48583
  containerHost: options.containerHost,
@@ -48633,7 +48618,7 @@ async function localRunTaskCommand(target, options) {
48633
48618
  * Lazy: callers should only invoke this when the resolved ARN is actually
48634
48619
  * going to be used (i.e. on the bare `--assume-task-role` path).
48635
48620
  */
48636
- async function resolvePlaceholderAccount$1(arn, region) {
48621
+ async function resolvePlaceholderAccount(arn, region) {
48637
48622
  if (!arn.includes("${AWS::AccountId}")) return arn;
48638
48623
  const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
48639
48624
  const sts = new STSClient({ ...region && { region } });
@@ -48649,7 +48634,7 @@ async function resolvePlaceholderAccount$1(arn, region) {
48649
48634
  * Assume `roleArn` and return temp credentials. Mirrors the same flow
48650
48635
  * `cdkd local invoke --assume-role` uses.
48651
48636
  */
48652
- async function assumeTaskRole$1(roleArn, region) {
48637
+ async function assumeTaskRole(roleArn, region) {
48653
48638
  const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
48654
48639
  const sts = new STSClient({ ...region && { region } });
48655
48640
  try {
@@ -48680,7 +48665,7 @@ async function assumeTaskRole$1(roleArn, region) {
48680
48665
  * `--from-state` and `--from-cfn-stack` produce the same downstream
48681
48666
  * context shape (issue #606).
48682
48667
  */
48683
- async function buildEcsImageResolutionContext$2(candidate, stateProvider, options) {
48668
+ async function buildEcsImageResolutionContext$1(candidate, stateProvider, options) {
48684
48669
  const logger = getLogger();
48685
48670
  if (!candidate) return void 0;
48686
48671
  const needs = detectEcsImageResolutionNeeds(candidate);
@@ -48692,7 +48677,7 @@ async function buildEcsImageResolutionContext$2(candidate, stateProvider, option
48692
48677
  if (!region) logger.warn("Resolver references ${AWS::Region} but cdkd could not determine the target region. Pass --region, set AWS_REGION, or declare env.region on the CDK stack.");
48693
48678
  let accountId;
48694
48679
  try {
48695
- accountId = await resolveCallerAccountId$2(region);
48680
+ accountId = await resolveCallerAccountId$1(region);
48696
48681
  } catch (err) {
48697
48682
  logger.warn(`Resolver needs \${AWS::AccountId} but STS GetCallerIdentity failed: ${err instanceof Error ? err.message : String(err)}. Substitution will be skipped; affected env / secret entries will be dropped with per-key warnings.`);
48698
48683
  }
@@ -48714,7 +48699,7 @@ async function buildEcsImageResolutionContext$2(candidate, stateProvider, option
48714
48699
  else if (!stateProvider && needs.needsEnvOrSecretSubstitution) logger.warn("Container Environment / Secrets entries contain CloudFormation intrinsics (Ref / Fn::GetAtt / Fn::Sub / Fn::Join). Pass --from-state (cdkd-deployed) or --from-cfn-stack (cdk-deployed) to substitute them against deployed state. Without a state source these entries are dropped (per-key warnings will follow).");
48715
48700
  return ctx;
48716
48701
  }
48717
- function pickCandidateStack$1(stackPattern, stacks) {
48702
+ function pickCandidateStack(stackPattern, stacks) {
48718
48703
  if (stackPattern === null) {
48719
48704
  if (stacks.length === 1) return stacks[0];
48720
48705
  return;
@@ -48722,7 +48707,7 @@ function pickCandidateStack$1(stackPattern, stacks) {
48722
48707
  const matched = matchStacks(stacks, [stackPattern]);
48723
48708
  if (matched.length === 1) return matched[0];
48724
48709
  }
48725
- async function resolveCallerAccountId$2(region) {
48710
+ async function resolveCallerAccountId$1(region) {
48726
48711
  const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
48727
48712
  const sts = new STSClient({ ...region && { region } });
48728
48713
  try {
@@ -48736,7 +48721,7 @@ async function resolveCallerAccountId$2(region) {
48736
48721
  * `cdkd local invoke --env-vars`: top-level keys are container names, with
48737
48722
  * `Parameters` reserved for global entries.
48738
48723
  */
48739
- function readEnvOverridesFile$3(filePath) {
48724
+ function readEnvOverridesFile$2(filePath) {
48740
48725
  if (!filePath) return void 0;
48741
48726
  let raw;
48742
48727
  try {
@@ -48788,1072 +48773,42 @@ function createLocalRunTaskCommand() {
48788
48773
  return cmd;
48789
48774
  }
48790
48775
 
48791
- //#endregion
48792
- //#region src/local/ecs-service-resolver.ts
48793
- /**
48794
- * Walk the synth template to locate an `AWS::ECS::Service` by display
48795
- * path or stack-qualified logical id, resolve its `TaskDefinition`
48796
- * reference, and chain into the existing `resolveEcsTaskTarget` machinery
48797
- * to produce a `ResolvedEcsService` carrying both the service knobs and
48798
- * the underlying task descriptor.
48799
- *
48800
- * Target shape mirrors `cdkd local run-task`: `<Stack>/<DisplayPath>` or
48801
- * `<Stack>:<LogicalId>`; single-stack apps may omit the stack prefix.
48802
- *
48803
- * Optional `context` (same as the task resolver) carries the ECR image
48804
- * substitution data — pseudo parameters (Tier 1) + state-recorded
48805
- * resources (Tier 2). The CLI builds it lazily when the candidate
48806
- * service's task definition actually needs substitution.
48807
- */
48808
- function resolveEcsServiceTarget(target, stacks, context) {
48809
- if (stacks.length === 0) throw new EcsTaskResolutionError("No stacks found in the synthesized assembly.");
48810
- const parsed = parseEcsTarget(target);
48811
- const stack = pickStack$1(parsed, stacks);
48812
- const resources = stack.template.Resources ?? {};
48813
- let serviceLogicalId;
48814
- let serviceResource;
48815
- if (parsed.isPath) {
48816
- const index = buildCdkPathIndex(stack.template);
48817
- const services = resolveCdkPathToLogicalIds(parsed.pathOrId, index).filter(({ logicalId: l }) => resources[l]?.Type === "AWS::ECS::Service");
48818
- if (services.length === 0) throw notFoundError(target, stack, resources);
48819
- if (services.length > 1) throw new EcsTaskResolutionError(`Target '${target}' matches ${services.length} ECS services in ${stack.stackName}: ` + services.map((s) => s.logicalId).join(", ") + ". Refine the path or use the stack:LogicalId form.");
48820
- serviceLogicalId = services[0].logicalId;
48821
- serviceResource = resources[serviceLogicalId];
48822
- } else {
48823
- serviceResource = resources[parsed.pathOrId];
48824
- if (!serviceResource) throw notFoundError(target, stack, resources);
48825
- serviceLogicalId = parsed.pathOrId;
48826
- }
48827
- if (!serviceLogicalId || !serviceResource) throw notFoundError(target, stack, resources);
48828
- if (serviceResource.Type === "AWS::ECS::TaskDefinition") throw new EcsTaskResolutionError(`Resource '${serviceLogicalId}' in ${stack.stackName} is an ECS TaskDefinition, not a Service. Use \`cdkd local run-task\` for one-shot tasks; \`cdkd local start-service\` is Service-only.`);
48829
- if (serviceResource.Type !== "AWS::ECS::Service") throw new EcsTaskResolutionError(`Resource '${serviceLogicalId}' in ${stack.stackName} is ${serviceResource.Type}, not an AWS::ECS::Service.`);
48830
- return extractServiceProperties(stack, serviceLogicalId, serviceResource, stacks, context);
48831
- }
48832
- /**
48833
- * Pure-functional extraction from the synth resource. Exposed for unit
48834
- * testing the per-field resolution rules (DesiredCount default, missing
48835
- * TaskDefinition, intrinsic shapes).
48836
- */
48837
- function extractServiceProperties(stack, serviceLogicalId, resource, stacks, context) {
48838
- const props = resource.Properties ?? {};
48839
- const warnings = [];
48840
- const taskDefRef = props["TaskDefinition"];
48841
- if (taskDefRef === void 0 || taskDefRef === null) throw new EcsTaskResolutionError(`ECS Service '${serviceLogicalId}' in ${stack.stackName} has no TaskDefinition property.`);
48842
- const taskDefLogicalId = resolveTaskDefinitionReference(taskDefRef, stack, serviceLogicalId);
48843
- const task = resolveEcsTaskTarget(`${stack.stackName}:${taskDefLogicalId}`, stacks, context);
48844
- const desiredCount = parseDesiredCount(props["DesiredCount"], serviceLogicalId);
48845
- const healthCheckGracePeriodSeconds = parseHealthCheckGrace(props["HealthCheckGracePeriodSeconds"], serviceLogicalId);
48846
- const serviceName = parseServiceName(props["ServiceName"], serviceLogicalId);
48847
- if (Array.isArray(props["LoadBalancers"]) && props["LoadBalancers"].length > 0) warnings.push(`ECS Service '${serviceLogicalId}' declares LoadBalancers, but local load-balancer emulation is deferred to a follow-up PR. Containers are NOT registered to a local listener; reach them via their published ports.`);
48848
- const serviceConnect = extractServiceConnect(props["ServiceConnectConfiguration"], task);
48849
- const out = {
48850
- stack,
48851
- serviceLogicalId,
48852
- resource,
48853
- serviceName,
48854
- desiredCount,
48855
- healthCheckGracePeriodSeconds,
48856
- task,
48857
- serviceRegistries: extractServiceRegistries(props["ServiceRegistries"], serviceLogicalId, warnings),
48858
- warnings
48859
- };
48860
- if (serviceConnect) out.serviceConnect = serviceConnect;
48861
- return out;
48862
- }
48863
- /**
48864
- * Parse `ServiceConnectConfiguration` against the producer TaskDef.
48865
- * Returns `undefined` when the block is missing OR `Enabled: false`.
48866
- *
48867
- * Reject conditions (surface as resolver-time errors so the user sees
48868
- * them BEFORE the docker network is created):
48869
- * - `Namespace` is not a literal string. CDK 2.x always emits a
48870
- * literal string here (verified 2026-05-22); cross-stack /
48871
- * intrinsic shapes are out of scope.
48872
- * - `Services[].PortName` doesn't match any of the TaskDef's
48873
- * `ContainerDefinitions[].PortMappings[].Name` entries.
48874
- *
48875
- * Note on `clientAliases[]` shape: each ClientAlias can declare a
48876
- * `DnsName` (the bare short-name peers connect to, e.g. `orders`) AND
48877
- * a `Port` (the listening port the alias maps to inside the consumer).
48878
- * cdkd surfaces both verbatim; the registry / `--add-host` overlay
48879
- * publishes each `DnsName` as a bare alias pointing at the same IP as
48880
- * the canonical fqdn.
48881
- */
48882
- function extractServiceConnect(raw, task) {
48883
- if (!raw || typeof raw !== "object") return void 0;
48884
- const cfg = raw;
48885
- if (cfg["Enabled"] === false) return void 0;
48886
- const namespaceName = pickServiceConnectNamespace(cfg["Namespace"]);
48887
- if (!namespaceName) throw new EcsTaskResolutionError(`ServiceConnectConfiguration.Namespace must be a literal string (the Cloud Map namespace name like 'cdkd-local.local'); got ${JSON.stringify(cfg["Namespace"])}. Intrinsic / cross-stack namespace references are not supported in v1.`);
48888
- const rawServices = cfg["Services"];
48889
- if (!Array.isArray(rawServices) || rawServices.length === 0) return {
48890
- namespaceName,
48891
- services: []
48892
- };
48893
- const portByName = /* @__PURE__ */ new Map();
48894
- for (const c of task.containers) for (const pm of c.portMappings) if (pm.name) portByName.set(pm.name, pm.containerPort);
48895
- const services = [];
48896
- for (const entry of rawServices) {
48897
- if (!entry || typeof entry !== "object") continue;
48898
- const e = entry;
48899
- const portName = typeof e["PortName"] === "string" ? e["PortName"] : void 0;
48900
- if (!portName) throw new EcsTaskResolutionError(`ServiceConnectConfiguration.Services[] entry has no PortName: ${JSON.stringify(entry)}. Every Service entry must reference a producer-side PortMappings[].Name.`);
48901
- const containerPort = portByName.get(portName);
48902
- if (containerPort === void 0) throw new EcsTaskResolutionError(`ServiceConnectConfiguration.Services[].PortName='${portName}' does not match any PortMappings[].Name on the producer TaskDef (available: ${[...portByName.keys()].join(", ") || "(none)"}).`);
48903
- const clientAliases = [];
48904
- if (Array.isArray(e["ClientAliases"])) for (const ca of e["ClientAliases"]) {
48905
- if (!ca || typeof ca !== "object") continue;
48906
- const caObj = ca;
48907
- const dnsName = typeof caObj["DnsName"] === "string" ? caObj["DnsName"] : void 0;
48908
- const aliasEntry = { port: typeof caObj["Port"] === "number" ? caObj["Port"] : containerPort };
48909
- if (dnsName !== void 0) aliasEntry.dnsName = dnsName;
48910
- clientAliases.push(aliasEntry);
48911
- }
48912
- const discoveryName = clientAliases.find((c) => c.dnsName !== void 0)?.dnsName ?? portName;
48913
- services.push({
48914
- portName,
48915
- containerPort,
48916
- discoveryName,
48917
- clientAliases
48918
- });
48919
- }
48920
- return {
48921
- namespaceName,
48922
- services
48923
- };
48924
- }
48925
- /**
48926
- * Parse `ServiceRegistries[]`. Each entry's `RegistryArn` is the
48927
- * canonical `Fn::GetAtt: [<CloudMapServiceLogicalId>, 'Arn']` shape;
48928
- * cdkd surfaces the logical id (the AWS-side ARN is irrelevant
48929
- * locally — the registry is in-process).
48930
- *
48931
- * Issue #544 — entries with a literal-string `RegistryArn` (rare
48932
- * locally — would imply the user bound to an existing Cloud Map
48933
- * service deployed out-of-band) are skipped with a warning, since the
48934
- * in-process registry cannot resolve an external Cloud Map service
48935
- * back to its `(namespace, name)` pair. Pre-fix this was a silent
48936
- * `continue` and the user got no feedback about why the registration
48937
- * didn't show up.
48938
- */
48939
- function extractServiceRegistries(raw, serviceLogicalId, warnings) {
48940
- if (!Array.isArray(raw)) return [];
48941
- const out = [];
48942
- for (const entry of raw) {
48943
- if (!entry || typeof entry !== "object") continue;
48944
- const e = entry;
48945
- const registryArn = e["RegistryArn"];
48946
- let cloudMapServiceLogicalId;
48947
- if (typeof registryArn === "string") {
48948
- warnings.push(`ECS Service '${serviceLogicalId}' ServiceRegistries[] entry has a literal-string RegistryArn ('${registryArn}'); cdkd cannot resolve external Cloud Map services locally. Skipping this registration; peer services will not discover this endpoint through the in-process registry. Use Fn::GetAtt: [<CloudMapServiceLogicalId>, "Arn"] instead so cdkd can resolve the namespace + service name from the synthesized template.`);
48949
- continue;
48950
- }
48951
- if (registryArn && typeof registryArn === "object" && !Array.isArray(registryArn)) {
48952
- const getAtt = registryArn["Fn::GetAtt"];
48953
- if (Array.isArray(getAtt) && typeof getAtt[0] === "string") cloudMapServiceLogicalId = getAtt[0];
48954
- }
48955
- if (!cloudMapServiceLogicalId) continue;
48956
- const reg = { cloudMapServiceLogicalId };
48957
- if (typeof e["ContainerName"] === "string") reg.containerName = e["ContainerName"];
48958
- if (typeof e["ContainerPort"] === "number") reg.containerPort = e["ContainerPort"];
48959
- out.push(reg);
48960
- }
48961
- return out;
48962
- }
48963
- function pickServiceConnectNamespace(raw) {
48964
- if (typeof raw === "string" && raw.length > 0) return raw;
48965
- }
48966
- /**
48967
- * Resolve `Properties.TaskDefinition` to a logical id in the same stack.
48968
- * Accepted shapes — verified against real CDK 2.x `cdk synth` output on
48969
- * 2026-05-22 (per `feedback_verify_cdk_synth_shape_before_resolver.md`):
48970
- * - `{Ref: '<TaskDefLogicalId>'}` — the CDK-canonical shape emitted by
48971
- * `new ecs.FargateService({ taskDefinition })`.
48972
- * - flat string `'<TaskDefLogicalId>'` — accepted defensively but CDK
48973
- * rarely emits this for cross-resource refs.
48974
- * Other intrinsic shapes (`Fn::ImportValue` / `Fn::GetAtt` / etc.) are
48975
- * rejected — cross-stack task definitions and `Fn::GetAtt` shapes have
48976
- * no clean local resolution and would land here only as user errors.
48977
- */
48978
- function resolveTaskDefinitionReference(taskDefRef, stack, serviceLogicalId) {
48979
- if (typeof taskDefRef === "string") return taskDefRef;
48980
- if (taskDefRef && typeof taskDefRef === "object" && !Array.isArray(taskDefRef)) {
48981
- const refValue = taskDefRef["Ref"];
48982
- if (typeof refValue === "string") {
48983
- const target = (stack.template.Resources ?? {})[refValue];
48984
- if (!target) throw new EcsTaskResolutionError(`ECS Service '${serviceLogicalId}' references TaskDefinition '${refValue}' but no such resource exists in ${stack.stackName}.`);
48985
- if (target.Type !== "AWS::ECS::TaskDefinition") throw new EcsTaskResolutionError(`ECS Service '${serviceLogicalId}' references '${refValue}' as TaskDefinition but it is of type ${target.Type}, not AWS::ECS::TaskDefinition.`);
48986
- return refValue;
48987
- }
48988
- }
48989
- throw new EcsTaskResolutionError(`ECS Service '${serviceLogicalId}' has an unsupported TaskDefinition reference shape: ${JSON.stringify(taskDefRef)}. cdkd local start-service v1 supports only Ref to a same-stack AWS::ECS::TaskDefinition; cross-stack TaskDefinitions are deferred.`);
48990
- }
48991
- function parseDesiredCount(raw, serviceLogicalId) {
48992
- if (raw === void 0 || raw === null) return 1;
48993
- if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0) return Math.floor(raw);
48994
- if (typeof raw === "string" && /^\d+$/.test(raw)) return parseInt(raw, 10);
48995
- throw new EcsTaskResolutionError(`ECS Service '${serviceLogicalId}' has an unsupported DesiredCount value: ${JSON.stringify(raw)}. Must be a non-negative integer.`);
48996
- }
48997
- function parseHealthCheckGrace(raw, _serviceLogicalId) {
48998
- if (raw === void 0 || raw === null) return 30;
48999
- if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0) return Math.floor(raw);
49000
- if (typeof raw === "string" && /^\d+$/.test(raw)) return parseInt(raw, 10);
49001
- return 30;
49002
- }
49003
- function parseServiceName(raw, serviceLogicalId) {
49004
- if (typeof raw === "string" && raw.length > 0) return raw;
49005
- return serviceLogicalId;
49006
- }
49007
- /**
49008
- * Local copy of the same `pickStack` helper used by the task resolver.
49009
- * Kept in-file rather than exported from `ecs-task-resolver.ts` so future
49010
- * service-specific extensions (e.g. cross-stack service-to-task refs)
49011
- * can diverge without breaking the run-task code path.
49012
- */
49013
- function pickStack$1(parsed, stacks) {
49014
- if (parsed.stackPattern === null) {
49015
- if (stacks.length === 1) return stacks[0];
49016
- 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'.`);
49017
- }
49018
- const matched = matchStacks(stacks, [parsed.stackPattern]);
49019
- if (matched.length === 0) throw new EcsTaskResolutionError(`No stack matches '${parsed.stackPattern}'. Available stacks: ${stacks.map((s) => s.stackName).join(", ")}.`);
49020
- if (matched.length > 1) throw new EcsTaskResolutionError(`Multiple stacks match '${parsed.stackPattern}': ${matched.map((s) => s.stackName).join(", ")}. Refine the pattern.`);
49021
- return matched[0];
49022
- }
49023
- function notFoundError(target, stack, resources) {
49024
- const services = Object.entries(resources).filter(([, r]) => r.Type === "AWS::ECS::Service").map(([id]) => id);
49025
- if (services.length === 0) return new EcsTaskResolutionError(`Target '${target}' did not match any resource in ${stack.stackName}, and the stack declares no AWS::ECS::Service resources at all.`);
49026
- return new EcsTaskResolutionError(`Target '${target}' did not match any ECS Service in ${stack.stackName}. Available services: ${services.join(", ")}.`);
49027
- }
49028
-
49029
- //#endregion
49030
- //#region src/local/ecs-service-runner.ts
49031
- /**
49032
- * Phase 2 of #262 — long-running ECS Service emulator. Wraps the existing
49033
- * `ecs-task-runner` machinery in a replica pool: N concurrent task
49034
- * instances per `DesiredCount`, each with its own docker network +
49035
- * metadata sidecar + container set. Tasks that exit non-zero AFTER the
49036
- * health-check grace period are restarted with exponential backoff so a
49037
- * crash-looping container does not hammer docker.
49038
- *
49039
- * v1 scope (per the issue's PR-split recommendation):
49040
- * - Replica pool sizing via `DesiredCount` clamped by `--max-tasks`.
49041
- * - Restart-on-exit with exponential backoff (1s → 30s, capped) +
49042
- * a per-instance retry counter so a permanently-broken container
49043
- * stops compounding cleanup work.
49044
- * - Long-running lifecycle (returns only on shutdown).
49045
- *
49046
- * Phase 3 of #262 (Issue #460) — Cloud Map / Service Connect peer
49047
- * discovery is wired through `ServiceRunnerOptions.discovery`. When
49048
- * supplied, every booted replica discovers its docker IP, registers
49049
- * itself into the shared in-process `CloudMapRegistry`, and emits
49050
- * `--add-host` flags so consumer containers reach peer services via
49051
- * the canonical `<discoveryName>.<namespace>` fqdn. Envoy L7 sidecar
49052
- * emulation (design Layer B) is deferred to a follow-up PR per the
49053
- * design's §O5 "--no-envoy by default" recommendation.
49054
- *
49055
- * Deferred to follow-up PRs:
49056
- * - Local load-balancer emulation (LB listener + target-group health
49057
- * check + round-robin) — separate PR per the issue's PR-split.
49058
- * - Envoy sidecar for Service Connect L7 routing / retries / circuit
49059
- * breaking (Cloud Map DNS-only mode ships now).
49060
- * - Rolling deployment (`--reload` / `--watch`).
49061
- */
49062
- var EcsServiceRunnerError = class EcsServiceRunnerError extends Error {
49063
- constructor(message) {
49064
- super(message);
49065
- this.name = "EcsServiceRunnerError";
49066
- Object.setPrototypeOf(this, EcsServiceRunnerError.prototype);
49067
- }
49068
- };
49069
- function createServiceRunState() {
49070
- return {
49071
- replicas: [],
49072
- shuttingDown: false
49073
- };
49074
- }
49075
- /**
49076
- * Compute the effective replica count for a service: the smaller of
49077
- * `service.desiredCount` and `--max-tasks`, floored at 1. Pure-
49078
- * functional so the CLI can show the user what cdkd is about to do
49079
- * before any docker calls fire.
49080
- */
49081
- function computeReplicaCount(desiredCount, maxTasks) {
49082
- if (maxTasks < 1) throw new EcsServiceRunnerError(`--max-tasks must be >= 1 (got ${maxTasks}); local dev needs at least one running replica.`);
49083
- if (desiredCount <= 0) return 1;
49084
- return Math.min(desiredCount, maxTasks);
49085
- }
49086
- /**
49087
- * Exponential backoff schedule: 1s, 2s, 4s, 8s, 16s, 30s, 30s, ... Used
49088
- * between restarts of a crash-looping replica so docker is not hammered
49089
- * by the watcher loop. Exposed for unit testing.
49090
- */
49091
- function backoffDelayMs(restartCount) {
49092
- return Math.min(1e3 * Math.pow(2, Math.min(restartCount, 10)), 3e4);
49093
- }
49094
- /**
49095
- * Maximum number of replica indices the per-replica subnet allocator
49096
- * can serve without modulo-wrap collision. The allocator below walks
49097
- * the link-local /24 range `169.254.170.0..169.254.253.0` (84 octets)
49098
- * and **skips 171** because that octet is owned by the shared-service
49099
- * network in design § 5 Option A (see `SHARED_SVC_SUBNET_OCTET`), so
49100
- * the usable count is 83. The CLI's `--max-tasks` parser enforces this
49101
- * cap before any boot work fires.
49102
- */
49103
- const SUBNET_ALLOCATOR_RANGE = 83;
49104
- /**
49105
- * Defensive per-replica subnet octet allocator (Issue #544). Only used
49106
- * when callers bypass the CLI's `sharedNetwork` construction — i.e.
49107
- * test paths that hand-build `ServiceRunnerOptions.discovery` without
49108
- * `sharedNetwork`, or the bare `cdkd local run-task`-shaped path that
49109
- * runs one network per task. Production `cdkd local start-service`
49110
- * runs always go through the shared network (design § 5 Option A) so
49111
- * this allocator is unreachable in the standard path.
49112
- *
49113
- * Returns the second-from-last octet of the per-replica /24 (170 →
49114
- * `169.254.170.0/24`). Walks the 83-slot output range
49115
- * `[170, 172, 173, ..., 253]` — 171 is intentionally **skipped**
49116
- * because it's reserved for the shared-service network sidecar
49117
- * (`SHARED_SVC_SUBNET_OCTET`), and assigning a per-replica network
49118
- * the same /24 would have docker reject the duplicate-subnet
49119
- * `network create` with the cryptic "Pool overlaps with other one on
49120
- * this address space" error.
49121
- */
49122
- function pickSubnetOctet(index) {
49123
- const candidate = 170 + (index % 83 + 83) % 83;
49124
- return candidate < 171 ? candidate : candidate + 1;
49125
- }
49126
- /**
49127
- * Decide whether a replica that just exited should restart. Pure-
49128
- * functional so the watcher loop's policy is easy to unit-test.
49129
- */
49130
- function shouldRestart(exitCode, policy) {
49131
- if (policy === "none") return false;
49132
- if (policy === "always") return true;
49133
- return exitCode !== 0;
49134
- }
49135
- /**
49136
- * Long-running entry point. Boots `replicaCount` instances of the
49137
- * service's task descriptor, returns a controller object the CLI uses
49138
- * to (1) wait for the first failure that gives up restarting and (2)
49139
- * shut every replica down on SIGINT / SIGTERM.
49140
- *
49141
- * The returned `shutdown()` is idempotent and safe to call from
49142
- * multiple SIGINT handlers (CLI's single-flight pattern wraps it
49143
- * anyway).
49144
- */
49145
- async function startEcsService(service, options, runState) {
49146
- const logger = getLogger().child("ecs-service");
49147
- for (const w of service.warnings) logger.warn(w);
49148
- const replicaCount = computeReplicaCount(service.desiredCount, options.maxTasks);
49149
- if (replicaCount < service.desiredCount) logger.warn(`Service '${service.serviceName}' template DesiredCount=${service.desiredCount} exceeds --max-tasks=${options.maxTasks}; running ${replicaCount} replica(s) locally. Raise --max-tasks to lift the cap, or accept the reduced concurrency for local dev.`);
49150
- logger.info(`Starting ECS service '${service.serviceName}' with ${replicaCount} replica(s) (restartPolicy=${options.restartPolicy})`);
49151
- for (let i = 0; i < replicaCount; i++) {
49152
- const instance = {
49153
- index: i,
49154
- state: createEcsRunState(),
49155
- restartCount: 0,
49156
- shuttingDown: false,
49157
- inFlightBoot: void 0,
49158
- cloudMapHandles: []
49159
- };
49160
- runState.replicas.push(instance);
49161
- const bootPromise = bootReplica(service, options, instance);
49162
- instance.inFlightBoot = bootPromise;
49163
- try {
49164
- await bootPromise;
49165
- } catch (err) {
49166
- instance.lastError = err instanceof Error ? err : new Error(String(err));
49167
- throw new EcsServiceRunnerError(`Failed to boot replica ${i} of service '${service.serviceName}': ${instance.lastError.message}`);
49168
- } finally {
49169
- instance.inFlightBoot = void 0;
49170
- }
49171
- }
49172
- for (const instance of runState.replicas) watchReplica(service, options, instance, runState);
49173
- return new ServiceController(service, runState, options);
49174
- }
49175
- /**
49176
- * Public controller surface. The CLI awaits `controller.waitForShutdown()`
49177
- * to block until the user ^Cs. `controller.shutdown()` is wired into the
49178
- * SIGINT / SIGTERM handlers.
49179
- */
49180
- var ServiceController = class {
49181
- service;
49182
- runState;
49183
- options;
49184
- shutdownResolve;
49185
- shutdownPromise;
49186
- /**
49187
- * Single-flight wrapper for `shutdown()` so the fan-out cleanup runs
49188
- * exactly once even when SIGINT and the CLI's outer `finally` both
49189
- * fire (the canonical pattern documented in
49190
- * `feedback_sigint_finally_cleanup_singleflight.md`). Built in the
49191
- * constructor so every call to `shutdown()` resolves against the same
49192
- * underlying promise.
49193
- */
49194
- runShutdown;
49195
- constructor(service, runState, options) {
49196
- this.service = service;
49197
- this.runState = runState;
49198
- this.options = options;
49199
- this.shutdownPromise = new Promise((resolve) => {
49200
- this.shutdownResolve = resolve;
49201
- });
49202
- this.runShutdown = singleFlight(() => this.doShutdown());
49203
- }
49204
- /**
49205
- * Returns the count of currently-active (non-shutting-down) replicas.
49206
- * Exposed so the CLI can surface a one-line "service is degraded"
49207
- * banner when restarts stop firing.
49208
- */
49209
- activeReplicaCount() {
49210
- return this.runState.replicas.filter((r) => !r.shuttingDown).length;
49211
- }
49212
- /**
49213
- * Block until `shutdown()` is called. Used by the CLI as the
49214
- * long-running blocking point — the SIGINT handler resolves it.
49215
- */
49216
- waitForShutdown() {
49217
- return this.shutdownPromise;
49218
- }
49219
- /**
49220
- * Idempotent fan-out shutdown across every active replica. Wired into
49221
- * both SIGINT and the outer `finally` of the CLI command; the
49222
- * `singleFlight`-wrapped `runShutdown` collapses concurrent / repeated
49223
- * callers to one underlying invocation.
49224
- */
49225
- async shutdown() {
49226
- await this.runShutdown();
49227
- return this.shutdownPromise;
49228
- }
49229
- async doShutdown() {
49230
- this.runState.shuttingDown = true;
49231
- const logger = getLogger().child("ecs-service");
49232
- logger.info(`Shutting down service '${this.service.serviceName}'...`);
49233
- for (const r of this.runState.replicas) r.shuttingDown = true;
49234
- const inFlightBoots = this.runState.replicas.map((r) => r.inFlightBoot).filter((p) => p !== void 0);
49235
- if (inFlightBoots.length > 0) {
49236
- logger.debug(`Awaiting ${inFlightBoots.length} in-flight bootReplica() call(s) before cleanup...`);
49237
- await Promise.allSettled(inFlightBoots);
49238
- }
49239
- await Promise.allSettled(this.runState.replicas.map(async (instance) => {
49240
- if (this.options.discovery) {
49241
- for (const handle of instance.cloudMapHandles) try {
49242
- this.options.discovery.registry.unregister(handle);
49243
- } catch {}
49244
- instance.cloudMapHandles = [];
49245
- }
49246
- try {
49247
- await cleanupEcsRun(instance.state, { keepRunning: this.options.taskOptions.keepRunning });
49248
- } catch (err) {
49249
- logger.debug(`Replica ${instance.index} cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
49250
- }
49251
- }));
49252
- this.shutdownResolve?.();
49253
- }
49254
- };
49255
- /**
49256
- * Build the `--network-alias` map for one service's containers (design
49257
- * doc § 5 Option A). For every Service Connect entry, attach the
49258
- * fqdn (`<discoveryName>.<namespaceName>`), the bare discoveryName,
49259
- * AND every ClientAlias DnsName to the container that owns the
49260
- * matching PortName. Other containers in the task get NO extra
49261
- * aliases (only their default `--name`-derived alias from
49262
- * `buildDockerRunArgs`).
49263
- *
49264
- * Aliases per container are de-duplicated so docker doesn't reject
49265
- * a `--network-alias X` repeated against the same container.
49266
- *
49267
- * Returns an empty map when the service has no Service Connect — the
49268
- * runner's `... .size > 0 ? { networkAliasesByContainer } : {}` guard
49269
- * short-circuits in that case so backward-compat callers pay no cost.
49270
- */
49271
- function buildNetworkAliasesByContainer(service) {
49272
- const out = /* @__PURE__ */ new Map();
49273
- const sc = service.serviceConnect;
49274
- if (!sc) return out;
49275
- for (const entry of sc.services) {
49276
- const owner = service.task.containers.find((c) => c.portMappings.some((pm) => pm.name === entry.portName));
49277
- if (!owner) continue;
49278
- const aliases = [];
49279
- aliases.push(entry.discoveryName);
49280
- aliases.push(`${entry.discoveryName}.${sc.namespaceName}`);
49281
- for (const ca of entry.clientAliases) if (ca.dnsName) aliases.push(ca.dnsName);
49282
- const existing = out.get(owner.name) ?? [];
49283
- for (const a of aliases) if (!existing.includes(a)) existing.push(a);
49284
- out.set(owner.name, existing);
49285
- }
49286
- return out;
49287
- }
49288
- /**
49289
- * Boot a single replica. Mutates the supplied `instance.state` so the
49290
- * shutdown path's `cleanupEcsRun(instance.state)` covers every partial
49291
- * side effect. Network names are suffixed with the replica index so
49292
- * docker doesn't collide on shared per-task network names when N > 1.
49293
- */
49294
- async function bootReplica(service, options, instance) {
49295
- const logger = getLogger().child("ecs-service");
49296
- const perReplicaCluster = `${options.taskOptions.cluster}-svc-${service.serviceLogicalId.toLowerCase()}-r${instance.index}`;
49297
- const ownerKeyPrefix = `${service.serviceLogicalId}:r${instance.index}`;
49298
- const addHostFlags = options.discovery?.registry ? options.discovery.registry.buildAddHostFlags(ownerKeyPrefix) : [];
49299
- const sharedNetwork = options.discovery?.sharedNetwork;
49300
- const networkAliasesByContainer = buildNetworkAliasesByContainer(service);
49301
- const skipHostPortPublish = computeReplicaCount(service.desiredCount, options.maxTasks) > 1;
49302
- const perReplicaTaskOptions = {
49303
- ...options.taskOptions,
49304
- cluster: perReplicaCluster,
49305
- detach: true,
49306
- ...skipHostPortPublish ? { skipHostPortPublish: true } : {},
49307
- ...sharedNetwork ? { existingNetwork: sharedNetwork } : { subnetOctet: pickSubnetOctet(instance.index) },
49308
- ...addHostFlags.length > 0 ? { addHostFlags } : {},
49309
- ...networkAliasesByContainer.size > 0 ? { networkAliasesByContainer } : {}
49310
- };
49311
- logger.info(`Booting replica ${instance.index} (${perReplicaCluster})`);
49312
- await runEcsTask(service.task, perReplicaTaskOptions, instance.state);
49313
- if (options.discovery) await publishReplicaToCloudMap(service, instance, options.discovery, ownerKeyPrefix);
49314
- }
49315
- /**
49316
- * After the replica's main container is up, discover its docker
49317
- * network IP and publish the configured Service Connect + Cloud Map
49318
- * endpoints into the shared registry. The handles are tracked on the
49319
- * instance so the shutdown / restart path can unregister symmetrically.
49320
- *
49321
- * Errors here are best-effort: docker inspect can fail right after run
49322
- * (container vanished, network not fully wired), and the registry is
49323
- * advisory — losing one replica's registration means peer services
49324
- * can't reach it via the overlay, but it doesn't break that replica's
49325
- * own work or AWS SDK calls.
49326
- */
49327
- async function publishReplicaToCloudMap(service, instance, discovery, ownerKeyPrefix) {
49328
- const logger = getLogger().child("ecs-service");
49329
- const networkName = instance.state.network?.networkName;
49330
- if (!networkName) return;
49331
- const essential = service.task.containers.find((c) => c.essential) ?? service.task.containers[0];
49332
- if (!essential) return;
49333
- const started = instance.state.startedContainers.find((c) => c.name === essential.name);
49334
- if (!started) return;
49335
- let ip;
49336
- try {
49337
- ip = await getContainerNetworkIp(started.id, networkName);
49338
- } catch (err) {
49339
- logger.warn(`Replica ${instance.index}: docker inspect failed before Cloud Map publish: ${err instanceof Error ? err.message : String(err)}`);
49340
- return;
49341
- }
49342
- if (!ip) {
49343
- logger.warn(`Replica ${instance.index}: no docker IP discovered on network ${networkName}; skipping Cloud Map publish for this replica.`);
49344
- return;
49345
- }
49346
- if (service.serviceConnect) {
49347
- const ns = service.serviceConnect.namespaceName;
49348
- const index = discovery.cloudMapIndexByStack.get(service.stack.stackName);
49349
- if (index && !index.namespacesByName.has(ns)) logger.warn(`ECS Service '${service.serviceLogicalId}' ServiceConnectConfiguration.Namespace='${ns}' does not match any AWS::ServiceDiscovery::PrivateDnsNamespace declared in stack ${service.stack.stackName}. Publishing under the literal name anyway; peer services using the same literal will still discover this endpoint.`);
49350
- let i = 0;
49351
- for (const entry of service.serviceConnect.services) {
49352
- const ownerKey = `${ownerKeyPrefix}:sc:${i}`;
49353
- const handle = discovery.registry.register(ns, entry.discoveryName, {
49354
- ip,
49355
- port: entry.containerPort,
49356
- ownerKey
49357
- });
49358
- instance.cloudMapHandles.push(handle);
49359
- for (const alias of entry.clientAliases) if (alias.dnsName) discovery.registry.registerAlias(alias.dnsName, handle.fqdn);
49360
- i++;
49361
- }
49362
- }
49363
- if (service.serviceRegistries.length > 0) {
49364
- const index = discovery.cloudMapIndexByStack.get(service.stack.stackName);
49365
- if (!index) {
49366
- logger.warn(`ECS Service '${service.serviceLogicalId}' declares ServiceRegistries[] but cdkd has no Cloud Map index for stack ${service.stack.stackName}. Skipping registration.`);
49367
- return;
49368
- }
49369
- let j = 0;
49370
- for (const reg of service.serviceRegistries) {
49371
- const cm = index.servicesByLogicalId.get(reg.cloudMapServiceLogicalId);
49372
- if (!cm) {
49373
- logger.warn(`ECS Service '${service.serviceLogicalId}' ServiceRegistries[].cloudMapServiceLogicalId='${reg.cloudMapServiceLogicalId}' did not resolve to an AWS::ServiceDiscovery::Service in stack ${service.stack.stackName}. Skipping this registration.`);
49374
- continue;
49375
- }
49376
- let port = reg.containerPort;
49377
- if (port === void 0 && essential.portMappings.length > 0) port = essential.portMappings[0].containerPort;
49378
- if (port === void 0) {
49379
- logger.warn(`ECS Service '${service.serviceLogicalId}' ServiceRegistries[] entry for Cloud Map service '${cm.logicalId}' has no resolvable container port; skipping.`);
49380
- continue;
49381
- }
49382
- const ownerKey = `${ownerKeyPrefix}:sr:${j}`;
49383
- const handle = discovery.registry.register(cm.namespaceName, cm.name, {
49384
- ip,
49385
- port,
49386
- ownerKey
49387
- });
49388
- instance.cloudMapHandles.push(handle);
49389
- j++;
49390
- }
49391
- }
49392
- }
49393
- /**
49394
- * Long-running watcher loop for one replica. Polls the essential
49395
- * container's exit code via `docker wait`; on exit, decides whether to
49396
- * restart per `restartPolicy` + applies exponential backoff. The loop
49397
- * exits only when the replica's `shuttingDown` flag is set.
49398
- */
49399
- async function watchReplica(service, options, instance, runState) {
49400
- const logger = getLogger().child("ecs-service");
49401
- while (!instance.shuttingDown && !runState.shuttingDown) {
49402
- const essentialId = pickEssentialContainerId(instance, service);
49403
- if (!essentialId) {
49404
- await sleep(500);
49405
- continue;
49406
- }
49407
- let exitCode;
49408
- try {
49409
- exitCode = await waitForExitImpl(essentialId);
49410
- } catch (err) {
49411
- logger.debug(`docker wait failed for replica ${instance.index}: ${err instanceof Error ? err.message : String(err)}`);
49412
- exitCode = -1;
49413
- }
49414
- if (instance.shuttingDown || runState.shuttingDown) return;
49415
- logger.warn(`Replica ${instance.index} essential container exited with code ${exitCode} (restartCount=${instance.restartCount}).`);
49416
- if (!shouldRestart(exitCode, options.restartPolicy)) {
49417
- logger.warn(`Replica ${instance.index} not restarting (policy=${options.restartPolicy}, exit=${exitCode}). Service running in degraded mode.`);
49418
- instance.shuttingDown = true;
49419
- return;
49420
- }
49421
- const delay = backoffDelayMs(instance.restartCount);
49422
- logger.info(`Restarting replica ${instance.index} in ${delay}ms...`);
49423
- await sleep(delay);
49424
- if (instance.shuttingDown || runState.shuttingDown) return;
49425
- if (options.discovery) {
49426
- for (const handle of instance.cloudMapHandles) try {
49427
- options.discovery.registry.unregister(handle);
49428
- } catch {}
49429
- instance.cloudMapHandles = [];
49430
- }
49431
- try {
49432
- await cleanupEcsRun(instance.state, { keepRunning: false });
49433
- } catch (err) {
49434
- logger.debug(`Replica ${instance.index} pre-restart cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
49435
- }
49436
- instance.state = createEcsRunState();
49437
- instance.restartCount += 1;
49438
- const bootPromise = bootReplica(service, options, instance);
49439
- instance.inFlightBoot = bootPromise;
49440
- try {
49441
- await bootPromise;
49442
- } catch (err) {
49443
- instance.lastError = err instanceof Error ? err : new Error(String(err));
49444
- logger.error(`Replica ${instance.index} restart failed: ${instance.lastError.message}. Service running in degraded mode.`);
49445
- instance.shuttingDown = true;
49446
- return;
49447
- } finally {
49448
- instance.inFlightBoot = void 0;
49449
- }
49450
- }
49451
- }
49452
- function pickEssentialContainerId(instance, service) {
49453
- if (service) {
49454
- const essential = service.task.containers.find((c) => c.essential) ?? service.task.containers[0];
49455
- if (essential) {
49456
- const started = instance.state.startedContainers.find((c) => c.name === essential.name);
49457
- if (started) return started.id;
49458
- }
49459
- }
49460
- return instance.state.startedContainers[0]?.id;
49461
- }
49462
- /**
49463
- * Production `docker wait <id>` implementation. Captured once so the
49464
- * test override can restore it without duplicating the body.
49465
- */
49466
- const defaultWaitForExitImpl = async (containerId) => {
49467
- const { execFile } = await import("node:child_process");
49468
- const { promisify } = await import("node:util");
49469
- const { getDockerCmd } = await import("./docker-cmd-iDMcWcre.js").then((n) => n.t);
49470
- const { stdout } = await promisify(execFile)(getDockerCmd(), ["wait", containerId], { maxBuffer: 1024 * 1024 });
49471
- const code = parseInt(stdout.trim(), 10);
49472
- return Number.isFinite(code) ? code : -1;
49473
- };
49474
- /**
49475
- * `docker wait <id>` returns the exit code on stdout. Extracted as a
49476
- * test-overridable function so unit tests do not need a real container.
49477
- */
49478
- let waitForExitImpl = defaultWaitForExitImpl;
49479
- const defaultSleepImpl = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
49480
- let sleepImpl = defaultSleepImpl;
49481
- function sleep(ms) {
49482
- return sleepImpl(ms);
49483
- }
49484
-
49485
48776
  //#endregion
49486
48777
  //#region src/cli/commands/local-start-service.ts
49487
48778
  /**
49488
- * `cdkd local start-service <Stack/Service>`Phase 2 of #262. Spins up
49489
- * `DesiredCount` task replicas locally (clamped by `--max-tasks`) using
49490
- * the existing `ecs-task-runner` per replica. Long-running; ^C cleans
49491
- * every replica + sidecar + per-task network.
49492
- *
49493
- * Deferred to follow-up PRs (matches the issue's PR-split):
49494
- * - Local LB emulator (listener + round-robin + target-group health
49495
- * check) — PR C of #466.
49496
- * - Rolling deployment (`--watch` / `--reload`) — PR D of #466.
49497
- * - Service Connect / Cloud Map — tracked separately in #460.
49498
- */
49499
- async function localStartServiceCommand(targets, options) {
49500
- const logger = getLogger();
49501
- if (options.verbose) logger.setLevel("debug");
49502
- warnIfDeprecatedRegion(options);
49503
- const skipPull = options.pull === false;
49504
- if (!targets || targets.length === 0) throw new LocalStartServiceError("cdkd local start-service requires at least one <target>. Pass one or more service paths like 'Stack/Orders' 'Stack/Frontend'.");
49505
- rejectExplicitCfnStackWithMultipleStacks(options, targets.length);
49506
- const perTarget = targets.map((t) => ({
49507
- target: t,
49508
- runState: createServiceRunState()
49509
- }));
49510
- let sigintHandler;
49511
- let sigintCount = 0;
49512
- let sharedNetwork;
49513
- let profileCredsFile;
49514
- const cleanup = singleFlight(async () => {
49515
- await Promise.allSettled(perTarget.map(async (pt) => {
49516
- if (pt.controller) await pt.controller.shutdown();
49517
- else {
49518
- await Promise.allSettled(pt.runState.replicas.map((r) => r.inFlightBoot).filter((p) => p !== void 0));
49519
- await Promise.allSettled(pt.runState.replicas.map((r) => cleanupEcsRun(r.state, { keepRunning: false }).catch(() => void 0)));
49520
- }
49521
- }));
49522
- if (profileCredsFile) {
49523
- try {
49524
- await profileCredsFile.dispose();
49525
- } catch (err) {
49526
- getLogger().warn(`Failed to remove profile credentials tmpdir ${profileCredsFile.hostPath}: ${err instanceof Error ? err.message : String(err)}`);
49527
- }
49528
- profileCredsFile = void 0;
49529
- }
49530
- if (sharedNetwork) {
49531
- try {
49532
- await destroyTaskNetwork(sharedNetwork);
49533
- } catch (err) {
49534
- getLogger().warn(`shared service network teardown failed: ${err instanceof Error ? err.message : String(err)}`);
49535
- }
49536
- sharedNetwork = void 0;
49537
- }
49538
- }, (err) => getLogger().warn(`service cleanup failed: ${err instanceof Error ? err.message : String(err)}`));
49539
- try {
49540
- await applyRoleArnIfSet({
49541
- roleArn: options.roleArn,
49542
- region: options.region
49543
- });
49544
- await ensureDockerAvailable();
49545
- const appCmd = resolveApp(options.app);
49546
- if (!appCmd) throw new Error("No CDK app specified. Pass --app, set CDKD_APP, or add \"app\" to cdk.json.");
49547
- logger.info("Synthesizing CDK app...");
49548
- const synthesizer = new Synthesizer();
49549
- const context = parseContextOptions(options.context);
49550
- const synthOpts = {
49551
- app: appCmd,
49552
- output: options.output,
49553
- ...options.region && { region: options.region },
49554
- ...options.profile && { profile: options.profile },
49555
- ...Object.keys(context).length > 0 && { context },
49556
- ...options.stateBucket && { stateBucket: options.stateBucket },
49557
- ...options.profile && { macroExpandS3ClientOpts: { profile: options.profile } }
49558
- };
49559
- const { stacks } = await synthesizer.synthesize(synthOpts);
49560
- const cloudMapIndexByStack = /* @__PURE__ */ new Map();
49561
- for (const stack of stacks) {
49562
- const index = buildCloudMapIndex(stack);
49563
- cloudMapIndexByStack.set(stack.stackName, index);
49564
- for (const w of index.warnings) logger.warn(w);
49565
- }
49566
- const registry = new CloudMapRegistry();
49567
- const sidecarCredentials = await resolveSharedSidecarCredentials$1(options);
49568
- try {
49569
- sharedNetwork = await createSharedSvcNetwork({
49570
- prefix: options.cluster,
49571
- skipPull,
49572
- cluster: options.cluster,
49573
- ...sidecarCredentials !== void 0 && { credentials: sidecarCredentials }
49574
- });
49575
- } catch (err) {
49576
- throw new LocalStartServiceError(`Failed to create shared service network: ${err instanceof Error ? err.message : String(err)}`);
49577
- }
49578
- if (options.profile && sidecarCredentials) profileCredsFile = await writeProfileCredentialsFile(options.profile, sidecarCredentials);
49579
- const discovery = {
49580
- registry,
49581
- cloudMapIndexByStack,
49582
- sharedNetwork
49583
- };
49584
- sigintHandler = () => {
49585
- sigintCount += 1;
49586
- if (sigintCount >= 2) {
49587
- process.stderr.write("Force-exit on second ^C; container cleanup skipped.\n");
49588
- process.exit(130);
49589
- }
49590
- logger.info("Stopping service(s)...");
49591
- cleanup().then(() => process.exit(130));
49592
- };
49593
- process.on("SIGINT", sigintHandler);
49594
- process.on("SIGTERM", sigintHandler);
49595
- for (const pt of perTarget) pt.controller = await bootOneTarget(pt.target, pt.runState, stacks, options, discovery, skipPull, profileCredsFile);
49596
- const summary = perTarget.map((pt) => `${pt.controller.service.serviceName} (${pt.controller.activeReplicaCount()} replica(s))`).join(", ");
49597
- logger.info(`Service(s) running: ${summary}. Press ^C to shut down.`);
49598
- await Promise.all(perTarget.map((pt) => pt.controller.waitForShutdown()));
49599
- } finally {
49600
- if (sigintHandler) {
49601
- process.off("SIGINT", sigintHandler);
49602
- process.off("SIGTERM", sigintHandler);
49603
- }
49604
- await cleanup();
49605
- }
49606
- }
49607
- /**
49608
- * Boot one target. Extracted from the loop so each per-service block
49609
- * (image context, cross-stack resolver, task-role credentials, runner
49610
- * options) is scoped locally. Returns the started controller for the
49611
- * outer code to wait + tear down.
48779
+ * `cdkl start-service` strategyname one or more ECS services and the engine
48780
+ * boots their replicas. There is no front-door listener (services are reached
48781
+ * directly via their published container ports). Mirrors `albStrategy` in
48782
+ * shape, with `frontDoor` omitted and `lbPortOverrides` empty.
49612
48783
  */
49613
- async function bootOneTarget(target, runState, stacks, options, discovery, skipPull, profileCredsFile) {
49614
- const candidate = pickCandidateStack(parseEcsTarget(target).stackPattern, stacks);
49615
- const stateProvider = createLocalStateProvider$1(options, candidate?.stackName ?? "", candidate?.region);
49616
- try {
49617
- return await runOneTarget(target, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile);
49618
- } finally {
49619
- if (stateProvider) stateProvider.dispose();
49620
- }
49621
- }
49622
- async function runOneTarget(target, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile) {
49623
- const logger = getLogger();
49624
- const imageContext = await buildEcsImageResolutionContext$1(target, stacks, options, stateProvider);
49625
- const service = resolveEcsServiceTarget(target, stacks, imageContext);
49626
- logger.info(`Target: ${service.stack.stackName}/${service.serviceLogicalId} (service=${service.serviceName}, desiredCount=${service.desiredCount}, task=${service.task.taskDefinitionLogicalId})`);
49627
- for (const w of service.warnings) logger.warn(w);
49628
- if (service.serviceConnect) logger.info(`Service Connect: namespace='${service.serviceConnect.namespaceName}', ${service.serviceConnect.services.length} service(s) registered for peer discovery.`);
49629
- if (service.serviceRegistries.length > 0) logger.info(`Cloud Map: ${service.serviceRegistries.length} ServiceRegistry binding(s).`);
49630
- const taskNeeds = detectEcsImageResolutionNeeds(stacks.find((s) => s.stackName === service.stack.stackName) ?? service.stack);
49631
- if (stateProvider && taskNeeds.needsCrossStackResolver) {
49632
- const consumerRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? service.stack.region ?? "us-east-1";
49633
- const resolver = await stateProvider.buildCrossStackResolver(consumerRegion);
49634
- if (resolver) {
49635
- const subContext = {
49636
- resources: imageContext?.stateResources ?? {},
49637
- ...imageContext?.pseudoParameters && { pseudoParameters: imageContext.pseudoParameters },
49638
- consumerRegion,
49639
- crossStackResolver: resolver
49640
- };
49641
- await applyCrossStackResolverToTask(service.task, subContext);
49642
- }
49643
- } else if (!stateProvider && taskNeeds.needsCrossStackResolver) logger.warn("Container Environment / Secrets entries contain Fn::ImportValue / Fn::GetStackOutput intrinsics. Pass --from-state (cdkd-deployed) or --from-cfn-stack (cdk-deployed) to substitute them against deployed state.");
49644
- let assumedCredentials;
49645
- let resolvedRoleArn;
49646
- if (options.assumeTaskRole === true) {
49647
- if (!service.task.taskRoleArn) throw new LocalStartServiceError(`--assume-task-role passed without an ARN but service '${service.serviceLogicalId}' has no resolvable TaskRoleArn. Pass the ARN explicitly: --assume-task-role <arn>`);
49648
- resolvedRoleArn = await resolvePlaceholderAccount(service.task.taskRoleArn, options.region);
49649
- assumedCredentials = await assumeTaskRole(resolvedRoleArn, options.region);
49650
- } else if (typeof options.assumeTaskRole === "string") {
49651
- resolvedRoleArn = options.assumeTaskRole;
49652
- assumedCredentials = await assumeTaskRole(resolvedRoleArn, options.region);
49653
- }
49654
- const envOverrides = readEnvOverridesFile$2(options.envVars);
49655
- const taskOpts = {
49656
- cluster: options.cluster,
49657
- containerHost: options.containerHost,
49658
- skipPull,
49659
- keepRunning: false,
49660
- detach: true
49661
- };
49662
- if (envOverrides) taskOpts.envOverrides = envOverrides;
49663
- if (assumedCredentials) taskOpts.taskCredentials = assumedCredentials;
49664
- if (resolvedRoleArn) taskOpts.taskRoleArn = resolvedRoleArn;
49665
- if (options.platform) taskOpts.platformOverride = options.platform;
49666
- if (options.region) taskOpts.region = options.region;
49667
- if (options.ecrRoleArn) taskOpts.ecrRoleArn = options.ecrRoleArn;
49668
- if (profileCredsFile && !assumedCredentials) taskOpts.profileCredentialsFile = {
49669
- hostPath: profileCredsFile.hostPath,
49670
- containerPath: profileCredsFile.containerPath,
49671
- profileName: profileCredsFile.profileName
48784
+ function serviceStrategy(_options) {
48785
+ return {
48786
+ pickEntries: (stacks) => listTargets(stacks).ecsServices,
48787
+ pickerMessage: "Select one or more ECS services to run",
48788
+ pickerNoun: "ECS services",
48789
+ onMissing: () => new LocalStartServiceError(`${getEmbedConfig().cliName} start-service requires at least one <target>. Pass one or more service paths like 'Stack/Orders' 'Stack/Frontend', or run it in a TTY to pick interactively.`),
48790
+ resolveBoots: (_stacks, chosenTargets) => ({
48791
+ boots: chosenTargets.map((target) => ({ target })),
48792
+ warnings: []
48793
+ }),
48794
+ lbPortOverrides: {}
49672
48795
  };
49673
- return startEcsService(service, {
49674
- maxTasks: options.maxTasks,
49675
- restartPolicy: options.restartPolicy,
49676
- taskOptions: taskOpts,
49677
- discovery
49678
- }, runState);
49679
- }
49680
- async function resolvePlaceholderAccount(arn, region) {
49681
- if (!arn.includes("${AWS::AccountId}")) return arn;
49682
- const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
49683
- const sts = new STSClient({ ...region && { region } });
49684
- try {
49685
- const account = (await sts.send(new GetCallerIdentityCommand({}))).Account;
49686
- if (!account) throw new LocalStartServiceError(`--assume-task-role: GetCallerIdentity returned no Account; cannot resolve placeholder ARN '${arn}'.`);
49687
- return arn.split(TASK_ROLE_ACCOUNT_PLACEHOLDER).join(account);
49688
- } finally {
49689
- sts.destroy();
49690
- }
49691
- }
49692
- async function assumeTaskRole(roleArn, region) {
49693
- const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
49694
- const sts = new STSClient({ ...region && { region } });
49695
- try {
49696
- const creds = (await sts.send(new AssumeRoleCommand({
49697
- RoleArn: roleArn,
49698
- RoleSessionName: `cdkd-local-start-service-${Date.now()}`,
49699
- DurationSeconds: 3600
49700
- }))).Credentials;
49701
- if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new LocalStartServiceError(`AssumeRole(${roleArn}) returned no usable credentials.`);
49702
- return {
49703
- accessKeyId: creds.AccessKeyId,
49704
- secretAccessKey: creds.SecretAccessKey,
49705
- sessionToken: creds.SessionToken
49706
- };
49707
- } finally {
49708
- sts.destroy();
49709
- }
49710
48796
  }
49711
48797
  /**
49712
- * Build the substitution context the ECS resolver consumes. Identical
49713
- * shape to `local-run-task.ts:buildEcsImageResolutionContext` only
49714
- * the candidate stack picker differs because services and tasks share
49715
- * the same stack-pattern grammar.
48798
+ * `cdkl start-service <Stack/Service>...` run one or more `AWS::ECS::Service`
48799
+ * resources locally as a long-running emulator. Spins up DesiredCount task
48800
+ * replicas per service (clamped by --max-tasks) using the same per-task
48801
+ * docker network + metadata sidecar pattern as `cdkd local run-task`, then
48802
+ * keeps each replica running and restarts it on exit per --restart-policy.
48803
+ * ^C tears every replica + sidecar + network down. When two or more
48804
+ * <target>s are supplied, every service is booted into a shared Cloud Map /
48805
+ * Service Connect registry so peer services discover each other via docker
48806
+ * --add-host overlay (Issue #460).
49716
48807
  */
49717
- async function buildEcsImageResolutionContext$1(target, stacks, options, stateProvider) {
49718
- const logger = getLogger();
49719
- const candidate = pickCandidateStack(parseEcsTarget(target).stackPattern, stacks);
49720
- if (!candidate) return void 0;
49721
- const needs = detectEcsImageResolutionNeeds(candidate);
49722
- if (!needs.needsPseudoParameters && !needs.needsStateResources && !needs.needsEnvOrSecretSubstitution) return;
49723
- const ctx = {};
49724
- const wantsPseudoForEnvOrSecret = !!stateProvider && needs.needsEnvOrSecretSubstitution;
49725
- if (needs.needsPseudoParameters || wantsPseudoForEnvOrSecret) {
49726
- const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? candidate.region;
49727
- if (!region) logger.warn("Resolver references ${AWS::Region} but cdkd could not determine the target region. Pass --region, set AWS_REGION, or declare env.region on the CDK stack.");
49728
- let accountId;
49729
- try {
49730
- accountId = await resolveCallerAccountId$1(region);
49731
- } catch (err) {
49732
- logger.warn(`Resolver needs \${AWS::AccountId} but STS GetCallerIdentity failed: ${err instanceof Error ? err.message : String(err)}. Substitution will be skipped; affected env / secret entries will be dropped with per-key warnings.`);
49733
- }
49734
- const partitionAndSuffix = region ? derivePartitionAndUrlSuffix(region) : void 0;
49735
- ctx.pseudoParameters = {
49736
- ...accountId !== void 0 && { accountId },
49737
- ...region !== void 0 && { region },
49738
- ...partitionAndSuffix && {
49739
- partition: partitionAndSuffix.partition,
49740
- urlSuffix: partitionAndSuffix.urlSuffix
49741
- }
49742
- };
49743
- }
49744
- const wantsState = needs.needsStateResources || needs.needsEnvOrSecretSubstitution;
49745
- if (stateProvider && wantsState) {
49746
- const loaded = await stateProvider.load(candidate.stackName, candidate.region);
49747
- if (loaded) ctx.stateResources = loaded.resources;
49748
- } else if (!stateProvider && needs.needsStateResources) logger.warn("Container Image references a same-stack AWS::ECR::Repository. Pass --from-state (cdkd-deployed) or --from-cfn-stack (cdk-deployed) to substitute the deployed repository URI.");
49749
- else if (!stateProvider && needs.needsEnvOrSecretSubstitution) logger.warn("Container Environment / Secrets entries contain CloudFormation intrinsics. Pass --from-state (cdkd-deployed) or --from-cfn-stack (cdk-deployed) to substitute them against the deployed cdkd state.");
49750
- return ctx;
49751
- }
49752
- function pickCandidateStack(stackPattern, stacks) {
49753
- if (stackPattern === null) {
49754
- if (stacks.length === 1) return stacks[0];
49755
- return;
49756
- }
49757
- const matched = matchStacks(stacks, [stackPattern]);
49758
- if (matched.length === 1) return matched[0];
49759
- }
49760
- async function resolveCallerAccountId$1(region) {
49761
- const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
49762
- const sts = new STSClient({ ...region && { region } });
49763
- try {
49764
- return (await sts.send(new GetCallerIdentityCommand({}))).Account;
49765
- } finally {
49766
- sts.destroy();
49767
- }
49768
- }
49769
- function readEnvOverridesFile$2(filePath) {
49770
- if (!filePath) return void 0;
49771
- let raw;
49772
- try {
49773
- raw = readFileSync(filePath, "utf-8");
49774
- } catch (err) {
49775
- throw new LocalStartServiceError(`Failed to read --env-vars file '${filePath}': ${err instanceof Error ? err.message : String(err)}`);
49776
- }
49777
- let parsed;
49778
- try {
49779
- parsed = JSON.parse(raw);
49780
- } catch (err) {
49781
- throw new LocalStartServiceError(`Failed to parse --env-vars file '${filePath}' as JSON: ${err instanceof Error ? err.message : String(err)}`);
49782
- }
49783
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new LocalStartServiceError(`--env-vars file '${filePath}' must contain a JSON object at the top level.`);
49784
- return parsed;
49785
- }
49786
- function parsePositiveInt(raw, flagName) {
49787
- const parsed = parseInt(raw, 10);
49788
- if (!Number.isFinite(parsed) || parsed < 1) throw new LocalStartServiceError(`${flagName} must be a positive integer (got '${raw}').`);
49789
- return parsed;
49790
- }
49791
- /**
49792
- * Hard cap on `--max-tasks` driven by the per-replica subnet allocator
49793
- * in `ecs-service-runner.ts:pickSubnetOctet`. The allocator walks the
49794
- * link-local /24 range `169.254.170.0..169.254.253.0` and **skips 171**
49795
- * because that octet is owned by the shared-service network
49796
- * (`SHARED_SVC_SUBNET_OCTET`) — assigning a per-replica network the
49797
- * same /24 would have docker reject the duplicate-subnet `network
49798
- * create`. The usable count is therefore 83 (Issue #544); beyond
49799
- * that, the modulo-wrap collapses replica N's `/24` onto replica 0's
49800
- * allocation and docker rejects the duplicate-subnet network creation
49801
- * with a cryptic "Pool overlaps with other one on this address space"
49802
- * error 30s into the boot — by which time some early replicas may
49803
- * have spent docker-run budget. Reject at parse time so the user
49804
- * gets an actionable error before any boot work fires.
49805
- *
49806
- * Raising this requires extending the allocator to walk a different
49807
- * IP range.
49808
- */
49809
- const MAX_TASKS_SUBNET_RANGE_CAP$1 = 83;
49810
- function parseMaxTasks$1(raw) {
49811
- const parsed = parsePositiveInt(raw, "--max-tasks");
49812
- 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.`);
49813
- return parsed;
49814
- }
49815
- function parseRestartPolicy$1(raw) {
49816
- if (raw === "on-failure" || raw === "always" || raw === "none") return raw;
49817
- throw new LocalStartServiceError(`--restart-policy must be one of 'on-failure', 'always', or 'none' (got '${raw}').`);
49818
- }
49819
- /**
49820
- * Issue #658: pick the credentials forwarded to the AWS-published
49821
- * `amazon-ecs-local-container-endpoints` sidecar. `cdkd local
49822
- * start-service`'s sidecar is SHARED across every replica boot in one
49823
- * CLI invocation (design § 5 Option A), so this resolves ONCE at
49824
- * startup. Precedence:
49825
- * 1. `--profile <p>` → resolved via {@link resolveProfileCredentials}
49826
- * (the SDK's default credential provider chain — SSO / IAM
49827
- * Identity Center / fromIni / role-assumption). NEW in this PR.
49828
- * 2. Not set → `undefined`; the sidecar runs with its own default
49829
- * credential chain (typically empty inside a fresh container —
49830
- * user containers will get 4xx from the credentials endpoint).
49831
- *
49832
- * Note: per-service `--assume-task-role <Service>=<arn>` overrides are
49833
- * INTENTIONALLY NOT consulted here. The shared sidecar has no concept
49834
- * of per-service IAM — per-service `TaskRoleArn` flows into each
49835
- * container's env via `buildMetadataEnv` at boot time, where the
49836
- * sidecar's `/role/<role-arn>` path resolves per-request. The shared
49837
- * sidecar's OWN startup credentials govern only the fallback path
49838
- * (containers that did not bind a `TaskRoleArn`).
49839
- *
49840
- * Extracted as an exported helper so a unit test can exercise both
49841
- * branches without having to mock the full Synth + Docker + AWS
49842
- * pipeline (the strategy PR #655 used for the Lambda container path).
49843
- */
49844
- async function resolveSharedSidecarCredentials$1(options) {
49845
- if (options.profile) return resolveProfileCredentials(options.profile);
49846
- }
49847
48808
  function createLocalStartServiceCommand() {
49848
- 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));
49849
- [
49850
- ...commonOptions,
49851
- ...appOptions,
49852
- ...contextOptions,
49853
- ...stateOptions
49854
- ].forEach((opt) => cmd.addOption(opt));
49855
- cmd.addOption(deprecatedRegionOption);
49856
- return cmd;
48809
+ return addCommonEcsServiceOptions(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). Omit <targets> in an interactive terminal to multi-select the ECS services from a list.").argument("[targets...]", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ECS::Service resources to run (omit to multi-select interactively in a TTY)").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. 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) => {
48810
+ await runEcsServiceEmulator(targets, options, serviceStrategy(options), cdkdExtraStateProviders);
48811
+ })));
49857
48812
  }
49858
48813
 
49859
48814
  //#endregion
@@ -52761,7 +51716,7 @@ function reorderArgs(argv) {
52761
51716
  */
52762
51717
  async function main() {
52763
51718
  const program = new Command();
52764
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.196.0");
51719
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.197.0");
52765
51720
  program.addCommand(createBootstrapCommand());
52766
51721
  program.addCommand(createSynthCommand());
52767
51722
  program.addCommand(createListCommand());