Skip to content

Commit

Permalink
feat(cloudwatch): throw ValidationErrors instead of untyped Errors (#…
Browse files Browse the repository at this point in the history
…33456)

### Issue 

Relates to #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 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
kaizencc authored Feb 17, 2025
1 parent 50b9b6f commit 6098816
Show file tree
Hide file tree
Showing 12 changed files with 54 additions and 47 deletions.
2 changes: 2 additions & 0 deletions packages/aws-cdk-lib/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const enableNoThrowDefaultErrorIn = [
'aws-cloudfront',
'aws-cloudfront-origins',
'aws-cloudtrail',
'aws-cloudwatch',
'aws-cloudwatch-actions',
'aws-elasticloadbalancing',
'aws-elasticloadbalancingv2',
'aws-elasticloadbalancingv2-actions',
Expand Down
14 changes: 7 additions & 7 deletions packages/aws-cdk-lib/aws-cloudwatch/lib/alarm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { dispatchMetric, metricPeriod } from './private/metric-util';
import { dropUndefined } from './private/object';
import { MetricSet } from './private/rendering';
import { normalizeStatistic, parseStatistic } from './private/statistic';
import { ArnFormat, Lazy, Stack, Token, Annotations } from '../../core';
import { ArnFormat, Lazy, Stack, Token, Annotations, ValidationError } from '../../core';
import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource';

/**
Expand Down Expand Up @@ -272,7 +272,7 @@ export class Alarm extends AlarmBase {
// Check per-instance metric
const metricConfig = this.metric.toMetricConfig();
if (metricConfig.metricStat?.dimensions?.length != 1 || !metricConfig.metricStat?.dimensions?.some(dimension => dimension.name === 'InstanceId')) {
throw new Error(`EC2 alarm actions requires an EC2 Per-Instance Metric. (${JSON.stringify(metricConfig)} does not have an 'InstanceId' dimension)`);
throw new ValidationError(`EC2 alarm actions requires an EC2 Per-Instance Metric. (${JSON.stringify(metricConfig)} does not have an 'InstanceId' dimension)`, this);
}
}
return actionArn;
Expand Down Expand Up @@ -355,7 +355,7 @@ export class Alarm extends AlarmBase {
const hasSubmetrics = mathExprHasSubmetrics(expr);

if (hasSubmetrics) {
assertSubmetricsCount(expr);
assertSubmetricsCount(self, expr);
}

self.validateMetricExpression(expr);
Expand All @@ -381,7 +381,7 @@ export class Alarm extends AlarmBase {
const stack = Stack.of(this);

if (definitelyDifferent(stat.region, stack.region)) {
throw new Error(`Cannot create an Alarm in region '${stack.region}' based on metric '${metric}' in '${stat.region}'`);
throw new ValidationError(`Cannot create an Alarm in region '${stack.region}' based on metric '${metric}' in '${stat.region}'`, this);
}
}

Expand All @@ -391,7 +391,7 @@ export class Alarm extends AlarmBase {
*/
private validateMetricExpression(expr: MetricExpressionConfig) {
if (expr.searchAccount !== undefined || expr.searchRegion !== undefined) {
throw new Error('Cannot create an Alarm based on a MathExpression which specifies a searchAccount or searchRegion');
throw new ValidationError('Cannot create an Alarm based on a MathExpression which specifies a searchAccount or searchRegion', this);
}
}

Expand Down Expand Up @@ -462,10 +462,10 @@ function mathExprHasSubmetrics(expr: MetricExpressionConfig) {
return Object.keys(expr.usingMetrics).length > 0;
}

function assertSubmetricsCount(expr: MetricExpressionConfig) {
function assertSubmetricsCount(scope: Construct, expr: MetricExpressionConfig) {
if (Object.keys(expr.usingMetrics).length > 10) {
// https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html#alarms-on-metric-math-expressions
throw new Error('Alarms on math expressions cannot contain more than 10 individual metrics');
throw new ValidationError('Alarms on math expressions cannot contain more than 10 individual metrics', scope);
}
}

Expand Down
6 changes: 3 additions & 3 deletions packages/aws-cdk-lib/aws-cloudwatch/lib/composite-alarm.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Construct } from 'constructs';
import { AlarmBase, IAlarm, IAlarmRule } from './alarm-base';
import { CfnCompositeAlarm } from './cloudwatch.generated';
import { ArnFormat, Lazy, Names, Stack, Duration } from '../../core';
import { ArnFormat, Lazy, Names, Stack, Duration, ValidationError } from '../../core';
import { addConstructMetadata } from '../../core/lib/metadata-resource';

/**
Expand Down Expand Up @@ -120,14 +120,14 @@ export class CompositeAlarm extends AlarmBase {
addConstructMetadata(this, props);

if (props.alarmRule.renderAlarmRule().length > 10240) {
throw new Error('Alarm Rule expression cannot be greater than 10240 characters, please reduce the conditions in the Alarm Rule');
throw new ValidationError('Alarm Rule expression cannot be greater than 10240 characters, please reduce the conditions in the Alarm Rule', this);
}

let extensionPeriod = props.actionsSuppressorExtensionPeriod;
let waitPeriod = props.actionsSuppressorWaitPeriod;
if (props.actionsSuppressor === undefined) {
if (extensionPeriod !== undefined || waitPeriod !== undefined) {
throw new Error('ActionsSuppressor Extension/Wait Periods require an ActionsSuppressor to be set.');
throw new ValidationError('ActionsSuppressor Extension/Wait Periods require an ActionsSuppressor to be set.', this);
}
} else {
extensionPeriod = extensionPeriod ?? Duration.minutes(1);
Expand Down
10 changes: 5 additions & 5 deletions packages/aws-cdk-lib/aws-cloudwatch/lib/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { CfnDashboard } from './cloudwatch.generated';
import { Column, Row } from './layout';
import { IVariable } from './variable';
import { IWidget } from './widget';
import { Lazy, Resource, Stack, Token, Annotations, Duration } from '../../core';
import { Lazy, Resource, Stack, Token, Annotations, Duration, ValidationError } from '../../core';
import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource';

/**
Expand Down Expand Up @@ -127,19 +127,19 @@ export class Dashboard extends Resource {
{
const { dashboardName } = props;
if (dashboardName && !Token.isUnresolved(dashboardName) && !dashboardName.match(/^[\w-]+$/)) {
throw new Error([
throw new ValidationError([
`The value ${dashboardName} for field dashboardName contains invalid characters.`,
'It can only contain alphanumerics, dash (-) and underscore (_).',
].join(' '));
].join(' '), this);
}
}

if (props.start !== undefined && props.defaultInterval !== undefined) {
throw new Error('both properties defaultInterval and start cannot be set at once');
throw new ValidationError('both properties defaultInterval and start cannot be set at once', this);
}

if (props.end !== undefined && props.start === undefined) {
throw new Error('If you specify a value for end, you must also specify a value for start.');
throw new ValidationError('If you specify a value for end, you must also specify a value for start.', this);
}

const dashboard = new CfnDashboard(this, 'Resource', {
Expand Down
12 changes: 6 additions & 6 deletions packages/aws-cdk-lib/aws-cloudwatch/lib/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ export class GaugeWidget extends ConcreteWidget {
this.copyMetricWarnings(...this.metrics);

if (props.end !== undefined && props.start === undefined) {
throw new Error('If you specify a value for end, you must also specify a value for start.');
throw new cdk.UnscopedValidationError('If you specify a value for end, you must also specify a value for start.');
}
}

Expand Down Expand Up @@ -440,7 +440,7 @@ export class GraphWidget extends ConcreteWidget {
props.verticalAnnotations?.forEach(annotation => {
const date = annotation.date;
if (!GraphWidget.isIso8601(date)) {
throw new Error(`Given date ${date} is not in ISO 8601 format`);
throw new cdk.UnscopedValidationError(`Given date ${date} is not in ISO 8601 format`);
}
});
this.props = props;
Expand All @@ -449,7 +449,7 @@ export class GraphWidget extends ConcreteWidget {
this.copyMetricWarnings(...this.leftMetrics, ...this.rightMetrics);

if (props.end !== undefined && props.start === undefined) {
throw new Error('If you specify a value for end, you must also specify a value for start.');
throw new cdk.UnscopedValidationError('If you specify a value for end, you must also specify a value for start.');
}
}

Expand Down Expand Up @@ -756,7 +756,7 @@ export class TableWidget extends ConcreteWidget {
this.copyMetricWarnings(...this.metrics);

if (props.end !== undefined && props.start === undefined) {
throw new Error('If you specify a value for end, you must also specify a value for start.');
throw new cdk.UnscopedValidationError('If you specify a value for end, you must also specify a value for start.');
}
}

Expand Down Expand Up @@ -885,11 +885,11 @@ export class SingleValueWidget extends ConcreteWidget {
this.copyMetricWarnings(...props.metrics);

if (props.setPeriodToTimeRange && props.sparkline) {
throw new Error('You cannot use setPeriodToTimeRange with sparkline');
throw new cdk.UnscopedValidationError('You cannot use setPeriodToTimeRange with sparkline');
}

if (props.end !== undefined && props.start === undefined) {
throw new Error('If you specify a value for end, you must also specify a value for start.');
throw new cdk.UnscopedValidationError('If you specify a value for end, you must also specify a value for start.');
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/aws-cdk-lib/aws-cloudwatch/lib/log-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,11 @@ export class LogQueryWidget extends ConcreteWidget {
this.props = props;

if (props.logGroupNames.length === 0) {
throw new Error('Specify at least one log group name.');
throw new cdk.UnscopedValidationError('Specify at least one log group name.');
}

if (!!props.queryString === !!props.queryLines) {
throw new Error('Specify exactly one of \'queryString\' and \'queryLines\'');
throw new cdk.UnscopedValidationError('Specify exactly one of \'queryString\' and \'queryLines\'');
}
}

Expand Down
26 changes: 13 additions & 13 deletions packages/aws-cdk-lib/aws-cloudwatch/lib/metric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ export class Metric implements IMetric {
this.period = props.period || cdk.Duration.minutes(5);
const periodSec = this.period.toSeconds();
if (periodSec !== 1 && periodSec !== 5 && periodSec !== 10 && periodSec !== 30 && periodSec % 60 !== 0) {
throw new Error(`'period' must be 1, 5, 10, 30, or a multiple of 60 seconds, received ${periodSec}`);
throw new cdk.UnscopedValidationError(`'period' must be 1, 5, 10, 30, or a multiple of 60 seconds, received ${periodSec}`);
}

this.warnings = undefined;
Expand Down Expand Up @@ -485,7 +485,7 @@ export class Metric implements IMetric {
public toAlarmConfig(): MetricAlarmConfig {
const metricConfig = this.toMetricConfig();
if (metricConfig.metricStat === undefined) {
throw new Error('Using a math expression is not supported here. Pass a \'Metric\' object instead');
throw new cdk.UnscopedValidationError('Using a math expression is not supported here. Pass a \'Metric\' object instead');
}

const parsed = parseStatistic(metricConfig.metricStat.statistic);
Expand Down Expand Up @@ -514,7 +514,7 @@ export class Metric implements IMetric {
public toGraphConfig(): MetricGraphConfig {
const metricConfig = this.toMetricConfig();
if (metricConfig.metricStat === undefined) {
throw new Error('Using a math expression is not supported here. Pass a \'Metric\' object instead');
throw new cdk.UnscopedValidationError('Using a math expression is not supported here. Pass a \'Metric\' object instead');
}

return {
Expand Down Expand Up @@ -586,19 +586,19 @@ export class Metric implements IMetric {

var dimsArray = Object.keys(dims);
if (dimsArray?.length > 30) {
throw new Error(`The maximum number of dimensions is 30, received ${dimsArray.length}`);
throw new cdk.UnscopedValidationError(`The maximum number of dimensions is 30, received ${dimsArray.length}`);
}

dimsArray.map(key => {
if (dims[key] === undefined || dims[key] === null) {
throw new Error(`Dimension value of '${dims[key]}' is invalid`);
throw new cdk.UnscopedValidationError(`Dimension value of '${dims[key]}' is invalid`);
}
if (key.length < 1 || key.length > 255) {
throw new Error(`Dimension name must be at least 1 and no more than 255 characters; received ${key}`);
throw new cdk.UnscopedValidationError(`Dimension name must be at least 1 and no more than 255 characters; received ${key}`);
}

if (dims[key].length < 1 || dims[key].length > 255) {
throw new Error(`Dimension value must be at least 1 and no more than 255 characters; received ${dims[key]}`);
throw new cdk.UnscopedValidationError(`Dimension value must be at least 1 and no more than 255 characters; received ${dims[key]}`);
}
});

Expand All @@ -609,7 +609,7 @@ export class Metric implements IMetric {
function asString(x?: unknown): string | undefined {
if (x === undefined) { return undefined; }
if (typeof x !== 'string') {
throw new Error(`Expected string, got ${x}`);
throw new cdk.UnscopedValidationError(`Expected string, got ${x}`);
}
return x;
}
Expand Down Expand Up @@ -696,7 +696,7 @@ export class MathExpression implements IMetric {

const invalidVariableNames = Object.keys(this.usingMetrics).filter(x => !validVariableName(x));
if (invalidVariableNames.length > 0) {
throw new Error(`Invalid variable names in expression: ${invalidVariableNames}. Must start with lowercase letter and only contain alphanumerics.`);
throw new cdk.UnscopedValidationError(`Invalid variable names in expression: ${invalidVariableNames}. Must start with lowercase letter and only contain alphanumerics.`);
}

this.validateNoIdConflicts();
Expand Down Expand Up @@ -756,14 +756,14 @@ export class MathExpression implements IMetric {
* @deprecated use toMetricConfig()
*/
public toAlarmConfig(): MetricAlarmConfig {
throw new Error('Using a math expression is not supported here. Pass a \'Metric\' object instead');
throw new cdk.UnscopedValidationError('Using a math expression is not supported here. Pass a \'Metric\' object instead');
}

/**
* @deprecated use toMetricConfig()
*/
public toGraphConfig(): MetricGraphConfig {
throw new Error('Using a math expression is not supported here. Pass a \'Metric\' object instead');
throw new cdk.UnscopedValidationError('Using a math expression is not supported here. Pass a \'Metric\' object instead');
}

public toMetricConfig(): MetricConfig {
Expand Down Expand Up @@ -822,7 +822,7 @@ export class MathExpression implements IMetric {
for (const [id, subMetric] of Object.entries(expr.usingMetrics)) {
const existing = seen.get(id);
if (existing && metricKey(existing) !== metricKey(subMetric)) {
throw new Error(`The ID '${id}' used for two metrics in the expression: '${subMetric}' and '${existing}'. Rename one.`);
throw new cdk.UnscopedValidationError(`The ID '${id}' used for two metrics in the expression: '${subMetric}' and '${existing}'. Rename one.`);
}
seen.set(id, subMetric);
visit(subMetric);
Expand Down Expand Up @@ -990,7 +990,7 @@ function changePeriod(metric: IMetric, period: cdk.Duration): { metric: IMetric;
return { metric: metric.with({ period }), overridden };
}

throw new Error(`Metric object should also implement 'with': ${metric}`);
throw new cdk.UnscopedValidationError(`Metric object should also implement 'with': ${metric}`);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Duration } from '../../../core';
import { Duration, UnscopedValidationError } from '../../../core';
import { MathExpression } from '../metric';
import { IMetric, MetricConfig, MetricExpressionConfig, MetricStatConfig } from '../metric-types';

Expand Down Expand Up @@ -119,12 +119,12 @@ export function metricPeriod(metric: IMetric): Duration {
export function dispatchMetric<A, B>(metric: IMetric, fns: { withStat: (x: MetricStatConfig, c: MetricConfig) => A; withExpression: (x: MetricExpressionConfig, c: MetricConfig) => B }): A | B {
const conf = metric.toMetricConfig();
if (conf.metricStat && conf.mathExpression) {
throw new Error('Metric object must not produce both \'metricStat\' and \'mathExpression\'');
throw new UnscopedValidationError('Metric object must not produce both \'metricStat\' and \'mathExpression\'');
} else if (conf.metricStat) {
return fns.withStat(conf.metricStat, conf);
} else if (conf.mathExpression) {
return fns.withExpression(conf.mathExpression, conf);
} else {
throw new Error('Metric object must have either \'metricStat\' or \'mathExpression\'');
throw new UnscopedValidationError('Metric object must have either \'metricStat\' or \'mathExpression\'');
}
}
3 changes: 2 additions & 1 deletion packages/aws-cdk-lib/aws-cloudwatch/lib/private/rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { DropEmptyObjectAtTheEndOfAnArray } from './drop-empty-object-at-the-end
import { accountIfDifferentFromStack, regionIfDifferentFromStack } from './env-tokens';
import { dispatchMetric, metricKey } from './metric-util';
import { dropUndefined } from './object';
import { UnscopedValidationError } from '../../../core';
import { IMetric } from '../metric-types';

/**
Expand Down Expand Up @@ -159,7 +160,7 @@ export class MetricSet<A> {
if (id) {
existingEntry = this.metricById.get(id);
if (existingEntry && metricKey(existingEntry.metric) !== key) {
throw new Error(`Cannot have two different metrics share the same id ('${id}') in one Alarm or Graph. Rename one of them.`);
throw new UnscopedValidationError(`Cannot have two different metrics share the same id ('${id}') in one Alarm or Graph. Rename one of them.`);
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/aws-cdk-lib/aws-cloudwatch/lib/stats.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { UnscopedValidationError } from '../../core';

/**
* Factory functions for standard statistics strings
Expand Down Expand Up @@ -197,7 +198,7 @@ export abstract class Stats {

function assertPercentage(x?: number) {
if (x !== undefined && (x < 0 || x > 100)) {
throw new Error(`Expecting a percentage, got: ${x}`);
throw new UnscopedValidationError(`Expecting a percentage, got: ${x}`);
}
}

Expand Down
12 changes: 7 additions & 5 deletions packages/aws-cdk-lib/aws-cloudwatch/lib/variable.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { UnscopedValidationError } from '../../core';

export enum VariableInputType {
/**
* Freeform text input box
Expand Down Expand Up @@ -85,10 +87,10 @@ export abstract class Values {
*/
public static fromSearchComponents(components: SearchComponents): Values {
if (components.dimensions.length === 0) {
throw new Error('Empty dimensions provided. Please specify one dimension at least');
throw new UnscopedValidationError('Empty dimensions provided. Please specify one dimension at least');
}
if (!components.dimensions.includes(components.populateFrom)) {
throw new Error(`populateFrom (${components.populateFrom}) is not present in dimensions`);
throw new UnscopedValidationError(`populateFrom (${components.populateFrom}) is not present in dimensions`);
}
const metricSchema = [components.namespace, ...components.dimensions];
return Values.fromSearch(`{${metricSchema.join(',')}} MetricName=\"${components.metricName}\"`, components.populateFrom);
Expand All @@ -109,7 +111,7 @@ export abstract class Values {
*/
public static fromValues(...values: VariableValue[]): Values {
if (values.length == 0) {
throw new Error('Empty values is not allowed');
throw new UnscopedValidationError('Empty values is not allowed');
}
return new StaticValues(values);
}
Expand Down Expand Up @@ -227,10 +229,10 @@ export interface DashboardVariableOptions {
export class DashboardVariable implements IVariable {
public constructor(private readonly options: DashboardVariableOptions) {
if (options.inputType !== VariableInputType.INPUT && !options.values) {
throw new Error(`Variable with inputType (${options.inputType}) requires values to be set`);
throw new UnscopedValidationError(`Variable with inputType (${options.inputType}) requires values to be set`);
}
if (options.inputType == VariableInputType.INPUT && options.values) {
throw new Error('inputType INPUT cannot be combined with values. Please choose either SELECT or RADIO or remove \'values\' from options.');
throw new UnscopedValidationError('inputType INPUT cannot be combined with values. Please choose either SELECT or RADIO or remove \'values\' from options.');
}
}

Expand Down
Loading

0 comments on commit 6098816

Please sign in to comment.