From 2f51d5a37cf877d230f9f6f311dc08722e377776 Mon Sep 17 00:00:00 2001 From: Momo Kornher Date: Mon, 12 May 2025 09:35:53 +0100 Subject: [PATCH] feat(ecs): throw typed errors --- packages/aws-cdk-lib/.eslintrc.js | 2 - .../application-load-balanced-service-base.ts | 29 ++++---- ...ion-multiple-target-groups-service-base.ts | 22 +++--- .../network-load-balanced-service-base.ts | 9 +-- ...ork-multiple-target-groups-service-base.ts | 16 ++--- .../lib/base/queue-processing-service-base.ts | 10 +-- .../lib/base/scheduled-task-base.ts | 4 +- .../application-load-balanced-ecs-service.ts | 6 +- ...tion-multiple-target-groups-ecs-service.ts | 8 +-- .../ecs/network-load-balanced-ecs-service.ts | 6 +- ...work-multiple-target-groups-ecs-service.ts | 10 +-- .../lib/ecs/queue-processing-ecs-service.ts | 4 +- .../lib/ecs/scheduled-ecs-task.ts | 5 +- ...plication-load-balanced-fargate-service.ts | 18 ++--- ...-multiple-target-groups-fargate-service.ts | 8 +-- .../network-load-balanced-fargate-service.ts | 6 +- ...-multiple-target-groups-fargate-service.ts | 10 +-- .../queue-processing-fargate-service.ts | 6 +- .../lib/fargate/scheduled-fargate-task.ts | 6 +- packages/aws-cdk-lib/aws-ecs/lib/amis.ts | 9 +-- .../lib/base/_imported-task-definition.ts | 10 +-- .../aws-ecs/lib/base/base-service.ts | 69 ++++++++++--------- .../lib/base/from-service-attributes.ts | 4 +- .../lib/base/service-managed-volume.ts | 18 ++--- .../aws-ecs/lib/base/task-definition.ts | 42 +++++------ packages/aws-cdk-lib/aws-ecs/lib/cluster.ts | 49 ++++++------- .../aws-ecs/lib/container-definition.ts | 61 ++++++++-------- .../aws-ecs/lib/credential-spec.ts | 3 +- .../aws-ecs/lib/ec2/ec2-service.ts | 30 ++++---- .../aws-ecs/lib/ec2/ec2-task-definition.ts | 8 +-- .../aws-ecs/lib/environment-file.ts | 5 +- .../aws-ecs/lib/external/external-service.ts | 32 ++++----- .../lib/external/external-task-definition.ts | 3 +- .../aws-ecs/lib/fargate/fargate-service.ts | 13 ++-- .../lib/fargate/fargate-task-definition.ts | 10 +-- .../aws-ecs/lib/firelens-log-router.ts | 2 +- .../images/tag-parameter-container-image.ts | 4 +- .../aws-ecs/lib/linux-parameters.ts | 5 +- .../aws-ecs/lib/log-drivers/aws-log-driver.ts | 6 +- .../lib/log-drivers/json-file-log-driver.ts | 3 +- .../lib/log-drivers/splunk-log-driver.ts | 4 +- .../aws-ecs/lib/log-drivers/utils.ts | 6 +- packages/aws-cdk-lib/aws-ecs/lib/placement.ts | 3 +- .../app-mesh-proxy-configuration.ts | 3 +- 44 files changed, 299 insertions(+), 288 deletions(-) diff --git a/packages/aws-cdk-lib/.eslintrc.js b/packages/aws-cdk-lib/.eslintrc.js index 9445191d72613..82051ee4542d7 100644 --- a/packages/aws-cdk-lib/.eslintrc.js +++ b/packages/aws-cdk-lib/.eslintrc.js @@ -17,8 +17,6 @@ baseConfig.rules['import/no-extraneous-dependencies'] = [ baseConfig.rules['@cdklabs/no-throw-default-error'] = ['error']; // not yet supported const noThrowDefaultErrorNotYetSupported = [ - 'aws-ecs-patterns', - 'aws-ecs', 'aws-elasticsearch', 'aws-globalaccelerator', 'aws-globalaccelerator-endpoints', diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts b/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts index e0865f8ad9433..5e3281644e424 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts +++ b/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts @@ -14,8 +14,7 @@ import { import { IRole } from '../../../aws-iam'; import { ARecord, IHostedZone, RecordTarget, CnameRecord } from '../../../aws-route53'; import { LoadBalancerTarget } from '../../../aws-route53-targets'; -import * as cdk from '../../../core'; -import { Duration } from '../../../core'; +import { CfnOutput, Duration, Stack, Token, ValidationError } from '../../../core'; /** * Describes the type of DNS record the service should create @@ -150,7 +149,7 @@ export interface ApplicationLoadBalancedServiceBaseProps { * * @default - defaults to 60 seconds if at least one load balancer is in-use and it is not already set */ - readonly healthCheckGracePeriod?: cdk.Duration; + readonly healthCheckGracePeriod?: Duration; /** * The maximum number of tasks, specified as a percentage of the Amazon ECS @@ -423,7 +422,7 @@ export abstract class ApplicationLoadBalancedServiceBase extends Construct { */ public get loadBalancer(): ApplicationLoadBalancer { if (!this._applicationLoadBalancer) { - throw new Error('.loadBalancer can only be accessed if the class was constructed with an owned, not imported, load balancer'); + throw new ValidationError('.loadBalancer can only be accessed if the class was constructed with an owned, not imported, load balancer', this); } return this._applicationLoadBalancer; } @@ -462,12 +461,12 @@ export abstract class ApplicationLoadBalancedServiceBase extends Construct { super(scope, id); if (props.cluster && props.vpc) { - throw new Error('You can only specify either vpc or cluster. Alternatively, you can leave both blank'); + throw new ValidationError('You can only specify either vpc or cluster. Alternatively, you can leave both blank', this); } this.cluster = props.cluster || this.getDefaultCluster(this, props.vpc); - if (props.desiredCount !== undefined && !cdk.Token.isUnresolved(props.desiredCount) && props.desiredCount < 1) { - throw new Error('You must specify a desiredCount greater than 0'); + if (props.desiredCount !== undefined && !Token.isUnresolved(props.desiredCount) && props.desiredCount < 1) { + throw new ValidationError('You must specify a desiredCount greater than 0', this); } this.desiredCount = props.desiredCount || 1; @@ -478,7 +477,7 @@ export abstract class ApplicationLoadBalancedServiceBase extends Construct { if (props.idleTimeout) { const idleTimeout = props.idleTimeout.toSeconds(); if (idleTimeout > Duration.seconds(4000).toSeconds() || idleTimeout < Duration.seconds(1).toSeconds()) { - throw new Error('Load balancer idle timeout must be between 1 and 4000 seconds.'); + throw new ValidationError('Load balancer idle timeout must be between 1 and 4000 seconds.', this); } } @@ -493,12 +492,12 @@ export abstract class ApplicationLoadBalancedServiceBase extends Construct { const loadBalancer = props.loadBalancer ?? new ApplicationLoadBalancer(this, 'LB', lbProps); if (props.certificate !== undefined && props.protocol !== undefined && props.protocol !== ApplicationProtocol.HTTPS) { - throw new Error('The HTTPS protocol must be used when a certificate is given'); + throw new ValidationError('The HTTPS protocol must be used when a certificate is given', this); } const protocol = props.protocol ?? (props.certificate ? ApplicationProtocol.HTTPS : ApplicationProtocol.HTTP); if (protocol !== ApplicationProtocol.HTTPS && props.redirectHTTP === true) { - throw new Error('The HTTPS protocol must be used when redirecting HTTP traffic'); + throw new ValidationError('The HTTPS protocol must be used when redirecting HTTP traffic', this); } const targetProps: AddApplicationTargetsProps = { @@ -519,7 +518,7 @@ export abstract class ApplicationLoadBalancedServiceBase extends Construct { this.certificate = props.certificate; } else { if (typeof props.domainName === 'undefined' || typeof props.domainZone === 'undefined') { - throw new Error('A domain name and zone is required when using the HTTPS protocol'); + throw new ValidationError('A domain name and zone is required when using the HTTPS protocol', this); } this.certificate = new Certificate(this, 'Certificate', { @@ -547,7 +546,7 @@ export abstract class ApplicationLoadBalancedServiceBase extends Construct { let domainName = loadBalancer.loadBalancerDnsName; if (typeof props.domainName !== 'undefined') { if (typeof props.domainZone === 'undefined') { - throw new Error('A Route53 hosted domain zone name is required to configure the specified domain name'); + throw new ValidationError('A Route53 hosted domain zone name is required to configure the specified domain name', this); } switch (props.recordType ?? ApplicationLoadBalancedServiceRecordType.ALIAS) { @@ -577,8 +576,8 @@ export abstract class ApplicationLoadBalancedServiceBase extends Construct { this._applicationLoadBalancer = loadBalancer; } - new cdk.CfnOutput(this, 'LoadBalancerDNS', { value: loadBalancer.loadBalancerDnsName }); - new cdk.CfnOutput(this, 'ServiceURL', { value: protocol.toLowerCase() + '://' + domainName }); + new CfnOutput(this, 'LoadBalancerDNS', { value: loadBalancer.loadBalancerDnsName }); + new CfnOutput(this, 'ServiceURL', { value: protocol.toLowerCase() + '://' + domainName }); } /** @@ -587,7 +586,7 @@ export abstract class ApplicationLoadBalancedServiceBase extends Construct { protected getDefaultCluster(scope: Construct, vpc?: IVpc): Cluster { // magic string to avoid collision with user-defined constructs const DEFAULT_CLUSTER_ID = `EcsDefaultClusterMnL3mNNYN${vpc ? vpc.node.id : ''}`; - const stack = cdk.Stack.of(scope); + const stack = Stack.of(scope); return stack.node.tryFindChild(DEFAULT_CLUSTER_ID) as Cluster || new Cluster(stack, DEFAULT_CLUSTER_ID, { vpc }); } diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts b/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts index 45c06cf5e3f28..99d2808b65b5f 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts +++ b/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts @@ -16,7 +16,7 @@ import { import { IRole } from '../../../aws-iam'; import { ARecord, IHostedZone, RecordTarget } from '../../../aws-route53'; import { LoadBalancerTarget } from '../../../aws-route53-targets'; -import { CfnOutput, Duration, Stack } from '../../../core'; +import { CfnOutput, Duration, Stack, ValidationError } from '../../../core'; /** * The properties for the base ApplicationMultipleTargetGroupsEc2Service or ApplicationMultipleTargetGroupsFargateService service. @@ -431,7 +431,7 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends Constru for (const listenerProps of lbProps.listeners) { const protocol = this.createListenerProtocol(listenerProps.protocol, listenerProps.certificate); if (listenerProps.certificate !== undefined && protocol !== undefined && protocol !== ApplicationProtocol.HTTPS) { - throw new Error('The HTTPS protocol must be used when a certificate is given'); + throw new ValidationError('The HTTPS protocol must be used when a certificate is given', this); } protocolType.add(protocol); const listener = this.configListener(protocol, { @@ -491,7 +491,7 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends Constru return listener; } } - throw new Error(`Listener ${name} is not defined. Did you define listener with name ${name}?`); + throw new ValidationError(`Listener ${name} is not defined. Did you define listener with name ${name}?`, this); } protected registerECSTargets(service: BaseService, container: ContainerDefinition, targets: ApplicationTargetProps[]): ApplicationTargetGroup { @@ -519,7 +519,7 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends Constru this.targetGroups.push(targetGroup); } if (this.targetGroups.length === 0) { - throw new Error('At least one target group should be specified.'); + throw new ValidationError('At least one target group should be specified.', this); } return this.targetGroups[0]; } @@ -561,20 +561,20 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends Constru private validateInput(props: ApplicationMultipleTargetGroupsServiceBaseProps) { if (props.cluster && props.vpc) { - throw new Error('You can only specify either vpc or cluster. Alternatively, you can leave both blank'); + throw new ValidationError('You can only specify either vpc or cluster. Alternatively, you can leave both blank', this); } if (props.desiredCount !== undefined && props.desiredCount < 1) { - throw new Error('You must specify a desiredCount greater than 0'); + throw new ValidationError('You must specify a desiredCount greater than 0', this); } if (props.loadBalancers) { if (props.loadBalancers.length === 0) { - throw new Error('At least one load balancer must be specified'); + throw new ValidationError('At least one load balancer must be specified', this); } for (const lbProps of props.loadBalancers) { if (lbProps.listeners.length === 0) { - throw new Error('At least one listener must be specified'); + throw new ValidationError('At least one listener must be specified', this); } } } @@ -585,7 +585,7 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends Constru if (prop.idleTimeout) { const idleTimeout = prop.idleTimeout.toSeconds(); if (idleTimeout > Duration.seconds(4000).toSeconds() || idleTimeout < Duration.seconds(1).toSeconds()) { - throw new Error('Load balancer idle timeout must be between 1 and 4000 seconds.'); + throw new ValidationError('Load balancer idle timeout must be between 1 and 4000 seconds.', this); } } } @@ -609,7 +609,7 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends Constru private createListenerCertificate(listenerName: string, certificate?: ICertificate, domainName?: string, domainZone?: IHostedZone): ICertificate { if (typeof domainName === 'undefined' || typeof domainZone === 'undefined') { - throw new Error('A domain name and zone is required when using the HTTPS protocol'); + throw new ValidationError('A domain name and zone is required when using the HTTPS protocol', this); } if (certificate !== undefined) { @@ -635,7 +635,7 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends Constru let domainName = loadBalancer.loadBalancerDnsName; if (typeof name !== 'undefined') { if (typeof zone === 'undefined') { - throw new Error('A Route53 hosted domain zone name is required to configure the specified domain name'); + throw new ValidationError('A Route53 hosted domain zone name is required to configure the specified domain name', this); } const record = new ARecord(this, `DNS${loadBalancer.node.id}`, { diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts b/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts index ec63cf6dee0d3..2926feb813898 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts +++ b/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts @@ -9,6 +9,7 @@ import { IRole } from '../../../aws-iam'; import { ARecord, CnameRecord, IHostedZone, RecordTarget } from '../../../aws-route53'; import { LoadBalancerTarget } from '../../../aws-route53-targets'; import * as cdk from '../../../core'; +import { ValidationError } from '../../../core'; /** * Describes the type of DNS record the service should create @@ -329,7 +330,7 @@ export abstract class NetworkLoadBalancedServiceBase extends Construct { */ public get loadBalancer(): NetworkLoadBalancer { if (!this._networkLoadBalancer) { - throw new Error('.loadBalancer can only be accessed if the class was constructed with an owned, not imported, load balancer'); + throw new ValidationError('.loadBalancer can only be accessed if the class was constructed with an owned, not imported, load balancer', this); } return this._networkLoadBalancer; } @@ -357,12 +358,12 @@ export abstract class NetworkLoadBalancedServiceBase extends Construct { super(scope, id); if (props.cluster && props.vpc) { - throw new Error('You can only specify either vpc or cluster. Alternatively, you can leave both blank'); + throw new ValidationError('You can only specify either vpc or cluster. Alternatively, you can leave both blank', this); } this.cluster = props.cluster || this.getDefaultCluster(this, props.vpc); if (props.desiredCount !== undefined && props.desiredCount < 1) { - throw new Error('You must specify a desiredCount greater than 0'); + throw new ValidationError('You must specify a desiredCount greater than 0', this); } this.desiredCount = props.desiredCount || 1; @@ -392,7 +393,7 @@ export abstract class NetworkLoadBalancedServiceBase extends Construct { if (typeof props.domainName !== 'undefined') { if (typeof props.domainZone === 'undefined') { - throw new Error('A Route53 hosted domain zone name is required to configure the specified domain name'); + throw new ValidationError('A Route53 hosted domain zone name is required to configure the specified domain name', this); } switch (props.recordType ?? NetworkLoadBalancedServiceRecordType.ALIAS) { diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts b/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts index 72d58e12ee2f2..0d7d3333b74cc 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts +++ b/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts @@ -8,7 +8,7 @@ import { NetworkListener, NetworkLoadBalancer, NetworkTargetGroup } from '../../ import { IRole } from '../../../aws-iam'; import { ARecord, IHostedZone, RecordTarget } from '../../../aws-route53'; import { LoadBalancerTarget } from '../../../aws-route53-targets'; -import { CfnOutput, Duration, Stack } from '../../../core'; +import { CfnOutput, Duration, Stack, ValidationError } from '../../../core'; /** * The properties for the base NetworkMultipleTargetGroupsEc2Service or NetworkMultipleTargetGroupsFargateService service. @@ -380,7 +380,7 @@ export abstract class NetworkMultipleTargetGroupsServiceBase extends Construct { return listener; } } - throw new Error(`Listener ${name} is not defined. Did you define listener with name ${name}?`); + throw new ValidationError(`Listener ${name} is not defined. Did you define listener with name ${name}?`, this); } protected registerECSTargets(service: BaseService, container: ContainerDefinition, targets: NetworkTargetProps[]): NetworkTargetGroup { @@ -397,7 +397,7 @@ export abstract class NetworkMultipleTargetGroupsServiceBase extends Construct { this.targetGroups.push(targetGroup); } if (this.targetGroups.length === 0) { - throw new Error('At least one target group should be specified.'); + throw new ValidationError('At least one target group should be specified.', this); } return this.targetGroups[0]; } @@ -423,20 +423,20 @@ export abstract class NetworkMultipleTargetGroupsServiceBase extends Construct { private validateInput(props: NetworkMultipleTargetGroupsServiceBaseProps) { if (props.cluster && props.vpc) { - throw new Error('You can only specify either vpc or cluster. Alternatively, you can leave both blank'); + throw new ValidationError('You can only specify either vpc or cluster. Alternatively, you can leave both blank', this); } if (props.desiredCount !== undefined && props.desiredCount < 1) { - throw new Error('You must specify a desiredCount greater than 0'); + throw new ValidationError('You must specify a desiredCount greater than 0', this); } if (props.loadBalancers) { if (props.loadBalancers.length === 0) { - throw new Error('At least one load balancer must be specified'); + throw new ValidationError('At least one load balancer must be specified', this); } for (const lbProps of props.loadBalancers) { if (lbProps.listeners.length === 0) { - throw new Error('At least one listener must be specified'); + throw new ValidationError('At least one listener must be specified', this); } } } @@ -461,7 +461,7 @@ export abstract class NetworkMultipleTargetGroupsServiceBase extends Construct { private createDomainName(loadBalancer: NetworkLoadBalancer, name?: string, zone?: IHostedZone) { if (typeof name !== 'undefined') { if (typeof zone === 'undefined') { - throw new Error('A Route53 hosted domain zone name is required to configure the specified domain name'); + throw new ValidationError('A Route53 hosted domain zone name is required to configure the specified domain name', this); } new ARecord(this, `DNS${loadBalancer.node.id}`, { diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/queue-processing-service-base.ts b/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/queue-processing-service-base.ts index d61567e55670e..0fe60698eaf61 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/queue-processing-service-base.ts +++ b/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/queue-processing-service-base.ts @@ -6,7 +6,7 @@ import { ICluster, LogDriver, PropagatedTagSource, Secret, } from '../../../aws-ecs'; import { IQueue, Queue } from '../../../aws-sqs'; -import { CfnOutput, Duration, FeatureFlags, Stack } from '../../../core'; +import { CfnOutput, Duration, FeatureFlags, Stack, ValidationError } from '../../../core'; import * as cxapi from '../../../cx-api'; /** @@ -335,13 +335,13 @@ export abstract class QueueProcessingServiceBase extends Construct { super(scope, id); if (props.cluster && props.vpc) { - throw new Error('You can only specify either vpc or cluster. Alternatively, you can leave both blank'); + throw new ValidationError('You can only specify either vpc or cluster. Alternatively, you can leave both blank', this); } this.cluster = props.cluster || this.getDefaultCluster(this, props.vpc); if (props.queue && (props.retentionPeriod || props.visibilityTimeout || props.maxReceiveCount)) { const errorProps = ['retentionPeriod', 'visibilityTimeout', 'maxReceiveCount'].filter(prop => props.hasOwnProperty(prop)); - throw new Error(`${errorProps.join(', ')} can be set only when queue is not set. Specify them in the QueueProps of the queue`); + throw new ValidationError(`${errorProps.join(', ')} can be set only when queue is not set. Specify them in the QueueProps of the queue`, this); } // Create the SQS queue and it's corresponding DLQ if one is not provided if (props.queue) { @@ -367,7 +367,7 @@ export abstract class QueueProcessingServiceBase extends Construct { this.scalingSteps = props.scalingSteps ?? defaultScalingSteps; if (props.cooldown && props.cooldown.toSeconds() > 999999999) { - throw new Error(`cooldown cannot be more than 999999999, found: ${props.cooldown.toSeconds()}`); + throw new ValidationError(`cooldown cannot be more than 999999999, found: ${props.cooldown.toSeconds()}`, this); } this.cooldown = props.cooldown; @@ -398,7 +398,7 @@ export abstract class QueueProcessingServiceBase extends Construct { } if (!this.desiredCount && !this.maxCapacity) { - throw new Error('maxScalingCapacity must be set and greater than 0 if desiredCount is 0'); + throw new ValidationError('maxScalingCapacity must be set and greater than 0 if desiredCount is 0', this); } new CfnOutput(this, 'SQSQueue', { value: this.sqsQueue.queueName }); diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/scheduled-task-base.ts b/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/scheduled-task-base.ts index e92eb2ee560c9..4ff2feda5e279 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/scheduled-task-base.ts +++ b/packages/aws-cdk-lib/aws-ecs-patterns/lib/base/scheduled-task-base.ts @@ -4,7 +4,7 @@ import { ISecurityGroup, IVpc, SubnetSelection, SubnetType } from '../../../aws- import { AwsLogDriver, Cluster, ContainerImage, ICluster, LogDriver, PropagatedTagSource, Secret, TaskDefinition } from '../../../aws-ecs'; import { Rule } from '../../../aws-events'; import { EcsTask, Tag } from '../../../aws-events-targets'; -import { Stack } from '../../../core'; +import { Stack, ValidationError } from '../../../core'; /** * The properties for the base ScheduledEc2Task or ScheduledFargateTask task. @@ -189,7 +189,7 @@ export abstract class ScheduledTaskBase extends Construct { this.cluster = props.cluster || this.getDefaultCluster(this, props.vpc); if (props.desiredTaskCount !== undefined && props.desiredTaskCount < 1) { - throw new Error('You must specify a desiredTaskCount greater than 0'); + throw new ValidationError('You must specify a desiredTaskCount greater than 0', this); } this.desiredTaskCount = props.desiredTaskCount || 1; this.subnetSelection = props.subnetSelection || { subnetType: SubnetType.PRIVATE_WITH_EGRESS }; diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts b/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts index 7105680b8d338..02fbf8a845785 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts +++ b/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts @@ -1,6 +1,6 @@ import { Construct } from 'constructs'; import { Ec2Service, Ec2TaskDefinition, PlacementConstraint, PlacementStrategy } from '../../../aws-ecs'; -import { FeatureFlags } from '../../../core'; +import { FeatureFlags, ValidationError } from '../../../core'; import * as cxapi from '../../../cx-api'; import { ApplicationLoadBalancedServiceBase, ApplicationLoadBalancedServiceBaseProps } from '../base/application-load-balanced-service-base'; @@ -102,7 +102,7 @@ export class ApplicationLoadBalancedEc2Service extends ApplicationLoadBalancedSe super(scope, id, props); if (props.taskDefinition && props.taskImageOptions) { - throw new Error('You must specify either a taskDefinition or taskImageOptions, not both.'); + throw new ValidationError('You must specify either a taskDefinition or taskImageOptions, not both.', this); } else if (props.taskDefinition) { this.taskDefinition = props.taskDefinition; } else if (props.taskImageOptions) { @@ -134,7 +134,7 @@ export class ApplicationLoadBalancedEc2Service extends ApplicationLoadBalancedSe containerPort: taskImageOptions.containerPort || 80, }); } else { - throw new Error('You must specify one of: taskDefinition or image'); + throw new ValidationError('You must specify one of: taskDefinition or image', this); } const desiredCount = FeatureFlags.of(this).isEnabled(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount; diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/application-multiple-target-groups-ecs-service.ts b/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/application-multiple-target-groups-ecs-service.ts index 90b948e68e291..e0a9e056f1264 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/application-multiple-target-groups-ecs-service.ts +++ b/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/application-multiple-target-groups-ecs-service.ts @@ -1,7 +1,7 @@ import { Construct } from 'constructs'; import { Ec2Service, Ec2TaskDefinition, PlacementConstraint, PlacementStrategy } from '../../../aws-ecs'; import { ApplicationTargetGroup } from '../../../aws-elasticloadbalancingv2'; -import { FeatureFlags } from '../../../core'; +import { FeatureFlags, ValidationError } from '../../../core'; import * as cxapi from '../../../cx-api'; import { ApplicationMultipleTargetGroupsServiceBase, @@ -102,7 +102,7 @@ export class ApplicationMultipleTargetGroupsEc2Service extends ApplicationMultip super(scope, id, props); if (props.taskDefinition && props.taskImageOptions) { - throw new Error('You must specify only one of TaskDefinition or TaskImageOptions.'); + throw new ValidationError('You must specify only one of TaskDefinition or TaskImageOptions.', this); } else if (props.taskDefinition) { this.taskDefinition = props.taskDefinition; } else if (props.taskImageOptions) { @@ -131,10 +131,10 @@ export class ApplicationMultipleTargetGroupsEc2Service extends ApplicationMultip } } } else { - throw new Error('You must specify one of: taskDefinition or image'); + throw new ValidationError('You must specify one of: taskDefinition or image', this); } if (!this.taskDefinition.defaultContainer) { - throw new Error('At least one essential container must be specified'); + throw new ValidationError('At least one essential container must be specified', this); } if (this.taskDefinition.defaultContainer.portMappings.length === 0) { this.taskDefinition.defaultContainer.addPortMappings({ diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts b/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts index beb47549b2efd..ca0f3f06b8552 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts +++ b/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts @@ -1,6 +1,6 @@ import { Construct } from 'constructs'; import { Ec2Service, Ec2TaskDefinition, PlacementConstraint, PlacementStrategy } from '../../../aws-ecs'; -import { FeatureFlags } from '../../../core'; +import { FeatureFlags, ValidationError } from '../../../core'; import * as cxapi from '../../../cx-api'; import { NetworkLoadBalancedServiceBase, NetworkLoadBalancedServiceBaseProps } from '../base/network-load-balanced-service-base'; @@ -100,7 +100,7 @@ export class NetworkLoadBalancedEc2Service extends NetworkLoadBalancedServiceBas super(scope, id, props); if (props.taskDefinition && props.taskImageOptions) { - throw new Error('You must specify either a taskDefinition or an image, not both.'); + throw new ValidationError('You must specify either a taskDefinition or an image, not both.', this); } else if (props.taskDefinition) { this.taskDefinition = props.taskDefinition; } else if (props.taskImageOptions) { @@ -130,7 +130,7 @@ export class NetworkLoadBalancedEc2Service extends NetworkLoadBalancedServiceBas containerPort: taskImageOptions.containerPort || 80, }); } else { - throw new Error('You must specify one of: taskDefinition or image'); + throw new ValidationError('You must specify one of: taskDefinition or image', this); } const desiredCount = FeatureFlags.of(this).isEnabled(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount; diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/network-multiple-target-groups-ecs-service.ts b/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/network-multiple-target-groups-ecs-service.ts index b520b44fd5b67..6976bf5d5b9f7 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/network-multiple-target-groups-ecs-service.ts +++ b/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/network-multiple-target-groups-ecs-service.ts @@ -1,7 +1,7 @@ import { Construct } from 'constructs'; import { Ec2Service, Ec2TaskDefinition, PlacementConstraint, PlacementStrategy } from '../../../aws-ecs'; import { NetworkTargetGroup } from '../../../aws-elasticloadbalancingv2'; -import { FeatureFlags } from '../../../core'; +import { FeatureFlags, ValidationError } from '../../../core'; import * as cxapi from '../../../cx-api'; import { NetworkMultipleTargetGroupsServiceBase, @@ -101,7 +101,7 @@ export class NetworkMultipleTargetGroupsEc2Service extends NetworkMultipleTarget super(scope, id, props); if (props.taskDefinition && props.taskImageOptions) { - throw new Error('You must specify only one of TaskDefinition or TaskImageOptions.'); + throw new ValidationError('You must specify only one of TaskDefinition or TaskImageOptions.', this); } else if (props.taskDefinition) { this.taskDefinition = props.taskDefinition; } else if (props.taskImageOptions) { @@ -130,11 +130,11 @@ export class NetworkMultipleTargetGroupsEc2Service extends NetworkMultipleTarget } } } else { - throw new Error('You must specify one of: taskDefinition or image'); + throw new ValidationError('You must specify one of: taskDefinition or image', this); } if (!this.taskDefinition.defaultContainer) { - throw new Error('At least one essential container must be specified'); + throw new ValidationError('At least one essential container must be specified', this); } if (this.taskDefinition.defaultContainer.portMappings.length === 0) { this.taskDefinition.defaultContainer.addPortMappings({ @@ -150,7 +150,7 @@ export class NetworkMultipleTargetGroupsEc2Service extends NetworkMultipleTarget const containerPort = this.taskDefinition.defaultContainer.portMappings[0].containerPort; if (!containerPort) { - throw new Error('The first port mapping added to the default container must expose a single port'); + throw new ValidationError('The first port mapping added to the default container must expose a single port', this); } this.targetGroup = this.listener.addTargets('ECS', { diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts b/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts index 99a17336b7adf..c4c473bbe9e59 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts +++ b/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts @@ -1,6 +1,6 @@ import { Construct } from 'constructs'; import { Ec2Service, Ec2TaskDefinition, PlacementConstraint, PlacementStrategy } from '../../../aws-ecs'; -import { FeatureFlags } from '../../../core'; +import { FeatureFlags, ValidationError } from '../../../core'; import * as cxapi from '../../../cx-api'; import { QueueProcessingServiceBase, QueueProcessingServiceBaseProps } from '../base/queue-processing-service-base'; @@ -106,7 +106,7 @@ export class QueueProcessingEc2Service extends QueueProcessingServiceBase { super(scope, id, props); if (!props.image) { - throw new Error('image must be specified for EC2 queue processing service'); + throw new ValidationError('image must be specified for EC2 queue processing service', this); } const containerName = props.containerName ?? 'QueueProcessingContainer'; diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/scheduled-ecs-task.ts b/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/scheduled-ecs-task.ts index 1822c61015105..3eb5a43263cd5 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/scheduled-ecs-task.ts +++ b/packages/aws-cdk-lib/aws-ecs-patterns/lib/ecs/scheduled-ecs-task.ts @@ -1,6 +1,7 @@ import { Construct } from 'constructs'; import { Ec2TaskDefinition } from '../../../aws-ecs'; import { EcsTask } from '../../../aws-events-targets'; +import { ValidationError } from '../../../core'; import { ScheduledTaskBase, ScheduledTaskBaseProps, ScheduledTaskImageProps } from '../base/scheduled-task-base'; /** @@ -97,7 +98,7 @@ export class ScheduledEc2Task extends ScheduledTaskBase { super(scope, id, props); if (props.scheduledEc2TaskDefinitionOptions && props.scheduledEc2TaskImageOptions) { - throw new Error('You must specify either a scheduledEc2TaskDefinitionOptions or scheduledEc2TaskOptions, not both.'); + throw new ValidationError('You must specify either a scheduledEc2TaskDefinitionOptions or scheduledEc2TaskOptions, not both.', this); } else if (props.scheduledEc2TaskDefinitionOptions) { this.taskDefinition = props.scheduledEc2TaskDefinitionOptions.taskDefinition; } else if (props.scheduledEc2TaskImageOptions) { @@ -117,7 +118,7 @@ export class ScheduledEc2Task extends ScheduledTaskBase { logging: taskImageOptions.logDriver ?? this.createAWSLogDriver(this.node.id), }); } else { - throw new Error('You must specify a taskDefinition or image'); + throw new ValidationError('You must specify a taskDefinition or image', this); } this.task = this.addTaskDefinitionToEventTarget(this.taskDefinition); diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts b/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts index b035f0687fff7..ef9c55b4b3c5c 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts +++ b/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts @@ -1,7 +1,7 @@ import { Construct } from 'constructs'; import { ISecurityGroup, SubnetSelection } from '../../../aws-ec2'; import { FargateService, FargateTaskDefinition, HealthCheck } from '../../../aws-ecs'; -import { FeatureFlags, Token } from '../../../core'; +import { FeatureFlags, Token, ValidationError } from '../../../core'; import { propertyInjectable } from '../../../core/lib/prop-injectable'; import * as cxapi from '../../../cx-api'; import { ApplicationLoadBalancedServiceBase, ApplicationLoadBalancedServiceBaseProps } from '../base/application-load-balanced-service-base'; @@ -89,7 +89,7 @@ export class ApplicationLoadBalancedFargateService extends ApplicationLoadBalanc this.assignPublicIp = props.assignPublicIp ?? false; if (props.taskDefinition && props.taskImageOptions) { - throw new Error('You must specify either a taskDefinition or an image, not both.'); + throw new ValidationError('You must specify either a taskDefinition or an image, not both.', this); } else if (props.taskDefinition) { this.taskDefinition = props.taskDefinition; } else if (props.taskImageOptions) { @@ -130,7 +130,7 @@ export class ApplicationLoadBalancedFargateService extends ApplicationLoadBalanc containerPort: taskImageOptions.containerPort || 80, }); } else { - throw new Error('You must specify one of: taskDefinition or image'); + throw new ValidationError('You must specify one of: taskDefinition or image', this); } this.validateHealthyPercentage('minHealthyPercent', props.minHealthyPercent); @@ -143,7 +143,7 @@ export class ApplicationLoadBalancedFargateService extends ApplicationLoadBalanc !Token.isUnresolved(props.maxHealthyPercent) && props.minHealthyPercent >= props.maxHealthyPercent ) { - throw new Error('Minimum healthy percent must be less than maximum healthy percent.'); + throw new ValidationError('Minimum healthy percent must be less than maximum healthy percent.', this); } const desiredCount = FeatureFlags.of(this).isEnabled(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount; @@ -177,7 +177,7 @@ export class ApplicationLoadBalancedFargateService extends ApplicationLoadBalanc private validateHealthyPercentage(name: string, value?: number) { if (value === undefined || Token.isUnresolved(value)) { return; } if (!Number.isInteger(value) || value < 0) { - throw new Error(`${name}: Must be a non-negative integer; received ${value}`); + throw new ValidationError(`${name}: Must be a non-negative integer; received ${value}`, this); } } @@ -186,11 +186,11 @@ export class ApplicationLoadBalancedFargateService extends ApplicationLoadBalanc return; } if (containerCpu > cpu) { - throw new Error(`containerCpu must be less than to cpu; received containerCpu: ${containerCpu}, cpu: ${cpu}`); + throw new ValidationError(`containerCpu must be less than to cpu; received containerCpu: ${containerCpu}, cpu: ${cpu}`, this); } // If containerCPU is 0, it is not an error. if (containerCpu < 0 || !Number.isInteger(containerCpu)) { - throw new Error(`containerCpu must be a non-negative integer; received ${containerCpu}`); + throw new ValidationError(`containerCpu must be a non-negative integer; received ${containerCpu}`, this); } } @@ -199,10 +199,10 @@ export class ApplicationLoadBalancedFargateService extends ApplicationLoadBalanc return; } if (containerMemoryLimitMiB > memoryLimitMiB) { - throw new Error(`containerMemoryLimitMiB must be less than to memoryLimitMiB; received containerMemoryLimitMiB: ${containerMemoryLimitMiB}, memoryLimitMiB: ${memoryLimitMiB}`); + throw new ValidationError(`containerMemoryLimitMiB must be less than to memoryLimitMiB; received containerMemoryLimitMiB: ${containerMemoryLimitMiB}, memoryLimitMiB: ${memoryLimitMiB}`, this); } if (containerMemoryLimitMiB <= 0 || !Number.isInteger(containerMemoryLimitMiB)) { - throw new Error(`containerMemoryLimitMiB must be a positive integer; received ${containerMemoryLimitMiB}`); + throw new ValidationError(`containerMemoryLimitMiB must be a positive integer; received ${containerMemoryLimitMiB}`, this); } } } diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/application-multiple-target-groups-fargate-service.ts b/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/application-multiple-target-groups-fargate-service.ts index 6e7a4749f0489..cb1aeaff6fa56 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/application-multiple-target-groups-fargate-service.ts +++ b/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/application-multiple-target-groups-fargate-service.ts @@ -1,7 +1,7 @@ import { Construct } from 'constructs'; import { FargateService, FargateTaskDefinition } from '../../../aws-ecs'; import { ApplicationTargetGroup } from '../../../aws-elasticloadbalancingv2'; -import { FeatureFlags } from '../../../core'; +import { FeatureFlags, ValidationError } from '../../../core'; import { propertyInjectable } from '../../../core/lib/prop-injectable'; import * as cxapi from '../../../cx-api'; import { @@ -63,7 +63,7 @@ export class ApplicationMultipleTargetGroupsFargateService extends ApplicationMu this.assignPublicIp = props.assignPublicIp ?? false; if (props.taskDefinition && props.taskImageOptions) { - throw new Error('You must specify only one of TaskDefinition or TaskImageOptions.'); + throw new ValidationError('You must specify only one of TaskDefinition or TaskImageOptions.', this); } else if (props.taskDefinition) { this.taskDefinition = props.taskDefinition; } else if (props.taskImageOptions) { @@ -94,10 +94,10 @@ export class ApplicationMultipleTargetGroupsFargateService extends ApplicationMu } } } else { - throw new Error('You must specify one of: taskDefinition or image'); + throw new ValidationError('You must specify one of: taskDefinition or image', this); } if (!this.taskDefinition.defaultContainer) { - throw new Error('At least one essential container must be specified'); + throw new ValidationError('At least one essential container must be specified', this); } if (this.taskDefinition.defaultContainer.portMappings.length === 0) { this.taskDefinition.defaultContainer.addPortMappings({ diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts b/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts index 9b2ad04330dcc..2c664c63509c9 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts +++ b/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts @@ -1,7 +1,7 @@ import { Construct } from 'constructs'; import { ISecurityGroup, SubnetSelection } from '../../../aws-ec2'; import { FargateService, FargateTaskDefinition } from '../../../aws-ecs'; -import { FeatureFlags } from '../../../core'; +import { FeatureFlags, ValidationError } from '../../../core'; import { propertyInjectable } from '../../../core/lib/prop-injectable'; import * as cxapi from '../../../cx-api'; import { FargateServiceBaseProps } from '../base/fargate-service-base'; @@ -63,7 +63,7 @@ export class NetworkLoadBalancedFargateService extends NetworkLoadBalancedServic this.assignPublicIp = props.assignPublicIp ?? false; if (props.taskDefinition && props.taskImageOptions) { - throw new Error('You must specify either a taskDefinition or an image, not both.'); + throw new ValidationError('You must specify either a taskDefinition or an image, not both.', this); } else if (props.taskDefinition) { this.taskDefinition = props.taskDefinition; } else if (props.taskImageOptions) { @@ -94,7 +94,7 @@ export class NetworkLoadBalancedFargateService extends NetworkLoadBalancedServic containerPort: taskImageOptions.containerPort || 80, }); } else { - throw new Error('You must specify one of: taskDefinition or image'); + throw new ValidationError('You must specify one of: taskDefinition or image', this); } const desiredCount = FeatureFlags.of(this).isEnabled(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount; diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/network-multiple-target-groups-fargate-service.ts b/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/network-multiple-target-groups-fargate-service.ts index c5e0c7119a410..4a8c485c8fd11 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/network-multiple-target-groups-fargate-service.ts +++ b/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/network-multiple-target-groups-fargate-service.ts @@ -1,7 +1,7 @@ import { Construct } from 'constructs'; import { FargateService, FargateTaskDefinition } from '../../../aws-ecs'; import { NetworkTargetGroup } from '../../../aws-elasticloadbalancingv2'; -import { FeatureFlags } from '../../../core'; +import { FeatureFlags, ValidationError } from '../../../core'; import { propertyInjectable } from '../../../core/lib/prop-injectable'; import * as cxapi from '../../../cx-api'; import { FargateServiceBaseProps } from '../base/fargate-service-base'; @@ -63,7 +63,7 @@ export class NetworkMultipleTargetGroupsFargateService extends NetworkMultipleTa this.assignPublicIp = props.assignPublicIp ?? false; if (props.taskDefinition && props.taskImageOptions) { - throw new Error('You must specify only one of TaskDefinition or TaskImageOptions.'); + throw new ValidationError('You must specify only one of TaskDefinition or TaskImageOptions.', this); } else if (props.taskDefinition) { this.taskDefinition = props.taskDefinition; } else if (props.taskImageOptions) { @@ -94,10 +94,10 @@ export class NetworkMultipleTargetGroupsFargateService extends NetworkMultipleTa } } } else { - throw new Error('You must specify one of: taskDefinition or image'); + throw new ValidationError('You must specify one of: taskDefinition or image', this); } if (!this.taskDefinition.defaultContainer) { - throw new Error('At least one essential container must be specified'); + throw new ValidationError('At least one essential container must be specified', this); } if (this.taskDefinition.defaultContainer.portMappings.length === 0) { this.taskDefinition.defaultContainer.addPortMappings({ @@ -113,7 +113,7 @@ export class NetworkMultipleTargetGroupsFargateService extends NetworkMultipleTa const containerPort = this.taskDefinition.defaultContainer.portMappings[0].containerPort; if (!containerPort) { - throw new Error('The first port mapping added to the default container must expose a single port'); + throw new ValidationError('The first port mapping added to the default container must expose a single port', this); } this.targetGroup = this.listener.addTargets('ECS', { diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts b/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts index 59aeb1cbcc845..e2364e65e0f23 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts +++ b/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts @@ -1,7 +1,7 @@ import { Construct } from 'constructs'; import * as ec2 from '../../../aws-ec2'; import { FargateService, FargateTaskDefinition, HealthCheck } from '../../../aws-ecs'; -import { FeatureFlags, Duration } from '../../../core'; +import { FeatureFlags, Duration, ValidationError } from '../../../core'; import * as cxapi from '../../../cx-api'; import { FargateServiceBaseProps } from '../base/fargate-service-base'; import { QueueProcessingServiceBase, QueueProcessingServiceBaseProps } from '../base/queue-processing-service-base'; @@ -77,7 +77,7 @@ export class QueueProcessingFargateService extends QueueProcessingServiceBase { super(scope, id, props); if (props.taskDefinition && props.image) { - throw new Error('You must specify only one of taskDefinition or image'); + throw new ValidationError('You must specify only one of taskDefinition or image', this); } else if (props.taskDefinition) { this.taskDefinition = props.taskDefinition; } else if (props.image) { @@ -100,7 +100,7 @@ export class QueueProcessingFargateService extends QueueProcessingServiceBase { healthCheck: props.healthCheck, }); } else { - throw new Error('You must specify one of: taskDefinition or image'); + throw new ValidationError('You must specify one of: taskDefinition or image', this); } // The desiredCount should be removed from the fargate service when the feature flag is removed. diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/scheduled-fargate-task.ts b/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/scheduled-fargate-task.ts index 0d2b5f6fd7fea..b0745a6184f2a 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/scheduled-fargate-task.ts +++ b/packages/aws-cdk-lib/aws-ecs-patterns/lib/fargate/scheduled-fargate-task.ts @@ -1,7 +1,7 @@ import { Construct } from 'constructs'; import { FargateTaskDefinition } from '../../../aws-ecs'; import { EcsTask } from '../../../aws-events-targets'; -import { Annotations } from '../../../core'; +import { Annotations, ValidationError } from '../../../core'; import { FargateServiceBaseProps } from '../base/fargate-service-base'; import { ScheduledTaskBase, ScheduledTaskBaseProps, ScheduledTaskImageProps } from '../base/scheduled-task-base'; @@ -68,7 +68,7 @@ export class ScheduledFargateTask extends ScheduledTaskBase { super(scope, id, props); if (props.scheduledFargateTaskDefinitionOptions && props.scheduledFargateTaskImageOptions) { - throw new Error('You must specify either a scheduledFargateTaskDefinitionOptions or scheduledFargateTaskOptions, not both.'); + throw new ValidationError('You must specify either a scheduledFargateTaskDefinitionOptions or scheduledFargateTaskOptions, not both.', this); } else if (props.scheduledFargateTaskDefinitionOptions) { this.taskDefinition = props.scheduledFargateTaskDefinitionOptions.taskDefinition; } else if (props.scheduledFargateTaskImageOptions) { @@ -87,7 +87,7 @@ export class ScheduledFargateTask extends ScheduledTaskBase { logging: taskImageOptions.logDriver ?? this.createAWSLogDriver(this.node.id), }); } else { - throw new Error('You must specify one of: taskDefinition or image'); + throw new ValidationError('You must specify one of: taskDefinition or image', this); } if (props.taskDefinition) { diff --git a/packages/aws-cdk-lib/aws-ecs/lib/amis.ts b/packages/aws-cdk-lib/aws-ecs/lib/amis.ts index d655daa020aa2..57812212db28c 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/amis.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/amis.ts @@ -4,6 +4,7 @@ import * as ssm from '../../aws-ssm'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. // eslint-disable-next-line import { Construct } from 'constructs'; +import { UnscopedValidationError } from '../../core'; /** * The ECS-optimized AMI variant to use. For more information, see @@ -122,15 +123,15 @@ export class EcsOptimizedAmi implements ec2.IMachineImage { this.hwType = (props && props.hardwareType) || AmiHardwareType.STANDARD; if (props && props.generation) { // generation defined in the props object if (props.generation === ec2.AmazonLinuxGeneration.AMAZON_LINUX && this.hwType !== AmiHardwareType.STANDARD) { - throw new Error('Amazon Linux does not support special hardware type. Use Amazon Linux 2 instead'); + throw new UnscopedValidationError('Amazon Linux does not support special hardware type. Use Amazon Linux 2 instead'); } else if (props.windowsVersion) { - throw new Error('"windowsVersion" and Linux image "generation" cannot be both set'); + throw new UnscopedValidationError('"windowsVersion" and Linux image "generation" cannot be both set'); } else { this.generation = props.generation; } } else if (props && props.windowsVersion) { if (this.hwType !== AmiHardwareType.STANDARD) { - throw new Error('Windows Server does not support special hardware type'); + throw new UnscopedValidationError('Windows Server does not support special hardware type'); } else { this.windowsVersion = props.windowsVersion; } @@ -265,7 +266,7 @@ export class EcsOptimizedImage implements ec2.IMachineImage { } else if (props.generation) { this.generation = props.generation; } else { - throw new Error('This error should never be thrown'); + throw new UnscopedValidationError('This error should never be thrown'); } // set the SSM parameter name diff --git a/packages/aws-cdk-lib/aws-ecs/lib/base/_imported-task-definition.ts b/packages/aws-cdk-lib/aws-ecs/lib/base/_imported-task-definition.ts index 6e01519c6f48a..de9f9ff9fe2df 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/base/_imported-task-definition.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/base/_imported-task-definition.ts @@ -1,7 +1,7 @@ import { Construct } from 'constructs'; import { Compatibility, NetworkMode, isEc2Compatible, isFargateCompatible, isExternalCompatible } from './task-definition'; import { IRole } from '../../../aws-iam'; -import { Resource } from '../../../core'; +import { Resource, ValidationError } from '../../../core'; import { addConstructMetadata } from '../../../core/lib/metadata-resource'; import { IEc2TaskDefinition } from '../ec2/ec2-task-definition'; import { IFargateTaskDefinition } from '../fargate/fargate-task-definition'; @@ -89,8 +89,8 @@ export class ImportedTaskDefinition extends Resource implements IEc2TaskDefiniti public get networkMode(): NetworkMode { if (this._networkMode == undefined) { - throw new Error('This operation requires the networkMode in ImportedTaskDefinition to be defined. ' + - 'Add the \'networkMode\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); + throw new ValidationError('This operation requires the networkMode in ImportedTaskDefinition to be defined. ' + + 'Add the \'networkMode\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition', this); } else { return this._networkMode; } @@ -98,8 +98,8 @@ export class ImportedTaskDefinition extends Resource implements IEc2TaskDefiniti public get taskRole(): IRole { if (this._taskRole == undefined) { - throw new Error('This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + - 'Add the \'taskRole\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); + throw new ValidationError('This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + + 'Add the \'taskRole\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition', this); } else { return this._taskRole; } diff --git a/packages/aws-cdk-lib/aws-ecs/lib/base/base-service.ts b/packages/aws-cdk-lib/aws-ecs/lib/base/base-service.ts index cf9a980a2dec6..f2a73a0745274 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/base/base-service.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/base/base-service.ts @@ -1,4 +1,4 @@ -import { Construct } from 'constructs'; +import { Construct, IConstruct } from 'constructs'; import { ScalableTaskCount } from './scalable-task-count'; import { ServiceManagedVolume } from './service-managed-volume'; import * as appscaling from '../../../aws-applicationautoscaling'; @@ -22,6 +22,7 @@ import { Token, Arn, Fn, + ValidationError, } from '../../../core'; import * as cxapi from '../../../cx-api'; import { RegionInfo } from '../../../region-info'; @@ -561,7 +562,7 @@ export abstract class BaseService extends Resource } else { const resourceNameParts = resourceName.split('/'); if (resourceNameParts.length !== 2) { - throw new Error(`resource name ${resourceName} from service ARN: ${serviceArn} is not using the ARN cluster format`); + throw new ValidationError(`resource name ${resourceName} from service ARN: ${serviceArn} is not using the ARN cluster format`, scope); } clusterName = resourceNameParts[0]; serviceName = resourceNameParts[1]; @@ -676,7 +677,7 @@ export abstract class BaseService extends Resource }); if (props.propagateTags && props.propagateTaskTagsFrom) { - throw new Error('You can only specify either propagateTags or propagateTaskTagsFrom. Alternatively, you can leave both blank'); + throw new ValidationError('You can only specify either propagateTags or propagateTaskTagsFrom. Alternatively, you can leave both blank', this); } this.taskDefinition = taskDefinition; @@ -731,7 +732,7 @@ export abstract class BaseService extends Resource if (props.deploymentAlarms && deploymentController && deploymentController.type !== DeploymentControllerType.ECS) { - throw new Error('Deployment alarms requires the ECS deployment controller.'); + throw new ValidationError('Deployment alarms requires the ECS deployment controller.', this); } if ( @@ -739,7 +740,7 @@ export abstract class BaseService extends Resource && props.taskDefinitionRevision && props.taskDefinitionRevision !== TaskDefinitionRevision.LATEST ) { - throw new Error('CODE_DEPLOY deploymentController can only be used with the `latest` task definition revision'); + throw new ValidationError('CODE_DEPLOY deploymentController can only be used with the `latest` task definition revision', this); } if (props.minHealthyPercent === undefined) { @@ -796,7 +797,7 @@ export abstract class BaseService extends Resource if (props.deploymentAlarms) { if (props.deploymentAlarms.alarmNames.length === 0) { - throw new Error('at least one alarm name is required when specifying deploymentAlarms, received empty array'); + throw new ValidationError('at least one alarm name is required when specifying deploymentAlarms, received empty array', this); } this.deploymentAlarms = { alarmNames: props.deploymentAlarms.alarmNames, @@ -828,7 +829,7 @@ export abstract class BaseService extends Resource private renderVolumes(): CfnService.ServiceVolumeConfigurationProperty[] { if (this.volumes.length > 1) { - throw new Error(`Only one EBS volume can be specified for 'volumeConfigurations', got: ${this.volumes.length}`); + throw new ValidationError(`Only one EBS volume can be specified for 'volumeConfigurations', got: ${this.volumes.length}`, this); } return this.volumes.map(renderVolume); function renderVolume(spec: ServiceManagedVolume): CfnService.ServiceVolumeConfigurationProperty { @@ -879,7 +880,7 @@ export abstract class BaseService extends Resource */ public enableDeploymentAlarms(alarmNames: string[], options?: DeploymentAlarmOptions) { if (alarmNames.length === 0 ) { - throw new Error('at least one alarm name is required when calling enableDeploymentAlarms(), received empty array'); + throw new ValidationError('at least one alarm name is required when calling enableDeploymentAlarms(), received empty array', this); } alarmNames.forEach(alarmName => { @@ -895,7 +896,7 @@ export abstract class BaseService extends Resource (AlarmBehavior.ROLLBACK_ON_ALARM === options.behavior && !this.deploymentAlarms.rollback) || (AlarmBehavior.FAIL_ON_ALARM === options.behavior && this.deploymentAlarms.rollback) ) { - throw new Error(`all deployment alarms on an ECS service must have the same AlarmBehavior. Attempted to enable deployment alarms with ${options.behavior}, but alarms were previously enabled with ${this.deploymentAlarms.rollback ? AlarmBehavior.ROLLBACK_ON_ALARM : AlarmBehavior.FAIL_ON_ALARM}`); + throw new ValidationError(`all deployment alarms on an ECS service must have the same AlarmBehavior. Attempted to enable deployment alarms with ${options.behavior}, but alarms were previously enabled with ${this.deploymentAlarms.rollback ? AlarmBehavior.ROLLBACK_ON_ALARM : AlarmBehavior.FAIL_ON_ALARM}`, this); } } @@ -917,7 +918,7 @@ export abstract class BaseService extends Resource */ public enableServiceConnect(config?: ServiceConnectProps) { if (this._serviceConnectConfig) { - throw new Error('Service connect configuration cannot be specified more than once.'); + throw new ValidationError('Service connect configuration cannot be specified more than once.', this); } this.validateServiceConnectConfiguration(config); @@ -947,7 +948,7 @@ export abstract class BaseService extends Resource const services = cfg.services?.map(svc => { const containerPort = this.taskDefinition.findPortMappingByName(svc.portMappingName)?.containerPort; if (!containerPort) { - throw new Error(`Port mapping with name ${svc.portMappingName} does not exist.`); + throw new ValidationError(`Port mapping with name ${svc.portMappingName} does not exist.`, this); } const alias = { port: svc.port || containerPort, @@ -992,12 +993,12 @@ export abstract class BaseService extends Resource */ private validateServiceConnectConfiguration(config?: ServiceConnectProps) { if (!this.taskDefinition.defaultContainer) { - throw new Error('Task definition must have at least one container to enable service connect.'); + throw new ValidationError('Task definition must have at least one container to enable service connect.', this); } // Check the implicit enable case; when config isn't specified or namespace isn't specified, we need to check that there is a namespace on the cluster. if ((!config || !config.namespace) && !this.cluster.defaultCloudMapNamespace) { - throw new Error('Namespace must be defined either in serviceConnectConfig or cluster.defaultCloudMapNamespace'); + throw new ValidationError('Namespace must be defined either in serviceConnectConfig or cluster.defaultCloudMapNamespace', this); } // When config isn't specified, return. @@ -1012,13 +1013,13 @@ export abstract class BaseService extends Resource config.services.forEach(serviceConnectService => { // port must exist on the task definition if (!this.taskDefinition.findPortMappingByName(serviceConnectService.portMappingName)) { - throw new Error(`Port Mapping '${serviceConnectService.portMappingName}' does not exist on the task definition.`); + throw new ValidationError(`Port Mapping '${serviceConnectService.portMappingName}' does not exist on the task definition.`, this); } // Check that no two service connect services use the same discovery name. const discoveryName = serviceConnectService.discoveryName || serviceConnectService.portMappingName; if (portNames.get(serviceConnectService.portMappingName)?.includes(discoveryName)) { - throw new Error(`Cannot create multiple services with the discoveryName '${discoveryName}'.`); + throw new ValidationError(`Cannot create multiple services with the discoveryName '${discoveryName}'.`, this); } let currentDiscoveries = portNames.get(serviceConnectService.portMappingName); @@ -1031,19 +1032,19 @@ export abstract class BaseService extends Resource // IngressPortOverride should be within the valid port range if it exists. if (serviceConnectService.ingressPortOverride && !this.isValidPort(serviceConnectService.ingressPortOverride)) { - throw new Error(`ingressPortOverride ${serviceConnectService.ingressPortOverride} is not valid.`); + throw new ValidationError(`ingressPortOverride ${serviceConnectService.ingressPortOverride} is not valid.`, this); } // clientAlias.port should be within the valid port range if (serviceConnectService.port && !this.isValidPort(serviceConnectService.port)) { - throw new Error(`Client Alias port ${serviceConnectService.port} is not valid.`); + throw new ValidationError(`Client Alias port ${serviceConnectService.port} is not valid.`, this); } // tls.awsPcaAuthorityArn should be an ARN const awsPcaAuthorityArn = serviceConnectService.tls?.awsPcaAuthorityArn; if (awsPcaAuthorityArn && !Token.isUnresolved(awsPcaAuthorityArn) && !awsPcaAuthorityArn.startsWith('arn:')) { - throw new Error(`awsPcaAuthorityArn must start with "arn:" and have at least 6 components; received ${awsPcaAuthorityArn}`); + throw new ValidationError(`awsPcaAuthorityArn must start with "arn:" and have at least 6 components; received ${awsPcaAuthorityArn}`, this); } }); } @@ -1278,7 +1279,7 @@ export abstract class BaseService extends Resource */ public autoScaleTaskCount(props: appscaling.EnableScalingProps) { if (this.scalableTaskCount) { - throw new Error('AutoScaling of task count already enabled for this service'); + throw new ValidationError('AutoScaling of task count already enabled for this service', this); } return this.scalableTaskCount = new ScalableTaskCount(this, 'TaskCount', { @@ -1298,17 +1299,17 @@ export abstract class BaseService extends Resource public enableCloudMap(options: CloudMapOptions): cloudmap.Service { const sdNamespace = options.cloudMapNamespace ?? this.cluster.defaultCloudMapNamespace; if (sdNamespace === undefined) { - throw new Error('Cannot enable service discovery if a Cloudmap Namespace has not been created in the cluster.'); + throw new ValidationError('Cannot enable service discovery if a Cloudmap Namespace has not been created in the cluster.', this); } if (sdNamespace.type === cloudmap.NamespaceType.HTTP) { - throw new Error('Cannot enable DNS service discovery for HTTP Cloudmap Namespace.'); + throw new ValidationError('Cannot enable DNS service discovery for HTTP Cloudmap Namespace.', this); } // Determine DNS type based on network mode const networkMode = this.taskDefinition.networkMode; if (networkMode === NetworkMode.NONE) { - throw new Error('Cannot use a service discovery if NetworkMode is None. Use Bridge, Host or AwsVpc instead.'); + throw new ValidationError('Cannot use a service discovery if NetworkMode is None. Use Bridge, Host or AwsVpc instead.', this); } // Bridge or host network mode requires SRV records @@ -1319,7 +1320,7 @@ export abstract class BaseService extends Resource dnsRecordType = cloudmap.DnsRecordType.SRV; } if (dnsRecordType !== cloudmap.DnsRecordType.SRV) { - throw new Error('SRV records must be used when network mode is Bridge or Host.'); + throw new ValidationError('SRV records must be used when network mode is Bridge or Host.', this); } } @@ -1330,7 +1331,7 @@ export abstract class BaseService extends Resource } } - const { containerName, containerPort } = determineContainerNameAndPort({ + const { containerName, containerPort } = determineContainerNameAndPort(this, { taskDefinition: this.taskDefinition, dnsRecordType: dnsRecordType!, container: options.container, @@ -1365,7 +1366,7 @@ export abstract class BaseService extends Resource public associateCloudMapService(options: AssociateCloudMapServiceOptions): void { const service = options.service; - const { containerName, containerPort } = determineContainerNameAndPort({ + const { containerName, containerPort } = determineContainerNameAndPort(this, { taskDefinition: this.taskDefinition, dnsRecordType: service.dnsRecordType, container: options.container, @@ -1469,10 +1470,10 @@ export abstract class BaseService extends Resource */ private attachToELB(loadBalancer: elb.LoadBalancer, containerName: string, containerPort: number): void { if (this.taskDefinition.networkMode === NetworkMode.AWS_VPC) { - throw new Error('Cannot use a Classic Load Balancer if NetworkMode is AwsVpc. Use Host or Bridge instead.'); + throw new ValidationError('Cannot use a Classic Load Balancer if NetworkMode is AwsVpc. Use Host or Bridge instead.', this); } if (this.taskDefinition.networkMode === NetworkMode.NONE) { - throw new Error('Cannot use a Classic Load Balancer if NetworkMode is None. Use Host or Bridge instead.'); + throw new ValidationError('Cannot use a Classic Load Balancer if NetworkMode is None. Use Host or Bridge instead.', this); } this.loadBalancers.push({ @@ -1487,7 +1488,7 @@ export abstract class BaseService extends Resource */ private attachToELBv2(targetGroup: elbv2.ITargetGroup, containerName: string, containerPort: number): elbv2.LoadBalancerTargetProps { if (this.taskDefinition.networkMode === NetworkMode.NONE) { - throw new Error('Cannot use a load balancer if NetworkMode is None. Use Bridge, Host or AwsVpc instead.'); + throw new ValidationError('Cannot use a load balancer if NetworkMode is None. Use Bridge, Host or AwsVpc instead.', this); } this.loadBalancers.push({ @@ -1528,7 +1529,7 @@ export abstract class BaseService extends Resource */ private addServiceRegistry(registry: ServiceRegistry) { if (this.serviceRegistries.length >= 1) { - throw new Error('Cannot associate with the given service discovery registry. ECS supports at most one service registry per service.'); + throw new ValidationError('Cannot associate with the given service discovery registry. ECS supports at most one service registry per service.', this); } const sr = this.renderServiceRegistry(registry); @@ -1569,10 +1570,10 @@ export abstract class BaseService extends Resource private renderTimeout(idleTimeout?: Duration, perRequestTimeout?: Duration): CfnService.TimeoutConfigurationProperty | undefined { if (!idleTimeout && !perRequestTimeout) return undefined; if (idleTimeout && idleTimeout.toMilliseconds() > 0 && idleTimeout.toMilliseconds() < Duration.seconds(1).toMilliseconds()) { - throw new Error(`idleTimeout must be at least 1 second or 0 to disable it, got ${idleTimeout.toMilliseconds()}ms.`); + throw new ValidationError(`idleTimeout must be at least 1 second or 0 to disable it, got ${idleTimeout.toMilliseconds()}ms.`, this); } if (perRequestTimeout && perRequestTimeout.toMilliseconds() > 0 && perRequestTimeout.toMilliseconds() < Duration.seconds(1).toMilliseconds()) { - throw new Error(`perRequestTimeout must be at least 1 second or 0 to disable it, got ${perRequestTimeout.toMilliseconds()}ms.`); + throw new ValidationError(`perRequestTimeout must be at least 1 second or 0 to disable it, got ${perRequestTimeout.toMilliseconds()}ms.`, this); } return { idleTimeoutSeconds: idleTimeout?.toSeconds(), @@ -1758,21 +1759,21 @@ interface DetermineContainerNameAndPortOptions { /** * Determine the name of the container and port to target for the service registry. */ -function determineContainerNameAndPort(options: DetermineContainerNameAndPortOptions) { +function determineContainerNameAndPort(scope: IConstruct, options: DetermineContainerNameAndPortOptions) { // If the record type is SRV, then provide the containerName and containerPort to target. // We use the name of the default container and the default port of the default container // unless the user specifies otherwise. if (options.dnsRecordType === cloudmap.DnsRecordType.SRV) { // Ensure the user-provided container is from the right task definition. if (options.container && options.container.taskDefinition != options.taskDefinition) { - throw new Error('Cannot add discovery for a container from another task definition'); + throw new ValidationError('Cannot add discovery for a container from another task definition', scope); } const container = options.container ?? options.taskDefinition.defaultContainer!; // Ensure that any port given by the user is mapped. if (options.containerPort && !container.portMappings.some(mapping => mapping.containerPort === options.containerPort)) { - throw new Error('Cannot add discovery for a container port that has not been mapped'); + throw new ValidationError('Cannot add discovery for a container port that has not been mapped', scope); } return { diff --git a/packages/aws-cdk-lib/aws-ecs/lib/base/from-service-attributes.ts b/packages/aws-cdk-lib/aws-ecs/lib/base/from-service-attributes.ts index 4d23add32e483..2dfbb5f930390 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/base/from-service-attributes.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/base/from-service-attributes.ts @@ -1,5 +1,5 @@ import { Construct } from 'constructs'; -import { ArnFormat, FeatureFlags, Fn, Resource, Stack, Token } from '../../../core'; +import { ArnFormat, FeatureFlags, Fn, Resource, Stack, Token, ValidationError } from '../../../core'; import { ECS_ARN_FORMAT_INCLUDES_CLUSTER_NAME } from '../../../cx-api'; import { IBaseService } from '../base/base-service'; import { ICluster } from '../cluster'; @@ -30,7 +30,7 @@ export interface ServiceAttributes { export function fromServiceAttributes(scope: Construct, id: string, attrs: ServiceAttributes): IBaseService { if ((attrs.serviceArn && attrs.serviceName) || (!attrs.serviceArn && !attrs.serviceName)) { - throw new Error('You can only specify either serviceArn or serviceName.'); + throw new ValidationError('You can only specify either serviceArn or serviceName.', scope); } const newArnFormat = FeatureFlags.of(scope).isEnabled(ECS_ARN_FORMAT_INCLUDES_CLUSTER_NAME); diff --git a/packages/aws-cdk-lib/aws-ecs/lib/base/service-managed-volume.ts b/packages/aws-cdk-lib/aws-ecs/lib/base/service-managed-volume.ts index b0a89786c6068..d0a83d31c355f 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/base/service-managed-volume.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/base/service-managed-volume.ts @@ -2,7 +2,7 @@ import { Construct } from 'constructs'; import * as ec2 from '../../../aws-ec2'; import * as iam from '../../../aws-iam'; import * as kms from '../../../aws-kms'; -import { Size, Token } from '../../../core'; +import { Size, Token, ValidationError } from '../../../core'; import { BaseMountPoint, ContainerDefinition } from '../container-definition'; /** @@ -251,11 +251,11 @@ export class ServiceManagedVolume extends Construct { // Validate if both size and snapShotId are not specified. if (size === undefined && snapShotId === undefined) { - throw new Error('\'size\' or \'snapShotId\' must be specified'); + throw new ValidationError('\'size\' or \'snapShotId\' must be specified', this); } if (snapShotId && !Token.isUnresolved(snapShotId) && !/^snap-[0-9a-fA-F]+$/.test(snapShotId)) { - throw new Error(`'snapshotId' does match expected pattern. Expected 'snap-' (ex: 'snap-05abe246af') or Token, got: ${snapShotId}`); + throw new ValidationError(`'snapshotId' does match expected pattern. Expected 'snap-' (ex: 'snap-05abe246af') or Token, got: ${snapShotId}`, this); } // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-service-servicemanagedebsvolumeconfiguration.html#cfn-ecs-service-servicemanagedebsvolumeconfiguration-sizeingib @@ -273,7 +273,7 @@ export class ServiceManagedVolume extends Construct { if (size !== undefined) { const { minSize, maxSize } = sizeInGiBRanges[volumeType]; if (size.toGibibytes() < minSize || size.toGibibytes() > maxSize) { - throw new Error(`'${volumeType}' volumes must have a size between ${minSize} and ${maxSize} GiB, got ${size.toGibibytes()} GiB`); + throw new ValidationError(`'${volumeType}' volumes must have a size between ${minSize} and ${maxSize} GiB, got ${size.toGibibytes()} GiB`, this); } } @@ -281,9 +281,9 @@ export class ServiceManagedVolume extends Construct { // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-service-servicemanagedebsvolumeconfiguration.html#cfn-ecs-service-servicemanagedebsvolumeconfiguration-throughput if (throughput !== undefined) { if (volumeType !== ec2.EbsDeviceVolumeType.GP3) { - throw new Error(`'throughput' can only be configured with gp3 volume type, got ${volumeType}`); + throw new ValidationError(`'throughput' can only be configured with gp3 volume type, got ${volumeType}`, this); } else if (!Token.isUnresolved(throughput) && throughput > 1000) { - throw new Error(`'throughput' must be less than or equal to 1000 MiB/s, got ${throughput} MiB/s`); + throw new ValidationError(`'throughput' must be less than or equal to 1000 MiB/s, got ${throughput} MiB/s`, this); } } @@ -291,12 +291,12 @@ export class ServiceManagedVolume extends Construct { // https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateVolume.html if ([ec2.EbsDeviceVolumeType.SC1, ec2.EbsDeviceVolumeType.ST1, ec2.EbsDeviceVolumeType.STANDARD, ec2.EbsDeviceVolumeType.GP2].includes(volumeType) && iops !== undefined) { - throw new Error(`'iops' cannot be specified with sc1, st1, gp2 and standard volume types, got ${volumeType}`); + throw new ValidationError(`'iops' cannot be specified with sc1, st1, gp2 and standard volume types, got ${volumeType}`, this); } // Check if IOPS is required but not provided. if ([ec2.EbsDeviceVolumeType.IO1, ec2.EbsDeviceVolumeType.IO2].includes(volumeType) && iops === undefined) { - throw new Error(`'iops' must be specified with io1 or io2 volume types, got ${volumeType}`); + throw new ValidationError(`'iops' must be specified with io1 or io2 volume types, got ${volumeType}`, this); } // Validate IOPS range if specified. @@ -307,7 +307,7 @@ export class ServiceManagedVolume extends Construct { if (iops !== undefined && !Token.isUnresolved(iops)) { const { min, max } = iopsRanges[volumeType]; if ((iops < min || iops > max)) { - throw new Error(`'${volumeType}' volumes must have 'iops' between ${min} and ${max}, got ${iops}`); + throw new ValidationError(`'${volumeType}' volumes must have 'iops' between ${min} and ${max}, got ${iops}`, this); } } } diff --git a/packages/aws-cdk-lib/aws-ecs/lib/base/task-definition.ts b/packages/aws-cdk-lib/aws-ecs/lib/base/task-definition.ts index 78ba502a95c52..51c9667fd0b9d 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/base/task-definition.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/base/task-definition.ts @@ -2,7 +2,7 @@ import { Construct } from 'constructs'; import { ImportedTaskDefinition } from './_imported-task-definition'; import * as ec2 from '../../../aws-ec2'; import * as iam from '../../../aws-iam'; -import { IResource, Lazy, Names, PhysicalName, Resource } from '../../../core'; +import { IResource, Lazy, Names, PhysicalName, Resource, UnscopedValidationError, ValidationError } from '../../../core'; import { addConstructMetadata, MethodMetadata } from '../../../core/lib/metadata-resource'; import { propertyInjectable } from '../../../core/lib/prop-injectable'; import { ContainerDefinition, ContainerDefinitionOptions, PortMapping, Protocol } from '../container-definition'; @@ -451,34 +451,34 @@ export class TaskDefinition extends TaskDefinitionBase { this.networkMode = props.networkMode ?? (this.isFargateCompatible ? NetworkMode.AWS_VPC : NetworkMode.BRIDGE); if (this.isFargateCompatible && this.networkMode !== NetworkMode.AWS_VPC) { - throw new Error(`Fargate tasks can only have AwsVpc network mode, got: ${this.networkMode}`); + throw new ValidationError(`Fargate tasks can only have AwsVpc network mode, got: ${this.networkMode}`, this); } if (props.proxyConfiguration && this.networkMode !== NetworkMode.AWS_VPC) { - throw new Error(`ProxyConfiguration can only be used with AwsVpc network mode, got: ${this.networkMode}`); + throw new ValidationError(`ProxyConfiguration can only be used with AwsVpc network mode, got: ${this.networkMode}`, this); } if (props.placementConstraints && props.placementConstraints.length > 0 && this.isFargateCompatible) { - throw new Error('Cannot set placement constraints on tasks that run on Fargate'); + throw new ValidationError('Cannot set placement constraints on tasks that run on Fargate', this); } if (this.isFargateCompatible && (!props.cpu || !props.memoryMiB)) { - throw new Error(`Fargate-compatible tasks require both CPU (${props.cpu}) and memory (${props.memoryMiB}) specifications`); + throw new ValidationError(`Fargate-compatible tasks require both CPU (${props.cpu}) and memory (${props.memoryMiB}) specifications`, this); } if (props.inferenceAccelerators && props.inferenceAccelerators.length > 0 && this.isFargateCompatible) { - throw new Error('Cannot use inference accelerators on tasks that run on Fargate'); + throw new ValidationError('Cannot use inference accelerators on tasks that run on Fargate', this); } if (this.isExternalCompatible && ![NetworkMode.BRIDGE, NetworkMode.HOST, NetworkMode.NONE].includes(this.networkMode)) { - throw new Error(`External tasks can only have Bridge, Host or None network mode, got: ${this.networkMode}`); + throw new ValidationError(`External tasks can only have Bridge, Host or None network mode, got: ${this.networkMode}`, this); } if (!this.isFargateCompatible && props.runtimePlatform) { - throw new Error('Cannot specify runtimePlatform in non-Fargate compatible tasks'); + throw new ValidationError('Cannot specify runtimePlatform in non-Fargate compatible tasks', this); } // https://docs.aws.amazon.com/AmazonECS/latest/developerguide/fault-injection.html if (props.enableFaultInjection && ![NetworkMode.AWS_VPC, NetworkMode.HOST].includes(this.networkMode)) { - throw new Error(`Only AWS_VPC and HOST Network Modes are supported for enabling Fault Injection, got ${this.networkMode} mode.`); + throw new ValidationError(`Only AWS_VPC and HOST Network Modes are supported for enabling Fault Injection, got ${this.networkMode} mode.`, this); } this._executionRole = props.executionRole; @@ -605,14 +605,14 @@ export class TaskDefinition extends TaskDefinitionBase { public _validateTarget(options: LoadBalancerTargetOptions): LoadBalancerTarget { const targetContainer = this.findContainer(options.containerName); if (targetContainer === undefined) { - throw new Error(`No container named '${options.containerName}'. Did you call "addContainer()"?`); + throw new ValidationError(`No container named '${options.containerName}'. Did you call "addContainer()"?`, this); } const targetProtocol = options.protocol || Protocol.TCP; const targetContainerPort = options.containerPort || targetContainer.containerPort; const portMapping = targetContainer.findPortMapping(targetContainerPort, targetProtocol); if (portMapping === undefined) { // eslint-disable-next-line max-len - throw new Error(`Container '${targetContainer}' has no mapping for port ${options.containerPort} and protocol ${targetProtocol}. Did you call "container.addPortMappings()"?`); + throw new ValidationError(`Container '${targetContainer}' has no mapping for port ${options.containerPort} and protocol ${targetProtocol}. Did you call "container.addPortMappings()"?`, this); } return { containerName: options.containerName, @@ -670,7 +670,7 @@ export class TaskDefinition extends TaskDefinitionBase { public addFirelensLogRouter(id: string, props: FirelensLogRouterDefinitionOptions) { // only one firelens log router is allowed in each task. if (this.containers.find(x => x instanceof FirelensLogRouter)) { - throw new Error('Firelens log router is already added in this task.'); + throw new ValidationError('Firelens log router is already added in this task.', this); } return new FirelensLogRouter(this, id, { taskDefinition: this, ...props }); @@ -685,7 +685,7 @@ export class TaskDefinition extends TaskDefinitionBase { const taskCpu = Number(this._cpu); const sumOfContainerCpu = [...this.containers, container].map(c => c.cpu).filter((cpu): cpu is number => typeof cpu === 'number').reduce((a, c) => a + c, 0); if (taskCpu < sumOfContainerCpu) { - throw new Error('The sum of all container cpu values cannot be greater than the value of the task cpu'); + throw new ValidationError('The sum of all container cpu values cannot be greater than the value of the task cpu', this); } } @@ -711,7 +711,7 @@ export class TaskDefinition extends TaskDefinitionBase { // Other volume configurations must not be specified. if (volume.host || volume.dockerVolumeConfiguration || volume.efsVolumeConfiguration) { - throw new Error(`Volume Configurations must not be specified for '${volume.name}' when 'configuredAtLaunch' is set to true`); + throw new ValidationError(`Volume Configurations must not be specified for '${volume.name}' when 'configuredAtLaunch' is set to true`, this); } } @@ -721,7 +721,7 @@ export class TaskDefinition extends TaskDefinitionBase { @MethodMetadata() public addPlacementConstraint(constraint: PlacementConstraint) { if (isFargateCompatible(this.compatibility)) { - throw new Error('Cannot set placement constraints on tasks that run on Fargate'); + throw new ValidationError('Cannot set placement constraints on tasks that run on Fargate', this); } this.placementConstraints.push(...constraint.toJson()); } @@ -744,7 +744,7 @@ export class TaskDefinition extends TaskDefinitionBase { @MethodMetadata() public addInferenceAccelerator(inferenceAccelerator: InferenceAccelerator) { if (isFargateCompatible(this.compatibility)) { - throw new Error('Cannot use inference accelerators on tasks that run on Fargate'); + throw new ValidationError('Cannot use inference accelerators on tasks that run on Fargate', this); } this._inferenceAccelerators.push(inferenceAccelerator); } @@ -922,18 +922,18 @@ export class TaskDefinition extends TaskDefinitionBase { private checkFargateWindowsBasedTasksSize(cpu: string, memory: string, runtimePlatform: RuntimePlatform) { if (Number(cpu) === 1024) { if (Number(memory) < 1024 || Number(memory) > 8192 || (Number(memory) % 1024 !== 0)) { - throw new Error(`If provided cpu is ${cpu}, then memoryMiB must have a min of 1024 and a max of 8192, in 1024 increments. Provided memoryMiB was ${Number(memory)}.`); + throw new ValidationError(`If provided cpu is ${cpu}, then memoryMiB must have a min of 1024 and a max of 8192, in 1024 increments. Provided memoryMiB was ${Number(memory)}.`, this); } } else if (Number(cpu) === 2048) { if (Number(memory) < 4096 || Number(memory) > 16384 || (Number(memory) % 1024 !== 0)) { - throw new Error(`If provided cpu is ${cpu}, then memoryMiB must have a min of 4096 and max of 16384, in 1024 increments. Provided memoryMiB ${Number(memory)}.`); + throw new ValidationError(`If provided cpu is ${cpu}, then memoryMiB must have a min of 4096 and max of 16384, in 1024 increments. Provided memoryMiB ${Number(memory)}.`, this); } } else if (Number(cpu) === 4096) { if (Number(memory) < 8192 || Number(memory) > 30720 || (Number(memory) % 1024 !== 0)) { - throw new Error(`If provided cpu is ${cpu}, then memoryMiB must have a min of 8192 and a max of 30720, in 1024 increments.Provided memoryMiB was ${Number(memory)}.`); + throw new ValidationError(`If provided cpu is ${cpu}, then memoryMiB must have a min of 8192 and a max of 30720, in 1024 increments.Provided memoryMiB was ${Number(memory)}.`, this); } } else { - throw new Error(`If operatingSystemFamily is ${runtimePlatform.operatingSystemFamily!._operatingSystemFamily}, then cpu must be in 1024 (1 vCPU), 2048 (2 vCPU), or 4096 (4 vCPU). Provided value was: ${cpu}`); + throw new ValidationError(`If operatingSystemFamily is ${runtimePlatform.operatingSystemFamily!._operatingSystemFamily}, then cpu must be in 1024 (1 vCPU), 2048 (2 vCPU), or 4096 (4 vCPU). Provided value was: ${cpu}`, this); } } } @@ -1342,7 +1342,7 @@ export class TaskDefinitionRevision { */ public static of(revision: number) { if (revision < 1) { - throw new Error(`A task definition revision must be 'latest' or a positive number, got ${revision}`); + throw new UnscopedValidationError(`A task definition revision must be 'latest' or a positive number, got ${revision}`); } return new TaskDefinitionRevision(revision.toString()); } diff --git a/packages/aws-cdk-lib/aws-ecs/lib/cluster.ts b/packages/aws-cdk-lib/aws-ecs/lib/cluster.ts index 31925aa0f3798..b93e61efebecf 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/cluster.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/cluster.ts @@ -25,6 +25,7 @@ import { Token, Names, FeatureFlags, Annotations, + ValidationError, } from '../../core'; import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource'; import { mutatingAspectPrio32333 } from '../../core/lib/private/aspect-prio'; @@ -176,7 +177,7 @@ export class Cluster extends Resource implements ICluster { const clusterName = arn.resourceName; if (!clusterName) { - throw new Error(`Missing required Cluster Name from Cluster ARN: ${clusterArn}`); + throw new ValidationError(`Missing required Cluster Name from Cluster ARN: ${clusterArn}`, scope); } const errorSuffix = 'is not available for a Cluster imported using fromClusterArn(), please use fromClusterAttributes() instead.'; @@ -185,13 +186,13 @@ export class Cluster extends Resource implements ICluster { public readonly clusterArn = clusterArn; public readonly clusterName = clusterName!; get hasEc2Capacity(): boolean { - throw new Error(`hasEc2Capacity ${errorSuffix}`); + throw new ValidationError(`hasEc2Capacity ${errorSuffix}`, this); } get connections(): ec2.Connections { - throw new Error(`connections ${errorSuffix}`); + throw new ValidationError(`connections ${errorSuffix}`, this); } get vpc(): ec2.IVpc { - throw new Error(`vpc ${errorSuffix}`); + throw new ValidationError(`vpc ${errorSuffix}`, this); } } @@ -272,7 +273,7 @@ export class Cluster extends Resource implements ICluster { addConstructMetadata(this, props); if ((props.containerInsights !== undefined) && props.containerInsightsV2) { - throw new Error('You cannot set both containerInsights and containerInsightsV2'); + throw new ValidationError('You cannot set both containerInsights and containerInsightsV2', this); } /** @@ -301,7 +302,7 @@ export class Cluster extends Resource implements ICluster { if (props.executeCommandConfiguration) { if ((props.executeCommandConfiguration.logging === ExecuteCommandLogging.OVERRIDE) !== (props.executeCommandConfiguration.logConfiguration !== undefined)) { - throw new Error('Execute command log configuration must only be specified when logging is OVERRIDE.'); + throw new ValidationError('Execute command log configuration must only be specified when logging is OVERRIDE.', this); } this._executeCommandConfiguration = props.executeCommandConfiguration; } @@ -408,22 +409,22 @@ export class Cluster extends Resource implements ICluster { @MethodMetadata() public addDefaultCapacityProviderStrategy(defaultCapacityProviderStrategy: CapacityProviderStrategy[]) { if (this._defaultCapacityProviderStrategy.length > 0) { - throw new Error('Cluster default capacity provider strategy is already set.'); + throw new ValidationError('Cluster default capacity provider strategy is already set.', this); } if (defaultCapacityProviderStrategy.some(dcp => dcp.capacityProvider.includes('FARGATE')) && defaultCapacityProviderStrategy.some(dcp => !dcp.capacityProvider.includes('FARGATE'))) { - throw new Error('A capacity provider strategy cannot contain a mix of capacity providers using Auto Scaling groups and Fargate providers. Specify one or the other and try again.'); + throw new ValidationError('A capacity provider strategy cannot contain a mix of capacity providers using Auto Scaling groups and Fargate providers. Specify one or the other and try again.', this); } defaultCapacityProviderStrategy.forEach(dcp => { if (!this._capacityProviderNames.includes(dcp.capacityProvider)) { - throw new Error(`Capacity provider ${dcp.capacityProvider} must be added to the cluster with addAsgCapacityProvider() before it can be used in a default capacity provider strategy.`); + throw new ValidationError(`Capacity provider ${dcp.capacityProvider} must be added to the cluster with addAsgCapacityProvider() before it can be used in a default capacity provider strategy.`, this); } }); const defaultCapacityProvidersWithBase = defaultCapacityProviderStrategy.filter(dcp => !!dcp.base); if (defaultCapacityProvidersWithBase.length > 1) { - throw new Error('Only 1 capacity provider in a capacity provider strategy can have a nonzero base.'); + throw new ValidationError('Only 1 capacity provider in a capacity provider strategy can have a nonzero base.', this); } this._defaultCapacityProviderStrategy = defaultCapacityProviderStrategy; } @@ -446,10 +447,10 @@ export class Cluster extends Resource implements ICluster { private renderExecuteCommandLogConfiguration(): CfnCluster.ExecuteCommandLogConfigurationProperty { const logConfiguration = this._executeCommandConfiguration?.logConfiguration; if (logConfiguration?.s3EncryptionEnabled && !logConfiguration?.s3Bucket) { - throw new Error('You must specify an S3 bucket name in the execute command log configuration to enable S3 encryption.'); + throw new ValidationError('You must specify an S3 bucket name in the execute command log configuration to enable S3 encryption.', this); } if (logConfiguration?.cloudWatchEncryptionEnabled && !logConfiguration?.cloudWatchLogGroup) { - throw new Error('You must specify a CloudWatch log group in the execute command log configuration to enable CloudWatch encryption.'); + throw new ValidationError('You must specify a CloudWatch log group in the execute command log configuration to enable CloudWatch encryption.', this); } return { cloudWatchEncryptionEnabled: logConfiguration?.cloudWatchEncryptionEnabled, @@ -468,7 +469,7 @@ export class Cluster extends Resource implements ICluster { @MethodMetadata() public addDefaultCloudMapNamespace(options: CloudMapNamespaceOptions): cloudmap.INamespace { if (this._defaultCloudMapNamespace !== undefined) { - throw new Error('Can only add default namespace once.'); + throw new ValidationError('Can only add default namespace once.', this); } const namespaceType = options.type !== undefined @@ -494,7 +495,7 @@ export class Cluster extends Resource implements ICluster { }); break; default: - throw new Error(`Namespace type ${namespaceType} is not supported.`); + throw new ValidationError(`Namespace type ${namespaceType} is not supported.`, this); } this._defaultCloudMapNamespace = sdNamespace; @@ -613,7 +614,7 @@ export class Cluster extends Resource implements ICluster { }; if (!(autoScalingGroup instanceof autoscaling.AutoScalingGroup)) { - throw new Error('Cannot configure the AutoScalingGroup because it is an imported resource.'); + throw new ValidationError('Cannot configure the AutoScalingGroup because it is an imported resource.', this); } if (autoScalingGroup.osType === ec2.OperatingSystemType.WINDOWS) { @@ -650,7 +651,7 @@ export class Cluster extends Resource implements ICluster { Annotations.of(this).addWarningV2('@aws-cdk/aws-ecs:unknownImageType', `Unknown ECS Image type: ${optionsClone.machineImageType}.`); if (optionsClone.canContainersAccessInstanceRole === false) { - throw new Error('The canContainersAccessInstanceRole option is not supported. See https://github.com/aws/aws-cdk/discussions/32609'); + throw new ValidationError('The canContainersAccessInstanceRole option is not supported. See https://github.com/aws/aws-cdk/discussions/32609', this); } break; } @@ -720,7 +721,7 @@ export class Cluster extends Resource implements ICluster { if (options.canContainersAccessInstanceRole === false && FeatureFlags.of(this).isEnabled(Disable_ECS_IMDS_Blocking)) { - throw new Error('The canContainersAccessInstanceRole option is not supported. See https://github.com/aws/aws-cdk/discussions/32609'); + throw new ValidationError('The canContainersAccessInstanceRole option is not supported. See https://github.com/aws/aws-cdk/discussions/32609', this); } } @@ -728,7 +729,7 @@ export class Cluster extends Resource implements ICluster { options: AddAutoScalingGroupCapacityOptions): void { if (options.canContainersAccessInstanceRole === false && FeatureFlags.of(this).isEnabled(Disable_ECS_IMDS_Blocking)) { - throw new Error('The canContainersAccessInstanceRole option is not supported. See https://github.com/aws/aws-cdk/discussions/32609'); + throw new ValidationError('The canContainersAccessInstanceRole option is not supported. See https://github.com/aws/aws-cdk/discussions/32609', this); } if (options.canContainersAccessInstanceRole === false || @@ -762,7 +763,7 @@ export class Cluster extends Resource implements ICluster { @MethodMetadata() public addCapacityProvider(provider: string) { if (!(provider === 'FARGATE' || provider === 'FARGATE_SPOT')) { - throw new Error('CapacityProvider not supported'); + throw new ValidationError('CapacityProvider not supported', this); } if (!this._capacityProviderNames.includes(provider)) { @@ -811,7 +812,7 @@ export class Cluster extends Resource implements ICluster { if (options.canContainersAccessInstanceRole === false && FeatureFlags.of(this).isEnabled(Disable_ECS_IMDS_Blocking)) { - throw new Error('The canContainersAccessInstanceRole option is not supported. See https://github.com/aws/aws-cdk/discussions/32609'); + throw new ValidationError('The canContainersAccessInstanceRole option is not supported. See https://github.com/aws/aws-cdk/discussions/32609', this); } // clear the cache of the agent @@ -1559,20 +1560,20 @@ export class AsgCapacityProvider extends Construct { } if (this.enableManagedTerminationProtection && props.enableManagedScaling === false) { - throw new Error('Cannot enable Managed Termination Protection on a Capacity Provider when Managed Scaling is disabled. Either enable Managed Scaling or disable Managed Termination Protection.'); + throw new ValidationError('Cannot enable Managed Termination Protection on a Capacity Provider when Managed Scaling is disabled. Either enable Managed Scaling or disable Managed Termination Protection.', this); } if (this.enableManagedTerminationProtection) { if (this.autoScalingGroup instanceof autoscaling.AutoScalingGroup) { this.autoScalingGroup.protectNewInstancesFromScaleIn(); } else { - throw new Error('Cannot enable Managed Termination Protection on a Capacity Provider when providing an imported AutoScalingGroup.'); + throw new ValidationError('Cannot enable Managed Termination Protection on a Capacity Provider when providing an imported AutoScalingGroup.', this); } } const capacityProviderNameRegex = /^(?!aws|ecs|fargate).+/gm; if (capacityProviderName) { if (!(capacityProviderNameRegex.test(capacityProviderName))) { - throw new Error(`Invalid Capacity Provider Name: ${capacityProviderName}, If a name is specified, it cannot start with aws, ecs, or fargate.`); + throw new ValidationError(`Invalid Capacity Provider Name: ${capacityProviderName}, If a name is specified, it cannot start with aws, ecs, or fargate.`, this); } } else { if (!(capacityProviderNameRegex.test(Stack.of(this).stackName))) { @@ -1585,7 +1586,7 @@ export class AsgCapacityProvider extends Construct { if (props.instanceWarmupPeriod && !Token.isUnresolved(props.instanceWarmupPeriod)) { if (props.instanceWarmupPeriod < 0 || props.instanceWarmupPeriod > 10000) { - throw new Error(`InstanceWarmupPeriod must be between 0 and 10000 inclusive, got: ${props.instanceWarmupPeriod}.`); + throw new ValidationError(`InstanceWarmupPeriod must be between 0 and 10000 inclusive, got: ${props.instanceWarmupPeriod}.`, this); } } diff --git a/packages/aws-cdk-lib/aws-ecs/lib/container-definition.ts b/packages/aws-cdk-lib/aws-ecs/lib/container-definition.ts index ac211ca662b26..cb6d59d3c9432 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/container-definition.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/container-definition.ts @@ -10,6 +10,7 @@ import * as iam from '../../aws-iam'; import * as secretsmanager from '../../aws-secretsmanager'; import * as ssm from '../../aws-ssm'; import * as cdk from '../../core'; +import { UnscopedValidationError, ValidationError } from '../../core'; import { propertyInjectable } from '../../core/lib/prop-injectable'; /** @@ -577,7 +578,7 @@ export class ContainerDefinition extends Construct { super(scope, id); if (props.memoryLimitMiB !== undefined && props.memoryReservationMiB !== undefined) { if (props.memoryLimitMiB < props.memoryReservationMiB) { - throw new Error('MemoryLimitMiB should not be less than MemoryReservationMiB.'); + throw new ValidationError('MemoryLimitMiB should not be less than MemoryReservationMiB.', this); } } this.essential = props.essential ?? true; @@ -623,7 +624,7 @@ export class ContainerDefinition extends Construct { this.credentialSpecs = []; if (props.credentialSpecs.length > 1) { - throw new Error('Only one credential spec is allowed per container definition.'); + throw new ValidationError('Only one credential spec is allowed per container definition.', this); } for (const credSpec of props.credentialSpecs) { @@ -662,7 +663,7 @@ export class ContainerDefinition extends Construct { */ public addLink(container: ContainerDefinition, alias?: string) { if (this.taskDefinition.networkMode !== NetworkMode.BRIDGE) { - throw new Error('You must use network mode Bridge to add container links.'); + throw new ValidationError('You must use network mode Bridge to add container links.', this); } if (alias !== undefined) { this.links.push(`${container.containerName}:${alias}`); @@ -754,7 +755,7 @@ export class ContainerDefinition extends Construct { return resource; } } - throw new Error(`Resource value ${resource} in container definition doesn't match any inference accelerator device name in the task definition.`); + throw new ValidationError(`Resource value ${resource} in container definition doesn't match any inference accelerator device name in the task definition.`, this); })); } @@ -813,7 +814,7 @@ export class ContainerDefinition extends Construct { private setNamedPort(pm: PortMapping) :void { if (!pm.name) return; if (this._namedPorts.has(pm.name)) { - throw new Error(`Port mapping name '${pm.name}' already exists on this container`); + throw new ValidationError(`Port mapping name '${pm.name}' already exists on this container`, this); } this._namedPorts.set(pm.name, pm); } @@ -835,13 +836,13 @@ export class ContainerDefinition extends Construct { private validateRestartPolicy(enableRestartPolicy?: boolean, restartIgnoredExitCodes?: number[], restartAttemptPeriod?: cdk.Duration) { if (enableRestartPolicy === false && (restartIgnoredExitCodes !== undefined || restartAttemptPeriod !== undefined)) { - throw new Error('The restartIgnoredExitCodes and restartAttemptPeriod cannot be specified if enableRestartPolicy is false'); + throw new ValidationError('The restartIgnoredExitCodes and restartAttemptPeriod cannot be specified if enableRestartPolicy is false', this); } if (restartIgnoredExitCodes && restartIgnoredExitCodes.length > 50) { - throw new Error(`Only up to 50 can be specified for restartIgnoredExitCodes, got: ${restartIgnoredExitCodes.length}`); + throw new ValidationError(`Only up to 50 can be specified for restartIgnoredExitCodes, got: ${restartIgnoredExitCodes.length}`, this); } if (restartAttemptPeriod && (restartAttemptPeriod.toSeconds() < 60 || restartAttemptPeriod.toSeconds() > 1800)) { - throw new Error(`The restartAttemptPeriod must be between 60 seconds and 1800 seconds, got ${restartAttemptPeriod.toSeconds()} seconds`); + throw new ValidationError(`The restartAttemptPeriod must be between 60 seconds and 1800 seconds, got ${restartAttemptPeriod.toSeconds()} seconds`, this); } } @@ -865,7 +866,7 @@ export class ContainerDefinition extends Construct { */ public get ingressPort(): number { if (this.portMappings.length === 0) { - throw new Error(`Container ${this.containerName} hasn't defined any ports. Call addPortMappings().`); + throw new ValidationError(`Container ${this.containerName} hasn't defined any ports. Call addPortMappings().`, this); } const defaultPortMapping = this.portMappings[0]; @@ -878,7 +879,7 @@ export class ContainerDefinition extends Construct { } if (defaultPortMapping.containerPortRange !== undefined) { - throw new Error(`The first port mapping of the container ${this.containerName} must expose a single port.`); + throw new ValidationError(`The first port mapping of the container ${this.containerName} must expose a single port.`, this); } return defaultPortMapping.containerPort; @@ -889,12 +890,12 @@ export class ContainerDefinition extends Construct { */ public get containerPort(): number { if (this.portMappings.length === 0) { - throw new Error(`Container ${this.containerName} hasn't defined any ports. Call addPortMappings().`); + throw new ValidationError(`Container ${this.containerName} hasn't defined any ports. Call addPortMappings().`, this); } const defaultPortMapping = this.portMappings[0]; if (defaultPortMapping.containerPortRange !== undefined) { - throw new Error(`The first port mapping of the container ${this.containerName} must expose a single port.`); + throw new ValidationError(`The first port mapping of the container ${this.containerName} must expose a single port.`, this); } return defaultPortMapping.containerPort; @@ -959,7 +960,7 @@ export class ContainerDefinition extends Construct { environmentFiles: this.environmentFiles && renderEnvironmentFiles(cdk.Stack.of(this).partition, this.environmentFiles), secrets: this.secrets.length ? this.secrets : undefined, extraHosts: this.props.extraHosts && renderKV(this.props.extraHosts, 'hostname', 'ipAddress'), - healthCheck: this.props.healthCheck && renderHealthCheck(this.props.healthCheck), + healthCheck: this.props.healthCheck && renderHealthCheck(this, this.props.healthCheck), links: cdk.Lazy.list({ produce: () => this.links }, { omitEmpty: true }), linuxParameters: this.linuxParameters && this.linuxParameters.renderLinuxParameters(), resourceRequirements: (!this.props.gpuCount && this.inferenceAcceleratorResources.length == 0 ) ? undefined : @@ -1054,26 +1055,26 @@ function renderCredentialSpec(credSpec: CredentialSpecConfig): string { return `${credSpec.typePrefix}:${credSpec.location}`; } -function renderHealthCheck(hc: HealthCheck): CfnTaskDefinition.HealthCheckProperty { +function renderHealthCheck(scope: Construct, hc: HealthCheck): CfnTaskDefinition.HealthCheckProperty { if (hc.interval?.toSeconds() !== undefined) { if (5 > hc.interval?.toSeconds() || hc.interval?.toSeconds() > 300) { - throw new Error('Interval must be between 5 seconds and 300 seconds.'); + throw new ValidationError('Interval must be between 5 seconds and 300 seconds.', scope); } } if (hc.timeout?.toSeconds() !== undefined) { if (2 > hc.timeout?.toSeconds() || hc.timeout?.toSeconds() > 120) { - throw new Error('Timeout must be between 2 seconds and 120 seconds.'); + throw new ValidationError('Timeout must be between 2 seconds and 120 seconds.', scope); } } if (hc.interval?.toSeconds() !== undefined && hc.timeout?.toSeconds() !== undefined) { if (hc.interval?.toSeconds() < hc.timeout?.toSeconds()) { - throw new Error('Health check interval should be longer than timeout.'); + throw new ValidationError('Health check interval should be longer than timeout.', scope); } } return { - command: getHealthCheckCommand(hc), + command: getHealthCheckCommand(scope, hc), interval: hc.interval?.toSeconds() ?? 30, retries: hc.retries ?? 3, startPeriod: hc.startPeriod?.toSeconds(), @@ -1081,12 +1082,12 @@ function renderHealthCheck(hc: HealthCheck): CfnTaskDefinition.HealthCheckProper }; } -function getHealthCheckCommand(hc: HealthCheck): string[] { +function getHealthCheckCommand(scope: Construct, hc: HealthCheck): string[] { const cmd = hc.command; const hcCommand = new Array(); if (cmd.length === 0) { - throw new Error('At least one argument must be supplied for health check command.'); + throw new ValidationError('At least one argument must be supplied for health check command.', scope); } if (cmd.length === 1) { @@ -1327,39 +1328,39 @@ export class PortMap { */ public validate(): void { if (!this.isvalidPortName()) { - throw new Error('Port mapping name cannot be an empty string.'); + throw new UnscopedValidationError('Port mapping name cannot be an empty string.'); } if (this.portmapping.containerPort === ContainerDefinition.CONTAINER_PORT_USE_RANGE && this.portmapping.containerPortRange === undefined) { - throw new Error(`The containerPortRange must be set when containerPort is equal to ${ContainerDefinition.CONTAINER_PORT_USE_RANGE}`); + throw new UnscopedValidationError(`The containerPortRange must be set when containerPort is equal to ${ContainerDefinition.CONTAINER_PORT_USE_RANGE}`); } if (this.portmapping.containerPort !== ContainerDefinition.CONTAINER_PORT_USE_RANGE && this.portmapping.containerPortRange !== undefined) { - throw new Error('Cannot set "containerPort" and "containerPortRange" at the same time.'); + throw new UnscopedValidationError('Cannot set "containerPort" and "containerPortRange" at the same time.'); } if (this.portmapping.containerPort !== ContainerDefinition.CONTAINER_PORT_USE_RANGE) { if ((this.networkmode === NetworkMode.AWS_VPC || this.networkmode === NetworkMode.HOST) && this.portmapping.hostPort !== undefined && this.portmapping.hostPort !== this.portmapping.containerPort) { - throw new Error('The host port must be left out or must be the same as the container port for AwsVpc or Host network mode.'); + throw new UnscopedValidationError('The host port must be left out or must be the same as the container port for AwsVpc or Host network mode.'); } } if (this.portmapping.containerPortRange !== undefined) { if (cdk.Token.isUnresolved(this.portmapping.containerPortRange)) { - throw new Error('The value of containerPortRange must be concrete (no Tokens)'); + throw new UnscopedValidationError('The value of containerPortRange must be concrete (no Tokens)'); } if (this.portmapping.hostPort !== undefined) { - throw new Error('Cannot set "hostPort" while using a port range for the container.'); + throw new UnscopedValidationError('Cannot set "hostPort" while using a port range for the container.'); } if (this.networkmode !== NetworkMode.BRIDGE && this.networkmode !== NetworkMode.AWS_VPC) { - throw new Error('Either AwsVpc or Bridge network mode is required to set a port range for the container.'); + throw new UnscopedValidationError('Either AwsVpc or Bridge network mode is required to set a port range for the container.'); } if (!/^\d+-\d+$/.test(this.portmapping.containerPortRange)) { - throw new Error('The containerPortRange must be a string in the format [start port]-[end port].'); + throw new UnscopedValidationError('The containerPortRange must be a string in the format [start port]-[end port].'); } } } @@ -1408,10 +1409,10 @@ export class ServiceConnect { */ public validate() :void { if (!this.isValidNetworkmode()) { - throw new Error(`Service connect related port mapping fields 'name' and 'appProtocol' are not supported for network mode ${this.networkmode}`); + throw new UnscopedValidationError(`Service connect related port mapping fields 'name' and 'appProtocol' are not supported for network mode ${this.networkmode}`); } if (!this.isValidPortName()) { - throw new Error('Service connect-related port mapping field \'appProtocol\' cannot be set without \'name\''); + throw new UnscopedValidationError('Service connect-related port mapping field \'appProtocol\' cannot be set without \'name\''); } } diff --git a/packages/aws-cdk-lib/aws-ecs/lib/credential-spec.ts b/packages/aws-cdk-lib/aws-ecs/lib/credential-spec.ts index 9e460902de8b7..5c236a4f4f621 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/credential-spec.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/credential-spec.ts @@ -1,5 +1,6 @@ import { IBucket } from '../../aws-s3'; import { IParameter } from '../../aws-ssm'; +import { ValidationError } from '../../core'; /** * Base construct for a credential specification (CredSpec). @@ -10,7 +11,7 @@ export class CredentialSpec { */ protected static arnForS3Object(bucket: IBucket, key: string) { if (!key) { - throw new Error('key is undefined'); + throw new ValidationError('key is undefined', bucket); } return bucket.arnForObjects(key); diff --git a/packages/aws-cdk-lib/aws-ecs/lib/ec2/ec2-service.ts b/packages/aws-cdk-lib/aws-ecs/lib/ec2/ec2-service.ts index 0106e4ea7eefa..0efc5c5307090 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/ec2/ec2-service.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/ec2/ec2-service.ts @@ -1,7 +1,7 @@ import { Construct } from 'constructs'; import * as ec2 from '../../../aws-ec2'; import * as elb from '../../../aws-elasticloadbalancing'; -import { Lazy, Resource, Stack, Annotations, Token } from '../../../core'; +import { Lazy, Resource, Stack, Annotations, Token, ValidationError } from '../../../core'; import { addConstructMetadata, MethodMetadata } from '../../../core/lib/metadata-resource'; import { propertyInjectable } from '../../../core/lib/prop-injectable'; import { AvailabilityZoneRebalancing } from '../availability-zone-rebalancing'; @@ -172,31 +172,31 @@ export class Ec2Service extends BaseService implements IEc2Service { */ constructor(scope: Construct, id: string, props: Ec2ServiceProps) { if (props.daemon && props.desiredCount !== undefined) { - throw new Error('Daemon mode launches one task on every instance. Don\'t supply desiredCount.'); + throw new ValidationError('Daemon mode launches one task on every instance. Don\'t supply desiredCount.', scope); } if (props.daemon && props.maxHealthyPercent !== undefined && props.maxHealthyPercent !== 100) { - throw new Error('Maximum percent must be 100 for daemon mode.'); + throw new ValidationError('Maximum percent must be 100 for daemon mode.', scope); } if (props.minHealthyPercent !== undefined && props.maxHealthyPercent !== undefined && props.minHealthyPercent >= props.maxHealthyPercent) { - throw new Error('Minimum healthy percent must be less than maximum healthy percent.'); + throw new ValidationError('Minimum healthy percent must be less than maximum healthy percent.', scope); } if (!props.taskDefinition.isEc2Compatible) { - throw new Error('Supplied TaskDefinition is not configured for compatibility with EC2'); + throw new ValidationError('Supplied TaskDefinition is not configured for compatibility with EC2', scope); } if (props.securityGroup !== undefined && props.securityGroups !== undefined) { - throw new Error('Only one of SecurityGroup or SecurityGroups can be populated.'); + throw new ValidationError('Only one of SecurityGroup or SecurityGroups can be populated.', scope); } if (props.availabilityZoneRebalancing === AvailabilityZoneRebalancing.ENABLED) { if (props.daemon) { - throw new Error('AvailabilityZoneRebalancing.ENABLED cannot be used with daemon mode'); + throw new ValidationError('AvailabilityZoneRebalancing.ENABLED cannot be used with daemon mode', scope); } if (!Token.isUnresolved(props.maxHealthyPercent) && props.maxHealthyPercent === 100) { - throw new Error('AvailabilityZoneRebalancing.ENABLED requires maxHealthyPercent > 100'); + throw new ValidationError('AvailabilityZoneRebalancing.ENABLED requires maxHealthyPercent > 100', scope); } } @@ -242,7 +242,7 @@ export class Ec2Service extends BaseService implements IEc2Service { // // In that case, reference the same security groups but make sure new rules are // created in the current scope (i.e., this stack) - validateNoNetworkingProps(props); + validateNoNetworkingProps(scope, props); this.connections.addSecurityGroup(...securityGroupsInThisStack(this, props.cluster.connections.securityGroups)); } @@ -269,13 +269,13 @@ export class Ec2Service extends BaseService implements IEc2Service { @MethodMetadata() public addPlacementStrategies(...strategies: PlacementStrategy[]) { if (strategies.length > 0 && this.daemon) { - throw new Error("Can't configure placement strategies when daemon=true"); + throw new ValidationError("Can't configure placement strategies when daemon=true", this); } if (strategies.length > 0 && this.strategies.length === 0 && this.availabilityZoneRebalancingEnabled) { const [placement] = strategies[0].toJson(); if (placement.type !== 'spread' || placement.field !== BuiltInAttributes.AVAILABILITY_ZONE) { - throw new Error(`AvailabilityZoneBalancing.ENABLED requires that the first placement strategy, if any, be 'spread across "${BuiltInAttributes.AVAILABILITY_ZONE}"'`); + throw new ValidationError(`AvailabilityZoneBalancing.ENABLED requires that the first placement strategy, if any, be 'spread across "${BuiltInAttributes.AVAILABILITY_ZONE}"'`, this); } } @@ -296,7 +296,7 @@ export class Ec2Service extends BaseService implements IEc2Service { if (this.availabilityZoneRebalancingEnabled) { for (const item of items) { if (item.type === 'memberOf' && item.expression?.includes(BuiltInAttributes.AVAILABILITY_ZONE)) { - throw new Error(`AvailabilityZoneBalancing.ENABLED disallows usage of "${BuiltInAttributes.AVAILABILITY_ZONE}"`); + throw new ValidationError(`AvailabilityZoneBalancing.ENABLED disallows usage of "${BuiltInAttributes.AVAILABILITY_ZONE}"`, this); } } } @@ -325,7 +325,7 @@ export class Ec2Service extends BaseService implements IEc2Service { @MethodMetadata() public attachToClassicLB(loadBalancer: elb.LoadBalancer): void { if (this.availabilityZoneRebalancingEnabled) { - throw new Error('AvailabilityZoneRebalancing.ENABLED disallows using the service as a target of a Classic Load Balancer'); + throw new ValidationError('AvailabilityZoneRebalancing.ENABLED disallows using the service as a target of a Classic Load Balancer', this); } super.attachToClassicLB(loadBalancer); } @@ -334,12 +334,12 @@ export class Ec2Service extends BaseService implements IEc2Service { /** * Validate combinations of networking arguments. */ -function validateNoNetworkingProps(props: Ec2ServiceProps) { +function validateNoNetworkingProps(scope: Construct, props: Ec2ServiceProps) { if (props.vpcSubnets !== undefined || props.securityGroup !== undefined || props.securityGroups !== undefined || props.assignPublicIp) { - throw new Error('vpcSubnets, securityGroup(s) and assignPublicIp can only be used in AwsVpc networking mode'); + throw new ValidationError('vpcSubnets, securityGroup(s) and assignPublicIp can only be used in AwsVpc networking mode', scope); } } diff --git a/packages/aws-cdk-lib/aws-ecs/lib/ec2/ec2-task-definition.ts b/packages/aws-cdk-lib/aws-ecs/lib/ec2/ec2-task-definition.ts index 2ef8489f5d2bb..ba294f6117560 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/ec2/ec2-task-definition.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/ec2/ec2-task-definition.ts @@ -1,5 +1,5 @@ import { Construct } from 'constructs'; -import { Stack } from '../../../core'; +import { Stack, ValidationError } from '../../../core'; import { addConstructMetadata } from '../../../core/lib/metadata-resource'; import { propertyInjectable } from '../../../core/lib/prop-injectable'; import { ImportedTaskDefinition } from '../base/_imported-task-definition'; @@ -123,7 +123,7 @@ export class Ec2TaskDefinition extends TaskDefinition implements IEc2TaskDefinit * Validates the placement constraints to make sure they are supported. * Currently, only 'memberOf' is a valid constraint for an Ec2TaskDefinition. */ - private static validatePlacementConstraints(constraints?: PlacementConstraint[]) { + private static validatePlacementConstraints(scope: Construct, constraints?: PlacementConstraint[]) { // List of valid constraints https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-taskdefinition-taskdefinitionplacementconstraint.html#cfn-ecs-taskdefinition-taskdefinitionplacementconstraint-type const validConstraints = new Set(['memberOf']); @@ -135,7 +135,7 @@ export class Ec2TaskDefinition extends TaskDefinition implements IEc2TaskDefinit if (invalidConstraints.length > 0) { const invalidConstraintTypes = invalidConstraints.map( constraint => constraint.toJson().map(constraintProperty => constraintProperty.type)).flat(); - throw new Error(`Invalid placement constraint(s): ${invalidConstraintTypes.join(', ')}. Only 'memberOf' is currently supported in the Ec2TaskDefinition class.`); + throw new ValidationError(`Invalid placement constraint(s): ${invalidConstraintTypes.join(', ')}. Only 'memberOf' is currently supported in the Ec2TaskDefinition class.`, scope); } } @@ -155,7 +155,7 @@ export class Ec2TaskDefinition extends TaskDefinition implements IEc2TaskDefinit addConstructMetadata(this, props); // Validate the placement constraints - Ec2TaskDefinition.validatePlacementConstraints(props.placementConstraints ?? []); + Ec2TaskDefinition.validatePlacementConstraints(scope, props.placementConstraints ?? []); } /** diff --git a/packages/aws-cdk-lib/aws-ecs/lib/environment-file.ts b/packages/aws-cdk-lib/aws-ecs/lib/environment-file.ts index 99a0668d12d11..8799f2a08a1fe 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/environment-file.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/environment-file.ts @@ -1,6 +1,7 @@ import { Construct } from 'constructs'; import { IBucket, Location } from '../../aws-s3'; import { Asset, AssetOptions } from '../../aws-s3-assets'; +import { ValidationError } from '../../core'; /** * Constructs for types of environment files @@ -59,7 +60,7 @@ export class AssetEnvironmentFile extends EnvironmentFile { } if (!this.asset.isFile) { - throw new Error(`Asset must be a single file (${this.path})`); + throw new ValidationError(`Asset must be a single file (${this.path})`, scope); } return { @@ -82,7 +83,7 @@ export class S3EnvironmentFile extends EnvironmentFile { super(); if (!bucket.bucketName) { - throw new Error('bucketName is undefined for the provided bucket'); + throw new ValidationError('bucketName is undefined for the provided bucket', bucket); } this.bucketName = bucket.bucketName; diff --git a/packages/aws-cdk-lib/aws-ecs/lib/external/external-service.ts b/packages/aws-cdk-lib/aws-ecs/lib/external/external-service.ts index eb4aebfae7e10..e1a56d933475b 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/external/external-service.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/external/external-service.ts @@ -3,7 +3,7 @@ import * as appscaling from '../../../aws-applicationautoscaling'; import * as ec2 from '../../../aws-ec2'; import * as elbv2 from '../../../aws-elasticloadbalancingv2'; import * as cloudmap from '../../../aws-servicediscovery'; -import { ArnFormat, Resource, Stack, Annotations } from '../../../core'; +import { ArnFormat, Resource, Stack, Annotations, ValidationError } from '../../../core'; import { addConstructMetadata, MethodMetadata } from '../../../core/lib/metadata-resource'; import { AssociateCloudMapServiceOptions, BaseService, BaseServiceOptions, CloudMapOptions, DeploymentControllerType, EcsTarget, IBaseService, IEcsLoadBalancerTarget, IService, LaunchType, PropagatedTagSource } from '../base/base-service'; import { fromServiceAttributes } from '../base/from-service-attributes'; @@ -104,34 +104,34 @@ export class ExternalService extends BaseService implements IExternalService { if (props.daemon) { if (props.deploymentController?.type === DeploymentControllerType.EXTERNAL || props.deploymentController?.type === DeploymentControllerType.CODE_DEPLOY) { - throw new Error('CODE_DEPLOY or EXTERNAL deployment controller types don\'t support the DAEMON scheduling strategy.'); + throw new ValidationError('CODE_DEPLOY or EXTERNAL deployment controller types don\'t support the DAEMON scheduling strategy.', scope); } if (props.desiredCount !== undefined) { - throw new Error('Daemon mode launches one task on every instance. Cannot specify desiredCount when daemon mode is enabled.'); + throw new ValidationError('Daemon mode launches one task on every instance. Cannot specify desiredCount when daemon mode is enabled.', scope); } if (props.maxHealthyPercent !== undefined && props.maxHealthyPercent !== 100) { - throw new Error('Maximum percent must be 100 when daemon mode is enabled.'); + throw new ValidationError('Maximum percent must be 100 when daemon mode is enabled.', scope); } } if (props.minHealthyPercent !== undefined && props.maxHealthyPercent !== undefined && props.minHealthyPercent >= props.maxHealthyPercent) { - throw new Error('Minimum healthy percent must be less than maximum healthy percent.'); + throw new ValidationError('Minimum healthy percent must be less than maximum healthy percent.', scope); } if (props.taskDefinition.compatibility !== Compatibility.EXTERNAL) { - throw new Error('Supplied TaskDefinition is not configured for compatibility with ECS Anywhere cluster'); + throw new ValidationError('Supplied TaskDefinition is not configured for compatibility with ECS Anywhere cluster', scope); } if (props.cluster.defaultCloudMapNamespace !== undefined) { - throw new Error(`Cloud map integration is not supported for External service ${props.cluster.defaultCloudMapNamespace}`); + throw new ValidationError(`Cloud map integration is not supported for External service ${props.cluster.defaultCloudMapNamespace}`, scope); } if (props.cloudMapOptions !== undefined) { - throw new Error('Cloud map options are not supported for External service'); + throw new ValidationError('Cloud map options are not supported for External service', scope); } if (props.capacityProviderStrategies !== undefined) { - throw new Error('Capacity Providers are not supported for External service'); + throw new ValidationError('Capacity Providers are not supported for External service', scope); } const propagateTagsFromSource = props.propagateTags ?? PropagatedTagSource.NONE; @@ -171,7 +171,7 @@ export class ExternalService extends BaseService implements IExternalService { */ @MethodMetadata() public attachToApplicationTargetGroup(_targetGroup: elbv2.IApplicationTargetGroup): elbv2.LoadBalancerTargetProps { - throw new Error('Application load balancer cannot be attached to an external service'); + throw new ValidationError('Application load balancer cannot be attached to an external service', this); } /** @@ -179,7 +179,7 @@ export class ExternalService extends BaseService implements IExternalService { */ @MethodMetadata() public loadBalancerTarget(_options: LoadBalancerTargetOptions): IEcsLoadBalancerTarget { - throw new Error('External service cannot be attached as load balancer targets'); + throw new ValidationError('External service cannot be attached as load balancer targets', this); } /** @@ -187,7 +187,7 @@ export class ExternalService extends BaseService implements IExternalService { */ @MethodMetadata() public registerLoadBalancerTargets(..._targets: EcsTarget[]) { - throw new Error('External service cannot be registered as load balancer targets'); + throw new ValidationError('External service cannot be registered as load balancer targets', this); } /** @@ -195,7 +195,7 @@ export class ExternalService extends BaseService implements IExternalService { */ // eslint-disable-next-line max-len, no-unused-vars protected configureAwsVpcNetworkingWithSecurityGroups(_vpc: ec2.IVpc, _assignPublicIp?: boolean, _vpcSubnets?: ec2.SubnetSelection, _securityGroups?: ec2.ISecurityGroup[]) { - throw new Error('Only Bridge network mode is supported for external service'); + throw new ValidationError('Only Bridge network mode is supported for external service', this); } /** @@ -203,7 +203,7 @@ export class ExternalService extends BaseService implements IExternalService { */ @MethodMetadata() public autoScaleTaskCount(_props: appscaling.EnableScalingProps): ScalableTaskCount { - throw new Error('Autoscaling not supported for external service'); + throw new ValidationError('Autoscaling not supported for external service', this); } /** @@ -211,7 +211,7 @@ export class ExternalService extends BaseService implements IExternalService { */ @MethodMetadata() public enableCloudMap(_options: CloudMapOptions): cloudmap.Service { - throw new Error('Cloud map integration not supported for an external service'); + throw new ValidationError('Cloud map integration not supported for an external service', this); } /** @@ -219,6 +219,6 @@ export class ExternalService extends BaseService implements IExternalService { */ @MethodMetadata() public associateCloudMapService(_options: AssociateCloudMapServiceOptions): void { - throw new Error('Cloud map service association is not supported for an external service'); + throw new ValidationError('Cloud map service association is not supported for an external service', this); } } diff --git a/packages/aws-cdk-lib/aws-ecs/lib/external/external-task-definition.ts b/packages/aws-cdk-lib/aws-ecs/lib/external/external-task-definition.ts index 190c9d919fc57..3b10bff353f39 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/external/external-task-definition.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/external/external-task-definition.ts @@ -1,4 +1,5 @@ import { Construct } from 'constructs'; +import { ValidationError } from '../../../core'; import { addConstructMetadata, MethodMetadata } from '../../../core/lib/metadata-resource'; import { propertyInjectable } from '../../../core/lib/prop-injectable'; import { ImportedTaskDefinition } from '../../lib/base/_imported-task-definition'; @@ -96,6 +97,6 @@ export class ExternalTaskDefinition extends TaskDefinition implements IExternalT */ @MethodMetadata() public addInferenceAccelerator(_inferenceAccelerator: InferenceAccelerator) { - throw new Error('Cannot use inference accelerators on tasks that run on External service'); + throw new ValidationError('Cannot use inference accelerators on tasks that run on External service', this); } } diff --git a/packages/aws-cdk-lib/aws-ecs/lib/fargate/fargate-service.ts b/packages/aws-cdk-lib/aws-ecs/lib/fargate/fargate-service.ts index 1ebb53c6c995f..1434d26d5bf22 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/fargate/fargate-service.ts @@ -2,6 +2,7 @@ import { Construct } from 'constructs'; import * as ec2 from '../../../aws-ec2'; import * as elb from '../../../aws-elasticloadbalancing'; import * as cdk from '../../../core'; +import { ValidationError } from '../../../core'; import { addConstructMetadata, MethodMetadata } from '../../../core/lib/metadata-resource'; import { propertyInjectable } from '../../../core/lib/prop-injectable'; import { AvailabilityZoneRebalancing } from '../availability-zone-rebalancing'; @@ -143,17 +144,17 @@ export class FargateService extends BaseService implements IFargateService { */ constructor(scope: Construct, id: string, props: FargateServiceProps) { if (!props.taskDefinition.isFargateCompatible) { - throw new Error('Supplied TaskDefinition is not configured for compatibility with Fargate'); + throw new ValidationError('Supplied TaskDefinition is not configured for compatibility with Fargate', scope); } if (props.securityGroup !== undefined && props.securityGroups !== undefined) { - throw new Error('Only one of SecurityGroup or SecurityGroups can be populated.'); + throw new ValidationError('Only one of SecurityGroup or SecurityGroups can be populated.', scope); } if (props.availabilityZoneRebalancing === AvailabilityZoneRebalancing.ENABLED && !cdk.Token.isUnresolved(props.maxHealthyPercent) && props.maxHealthyPercent === 100) { - throw new Error('AvailabilityZoneRebalancing.ENABLED requires maxHealthyPercent > 100'); + throw new ValidationError('AvailabilityZoneRebalancing.ENABLED requires maxHealthyPercent > 100', scope); } // Platform versions not supporting referencesSecretJsonField, ephemeralStorageGiB, or pidMode on a task definition @@ -166,11 +167,11 @@ export class FargateService extends BaseService implements IFargateService { const isUnsupportedPlatformVersion = props.platformVersion && unsupportedPlatformVersions.includes(props.platformVersion); if (props.taskDefinition.ephemeralStorageGiB && isUnsupportedPlatformVersion) { - throw new Error(`The ephemeralStorageGiB feature requires platform version ${FargatePlatformVersion.VERSION1_4} or later, got ${props.platformVersion}.`); + throw new ValidationError(`The ephemeralStorageGiB feature requires platform version ${FargatePlatformVersion.VERSION1_4} or later, got ${props.platformVersion}.`, scope); } if (props.taskDefinition.pidMode && isUnsupportedPlatformVersion) { - throw new Error(`The pidMode feature requires platform version ${FargatePlatformVersion.VERSION1_4} or later, got ${props.platformVersion}.`); + throw new ValidationError(`The pidMode feature requires platform version ${FargatePlatformVersion.VERSION1_4} or later, got ${props.platformVersion}.`, scope); } super(scope, id, { @@ -224,7 +225,7 @@ export class FargateService extends BaseService implements IFargateService { @MethodMetadata() public attachToClassicLB(loadBalancer: elb.LoadBalancer): void { if (this.availabilityZoneRebalancingEnabled) { - throw new Error('AvailabilityZoneRebalancing.ENABLED disallows using the service as a target of a Classic Load Balancer'); + throw new ValidationError('AvailabilityZoneRebalancing.ENABLED disallows using the service as a target of a Classic Load Balancer', this); } super.attachToClassicLB(loadBalancer); } diff --git a/packages/aws-cdk-lib/aws-ecs/lib/fargate/fargate-task-definition.ts b/packages/aws-cdk-lib/aws-ecs/lib/fargate/fargate-task-definition.ts index 4f29fd552520c..109c04964b989 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/fargate/fargate-task-definition.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/fargate/fargate-task-definition.ts @@ -1,5 +1,5 @@ import { Construct } from 'constructs'; -import { Tokenization, Token } from '../../../core'; +import { Tokenization, Token, ValidationError } from '../../../core'; import { addConstructMetadata } from '../../../core/lib/metadata-resource'; import { propertyInjectable } from '../../../core/lib/prop-injectable'; import { ImportedTaskDefinition } from '../base/_imported-task-definition'; @@ -192,20 +192,20 @@ export class FargateTaskDefinition extends TaskDefinition implements IFargateTas // eslint-disable-next-line max-len if (props.ephemeralStorageGiB && !Token.isUnresolved(props.ephemeralStorageGiB) && (props.ephemeralStorageGiB < 21 || props.ephemeralStorageGiB > 200)) { - throw new Error('Ephemeral storage size must be between 21GiB and 200GiB'); + throw new ValidationError('Ephemeral storage size must be between 21GiB and 200GiB', this); } if (props.pidMode) { if (!props.runtimePlatform?.operatingSystemFamily) { - throw new Error('Specifying \'pidMode\' requires that operating system family also be provided.'); + throw new ValidationError('Specifying \'pidMode\' requires that operating system family also be provided.', this); } if (props.runtimePlatform?.operatingSystemFamily?.isWindows()) { - throw new Error('\'pidMode\' is not supported for Windows containers.'); + throw new ValidationError('\'pidMode\' is not supported for Windows containers.', this); } if (!Token.isUnresolved(props.pidMode) && props.runtimePlatform?.operatingSystemFamily?.isLinux() && props.pidMode !== PidMode.TASK) { - throw new Error(`\'pidMode\' can only be set to \'${PidMode.TASK}\' for Linux Fargate containers, got: \'${props.pidMode}\'.`); + throw new ValidationError(`\'pidMode\' can only be set to \'${PidMode.TASK}\' for Linux Fargate containers, got: \'${props.pidMode}\'.`, this); } } diff --git a/packages/aws-cdk-lib/aws-ecs/lib/firelens-log-router.ts b/packages/aws-cdk-lib/aws-ecs/lib/firelens-log-router.ts index d26524e5f964f..8c8f878e24c6c 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/firelens-log-router.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/firelens-log-router.ts @@ -230,7 +230,7 @@ export class FirelensLogRouter extends ContainerDefinition { const options = props.firelensConfig.options; if (options) { if ((options.configFileValue && options.configFileType === undefined) || (options.configFileValue === undefined && options.configFileType)) { - throw new Error('configFileValue and configFileType must be set together to define a custom config source'); + throw new cdk.ValidationError('configFileValue and configFileType must be set together to define a custom config source', this); } const hasConfig = (options.configFileValue !== undefined); diff --git a/packages/aws-cdk-lib/aws-ecs/lib/images/tag-parameter-container-image.ts b/packages/aws-cdk-lib/aws-ecs/lib/images/tag-parameter-container-image.ts index 54ab32ce6311a..0f478154243fa 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/images/tag-parameter-container-image.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/images/tag-parameter-container-image.ts @@ -41,7 +41,7 @@ export class TagParameterContainerImage extends ContainerImage { if (this.imageTagParameter) { return this.imageTagParameter.logicalId; } else { - throw new Error('TagParameterContainerImage must be used in a container definition when using tagParameterName'); + throw new cdk.UnscopedValidationError('TagParameterContainerImage must be used in a container definition when using tagParameterName'); } }, }); @@ -57,7 +57,7 @@ export class TagParameterContainerImage extends ContainerImage { if (this.imageTagParameter) { return this.imageTagParameter.valueAsString; } else { - throw new Error('TagParameterContainerImage must be used in a container definition when using tagParameterValue'); + throw new cdk.UnscopedValidationError('TagParameterContainerImage must be used in a container definition when using tagParameterValue'); } }, }); diff --git a/packages/aws-cdk-lib/aws-ecs/lib/linux-parameters.ts b/packages/aws-cdk-lib/aws-ecs/lib/linux-parameters.ts index 78030d180f20d..24cd32bc2dd0c 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/linux-parameters.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/linux-parameters.ts @@ -1,6 +1,7 @@ import { Construct } from 'constructs'; import { CfnTaskDefinition } from './ecs.generated'; import * as cdk from '../../core'; +import { ValidationError } from '../../core'; /** * The properties for defining Linux-specific options that are applied to the container. @@ -111,7 +112,7 @@ export class LinuxParameters extends Construct { props.sharedMemorySize !== undefined && (!Number.isInteger(props.sharedMemorySize) || props.sharedMemorySize < 0) ) { - throw new Error(`sharedMemorySize: Must be an integer greater than 0; received ${props.sharedMemorySize}.`); + throw new ValidationError(`sharedMemorySize: Must be an integer greater than 0; received ${props.sharedMemorySize}.`, this); } if ( @@ -119,7 +120,7 @@ export class LinuxParameters extends Construct { props.swappiness !== undefined && (!Number.isInteger(props.swappiness) || props.swappiness < 0 || props.swappiness > 100) ) { - throw new Error(`swappiness: Must be an integer between 0 and 100; received ${props.swappiness}.`); + throw new ValidationError(`swappiness: Must be an integer between 0 and 100; received ${props.swappiness}.`, this); } } diff --git a/packages/aws-cdk-lib/aws-ecs/lib/log-drivers/aws-log-driver.ts b/packages/aws-cdk-lib/aws-ecs/lib/log-drivers/aws-log-driver.ts index 9d0ff4e970a0a..ff479b9028367 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/log-drivers/aws-log-driver.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/log-drivers/aws-log-driver.ts @@ -3,7 +3,7 @@ import { LogDriver, LogDriverConfig } from './log-driver'; import { removeEmpty } from './utils'; import * as iam from '../../../aws-iam'; import * as logs from '../../../aws-logs'; -import { Size, SizeRoundingBehavior } from '../../../core'; +import { Size, SizeRoundingBehavior, UnscopedValidationError } from '../../../core'; import { ContainerDefinition } from '../container-definition'; /** @@ -116,11 +116,11 @@ export class AwsLogDriver extends LogDriver { super(); if (props.logGroup && props.logRetention) { - throw new Error('Cannot specify both `logGroup` and `logRetentionDays`.'); + throw new UnscopedValidationError('Cannot specify both `logGroup` and `logRetentionDays`.'); } if (props.maxBufferSize && props.mode !== AwsLogDriverMode.NON_BLOCKING) { - throw new Error('Cannot specify `maxBufferSize` when the driver mode is blocking'); + throw new UnscopedValidationError('Cannot specify `maxBufferSize` when the driver mode is blocking'); } } diff --git a/packages/aws-cdk-lib/aws-ecs/lib/log-drivers/json-file-log-driver.ts b/packages/aws-cdk-lib/aws-ecs/lib/log-drivers/json-file-log-driver.ts index 6dae3ba91293b..ccf51d523d2c9 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/log-drivers/json-file-log-driver.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/log-drivers/json-file-log-driver.ts @@ -2,6 +2,7 @@ import { Construct } from 'constructs'; import { BaseLogDriverProps } from './base-log-driver'; import { LogDriver, LogDriverConfig } from './log-driver'; import { joinWithCommas, stringifyOptions } from './utils'; +import { UnscopedValidationError } from '../../../core'; import { ContainerDefinition } from '../container-definition'; /** @@ -49,7 +50,7 @@ export class JsonFileLogDriver extends LogDriver { // Validation if (props.maxFile && props.maxFile < 0) { - throw new Error('`maxFile` must be a positive integer.'); + throw new UnscopedValidationError('`maxFile` must be a positive integer.'); } } diff --git a/packages/aws-cdk-lib/aws-ecs/lib/log-drivers/splunk-log-driver.ts b/packages/aws-cdk-lib/aws-ecs/lib/log-drivers/splunk-log-driver.ts index 074427dec0464..f7b9dde143cc9 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/log-drivers/splunk-log-driver.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/log-drivers/splunk-log-driver.ts @@ -2,7 +2,7 @@ import { Construct } from 'constructs'; import { BaseLogDriverProps } from './base-log-driver'; import { LogDriver, LogDriverConfig } from './log-driver'; import { ensureInRange, renderCommonLogDriverOptions, renderLogDriverSecretOptions, stringifyOptions } from './utils'; -import { SecretValue } from '../../../core'; +import { SecretValue, UnscopedValidationError } from '../../../core'; import { ContainerDefinition, Secret } from '../container-definition'; /** @@ -134,7 +134,7 @@ export class SplunkLogDriver extends LogDriver { super(); if (!props.token && !props.secretToken) { - throw new Error('Please provide either token or secretToken.'); + throw new UnscopedValidationError('Please provide either token or secretToken.'); } if (props.gzipLevel) { ensureInRange(props.gzipLevel, -1, 9); diff --git a/packages/aws-cdk-lib/aws-ecs/lib/log-drivers/utils.ts b/packages/aws-cdk-lib/aws-ecs/lib/log-drivers/utils.ts index a2a30bb026404..57d52c2715b63 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/log-drivers/utils.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/log-drivers/utils.ts @@ -1,5 +1,5 @@ import { BaseLogDriverProps } from './base-log-driver'; -import { Duration, SecretValue, Token } from '../../../core'; +import { Duration, SecretValue, Token, UnscopedValidationError } from '../../../core'; import { TaskDefinition } from '../base/task-definition'; import { Secret } from '../container-definition'; import { CfnTaskDefinition } from '../ecs.generated'; @@ -21,7 +21,7 @@ export function removeEmpty(x: { [key: string]: (T | undefined | string) }): */ export function ensurePositiveInteger(val: number) { if (!Token.isUnresolved(val) && Number.isInteger(val) && val < 0) { - throw new Error(`\`${val}\` must be a positive integer.`); + throw new UnscopedValidationError(`\`${val}\` must be a positive integer.`); } } @@ -30,7 +30,7 @@ export function ensurePositiveInteger(val: number) { */ export function ensureInRange(val: number, start: number, end: number) { if (!Token.isUnresolved(val) && !(val >= start && val <= end)) { - throw new Error(`\`${val}\` must be within range ${start}:${end}`); + throw new UnscopedValidationError(`\`${val}\` must be within range ${start}:${end}`); } } diff --git a/packages/aws-cdk-lib/aws-ecs/lib/placement.ts b/packages/aws-cdk-lib/aws-ecs/lib/placement.ts index d86d36b2950fb..df2f8260479e5 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/placement.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/placement.ts @@ -1,3 +1,4 @@ +import { UnscopedValidationError } from '../../core'; import { BuiltInAttributes } from './ec2/ec2-service'; import { CfnService } from './ecs.generated'; @@ -41,7 +42,7 @@ export class PlacementStrategy { */ public static spreadAcross(...fields: string[]) { if (fields.length === 0) { - throw new Error('spreadAcross: give at least one field to spread by'); + throw new UnscopedValidationError('spreadAcross: give at least one field to spread by'); } return new PlacementStrategy(fields.map(field => ({ type: 'spread', field }))); } diff --git a/packages/aws-cdk-lib/aws-ecs/lib/proxy-configuration/app-mesh-proxy-configuration.ts b/packages/aws-cdk-lib/aws-ecs/lib/proxy-configuration/app-mesh-proxy-configuration.ts index c8b73480ffcb0..e815f007faa60 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/proxy-configuration/app-mesh-proxy-configuration.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/proxy-configuration/app-mesh-proxy-configuration.ts @@ -1,5 +1,6 @@ import { Construct } from 'constructs'; import { ProxyConfiguration } from './proxy-configuration'; +import { UnscopedValidationError } from '../../../core'; import { TaskDefinition } from '../base/task-definition'; import { CfnTaskDefinition } from '../ecs.generated'; @@ -79,7 +80,7 @@ export class AppMeshProxyConfiguration extends ProxyConfiguration { super(); if (props.properties) { if (!props.properties.ignoredUID && !props.properties.ignoredGID) { - throw new Error('At least one of ignoredUID or ignoredGID should be specified.'); + throw new UnscopedValidationError('At least one of ignoredUID or ignoredGID should be specified.'); } } }