@go-to-k/cdkd 0.196.0 → 0.198.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/{aws-clients-B15NAPbL.js → aws-clients-DWUnLza1.js} +18 -2
- package/dist/{aws-clients-B15NAPbL.js.map → aws-clients-DWUnLza1.js.map} +1 -1
- package/dist/cli.js +248 -1185
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-C6v_fcDw.js → deploy-engine-B3Xc-bxB.js} +998 -9
- package/dist/deploy-engine-B3Xc-bxB.js.map +1 -0
- package/dist/index.js +2 -3
- package/package.json +1 -1
- package/dist/deploy-engine-C6v_fcDw.js.map +0 -1
- package/dist/docker-cmd-iDMcWcre.js +0 -1004
- package/dist/docker-cmd-iDMcWcre.js.map +0 -1
- package/dist/rolldown-runtime-CjeV3_4I.js +0 -18
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { A as S3StateBackend, B as
|
|
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-B3Xc-bxB.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";
|
|
@@ -17,9 +16,9 @@ import { APIGatewayClient, CreateAuthorizerCommand, CreateDeploymentCommand, Cre
|
|
|
17
16
|
import { CreateEventBusCommand, DeleteEventBusCommand, DeleteRuleCommand, DescribeEventBusCommand, DescribeRuleCommand, EventBridgeClient, ListEventBusesCommand, ListRulesCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$1, ListTargetsByRuleCommand, PutRuleCommand, PutTargetsCommand, RemoveTargetsCommand, ResourceNotFoundException as ResourceNotFoundException$2, TagResourceCommand as TagResourceCommand$4, UntagResourceCommand as UntagResourceCommand$4, UpdateEventBusCommand } from "@aws-sdk/client-eventbridge";
|
|
18
17
|
import { CreateSecretCommand, DeleteSecretCommand, DescribeSecretCommand, GetSecretValueCommand, ListSecretsCommand, RemoveRegionsFromReplicationCommand, ReplicateSecretToRegionsCommand, ResourceNotFoundException as ResourceNotFoundException$3, SecretsManagerClient, TagResourceCommand as TagResourceCommand$5, UntagResourceCommand as UntagResourceCommand$5, UpdateSecretCommand } from "@aws-sdk/client-secrets-manager";
|
|
19
18
|
import { AddTagsToResourceCommand, DeleteParameterCommand, DescribeParametersCommand, GetParameterCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$2, ParameterNotFound, PutParameterCommand, RemoveTagsFromResourceCommand, SSMClient } from "@aws-sdk/client-ssm";
|
|
20
|
-
import { CloudFrontClient, CreateCloudFrontOriginAccessIdentityCommand, CreateDistributionCommand, DeleteCloudFrontOriginAccessIdentityCommand, DeleteDistributionCommand, GetCloudFrontOriginAccessIdentityCommand, GetDistributionCommand, GetDistributionConfigCommand, ListDistributionsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$3, NoSuchCloudFrontOriginAccessIdentity, NoSuchDistribution, UpdateCloudFrontOriginAccessIdentityCommand, UpdateDistributionCommand } from "@aws-sdk/client-cloudfront";
|
|
21
|
-
import { CloudWatchClient, DeleteAlarmsCommand, DescribeAlarmsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$4, PutMetricAlarmCommand, TagResourceCommand as TagResourceCommand$
|
|
22
|
-
import { CloudWatchLogsClient, CreateLogGroupCommand, DeleteDataProtectionPolicyCommand, DeleteIndexPolicyCommand, DeleteLogGroupCommand, DeleteRetentionPolicyCommand, DescribeIndexPoliciesCommand, DescribeLogGroupsCommand, GetDataProtectionPolicyCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$5, PutBearerTokenAuthenticationCommand, PutDataProtectionPolicyCommand, PutIndexPolicyCommand, PutLogGroupDeletionProtectionCommand, PutRetentionPolicyCommand, ResourceAlreadyExistsException, ResourceNotFoundException as ResourceNotFoundException$4, TagResourceCommand as TagResourceCommand$
|
|
19
|
+
import { CloudFrontClient, CreateCloudFrontOriginAccessIdentityCommand, CreateDistributionCommand, CreateDistributionWithTagsCommand, DeleteCloudFrontOriginAccessIdentityCommand, DeleteDistributionCommand, GetCloudFrontOriginAccessIdentityCommand, GetDistributionCommand, GetDistributionConfigCommand, ListDistributionsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$3, NoSuchCloudFrontOriginAccessIdentity, NoSuchDistribution, TagResourceCommand as TagResourceCommand$6, UntagResourceCommand as UntagResourceCommand$6, UpdateCloudFrontOriginAccessIdentityCommand, UpdateDistributionCommand } from "@aws-sdk/client-cloudfront";
|
|
20
|
+
import { CloudWatchClient, DeleteAlarmsCommand, DescribeAlarmsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$4, PutMetricAlarmCommand, TagResourceCommand as TagResourceCommand$7, UntagResourceCommand as UntagResourceCommand$7 } from "@aws-sdk/client-cloudwatch";
|
|
21
|
+
import { CloudWatchLogsClient, CreateLogGroupCommand, DeleteDataProtectionPolicyCommand, DeleteIndexPolicyCommand, DeleteLogGroupCommand, DeleteRetentionPolicyCommand, DescribeIndexPoliciesCommand, DescribeLogGroupsCommand, GetDataProtectionPolicyCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$5, PutBearerTokenAuthenticationCommand, PutDataProtectionPolicyCommand, PutIndexPolicyCommand, PutLogGroupDeletionProtectionCommand, PutRetentionPolicyCommand, ResourceAlreadyExistsException, ResourceNotFoundException as ResourceNotFoundException$4, TagResourceCommand as TagResourceCommand$8, UntagResourceCommand as UntagResourceCommand$8 } from "@aws-sdk/client-cloudwatch-logs";
|
|
23
22
|
import { BedrockAgentCoreControlClient, CreateAgentRuntimeCommand, DeleteAgentRuntimeCommand, GetAgentRuntimeCommand, ResourceNotFoundException as ResourceNotFoundException$5, UpdateAgentRuntimeCommand } from "@aws-sdk/client-bedrock-agentcore-control";
|
|
24
23
|
import { ACMClient, AddTagsToCertificateCommand, DeleteCertificateCommand, DescribeCertificateCommand, ListCertificatesCommand, ListTagsForCertificateCommand, RemoveTagsFromCertificateCommand, RequestCertificateCommand, ResourceNotFoundException as ResourceNotFoundException$6, UpdateCertificateOptionsCommand } from "@aws-sdk/client-acm";
|
|
25
24
|
import * as fs from "node:fs";
|
|
@@ -30,8 +29,8 @@ import { execFile, spawn } from "node:child_process";
|
|
|
30
29
|
import { tmpdir } from "node:os";
|
|
31
30
|
import { AssociateVPCWithHostedZoneCommand, ChangeResourceRecordSetsCommand, ChangeTagsForResourceCommand, CreateHostedZoneCommand, CreateQueryLoggingConfigCommand, DeleteHostedZoneCommand, DeleteQueryLoggingConfigCommand, DisassociateVPCFromHostedZoneCommand, GetHostedZoneCommand, ListHostedZonesByNameCommand, ListHostedZonesCommand, ListQueryLoggingConfigsCommand, ListResourceRecordSetsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$6, Route53Client, UpdateHostedZoneCommentCommand, UpdateHostedZoneFeaturesCommand } from "@aws-sdk/client-route-53";
|
|
32
31
|
import { AddTagsCommand, CreateListenerCommand, CreateLoadBalancerCommand, CreateTargetGroupCommand, DeleteListenerCommand, DeleteLoadBalancerCommand, DeleteTargetGroupCommand, DescribeListenersCommand, DescribeLoadBalancerAttributesCommand, DescribeLoadBalancersCommand, DescribeTagsCommand, DescribeTargetGroupsCommand, ElasticLoadBalancingV2Client, ModifyListenerCommand, ModifyLoadBalancerAttributesCommand, ModifyTargetGroupCommand, RemoveTagsCommand, SetIpAddressTypeCommand, SetSecurityGroupsCommand, SetSubnetsCommand } from "@aws-sdk/client-elastic-load-balancing-v2";
|
|
33
|
-
import { CreateAliasCommand, CreateKeyCommand, DeleteAliasCommand, DescribeKeyCommand, DisableKeyCommand, DisableKeyRotationCommand, EnableKeyCommand, EnableKeyRotationCommand, GetKeyPolicyCommand, GetKeyRotationStatusCommand, KMSClient, ListAliasesCommand, ListKeysCommand, ListResourceTagsCommand, NotFoundException as NotFoundException$2, PutKeyPolicyCommand, ScheduleKeyDeletionCommand, TagResourceCommand as TagResourceCommand$
|
|
34
|
-
import { CreateRepositoryCommand, DeleteLifecyclePolicyCommand, DeleteRepositoryCommand, DeleteRepositoryPolicyCommand, DescribeRepositoriesCommand, ECRClient, GetAuthorizationTokenCommand, GetLifecyclePolicyCommand, LifecyclePolicyNotFoundException, ListTagsForResourceCommand as ListTagsForResourceCommand$7, PutImageScanningConfigurationCommand, PutImageTagMutabilityCommand, PutLifecyclePolicyCommand, RepositoryNotFoundException, SetRepositoryPolicyCommand, TagResourceCommand as TagResourceCommand$
|
|
32
|
+
import { CreateAliasCommand, CreateKeyCommand, DeleteAliasCommand, DescribeKeyCommand, DisableKeyCommand, DisableKeyRotationCommand, EnableKeyCommand, EnableKeyRotationCommand, GetKeyPolicyCommand, GetKeyRotationStatusCommand, KMSClient, ListAliasesCommand, ListKeysCommand, ListResourceTagsCommand, NotFoundException as NotFoundException$2, PutKeyPolicyCommand, ScheduleKeyDeletionCommand, TagResourceCommand as TagResourceCommand$9, UntagResourceCommand as UntagResourceCommand$9, UpdateAliasCommand, UpdateKeyDescriptionCommand } from "@aws-sdk/client-kms";
|
|
33
|
+
import { CreateRepositoryCommand, DeleteLifecyclePolicyCommand, DeleteRepositoryCommand, DeleteRepositoryPolicyCommand, DescribeRepositoriesCommand, ECRClient, GetAuthorizationTokenCommand, GetLifecyclePolicyCommand, LifecyclePolicyNotFoundException, ListTagsForResourceCommand as ListTagsForResourceCommand$7, PutImageScanningConfigurationCommand, PutImageTagMutabilityCommand, PutLifecyclePolicyCommand, RepositoryNotFoundException, SetRepositoryPolicyCommand, TagResourceCommand as TagResourceCommand$10 } from "@aws-sdk/client-ecr";
|
|
35
34
|
import graphlib from "graphlib";
|
|
36
35
|
import { AddTagsToResourceCommand as AddTagsToResourceCommand$1, CreateDBClusterCommand, CreateDBInstanceCommand, CreateDBProxyCommand, CreateDBProxyEndpointCommand, CreateDBSubnetGroupCommand, DBProxyEndpointNotFoundFault, DBProxyNotFoundFault, DBProxyTargetGroupNotFoundFault, DBProxyTargetNotFoundFault, DeleteDBClusterCommand, DeleteDBInstanceCommand, DeleteDBProxyCommand, DeleteDBProxyEndpointCommand, DeleteDBSubnetGroupCommand, DeregisterDBProxyTargetsCommand, DescribeDBClustersCommand, DescribeDBInstancesCommand, DescribeDBProxiesCommand, DescribeDBProxyEndpointsCommand, DescribeDBProxyTargetGroupsCommand, DescribeDBProxyTargetsCommand, DescribeDBSubnetGroupsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$8, ModifyDBClusterCommand, ModifyDBInstanceCommand, ModifyDBProxyCommand, ModifyDBProxyEndpointCommand, ModifyDBProxyTargetGroupCommand, ModifyDBSubnetGroupCommand, RDSClient, RegisterDBProxyTargetsCommand, RemoveTagsFromResourceCommand as RemoveTagsFromResourceCommand$1 } from "@aws-sdk/client-rds";
|
|
37
36
|
import { Command, Option } from "commander";
|
|
@@ -42,18 +41,18 @@ import readline from "node:readline/promises";
|
|
|
42
41
|
import * as zlib from "node:zlib";
|
|
43
42
|
import { ApplicationAutoScalingClient, DeleteScalingPolicyCommand, DeregisterScalableTargetCommand, DescribeScalableTargetsCommand, DescribeScalingPoliciesCommand, PutScalingPolicyCommand, RegisterScalableTargetCommand } from "@aws-sdk/client-application-auto-scaling";
|
|
44
43
|
import { ApiGatewayV2Client, CreateApiCommand, CreateAuthorizerCommand as CreateAuthorizerCommand$1, CreateIntegrationCommand, CreateRouteCommand as CreateRouteCommand$1, CreateStageCommand as CreateStageCommand$1, DeleteApiCommand, DeleteAuthorizerCommand as DeleteAuthorizerCommand$1, DeleteIntegrationCommand, DeleteRouteCommand as DeleteRouteCommand$1, DeleteStageCommand as DeleteStageCommand$1, GetApiCommand, GetApisCommand, GetAuthorizerCommand as GetAuthorizerCommand$1, GetIntegrationCommand, GetRouteCommand, GetStageCommand as GetStageCommand$1, NotFoundException as NotFoundException$3, UpdateApiCommand, UpdateAuthorizerCommand as UpdateAuthorizerCommand$1, UpdateIntegrationCommand, UpdateRouteCommand, UpdateStageCommand as UpdateStageCommand$1 } from "@aws-sdk/client-apigatewayv2";
|
|
45
|
-
import { CreateStateMachineCommand, DeleteStateMachineCommand, DescribeStateMachineCommand, ListStateMachinesCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$9, SFNClient, StateMachineDoesNotExist, TagResourceCommand as TagResourceCommand$
|
|
46
|
-
import { CreateClusterCommand, CreateServiceCommand, DeleteClusterCommand, DeleteServiceCommand, DeregisterTaskDefinitionCommand, DescribeClustersCommand, DescribeServicesCommand, DescribeTaskDefinitionCommand, ECSClient, ListClustersCommand, ListServicesCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$10, PutClusterCapacityProvidersCommand, RegisterTaskDefinitionCommand, TagResourceCommand as TagResourceCommand$
|
|
44
|
+
import { CreateStateMachineCommand, DeleteStateMachineCommand, DescribeStateMachineCommand, ListStateMachinesCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$9, SFNClient, StateMachineDoesNotExist, TagResourceCommand as TagResourceCommand$11, UntagResourceCommand as UntagResourceCommand$10, UpdateStateMachineCommand } from "@aws-sdk/client-sfn";
|
|
45
|
+
import { CreateClusterCommand, CreateServiceCommand, DeleteClusterCommand, DeleteServiceCommand, DeregisterTaskDefinitionCommand, DescribeClustersCommand, DescribeServicesCommand, DescribeTaskDefinitionCommand, ECSClient, ListClustersCommand, ListServicesCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$10, PutClusterCapacityProvidersCommand, RegisterTaskDefinitionCommand, TagResourceCommand as TagResourceCommand$12, UntagResourceCommand as UntagResourceCommand$11, UpdateClusterCommand, UpdateServiceCommand } from "@aws-sdk/client-ecs";
|
|
47
46
|
import { AddTagsToResourceCommand as AddTagsToResourceCommand$2, CreateDBClusterCommand as CreateDBClusterCommand$1, CreateDBInstanceCommand as CreateDBInstanceCommand$1, CreateDBSubnetGroupCommand as CreateDBSubnetGroupCommand$1, DeleteDBClusterCommand as DeleteDBClusterCommand$1, DeleteDBInstanceCommand as DeleteDBInstanceCommand$1, DeleteDBSubnetGroupCommand as DeleteDBSubnetGroupCommand$1, DescribeDBClustersCommand as DescribeDBClustersCommand$1, DescribeDBInstancesCommand as DescribeDBInstancesCommand$1, DescribeDBSubnetGroupsCommand as DescribeDBSubnetGroupsCommand$1, DocDBClient, ListTagsForResourceCommand as ListTagsForResourceCommand$11, ModifyDBClusterCommand as ModifyDBClusterCommand$1, ModifyDBInstanceCommand as ModifyDBInstanceCommand$1, ModifyDBSubnetGroupCommand as ModifyDBSubnetGroupCommand$1, RemoveTagsFromResourceCommand as RemoveTagsFromResourceCommand$2 } from "@aws-sdk/client-docdb";
|
|
48
47
|
import { AddTagsToResourceCommand as AddTagsToResourceCommand$3, CreateDBClusterCommand as CreateDBClusterCommand$2, CreateDBInstanceCommand as CreateDBInstanceCommand$2, CreateDBSubnetGroupCommand as CreateDBSubnetGroupCommand$2, DeleteDBClusterCommand as DeleteDBClusterCommand$2, DeleteDBInstanceCommand as DeleteDBInstanceCommand$2, DeleteDBSubnetGroupCommand as DeleteDBSubnetGroupCommand$2, DescribeDBClustersCommand as DescribeDBClustersCommand$2, DescribeDBInstancesCommand as DescribeDBInstancesCommand$2, DescribeDBSubnetGroupsCommand as DescribeDBSubnetGroupsCommand$2, ListTagsForResourceCommand as ListTagsForResourceCommand$12, ModifyDBClusterCommand as ModifyDBClusterCommand$2, ModifyDBInstanceCommand as ModifyDBInstanceCommand$2, ModifyDBSubnetGroupCommand as ModifyDBSubnetGroupCommand$2, NeptuneClient, RemoveTagsFromResourceCommand as RemoveTagsFromResourceCommand$3 } from "@aws-sdk/client-neptune";
|
|
49
|
-
import { CreateWebACLCommand, DeleteWebACLCommand, GetWebACLCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$13, ListWebACLsCommand, TagResourceCommand as TagResourceCommand$
|
|
48
|
+
import { CreateWebACLCommand, DeleteWebACLCommand, GetWebACLCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$13, ListWebACLsCommand, TagResourceCommand as TagResourceCommand$13, UntagResourceCommand as UntagResourceCommand$12, UpdateWebACLCommand, WAFNonexistentItemException, WAFV2Client } from "@aws-sdk/client-wafv2";
|
|
50
49
|
import { CognitoIdentityProviderClient, CreateUserPoolCommand, DeleteUserPoolCommand, DescribeUserPoolCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$14, ListUserPoolsCommand, ResourceNotFoundException as ResourceNotFoundException$7, UpdateUserPoolCommand } from "@aws-sdk/client-cognito-identity-provider";
|
|
51
50
|
import { AddTagsToResourceCommand as AddTagsToResourceCommand$4, CreateCacheClusterCommand, CreateCacheSubnetGroupCommand, DeleteCacheClusterCommand, DeleteCacheSubnetGroupCommand, DescribeCacheClustersCommand, DescribeCacheSubnetGroupsCommand, ElastiCacheClient, ListTagsForResourceCommand as ListTagsForResourceCommand$15, ModifyCacheClusterCommand, ModifyCacheSubnetGroupCommand, RemoveTagsFromResourceCommand as RemoveTagsFromResourceCommand$4 } from "@aws-sdk/client-elasticache";
|
|
52
51
|
import { CreatePrivateDnsNamespaceCommand, CreateServiceCommand as CreateServiceCommand$1, DeleteNamespaceCommand, DeleteServiceCommand as DeleteServiceCommand$1, GetNamespaceCommand, GetOperationCommand, GetServiceCommand, ListNamespacesCommand, ListServicesCommand as ListServicesCommand$1, ListTagsForResourceCommand as ListTagsForResourceCommand$16, NamespaceNotFound, ServiceDiscoveryClient, ServiceNotFound, UpdatePrivateDnsNamespaceCommand, UpdateServiceCommand as UpdateServiceCommand$1 } from "@aws-sdk/client-servicediscovery";
|
|
53
|
-
import { AppSyncClient, CreateApiKeyCommand, CreateDataSourceCommand, CreateGraphqlApiCommand, CreateResolverCommand, DeleteApiKeyCommand, DeleteDataSourceCommand, DeleteGraphqlApiCommand, DeleteResolverCommand, GetDataSourceCommand, GetGraphqlApiCommand, GetIntrospectionSchemaCommand, GetResolverCommand, ListApiKeysCommand, ListGraphqlApisCommand, NotFoundException as NotFoundException$4, StartSchemaCreationCommand, TagResourceCommand as TagResourceCommand$
|
|
52
|
+
import { AppSyncClient, CreateApiKeyCommand, CreateDataSourceCommand, CreateGraphqlApiCommand, CreateResolverCommand, DeleteApiKeyCommand, DeleteDataSourceCommand, DeleteGraphqlApiCommand, DeleteResolverCommand, GetDataSourceCommand, GetGraphqlApiCommand, GetIntrospectionSchemaCommand, GetResolverCommand, ListApiKeysCommand, ListGraphqlApisCommand, NotFoundException as NotFoundException$4, StartSchemaCreationCommand, TagResourceCommand as TagResourceCommand$14, UntagResourceCommand as UntagResourceCommand$13, UpdateApiKeyCommand, UpdateDataSourceCommand, UpdateGraphqlApiCommand, UpdateResolverCommand } from "@aws-sdk/client-appsync";
|
|
54
53
|
import { parse, print } from "graphql";
|
|
55
54
|
import { CreateConnectionCommand, CreateCrawlerCommand, CreateDatabaseCommand, CreateJobCommand, CreateSecurityConfigurationCommand, CreateTableCommand as CreateTableCommand$1, CreateTriggerCommand, CreateWorkflowCommand, DeleteConnectionCommand, DeleteCrawlerCommand, DeleteDatabaseCommand, DeleteJobCommand, DeleteSecurityConfigurationCommand, DeleteTableCommand as DeleteTableCommand$1, DeleteTriggerCommand, DeleteWorkflowCommand, EntityNotFoundException, GetConnectionCommand, GetCrawlerCommand, GetDatabaseCommand, GetDatabasesCommand, GetJobCommand, GetSecurityConfigurationCommand, GetSecurityConfigurationsCommand, GetTableCommand, GetTablesCommand, GetTagsCommand, GetTriggerCommand, GetWorkflowCommand, GlueClient, ListWorkflowsCommand, StartCrawlerScheduleCommand, StartTriggerCommand, StopCrawlerScheduleCommand, StopTriggerCommand, UpdateConnectionCommand, UpdateCrawlerCommand, UpdateDatabaseCommand, UpdateJobCommand, UpdateTableCommand as UpdateTableCommand$1, UpdateTriggerCommand, UpdateWorkflowCommand } from "@aws-sdk/client-glue";
|
|
56
|
-
import { AddTagsToStreamCommand, CreateStreamCommand, DecreaseStreamRetentionPeriodCommand, DeleteStreamCommand, DeregisterStreamConsumerCommand, DescribeStreamCommand, DescribeStreamConsumerCommand, IncreaseStreamRetentionPeriodCommand, KinesisClient, ListStreamsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$17, ListTagsForStreamCommand, RegisterStreamConsumerCommand, RemoveTagsFromStreamCommand, ResourceNotFoundException as ResourceNotFoundException$8, StartStreamEncryptionCommand, StopStreamEncryptionCommand, TagResourceCommand as TagResourceCommand$
|
|
55
|
+
import { AddTagsToStreamCommand, CreateStreamCommand, DecreaseStreamRetentionPeriodCommand, DeleteStreamCommand, DeregisterStreamConsumerCommand, DescribeStreamCommand, DescribeStreamConsumerCommand, IncreaseStreamRetentionPeriodCommand, KinesisClient, ListStreamsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$17, ListTagsForStreamCommand, RegisterStreamConsumerCommand, RemoveTagsFromStreamCommand, ResourceNotFoundException as ResourceNotFoundException$8, StartStreamEncryptionCommand, StopStreamEncryptionCommand, TagResourceCommand as TagResourceCommand$15, UntagResourceCommand as UntagResourceCommand$14, UpdateShardCountCommand } from "@aws-sdk/client-kinesis";
|
|
57
56
|
import { AccessPointNotFound, CreateAccessPointCommand, CreateFileSystemCommand, CreateMountTargetCommand, DeleteAccessPointCommand, DeleteFileSystemCommand, DeleteMountTargetCommand, DescribeAccessPointsCommand, DescribeBackupPolicyCommand, DescribeFileSystemsCommand, DescribeLifecycleConfigurationCommand, DescribeMountTargetSecurityGroupsCommand, DescribeMountTargetsCommand, EFSClient, FileSystemNotFound, ModifyMountTargetSecurityGroupsCommand, MountTargetNotFound, UpdateFileSystemCommand } from "@aws-sdk/client-efs";
|
|
58
57
|
import { CreateDeliveryStreamCommand, DeleteDeliveryStreamCommand, DescribeDeliveryStreamCommand, FirehoseClient, ListDeliveryStreamsCommand, ListTagsForDeliveryStreamCommand, ResourceNotFoundException as ResourceNotFoundException$9, TagDeliveryStreamCommand, UntagDeliveryStreamCommand, UpdateDestinationCommand } from "@aws-sdk/client-firehose";
|
|
59
58
|
import { AddTagsCommand as AddTagsCommand$1, CloudTrailClient, CreateTrailCommand, DeleteTrailCommand, GetEventSelectorsCommand, GetInsightSelectorsCommand, GetTrailCommand, GetTrailStatusCommand, ListTagsCommand as ListTagsCommand$1, ListTrailsCommand, PutEventSelectorsCommand, PutInsightSelectorsCommand, RemoveTagsCommand as RemoveTagsCommand$1, StartLoggingCommand, StopLoggingCommand, TrailNotFoundException, UpdateTrailCommand } from "@aws-sdk/client-cloudtrail";
|
|
@@ -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,
|
|
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;
|
|
@@ -10862,14 +10894,14 @@ var LogsLogGroupProvider = class {
|
|
|
10862
10894
|
const arn = await this.buildArn(physicalId);
|
|
10863
10895
|
if (oldTags && oldTags.length > 0) {
|
|
10864
10896
|
const oldTagKeys = oldTags.map((t) => t.Key);
|
|
10865
|
-
await this.logsClient.send(new UntagResourceCommand$
|
|
10897
|
+
await this.logsClient.send(new UntagResourceCommand$8({
|
|
10866
10898
|
resourceArn: arn,
|
|
10867
10899
|
tagKeys: oldTagKeys
|
|
10868
10900
|
}));
|
|
10869
10901
|
}
|
|
10870
10902
|
if (newTags && newTags.length > 0) {
|
|
10871
10903
|
const tagsMap = Object.fromEntries(newTags.map((t) => [t.Key, t.Value]));
|
|
10872
|
-
await this.logsClient.send(new TagResourceCommand$
|
|
10904
|
+
await this.logsClient.send(new TagResourceCommand$8({
|
|
10873
10905
|
resourceArn: arn,
|
|
10874
10906
|
tags: tagsMap
|
|
10875
10907
|
}));
|
|
@@ -11225,14 +11257,14 @@ var CloudWatchAlarmProvider = class {
|
|
|
11225
11257
|
const tagsToRemove = [];
|
|
11226
11258
|
for (const k of oldMap.keys()) if (!newMap.has(k)) tagsToRemove.push(k);
|
|
11227
11259
|
if (tagsToRemove.length > 0) {
|
|
11228
|
-
await this.cloudWatchClient.send(new UntagResourceCommand$
|
|
11260
|
+
await this.cloudWatchClient.send(new UntagResourceCommand$7({
|
|
11229
11261
|
ResourceARN: resourceArn,
|
|
11230
11262
|
TagKeys: tagsToRemove
|
|
11231
11263
|
}));
|
|
11232
11264
|
this.logger.debug(`Removed ${tagsToRemove.length} tag(s) from alarm ${resourceArn}`);
|
|
11233
11265
|
}
|
|
11234
11266
|
if (tagsToAdd.length > 0) {
|
|
11235
|
-
await this.cloudWatchClient.send(new TagResourceCommand$
|
|
11267
|
+
await this.cloudWatchClient.send(new TagResourceCommand$7({
|
|
11236
11268
|
ResourceARN: resourceArn,
|
|
11237
11269
|
Tags: tagsToAdd
|
|
11238
11270
|
}));
|
|
@@ -17106,7 +17138,7 @@ const CACHE_BEHAVIOR_QUANTITY_FIELDS = [
|
|
|
17106
17138
|
var CloudFrontDistributionProvider = class {
|
|
17107
17139
|
cloudFrontClient;
|
|
17108
17140
|
logger = getLogger().child("CloudFrontDistributionProvider");
|
|
17109
|
-
handledProperties = new Map([["AWS::CloudFront::Distribution", new Set(["DistributionConfig"])]]);
|
|
17141
|
+
handledProperties = new Map([["AWS::CloudFront::Distribution", new Set(["DistributionConfig", "Tags"])]]);
|
|
17110
17142
|
constructor() {
|
|
17111
17143
|
const awsClients = getAwsClients();
|
|
17112
17144
|
this.cloudFrontClient = awsClients.cloudFront;
|
|
@@ -17122,7 +17154,11 @@ var CloudFrontDistributionProvider = class {
|
|
|
17122
17154
|
...distributionConfig,
|
|
17123
17155
|
CallerReference: `${Date.now()}-${logicalId}-${Math.random().toString(36).slice(2, 8)}`
|
|
17124
17156
|
});
|
|
17125
|
-
const
|
|
17157
|
+
const sdkTags = this.toSdkTags(properties["Tags"]);
|
|
17158
|
+
const distribution = (sdkTags ? await this.cloudFrontClient.send(new CreateDistributionWithTagsCommand({ DistributionConfigWithTags: {
|
|
17159
|
+
DistributionConfig: sdkConfig,
|
|
17160
|
+
Tags: { Items: sdkTags }
|
|
17161
|
+
} })) : await this.cloudFrontClient.send(new CreateDistributionCommand({ DistributionConfig: sdkConfig }))).Distribution;
|
|
17126
17162
|
const distributionId = distribution.Id;
|
|
17127
17163
|
const domainName = distribution.DomainName;
|
|
17128
17164
|
this.logger.debug(`Created CloudFront Distribution: ${distributionId} (${domainName})`);
|
|
@@ -17149,7 +17185,7 @@ var CloudFrontDistributionProvider = class {
|
|
|
17149
17185
|
* Gets the current config via GetDistributionConfigCommand, merges new properties,
|
|
17150
17186
|
* then calls UpdateDistributionCommand with the required IfMatch ETag.
|
|
17151
17187
|
*/
|
|
17152
|
-
async update(logicalId, physicalId, resourceType, properties,
|
|
17188
|
+
async update(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
17153
17189
|
this.logger.debug(`Updating CloudFront Distribution ${logicalId}: ${physicalId}`);
|
|
17154
17190
|
try {
|
|
17155
17191
|
const getConfigResponse = await this.cloudFrontClient.send(new GetDistributionConfigCommand({ Id: physicalId }));
|
|
@@ -17165,7 +17201,10 @@ var CloudFrontDistributionProvider = class {
|
|
|
17165
17201
|
IfMatch: etag,
|
|
17166
17202
|
DistributionConfig: sdkConfig
|
|
17167
17203
|
}));
|
|
17168
|
-
const
|
|
17204
|
+
const getResponse = await this.cloudFrontClient.send(new GetDistributionCommand({ Id: physicalId }));
|
|
17205
|
+
const domainName = getResponse.Distribution?.DomainName ?? "";
|
|
17206
|
+
const arn = getResponse.Distribution?.ARN;
|
|
17207
|
+
await this.tryApplyTagDiff(arn, previousProperties["Tags"], properties["Tags"], physicalId);
|
|
17169
17208
|
this.logger.debug(`Updated CloudFront Distribution ${physicalId}`);
|
|
17170
17209
|
return {
|
|
17171
17210
|
physicalId,
|
|
@@ -17379,6 +17418,107 @@ var CloudFrontDistributionProvider = class {
|
|
|
17379
17418
|
}
|
|
17380
17419
|
}
|
|
17381
17420
|
/**
|
|
17421
|
+
* Convert CFn `Tags: [{ Key, Value }]` to the CloudFront SDK's `Tag[]`
|
|
17422
|
+
* shape (which happens to be the same `{ Key, Value }` per-entry shape),
|
|
17423
|
+
* dropping entries missing a Key and normalizing missing-Value to `''`
|
|
17424
|
+
* (matching `tagsArrayToMap`'s shape so the create-path and update-diff-
|
|
17425
|
+
* path agree on what counts as "the same tag"). Returns `undefined`
|
|
17426
|
+
* when the input is absent or empty so the caller can route to
|
|
17427
|
+
* `CreateDistributionCommand` instead of `CreateDistributionWithTagsCommand`
|
|
17428
|
+
* — passing an empty `Tags.Items: []` to the latter is a silent no-op
|
|
17429
|
+
* but uses the tags-enabled control-plane path for nothing.
|
|
17430
|
+
*/
|
|
17431
|
+
toSdkTags(value) {
|
|
17432
|
+
const map = this.tagsArrayToMap(value);
|
|
17433
|
+
if (map.size === 0) return void 0;
|
|
17434
|
+
return [...map.entries()].map(([Key, Value]) => ({
|
|
17435
|
+
Key,
|
|
17436
|
+
Value
|
|
17437
|
+
}));
|
|
17438
|
+
}
|
|
17439
|
+
/**
|
|
17440
|
+
* Compute the (removed-keys, upserted-tags) diff between two CFn `Tags`
|
|
17441
|
+
* snapshots. Pure function — does NOT touch AWS, so the caller can
|
|
17442
|
+
* decide on the basis of the result whether the ARN is actually needed
|
|
17443
|
+
* (no diff = no ARN required).
|
|
17444
|
+
*/
|
|
17445
|
+
computeTagDiff(previousTags, newTags) {
|
|
17446
|
+
const prev = this.tagsArrayToMap(previousTags);
|
|
17447
|
+
const next = this.tagsArrayToMap(newTags);
|
|
17448
|
+
const removed = [...prev.keys()].filter((k) => !next.has(k));
|
|
17449
|
+
const upserts = [];
|
|
17450
|
+
for (const [k, v] of next.entries()) if (prev.get(k) !== v) upserts.push({
|
|
17451
|
+
Key: k,
|
|
17452
|
+
Value: v
|
|
17453
|
+
});
|
|
17454
|
+
return {
|
|
17455
|
+
removed,
|
|
17456
|
+
upserts
|
|
17457
|
+
};
|
|
17458
|
+
}
|
|
17459
|
+
/**
|
|
17460
|
+
* Apply a tag diff to a distribution, best-effort.
|
|
17461
|
+
*
|
|
17462
|
+
* CloudFront has no atomic overlay API for tags — `TagResource` adds /
|
|
17463
|
+
* overwrites and `UntagResource` removes. Run the removal first, then
|
|
17464
|
+
* the upsert, so a same-key value rewrite (which lands in `upserts`)
|
|
17465
|
+
* is not accidentally cleared by a stale Untag.
|
|
17466
|
+
*
|
|
17467
|
+
* Errors are logged but not rethrown — `update()` already succeeded
|
|
17468
|
+
* the `UpdateDistribution` call before this is invoked, so propagating
|
|
17469
|
+
* a tag-side error would flip a successful config update into a deploy
|
|
17470
|
+
* failure that triggers an idempotent retry. A `warn` surfaces the
|
|
17471
|
+
* unapplied delta to the operator without breaking the deploy.
|
|
17472
|
+
*
|
|
17473
|
+
* The ARN is unexpectedly absent only on a hypothetical SDK regression
|
|
17474
|
+
* (`GetDistribution` returns ARN as a required string in every SDK
|
|
17475
|
+
* shape verified so far). When that happens AND a tag delta exists,
|
|
17476
|
+
* log a warn so the silent-drop this PR is closing does not silently
|
|
17477
|
+
* resurface; when no delta exists, return without needing ARN.
|
|
17478
|
+
*/
|
|
17479
|
+
async tryApplyTagDiff(arn, previousTags, newTags, physicalId) {
|
|
17480
|
+
const { removed, upserts } = this.computeTagDiff(previousTags, newTags);
|
|
17481
|
+
if (removed.length === 0 && upserts.length === 0) return;
|
|
17482
|
+
if (!arn) {
|
|
17483
|
+
this.logger.warn(`CloudFront Distribution ${physicalId}: GetDistribution returned no ARN; skipping tag diff (removed=${removed.length}, upserts=${upserts.length}). Tags on AWS may drift from the template.`);
|
|
17484
|
+
return;
|
|
17485
|
+
}
|
|
17486
|
+
try {
|
|
17487
|
+
if (removed.length > 0) {
|
|
17488
|
+
this.logger.debug(`Untagging CloudFront Distribution ${arn}: ${removed.join(", ")}`);
|
|
17489
|
+
await this.cloudFrontClient.send(new UntagResourceCommand$6({
|
|
17490
|
+
Resource: arn,
|
|
17491
|
+
TagKeys: { Items: removed }
|
|
17492
|
+
}));
|
|
17493
|
+
}
|
|
17494
|
+
if (upserts.length > 0) {
|
|
17495
|
+
this.logger.debug(`Tagging CloudFront Distribution ${arn}: ${upserts.map((t) => t.Key).join(", ")}`);
|
|
17496
|
+
await this.cloudFrontClient.send(new TagResourceCommand$6({
|
|
17497
|
+
Resource: arn,
|
|
17498
|
+
Tags: { Items: upserts }
|
|
17499
|
+
}));
|
|
17500
|
+
}
|
|
17501
|
+
} catch (err) {
|
|
17502
|
+
this.logger.warn(`CloudFront Distribution ${physicalId}: tag diff failed (removed=${removed.length}, upserts=${upserts.length}): ${err instanceof Error ? err.message : String(err)}. UpdateDistribution itself succeeded; tags on AWS may drift from the template until the next deploy.`);
|
|
17503
|
+
}
|
|
17504
|
+
}
|
|
17505
|
+
/**
|
|
17506
|
+
* Convert a CFn `Tags: [{ Key, Value }]` array to a plain map. Entries
|
|
17507
|
+
* missing a `Key` are dropped; a missing `Value` becomes `''` so the
|
|
17508
|
+
* diff treats `{ Key: 'k' }` and `{ Key: 'k', Value: '' }` the same.
|
|
17509
|
+
*/
|
|
17510
|
+
tagsArrayToMap(value) {
|
|
17511
|
+
const map = /* @__PURE__ */ new Map();
|
|
17512
|
+
if (!Array.isArray(value)) return map;
|
|
17513
|
+
for (const entry of value) {
|
|
17514
|
+
const key = entry["Key"];
|
|
17515
|
+
if (typeof key !== "string") continue;
|
|
17516
|
+
const val = entry["Value"];
|
|
17517
|
+
map.set(key, typeof val === "string" ? val : "");
|
|
17518
|
+
}
|
|
17519
|
+
return map;
|
|
17520
|
+
}
|
|
17521
|
+
/**
|
|
17382
17522
|
* Adopt an existing CloudFront distribution into cdkd state.
|
|
17383
17523
|
*
|
|
17384
17524
|
* CloudFront distributions don't carry a template-supplied name
|
|
@@ -17942,14 +18082,14 @@ var StepFunctionsProvider = class {
|
|
|
17942
18082
|
const tagsToRemove = [];
|
|
17943
18083
|
for (const k of oldMap.keys()) if (!newMap.has(k)) tagsToRemove.push(k);
|
|
17944
18084
|
if (tagsToRemove.length > 0) {
|
|
17945
|
-
await this.getClient().send(new UntagResourceCommand$
|
|
18085
|
+
await this.getClient().send(new UntagResourceCommand$10({
|
|
17946
18086
|
resourceArn: stateMachineArn,
|
|
17947
18087
|
tagKeys: tagsToRemove
|
|
17948
18088
|
}));
|
|
17949
18089
|
this.logger.debug(`Removed ${tagsToRemove.length} tag(s) from SFN state machine ${stateMachineArn}`);
|
|
17950
18090
|
}
|
|
17951
18091
|
if (tagsToAdd.length > 0) {
|
|
17952
|
-
await this.getClient().send(new TagResourceCommand$
|
|
18092
|
+
await this.getClient().send(new TagResourceCommand$11({
|
|
17953
18093
|
resourceArn: stateMachineArn,
|
|
17954
18094
|
tags: tagsToAdd
|
|
17955
18095
|
}));
|
|
@@ -18457,14 +18597,14 @@ var ECSProvider = class {
|
|
|
18457
18597
|
const tagsToRemove = [];
|
|
18458
18598
|
for (const k of oldMap.keys()) if (!newMap.has(k)) tagsToRemove.push(k);
|
|
18459
18599
|
if (tagsToRemove.length > 0) {
|
|
18460
|
-
await this.getClient().send(new UntagResourceCommand$
|
|
18600
|
+
await this.getClient().send(new UntagResourceCommand$11({
|
|
18461
18601
|
resourceArn,
|
|
18462
18602
|
tagKeys: tagsToRemove
|
|
18463
18603
|
}));
|
|
18464
18604
|
this.logger.debug(`Removed ${tagsToRemove.length} tag(s) from ECS resource ${resourceArn}`);
|
|
18465
18605
|
}
|
|
18466
18606
|
if (tagsToAdd.length > 0) {
|
|
18467
|
-
await this.getClient().send(new TagResourceCommand$
|
|
18607
|
+
await this.getClient().send(new TagResourceCommand$12({
|
|
18468
18608
|
resourceArn,
|
|
18469
18609
|
tags: tagsToAdd
|
|
18470
18610
|
}));
|
|
@@ -23600,14 +23740,14 @@ var WAFv2WebACLProvider = class {
|
|
|
23600
23740
|
const tagsToRemove = [];
|
|
23601
23741
|
for (const k of oldMap.keys()) if (!newMap.has(k)) tagsToRemove.push(k);
|
|
23602
23742
|
if (tagsToRemove.length > 0) {
|
|
23603
|
-
await this.getClient().send(new UntagResourceCommand$
|
|
23743
|
+
await this.getClient().send(new UntagResourceCommand$12({
|
|
23604
23744
|
ResourceARN: arn,
|
|
23605
23745
|
TagKeys: tagsToRemove
|
|
23606
23746
|
}));
|
|
23607
23747
|
this.logger.debug(`Removed ${tagsToRemove.length} tag(s) from WAFv2 WebACL ${arn}`);
|
|
23608
23748
|
}
|
|
23609
23749
|
if (tagsToAdd.length > 0) {
|
|
23610
|
-
await this.getClient().send(new TagResourceCommand$
|
|
23750
|
+
await this.getClient().send(new TagResourceCommand$13({
|
|
23611
23751
|
ResourceARN: arn,
|
|
23612
23752
|
Tags: tagsToAdd
|
|
23613
23753
|
}));
|
|
@@ -25427,7 +25567,7 @@ var AppSyncProvider = class {
|
|
|
25427
25567
|
const tagsToAdd = {};
|
|
25428
25568
|
for (const [k, v] of Object.entries(newMap)) if (oldMap[k] !== v) tagsToAdd[k] = v;
|
|
25429
25569
|
if (tagKeysToRemove.length > 0) try {
|
|
25430
|
-
await this.getClient().send(new UntagResourceCommand$
|
|
25570
|
+
await this.getClient().send(new UntagResourceCommand$13({
|
|
25431
25571
|
resourceArn: arn,
|
|
25432
25572
|
tagKeys: tagKeysToRemove
|
|
25433
25573
|
}));
|
|
@@ -25435,7 +25575,7 @@ var AppSyncProvider = class {
|
|
|
25435
25575
|
throw this.wrapUpdateError(error, resourceType, logicalId, apiId, "GraphqlApi (untag)");
|
|
25436
25576
|
}
|
|
25437
25577
|
if (Object.keys(tagsToAdd).length > 0) try {
|
|
25438
|
-
await this.getClient().send(new TagResourceCommand$
|
|
25578
|
+
await this.getClient().send(new TagResourceCommand$14({
|
|
25439
25579
|
resourceArn: arn,
|
|
25440
25580
|
tags: tagsToAdd
|
|
25441
25581
|
}));
|
|
@@ -27889,14 +28029,14 @@ var KMSProvider = class {
|
|
|
27889
28029
|
const tagsToRemove = [];
|
|
27890
28030
|
for (const k of oldMap.keys()) if (!newMap.has(k)) tagsToRemove.push(k);
|
|
27891
28031
|
if (tagsToRemove.length > 0) {
|
|
27892
|
-
await this.getClient().send(new UntagResourceCommand$
|
|
28032
|
+
await this.getClient().send(new UntagResourceCommand$9({
|
|
27893
28033
|
KeyId: keyId,
|
|
27894
28034
|
TagKeys: tagsToRemove
|
|
27895
28035
|
}));
|
|
27896
28036
|
this.logger.debug(`Removed ${tagsToRemove.length} tag(s) from KMS Key ${keyId}`);
|
|
27897
28037
|
}
|
|
27898
28038
|
if (tagsToAdd.length > 0) {
|
|
27899
|
-
await this.getClient().send(new TagResourceCommand$
|
|
28039
|
+
await this.getClient().send(new TagResourceCommand$9({
|
|
27900
28040
|
KeyId: keyId,
|
|
27901
28041
|
Tags: tagsToAdd
|
|
27902
28042
|
}));
|
|
@@ -28678,14 +28818,14 @@ var KinesisStreamConsumerProvider = class {
|
|
|
28678
28818
|
const tagsToRemove = [];
|
|
28679
28819
|
for (const k of Object.keys(oldMap)) if (!(k in newMap)) tagsToRemove.push(k);
|
|
28680
28820
|
if (tagsToRemove.length > 0) {
|
|
28681
|
-
await this.getClient().send(new UntagResourceCommand$
|
|
28821
|
+
await this.getClient().send(new UntagResourceCommand$14({
|
|
28682
28822
|
ResourceARN: consumerArn,
|
|
28683
28823
|
TagKeys: tagsToRemove
|
|
28684
28824
|
}));
|
|
28685
28825
|
this.logger.debug(`Removed ${tagsToRemove.length} tag(s) from Kinesis stream consumer ${consumerArn}`);
|
|
28686
28826
|
}
|
|
28687
28827
|
if (Object.keys(tagsToAdd).length > 0) {
|
|
28688
|
-
await this.getClient().send(new TagResourceCommand$
|
|
28828
|
+
await this.getClient().send(new TagResourceCommand$15({
|
|
28689
28829
|
ResourceARN: consumerArn,
|
|
28690
28830
|
Tags: tagsToAdd
|
|
28691
28831
|
}));
|
|
@@ -32352,7 +32492,7 @@ var ECRProvider = class {
|
|
|
32352
32492
|
if (JSON.stringify(newTags) !== JSON.stringify(oldTags)) {
|
|
32353
32493
|
const repoArn = (await this.getClient().send(new DescribeRepositoriesCommand({ repositoryNames: [physicalId] }))).repositories?.[0]?.repositoryArn;
|
|
32354
32494
|
if (repoArn && newTags) {
|
|
32355
|
-
await this.getClient().send(new TagResourceCommand$
|
|
32495
|
+
await this.getClient().send(new TagResourceCommand$10({
|
|
32356
32496
|
resourceArn: repoArn,
|
|
32357
32497
|
tags: newTags
|
|
32358
32498
|
}));
|
|
@@ -43136,14 +43276,14 @@ function parseTarget(target) {
|
|
|
43136
43276
|
function resolveLambdaTarget(target, stacks) {
|
|
43137
43277
|
if (stacks.length === 0) throw new LocalInvokeResolutionError("No stacks found in the synthesized assembly.");
|
|
43138
43278
|
const parsed = parseTarget(target);
|
|
43139
|
-
const stack = pickStack$
|
|
43279
|
+
const stack = pickStack$2(parsed, stacks);
|
|
43140
43280
|
const template = stack.template;
|
|
43141
43281
|
const resources = template.Resources ?? {};
|
|
43142
43282
|
let match;
|
|
43143
43283
|
if (parsed.isPath) {
|
|
43144
43284
|
const index = buildCdkPathIndex(template);
|
|
43145
43285
|
const lambdaMatches = resolveCdkPathToLogicalIds(parsed.pathOrId, index).filter(({ logicalId }) => resources[logicalId]?.Type === "AWS::Lambda::Function");
|
|
43146
|
-
if (lambdaMatches.length === 0) throw notFoundError$
|
|
43286
|
+
if (lambdaMatches.length === 0) throw notFoundError$1(target, stack, resources);
|
|
43147
43287
|
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
43288
|
const m = lambdaMatches[0];
|
|
43149
43289
|
match = {
|
|
@@ -43152,7 +43292,7 @@ function resolveLambdaTarget(target, stacks) {
|
|
|
43152
43292
|
};
|
|
43153
43293
|
} else {
|
|
43154
43294
|
const resource = resources[parsed.pathOrId];
|
|
43155
|
-
if (!resource) throw notFoundError$
|
|
43295
|
+
if (!resource) throw notFoundError$1(target, stack, resources);
|
|
43156
43296
|
match = {
|
|
43157
43297
|
logicalId: parsed.pathOrId,
|
|
43158
43298
|
resource
|
|
@@ -43170,7 +43310,7 @@ function resolveLambdaTarget(target, stacks) {
|
|
|
43170
43310
|
* user may omit the stack prefix. Otherwise an explicit stack pattern is
|
|
43171
43311
|
* required.
|
|
43172
43312
|
*/
|
|
43173
|
-
function pickStack$
|
|
43313
|
+
function pickStack$2(parsed, stacks) {
|
|
43174
43314
|
if (parsed.stackPattern === null) {
|
|
43175
43315
|
if (stacks.length === 1) return stacks[0];
|
|
43176
43316
|
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 +43648,7 @@ function describeLayerEntry(entry) {
|
|
|
43508
43648
|
* the resolved stack so the user can copy/paste a valid target. Mirrors
|
|
43509
43649
|
* the format the issue spec calls out.
|
|
43510
43650
|
*/
|
|
43511
|
-
function notFoundError$
|
|
43651
|
+
function notFoundError$1(target, stack, resources) {
|
|
43512
43652
|
const lambdas = [];
|
|
43513
43653
|
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
43514
43654
|
if (resource.Type !== "AWS::Lambda::Function") continue;
|
|
@@ -43793,28 +43933,28 @@ function parseEcsTarget(target) {
|
|
|
43793
43933
|
function resolveEcsTaskTarget(target, stacks, context) {
|
|
43794
43934
|
if (stacks.length === 0) throw new EcsTaskResolutionError("No stacks found in the synthesized assembly.");
|
|
43795
43935
|
const parsed = parseEcsTarget(target);
|
|
43796
|
-
const stack = pickStack$
|
|
43936
|
+
const stack = pickStack$1(parsed, stacks);
|
|
43797
43937
|
const resources = stack.template.Resources ?? {};
|
|
43798
43938
|
let logicalId;
|
|
43799
43939
|
let resource;
|
|
43800
43940
|
if (parsed.isPath) {
|
|
43801
43941
|
const index = buildCdkPathIndex(stack.template);
|
|
43802
43942
|
const taskDefs = resolveCdkPathToLogicalIds(parsed.pathOrId, index).filter(({ logicalId: l }) => resources[l]?.Type === "AWS::ECS::TaskDefinition");
|
|
43803
|
-
if (taskDefs.length === 0) throw notFoundError
|
|
43943
|
+
if (taskDefs.length === 0) throw notFoundError(target, stack, resources);
|
|
43804
43944
|
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
43945
|
logicalId = taskDefs[0].logicalId;
|
|
43806
43946
|
resource = resources[logicalId];
|
|
43807
43947
|
} else {
|
|
43808
43948
|
resource = resources[parsed.pathOrId];
|
|
43809
|
-
if (!resource) throw notFoundError
|
|
43949
|
+
if (!resource) throw notFoundError(target, stack, resources);
|
|
43810
43950
|
logicalId = parsed.pathOrId;
|
|
43811
43951
|
}
|
|
43812
|
-
if (!logicalId || !resource) throw notFoundError
|
|
43952
|
+
if (!logicalId || !resource) throw notFoundError(target, stack, resources);
|
|
43813
43953
|
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
43954
|
if (resource.Type !== "AWS::ECS::TaskDefinition") throw new EcsTaskResolutionError(`Resource '${logicalId}' in ${stack.stackName} is ${resource.Type}, not an AWS::ECS::TaskDefinition.`);
|
|
43815
43955
|
return extractTaskDefinitionProperties(stack, logicalId, resource, context);
|
|
43816
43956
|
}
|
|
43817
|
-
function pickStack$
|
|
43957
|
+
function pickStack$1(parsed, stacks) {
|
|
43818
43958
|
if (parsed.stackPattern === null) {
|
|
43819
43959
|
if (stacks.length === 1) return stacks[0];
|
|
43820
43960
|
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 +44480,7 @@ function pickStringArray(value) {
|
|
|
44340
44480
|
for (const v of value) if (typeof v === "string") out.push(v);
|
|
44341
44481
|
return out;
|
|
44342
44482
|
}
|
|
44343
|
-
function notFoundError
|
|
44483
|
+
function notFoundError(target, stack, resources) {
|
|
44344
44484
|
const tasks = [];
|
|
44345
44485
|
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
44346
44486
|
if (resource.Type !== "AWS::ECS::TaskDefinition") continue;
|
|
@@ -46233,7 +46373,7 @@ async function localStartApiCommand(target, options) {
|
|
|
46233
46373
|
await ensureDockerAvailable();
|
|
46234
46374
|
const appCmd = resolveApp(options.app);
|
|
46235
46375
|
if (!appCmd) throw new Error("No CDK app specified. Pass --app, set CDKD_APP, or add \"app\" to cdk.json.");
|
|
46236
|
-
const overrides = readEnvOverridesFile$
|
|
46376
|
+
const overrides = readEnvOverridesFile$3(options.envVars);
|
|
46237
46377
|
const debugPortBase = options.debugPortBase ? parseDebugPort(options.debugPortBase) : void 0;
|
|
46238
46378
|
const perLambdaConcurrency = parsePerLambdaConcurrency(options.perLambdaConcurrency);
|
|
46239
46379
|
const inlineTmpDirs = /* @__PURE__ */ new Set();
|
|
@@ -47199,7 +47339,7 @@ function getTemplateEnv$1(resource) {
|
|
|
47199
47339
|
return vars;
|
|
47200
47340
|
}
|
|
47201
47341
|
/** Read the SAM-shape `--env-vars` JSON file. */
|
|
47202
|
-
function readEnvOverridesFile$
|
|
47342
|
+
function readEnvOverridesFile$3(filePath) {
|
|
47203
47343
|
if (!filePath) return void 0;
|
|
47204
47344
|
let raw;
|
|
47205
47345
|
try {
|
|
@@ -47637,7 +47777,9 @@ const execFileAsync$2 = promisify(execFile);
|
|
|
47637
47777
|
* metadata AND `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=/role/<role-arn>`
|
|
47638
47778
|
* for IAM task-role credentials. cdkd does NOT re-implement the sidecar
|
|
47639
47779
|
* — pulling the AWS-published image keeps cdkd in lock-step with whatever
|
|
47640
|
-
* ECS-Agent fidelity AWS chooses to provide.
|
|
47780
|
+
* ECS-Agent fidelity AWS chooses to provide. The `cdkd local start-service`
|
|
47781
|
+
* / `start-alb` shared-network shape lives in cdk-local's bundled ECS
|
|
47782
|
+
* service emulator engine (see `src/cli/commands/ecs-service-emulator.ts`).
|
|
47641
47783
|
*/
|
|
47642
47784
|
/** AWS-published sidecar image (latest tag). amd64 is the only image AWS ships. */
|
|
47643
47785
|
const METADATA_ENDPOINT_IMAGE = "amazon/amazon-ecs-local-container-endpoints:latest-amd64";
|
|
@@ -47645,29 +47787,18 @@ const METADATA_ENDPOINT_IMAGE = "amazon/amazon-ecs-local-container-endpoints:lat
|
|
|
47645
47787
|
* Default well-known IP for the ECS local-container-endpoints sidecar —
|
|
47646
47788
|
* matches the documented AWS task-metadata endpoint address. Containers
|
|
47647
47789
|
* inject `ECS_CONTAINER_METADATA_URI_V4=http://169.254.170.2/v4/<id>`
|
|
47648
|
-
* to reach it.
|
|
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.
|
|
47790
|
+
* to reach it.
|
|
47653
47791
|
*/
|
|
47654
47792
|
const METADATA_ENDPOINT_IP = "169.254.170.2";
|
|
47655
47793
|
/** Default subnet — used when no `subnetOctet` override is supplied. */
|
|
47656
47794
|
const DEFAULT_METADATA_ENDPOINT_SUBNET = "169.254.170.0/24";
|
|
47657
47795
|
/**
|
|
47658
47796
|
* Pure-functional subnet allocator. `cdkd local run-task` uses the
|
|
47659
|
-
* default subnet
|
|
47660
|
-
*
|
|
47661
|
-
*
|
|
47662
|
-
*
|
|
47663
|
-
*
|
|
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.
|
|
47797
|
+
* default subnet (`subnetOctet=170`). The link-local 169.254.0.0/16
|
|
47798
|
+
* space is reserved AWS-wide for cloud metadata so collisions with
|
|
47799
|
+
* user workloads are unlikely. `subnetOctet` is the second-from-last
|
|
47800
|
+
* byte of the network: 170 → 169.254.170.0/24 (default). Valid range
|
|
47801
|
+
* is 1..254.
|
|
47671
47802
|
*/
|
|
47672
47803
|
function buildEndpointSubnet(subnetOctet) {
|
|
47673
47804
|
if (subnetOctet < 1 || subnetOctet > 254 || !Number.isInteger(subnetOctet)) throw new Error(`buildEndpointSubnet: subnetOctet must be an integer in 1..254 (got ${subnetOctet}).`);
|
|
@@ -47677,50 +47808,12 @@ function buildEndpointSubnet(subnetOctet) {
|
|
|
47677
47808
|
};
|
|
47678
47809
|
}
|
|
47679
47810
|
/**
|
|
47680
|
-
*
|
|
47681
|
-
*
|
|
47682
|
-
*
|
|
47683
|
-
*
|
|
47684
|
-
*
|
|
47685
|
-
*
|
|
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).
|
|
47811
|
+
* Internal helper that creates the docker network, pulls the sidecar image,
|
|
47812
|
+
* and starts the sidecar at the documented IP. Throws `DockerRunnerError`
|
|
47813
|
+
* with a hint when the network already exists (the typical "leftover from
|
|
47814
|
+
* previous run" path). Used by `createTaskNetwork` (per-task) only;
|
|
47815
|
+
* `cdkd local start-service` / `start-alb` share-network creation is owned
|
|
47816
|
+
* by cdk-local's bundled ECS service emulator engine.
|
|
47724
47817
|
*/
|
|
47725
47818
|
async function createNetworkAndSidecar(args) {
|
|
47726
47819
|
const logger = getLogger().child("ecs-network");
|
|
@@ -48226,7 +48319,7 @@ async function waitForContainerHealthy(containerId, displayName) {
|
|
|
48226
48319
|
if (err instanceof EcsTaskRunnerError) throw err;
|
|
48227
48320
|
logger.debug(`docker inspect on '${displayName}' failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
48228
48321
|
}
|
|
48229
|
-
await sleep
|
|
48322
|
+
await sleep(1e3);
|
|
48230
48323
|
}
|
|
48231
48324
|
throw new EcsTaskRunnerError(`Container '${displayName}' did not become healthy within 5 minutes.`);
|
|
48232
48325
|
}
|
|
@@ -48250,7 +48343,7 @@ async function stopContainer(containerId, graceSeconds) {
|
|
|
48250
48343
|
]);
|
|
48251
48344
|
} catch {}
|
|
48252
48345
|
}
|
|
48253
|
-
function sleep
|
|
48346
|
+
function sleep(ms) {
|
|
48254
48347
|
return new Promise((res) => setTimeout(res, ms));
|
|
48255
48348
|
}
|
|
48256
48349
|
/**
|
|
@@ -48554,9 +48647,9 @@ async function localRunTaskCommand(target, options) {
|
|
|
48554
48647
|
...options.profile && { macroExpandS3ClientOpts: { profile: options.profile } }
|
|
48555
48648
|
};
|
|
48556
48649
|
const { stacks } = await synthesizer.synthesize(synthOpts);
|
|
48557
|
-
const candidate = pickCandidateStack
|
|
48650
|
+
const candidate = pickCandidateStack(parseEcsTarget(target).stackPattern, stacks);
|
|
48558
48651
|
stateProvider = createLocalStateProvider$1(options, candidate?.stackName ?? "", candidate?.region);
|
|
48559
|
-
const imageContext = await buildEcsImageResolutionContext$
|
|
48652
|
+
const imageContext = await buildEcsImageResolutionContext$1(candidate, stateProvider, options);
|
|
48560
48653
|
const task = resolveEcsTaskTarget(target, stacks, imageContext);
|
|
48561
48654
|
logger.info(`Target: ${task.stack.stackName}/${task.taskDefinitionLogicalId} (family=${task.family}, containers=${task.containers.length})`);
|
|
48562
48655
|
const taskNeeds = detectEcsImageResolutionNeeds(stacks.find((s) => s.stackName === task.stack.stackName) ?? task.stack);
|
|
@@ -48584,15 +48677,15 @@ async function localRunTaskCommand(target, options) {
|
|
|
48584
48677
|
let resolvedRoleArn;
|
|
48585
48678
|
if (options.assumeTaskRole === true) {
|
|
48586
48679
|
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
|
|
48588
|
-
assumedCredentials = await assumeTaskRole
|
|
48680
|
+
resolvedRoleArn = await resolvePlaceholderAccount(task.taskRoleArn, options.region);
|
|
48681
|
+
assumedCredentials = await assumeTaskRole(resolvedRoleArn, options.region);
|
|
48589
48682
|
} else if (typeof options.assumeTaskRole === "string") {
|
|
48590
48683
|
resolvedRoleArn = options.assumeTaskRole;
|
|
48591
|
-
assumedCredentials = await assumeTaskRole
|
|
48684
|
+
assumedCredentials = await assumeTaskRole(resolvedRoleArn, options.region);
|
|
48592
48685
|
}
|
|
48593
48686
|
const sidecarCredentials = await resolveSidecarCredentials(options, assumedCredentials);
|
|
48594
48687
|
if (options.profile && sidecarCredentials && !assumedCredentials) profileCredsFile = await writeProfileCredentialsFile(options.profile, sidecarCredentials);
|
|
48595
|
-
const envOverrides = readEnvOverridesFile$
|
|
48688
|
+
const envOverrides = readEnvOverridesFile$2(options.envVars);
|
|
48596
48689
|
const runOpts = {
|
|
48597
48690
|
cluster: options.cluster,
|
|
48598
48691
|
containerHost: options.containerHost,
|
|
@@ -48633,7 +48726,7 @@ async function localRunTaskCommand(target, options) {
|
|
|
48633
48726
|
* Lazy: callers should only invoke this when the resolved ARN is actually
|
|
48634
48727
|
* going to be used (i.e. on the bare `--assume-task-role` path).
|
|
48635
48728
|
*/
|
|
48636
|
-
async function resolvePlaceholderAccount
|
|
48729
|
+
async function resolvePlaceholderAccount(arn, region) {
|
|
48637
48730
|
if (!arn.includes("${AWS::AccountId}")) return arn;
|
|
48638
48731
|
const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
|
|
48639
48732
|
const sts = new STSClient({ ...region && { region } });
|
|
@@ -48649,7 +48742,7 @@ async function resolvePlaceholderAccount$1(arn, region) {
|
|
|
48649
48742
|
* Assume `roleArn` and return temp credentials. Mirrors the same flow
|
|
48650
48743
|
* `cdkd local invoke --assume-role` uses.
|
|
48651
48744
|
*/
|
|
48652
|
-
async function assumeTaskRole
|
|
48745
|
+
async function assumeTaskRole(roleArn, region) {
|
|
48653
48746
|
const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
|
|
48654
48747
|
const sts = new STSClient({ ...region && { region } });
|
|
48655
48748
|
try {
|
|
@@ -48680,7 +48773,7 @@ async function assumeTaskRole$1(roleArn, region) {
|
|
|
48680
48773
|
* `--from-state` and `--from-cfn-stack` produce the same downstream
|
|
48681
48774
|
* context shape (issue #606).
|
|
48682
48775
|
*/
|
|
48683
|
-
async function buildEcsImageResolutionContext$
|
|
48776
|
+
async function buildEcsImageResolutionContext$1(candidate, stateProvider, options) {
|
|
48684
48777
|
const logger = getLogger();
|
|
48685
48778
|
if (!candidate) return void 0;
|
|
48686
48779
|
const needs = detectEcsImageResolutionNeeds(candidate);
|
|
@@ -48692,7 +48785,7 @@ async function buildEcsImageResolutionContext$2(candidate, stateProvider, option
|
|
|
48692
48785
|
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
48786
|
let accountId;
|
|
48694
48787
|
try {
|
|
48695
|
-
accountId = await resolveCallerAccountId$
|
|
48788
|
+
accountId = await resolveCallerAccountId$1(region);
|
|
48696
48789
|
} catch (err) {
|
|
48697
48790
|
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
48791
|
}
|
|
@@ -48714,7 +48807,7 @@ async function buildEcsImageResolutionContext$2(candidate, stateProvider, option
|
|
|
48714
48807
|
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
48808
|
return ctx;
|
|
48716
48809
|
}
|
|
48717
|
-
function pickCandidateStack
|
|
48810
|
+
function pickCandidateStack(stackPattern, stacks) {
|
|
48718
48811
|
if (stackPattern === null) {
|
|
48719
48812
|
if (stacks.length === 1) return stacks[0];
|
|
48720
48813
|
return;
|
|
@@ -48722,7 +48815,7 @@ function pickCandidateStack$1(stackPattern, stacks) {
|
|
|
48722
48815
|
const matched = matchStacks(stacks, [stackPattern]);
|
|
48723
48816
|
if (matched.length === 1) return matched[0];
|
|
48724
48817
|
}
|
|
48725
|
-
async function resolveCallerAccountId$
|
|
48818
|
+
async function resolveCallerAccountId$1(region) {
|
|
48726
48819
|
const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
|
|
48727
48820
|
const sts = new STSClient({ ...region && { region } });
|
|
48728
48821
|
try {
|
|
@@ -48736,7 +48829,7 @@ async function resolveCallerAccountId$2(region) {
|
|
|
48736
48829
|
* `cdkd local invoke --env-vars`: top-level keys are container names, with
|
|
48737
48830
|
* `Parameters` reserved for global entries.
|
|
48738
48831
|
*/
|
|
48739
|
-
function readEnvOverridesFile$
|
|
48832
|
+
function readEnvOverridesFile$2(filePath) {
|
|
48740
48833
|
if (!filePath) return void 0;
|
|
48741
48834
|
let raw;
|
|
48742
48835
|
try {
|
|
@@ -48788,1072 +48881,42 @@ function createLocalRunTaskCommand() {
|
|
|
48788
48881
|
return cmd;
|
|
48789
48882
|
}
|
|
48790
48883
|
|
|
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
48884
|
//#endregion
|
|
49486
48885
|
//#region src/cli/commands/local-start-service.ts
|
|
49487
48886
|
/**
|
|
49488
|
-
* `
|
|
49489
|
-
*
|
|
49490
|
-
*
|
|
49491
|
-
*
|
|
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.
|
|
48887
|
+
* `cdkl start-service` strategy — name one or more ECS services and the engine
|
|
48888
|
+
* boots their replicas. There is no front-door listener (services are reached
|
|
48889
|
+
* directly via their published container ports). Mirrors `albStrategy` in
|
|
48890
|
+
* shape, with `frontDoor` omitted and `lbPortOverrides` empty.
|
|
49498
48891
|
*/
|
|
49499
|
-
|
|
49500
|
-
|
|
49501
|
-
|
|
49502
|
-
|
|
49503
|
-
|
|
49504
|
-
|
|
49505
|
-
|
|
49506
|
-
|
|
49507
|
-
|
|
49508
|
-
|
|
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.
|
|
49612
|
-
*/
|
|
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
|
|
48892
|
+
function serviceStrategy(_options) {
|
|
48893
|
+
return {
|
|
48894
|
+
pickEntries: (stacks) => listTargets(stacks).ecsServices,
|
|
48895
|
+
pickerMessage: "Select one or more ECS services to run",
|
|
48896
|
+
pickerNoun: "ECS services",
|
|
48897
|
+
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.`),
|
|
48898
|
+
resolveBoots: (_stacks, chosenTargets) => ({
|
|
48899
|
+
boots: chosenTargets.map((target) => ({ target })),
|
|
48900
|
+
warnings: []
|
|
48901
|
+
}),
|
|
48902
|
+
lbPortOverrides: {}
|
|
49672
48903
|
};
|
|
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
48904
|
}
|
|
49711
48905
|
/**
|
|
49712
|
-
*
|
|
49713
|
-
*
|
|
49714
|
-
*
|
|
49715
|
-
*
|
|
48906
|
+
* `cdkl start-service <Stack/Service>...` — run one or more `AWS::ECS::Service`
|
|
48907
|
+
* resources locally as a long-running emulator. Spins up DesiredCount task
|
|
48908
|
+
* replicas per service (clamped by --max-tasks) using the same per-task
|
|
48909
|
+
* docker network + metadata sidecar pattern as `cdkd local run-task`, then
|
|
48910
|
+
* keeps each replica running and restarts it on exit per --restart-policy.
|
|
48911
|
+
* ^C tears every replica + sidecar + network down. When two or more
|
|
48912
|
+
* <target>s are supplied, every service is booted into a shared Cloud Map /
|
|
48913
|
+
* Service Connect registry so peer services discover each other via docker
|
|
48914
|
+
* --add-host overlay (Issue #460).
|
|
49716
48915
|
*/
|
|
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
48916
|
function createLocalStartServiceCommand() {
|
|
49848
|
-
|
|
49849
|
-
|
|
49850
|
-
|
|
49851
|
-
...appOptions,
|
|
49852
|
-
...contextOptions,
|
|
49853
|
-
...stateOptions
|
|
49854
|
-
].forEach((opt) => cmd.addOption(opt));
|
|
49855
|
-
cmd.addOption(deprecatedRegionOption);
|
|
49856
|
-
return cmd;
|
|
48917
|
+
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) => {
|
|
48918
|
+
await runEcsServiceEmulator(targets, options, serviceStrategy(options), cdkdExtraStateProviders);
|
|
48919
|
+
})));
|
|
49857
48920
|
}
|
|
49858
48921
|
|
|
49859
48922
|
//#endregion
|
|
@@ -52761,7 +51824,7 @@ function reorderArgs(argv) {
|
|
|
52761
51824
|
*/
|
|
52762
51825
|
async function main() {
|
|
52763
51826
|
const program = new Command();
|
|
52764
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
51827
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.198.0");
|
|
52765
51828
|
program.addCommand(createBootstrapCommand());
|
|
52766
51829
|
program.addCommand(createSynthCommand());
|
|
52767
51830
|
program.addCommand(createListCommand());
|