Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat(applicationautoscaling): throw ValidationError instead of untyped errors #33172

Merged
merged 4 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/aws-cdk-lib/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const enableNoThrowDefaultErrorIn = [
'aws-apigatewayv2',
'aws-apigatewayv2-authorizers',
'aws-apigatewayv2-integrations',
'aws-applicationautoscaling',
'aws-cognito',
'aws-elasticloadbalancing',
'aws-elasticloadbalancingv2',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { BasicStepScalingPolicyProps, StepScalingPolicy } from './step-scaling-p
import { BasicTargetTrackingScalingPolicyProps, TargetTrackingScalingPolicy } from './target-tracking-scaling-policy';
import * as iam from '../../aws-iam';
import { IResource, Lazy, Resource, TimeZone, withResolved } from '../../core';
import { ValidationError } from '../../core/lib/errors';

export interface IScalableTarget extends IResource {
/**
Expand Down Expand Up @@ -100,19 +101,19 @@ export class ScalableTarget extends Resource implements IScalableTarget {

withResolved(props.maxCapacity, max => {
if (max < 0) {
throw new RangeError(`maxCapacity cannot be negative, got: ${props.maxCapacity}`);
throw new ValidationError(`maxCapacity cannot be negative, got: ${props.maxCapacity}`, scope);
}
});

withResolved(props.minCapacity, min => {
if (min < 0) {
throw new RangeError(`minCapacity cannot be negative, got: ${props.minCapacity}`);
throw new ValidationError(`minCapacity cannot be negative, got: ${props.minCapacity}`, scope);
}
});

withResolved(props.minCapacity, props.maxCapacity, (min, max) => {
if (max < min) {
throw new RangeError(`minCapacity (${props.minCapacity}) should be lower than maxCapacity (${props.maxCapacity})`);
throw new ValidationError(`minCapacity (${props.minCapacity}) should be lower than maxCapacity (${props.maxCapacity})`, scope);
}
});

Expand Down Expand Up @@ -145,7 +146,7 @@ export class ScalableTarget extends Resource implements IScalableTarget {
*/
public scaleOnSchedule(id: string, action: ScalingSchedule) {
if (action.minCapacity === undefined && action.maxCapacity === undefined) {
throw new Error(`You must supply at least one of minCapacity or maxCapacity, got ${JSON.stringify(action)}`);
throw new ValidationError(`You must supply at least one of minCapacity or maxCapacity, got ${JSON.stringify(action)}`, this);
}

// add a warning on synth when minute is not defined in a cron schedule
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Construct } from 'constructs';
import { Annotations, Duration } from '../../core';
import { UnscopedValidationError } from '../../core/lib/errors';

/**
* Schedule for scheduled scaling actions
Expand All @@ -21,12 +22,12 @@ export abstract class Schedule {
if (duration.isUnresolved()) {
const validDurationUnit = ['minute', 'minutes', 'hour', 'hours', 'day', 'days'];
if (!validDurationUnit.includes(duration.unitLabel())) {
throw new Error("Allowed units for scheduling are: 'minute', 'minutes', 'hour', 'hours', 'day' or 'days'");
throw new UnscopedValidationError("Allowed units for scheduling are: 'minute', 'minutes', 'hour', 'hours', 'day' or 'days'");
Copy link
Contributor Author

@HBobertz HBobertz Jan 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are public static methods so didn't want to update to turn into validation errors

}
return new LiteralSchedule(`rate(${duration.formatTokenToNumber()})`);
}
if (duration.toSeconds() === 0) {
throw new Error('Duration cannot be 0');
throw new UnscopedValidationError('Duration cannot be 0');
}

let rate = maybeRate(duration.toDays({ integral: false }), 'day');
Expand All @@ -47,7 +48,7 @@ export abstract class Schedule {
*/
public static cron(options: CronOptions): Schedule {
if (options.weekDay !== undefined && options.day !== undefined) {
throw new Error('Cannot supply both \'day\' and \'weekDay\', use at most one');
throw new UnscopedValidationError('Cannot supply both \'day\' and \'weekDay\', use at most one');
}

const minute = fallback(options.minute, '*');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Construct } from 'constructs';
import { CfnScalingPolicy } from './applicationautoscaling.generated';
import { IScalableTarget } from './scalable-target';
import * as cdk from '../../core';
import { ValidationError } from '../../core/lib/errors';

/**
* Properties for a scaling policy
Expand Down Expand Up @@ -102,7 +103,7 @@ export class StepScalingAction extends Construct {
*/
public addAdjustment(adjustment: AdjustmentTier) {
if (adjustment.lowerBound === undefined && adjustment.upperBound === undefined) {
throw new Error('At least one of lowerBound or upperBound is required');
throw new ValidationError('At least one of lowerBound or upperBound is required', this);
}
this.adjustments.push({
metricIntervalLowerBound: adjustment.lowerBound,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AdjustmentType, MetricAggregationType, StepScalingAction } from './step
import { findAlarmThresholds, normalizeIntervals } from '../../aws-autoscaling-common';
import * as cloudwatch from '../../aws-cloudwatch';
import * as cdk from '../../core';
import { ValidationError } from '../../core/lib/errors';

export interface BasicStepScalingPolicyProps {
/**
Expand Down Expand Up @@ -110,28 +111,28 @@ export class StepScalingPolicy extends Construct {
super(scope, id);

if (props.scalingSteps.length < 2) {
throw new Error('You must supply at least 2 intervals for autoscaling');
throw new ValidationError('You must supply at least 2 intervals for autoscaling', scope);
}

if (props.scalingSteps.length > 40) {
throw new Error(`'scalingSteps' can have at most 40 steps, got ${props.scalingSteps.length}`);
throw new ValidationError(`'scalingSteps' can have at most 40 steps, got ${props.scalingSteps.length}`, scope);
}

if (props.evaluationPeriods !== undefined && !cdk.Token.isUnresolved(props.evaluationPeriods) && props.evaluationPeriods < 1) {
throw new Error(`evaluationPeriods cannot be less than 1, got: ${props.evaluationPeriods}`);
throw new ValidationError(`evaluationPeriods cannot be less than 1, got: ${props.evaluationPeriods}`, scope);
}
if (props.datapointsToAlarm !== undefined) {
if (props.evaluationPeriods === undefined) {
throw new Error('evaluationPeriods must be set if datapointsToAlarm is set');
throw new ValidationError('evaluationPeriods must be set if datapointsToAlarm is set', scope);
}
if (!cdk.Token.isUnresolved(props.datapointsToAlarm) && props.datapointsToAlarm < 1) {
throw new Error(`datapointsToAlarm cannot be less than 1, got: ${props.datapointsToAlarm}`);
throw new ValidationError(`datapointsToAlarm cannot be less than 1, got: ${props.datapointsToAlarm}`, scope);
}
if (!cdk.Token.isUnresolved(props.datapointsToAlarm)
&& !cdk.Token.isUnresolved(props.evaluationPeriods)
&& props.evaluationPeriods < props.datapointsToAlarm
) {
throw new Error(`datapointsToAlarm must be less than or equal to evaluationPeriods, got datapointsToAlarm: ${props.datapointsToAlarm}, evaluationPeriods: ${props.evaluationPeriods}`);
throw new ValidationError(`datapointsToAlarm must be less than or equal to evaluationPeriods, got datapointsToAlarm: ${props.datapointsToAlarm}, evaluationPeriods: ${props.evaluationPeriods}`, scope);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CfnScalingPolicy } from './applicationautoscaling.generated';
import { IScalableTarget } from './scalable-target';
import * as cloudwatch from '../../aws-cloudwatch';
import * as cdk from '../../core';
import { ValidationError } from '../../core/lib/errors';

/**
* Base interface for target tracking props
Expand Down Expand Up @@ -123,11 +124,11 @@ export class TargetTrackingScalingPolicy extends Construct {

constructor(scope: Construct, id: string, props: TargetTrackingScalingPolicyProps) {
if ((props.customMetric === undefined) === (props.predefinedMetric === undefined)) {
throw new Error('Exactly one of \'customMetric\' or \'predefinedMetric\' must be specified.');
throw new ValidationError('Exactly one of \'customMetric\' or \'predefinedMetric\' must be specified.', scope);
}

if (props.customMetric && !props.customMetric.toMetricConfig().metricStat) {
throw new Error('Only direct metrics are supported for Target Tracking. Use Step Scaling or supply a Metric object.');
throw new ValidationError('Only direct metrics are supported for Target Tracking. Use Step Scaling or supply a Metric object.', scope);
}

super(scope, id);
Expand All @@ -142,7 +143,7 @@ export class TargetTrackingScalingPolicy extends Construct {
policyType: 'TargetTrackingScaling',
scalingTargetId: props.scalingTarget.scalableTargetId,
targetTrackingScalingPolicyConfiguration: {
customizedMetricSpecification: renderCustomMetric(props.customMetric),
customizedMetricSpecification: renderCustomMetric(this, props.customMetric),
disableScaleIn: props.disableScaleIn,
predefinedMetricSpecification: predefinedMetric !== undefined ? {
predefinedMetricType: predefinedMetric,
Expand All @@ -158,12 +159,12 @@ export class TargetTrackingScalingPolicy extends Construct {
}
}

function renderCustomMetric(metric?: cloudwatch.IMetric): CfnScalingPolicy.CustomizedMetricSpecificationProperty | undefined {
function renderCustomMetric(scope: Construct, metric?: cloudwatch.IMetric): CfnScalingPolicy.CustomizedMetricSpecificationProperty | undefined {
if (!metric) { return undefined; }
const c = metric.toMetricConfig().metricStat!;

if (c.statistic.startsWith('p')) {
throw new Error(`Cannot use statistic '${c.statistic}' for Target Tracking: only 'Average', 'Minimum', 'Maximum', 'SampleCount', and 'Sum' are supported.`);
throw new ValidationError(`Cannot use statistic '${c.statistic}' for Target Tracking: only 'Average', 'Minimum', 'Maximum', 'SampleCount', and 'Sum' are supported.`, scope);
}

return {
Expand Down
Loading