Skip to content

Commit

Permalink
feat(applicationautoscaling): throw ValidationError instead of unty…
Browse files Browse the repository at this point in the history
…ped errors (#33172)

### Issue 

`aws-applicationautoscaling ` for #32569 

### Description of changes

ValidationErrors everywhere

### Describe any new or updated permissions being added

n/a

### Description of how you validated changes

Existing tests. Exemptions granted as this is basically a refactor of existing code.

### Checklist
- [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md)

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
HBobertz authored Jan 27, 2025
1 parent 04efe6c commit abd4a3e
Show file tree
Hide file tree
Showing 6 changed files with 25 additions and 19 deletions.
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'");
}
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

0 comments on commit abd4a3e

Please sign in to comment.