diff --git a/packages/@aws-cdk/aws-gamelift-alpha/lib/fleet-base.ts b/packages/@aws-cdk/aws-gamelift-alpha/lib/fleet-base.ts index 68d907c899fe6..e1e8fd7dcca94 100644 --- a/packages/@aws-cdk/aws-gamelift-alpha/lib/fleet-base.ts +++ b/packages/@aws-cdk/aws-gamelift-alpha/lib/fleet-base.ts @@ -639,10 +639,10 @@ export abstract class FleetBase extends cdk.Resource implements IFleet { } protected warnVpcPeeringAuthorizations(scope: Construct): void { - cdk.Annotations.of(scope).addWarning([ + cdk.Annotations.of(scope).addWarningV2('@aws-cdk/aws-gamelift:fleetAutorizeVpcPeering', [ 'To authorize the VPC peering, call the GameLift service API CreateVpcPeeringAuthorization() or use the AWS CLI command create-vpc-peering-authorization.', 'Make this call using the account that manages your non-GameLift resources.', 'See: https://docs.aws.amazon.com/gamelift/latest/developerguide/vpc-peering.html', ].join('\n')); } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry-alpha/lib/aspects/stack-associator.ts b/packages/@aws-cdk/aws-servicecatalogappregistry-alpha/lib/aspects/stack-associator.ts index c3f5692ea22e2..3a4a8248b6ce9 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry-alpha/lib/aspects/stack-associator.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry-alpha/lib/aspects/stack-associator.ts @@ -38,7 +38,7 @@ abstract class StackAssociatorBase implements IAspect { if (Stage.isStage(childNode)) { var stageAssociated = this.applicationAssociator?.isStageAssociated(childNode); if (stageAssociated === false) { - this.warning(childNode, 'Associate Stage: ' + childNode.stageName + ' to ensure all stacks in your cdk app are associated with AppRegistry. ' + this.warning('StackNotAssociated', childNode, 'Associate Stage: ' + childNode.stageName + ' to ensure all stacks in your cdk app are associated with AppRegistry. ' + 'You can use ApplicationAssociator.associateStage to associate any stage.'); } } @@ -73,8 +73,8 @@ abstract class StackAssociatorBase implements IAspect { * @param node The scope to add the warning to. * @param message The error message. */ - private warning(node: IConstruct, message: string): void { - Annotations.of(node).addWarning(message); + private warning(id: string, node: IConstruct, message: string): void { + Annotations.of(node).addWarningV2(`@aws-cdk/servicecatalogappregistry:${id}`, message); } /** @@ -87,12 +87,12 @@ abstract class StackAssociatorBase implements IAspect { */ private handleCrossRegionStack(node: Stack): void { if (isRegionUnresolved(this.application.env.region, node.region)) { - this.warning(node, 'Environment agnostic stack determined, AppRegistry association might not work as expected in case you deploy cross-region or cross-account stack.'); + this.warning('EnvironmentAgnosticStack', node, 'Environment agnostic stack determined, AppRegistry association might not work as expected in case you deploy cross-region or cross-account stack.'); return; } if (node.region != this.application.env.region) { - this.warning(node, 'AppRegistry does not support cross region associations, deployment might fail if there is cross region stacks in the app. Application region ' + this.warning('CrossRegionAssociation', node, 'AppRegistry does not support cross region associations, deployment might fail if there is cross region stacks in the app. Application region ' + this.application.env.region + ', stack region ' + node.region); } } @@ -106,7 +106,7 @@ abstract class StackAssociatorBase implements IAspect { */ private handleCrossAccountStack(node: Stack): void { if (isAccountUnresolved(this.application.env.account!, node.account)) { - this.warning(node, 'Environment agnostic stack determined, AppRegistry association might not work as expected in case you deploy cross-region or cross-account stack.'); + this.warning('EnvironmentAgnosticStack', node, 'Environment agnostic stack determined, AppRegistry association might not work as expected in case you deploy cross-region or cross-account stack.'); return; } @@ -121,7 +121,7 @@ abstract class StackAssociatorBase implements IAspect { this.sharedAccounts.add(node.account); } else { - this.warning(node, 'Cross-account stack detected but application sharing and association will be skipped because cross-account option is not enabled.'); + this.warning('AssociationSkipped', node, 'Cross-account stack detected but application sharing and association will be skipped because cross-account option is not enabled.'); return; } } diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry-alpha/test/application-associator.test.ts b/packages/@aws-cdk/aws-servicecatalogappregistry-alpha/test/application-associator.test.ts index 4e975579d062f..41db8ade2d91d 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry-alpha/test/application-associator.test.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry-alpha/test/application-associator.test.ts @@ -191,7 +191,7 @@ describe('Scope based Associations with Application with Cross Region/Account', const crossAccountStack = new cdk.Stack(app, 'crossRegionStack', { env: { account: 'account', region: 'region' }, }); - Annotations.fromStack(crossAccountStack).hasWarning('*', 'Cross-account stack detected but application sharing and association will be skipped because cross-account option is not enabled.'); + Annotations.fromStack(crossAccountStack).hasWarning('*', 'Cross-account stack detected but application sharing and association will be skipped because cross-account option is not enabled. [ack: @aws-cdk/servicecatalogappregistry:AssociationSkipped]'); }); test('ApplicationAssociator with cross account stacks inside cdkApp does not give warning if associateCrossAccountStacks is set to true', () => { @@ -223,7 +223,7 @@ describe('Scope based Associations with Application with Cross Region/Account', env: { account: 'account', region: 'region' }, }); Annotations.fromStack(crossRegionStack).hasWarning('*', 'AppRegistry does not support cross region associations, deployment might fail if there is cross region stacks in the app.' - + ' Application region region2, stack region region'); + + ' Application region region2, stack region region [ack: @aws-cdk/servicecatalogappregistry:CrossRegionAssociation]'); }); test('Environment Agnostic ApplicationAssociator with cross region stacks inside cdkApp gives warning', () => { @@ -237,7 +237,7 @@ describe('Scope based Associations with Application with Cross Region/Account', const crossRegionStack = new cdk.Stack(app, 'crossRegionStack', { env: { account: 'account', region: 'region' }, }); - Annotations.fromStack(crossRegionStack).hasWarning('*', 'Environment agnostic stack determined, AppRegistry association might not work as expected in case you deploy cross-region or cross-account stack.'); + Annotations.fromStack(crossRegionStack).hasWarning('*', 'Environment agnostic stack determined, AppRegistry association might not work as expected in case you deploy cross-region or cross-account stack. [ack: @aws-cdk/servicecatalogappregistry:EnvironmentAgnosticStack]'); }); test('Cdk App Containing Pipeline with stage but stage not associated throws error', () => { @@ -253,7 +253,7 @@ describe('Scope based Associations with Application with Cross Region/Account', }); app.synth(); Annotations.fromStack(pipelineStack).hasWarning('*', - 'Associate Stage: SampleStage to ensure all stacks in your cdk app are associated with AppRegistry. You can use ApplicationAssociator.associateStage to associate any stage.'); + 'Associate Stage: SampleStage to ensure all stacks in your cdk app are associated with AppRegistry. You can use ApplicationAssociator.associateStage to associate any stage. [ack: @aws-cdk/servicecatalogappregistry:StackNotAssociated]'); }); test('Cdk App Containing Pipeline with stage and stage associated successfully gets synthesized', () => { diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry-alpha/test/application.test.ts b/packages/@aws-cdk/aws-servicecatalogappregistry-alpha/test/application.test.ts index 92b17ea416764..77ea14a2ccef7 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry-alpha/test/application.test.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry-alpha/test/application.test.ts @@ -512,7 +512,7 @@ describe('Scope based Associations with Application with Cross Region/Account', application.associateAllStacksInScope(stage); Annotations.fromStack(stageStack).hasWarning('*', 'AppRegistry does not support cross region associations, deployment might fail if there is cross region stacks in the app.' - + ' Application region region, stack region region1'); + + ' Application region region, stack region region1 [ack: @aws-cdk/servicecatalogappregistry:CrossRegionAssociation]'); }); }); diff --git a/packages/aws-cdk-lib/assertions/test/annotations.test.ts b/packages/aws-cdk-lib/assertions/test/annotations.test.ts index 7d497cd73973a..fea53bc6ce845 100644 --- a/packages/aws-cdk-lib/assertions/test/annotations.test.ts +++ b/packages/aws-cdk-lib/assertions/test/annotations.test.ts @@ -75,7 +75,7 @@ describe('Messages', () => { describe('hasWarning', () => { test('match', () => { - annotations.hasWarning('/Default/Fred', 'this is a warning'); + annotations.hasWarning('/Default/Fred', 'this is a warning [ack: Fred]'); }); test('no match', () => { @@ -89,7 +89,7 @@ describe('Messages', () => { }); test('no match', () => { - expect(() => annotations.hasNoWarning('/Default/Fred', 'this is a warning')) + expect(() => annotations.hasNoWarning('/Default/Fred', 'this is a warning [ack: Fred]')) .toThrowError(/Expected no matches, but stack has 1 messages as follows:/); }); }); @@ -183,7 +183,7 @@ describe('Multiple Messages on the Resource', () => { test('succeeds on hasXxx APIs', () => { annotations.hasError('/Default/Foo', 'error: this is an error'); annotations.hasError('/Default/Foo', 'error: unsupported type Foo::Bar'); - annotations.hasWarning('/Default/Foo', 'warning: Foo::Bar is deprecated'); + annotations.hasWarning('/Default/Foo', 'warning: Foo::Bar is deprecated [ack: Foo]'); }); test('succeeds on findXxx APIs', () => { @@ -191,8 +191,8 @@ describe('Multiple Messages on the Resource', () => { expect(result1.length).toEqual(4); const result2 = annotations.findError('/Default/Bar', Match.stringLikeRegexp('error:.*')); expect(result2.length).toEqual(2); - const result3 = annotations.findWarning('/Default/Bar', 'warning: Foo::Bar is deprecated'); - expect(result3[0].entry.data).toEqual('warning: Foo::Bar is deprecated'); + const result3 = annotations.findWarning('/Default/Bar', 'warning: Foo::Bar is deprecated [ack: Bar]'); + expect(result3[0].entry.data).toEqual('warning: Foo::Bar is deprecated [ack: Bar]'); }); }); class MyAspect implements IAspect { @@ -209,7 +209,8 @@ class MyAspect implements IAspect { }; protected warn(node: IConstruct, message: string): void { - Annotations.of(node).addWarning(message); + // Use construct ID as suppression string, just to make it unique easily + Annotations.of(node).addWarningV2(node.node.id, message); } protected error(node: IConstruct, message: string): void { @@ -231,10 +232,10 @@ class MultipleAspectsPerNode implements IAspect { } protected warn(node: IConstruct, message: string): void { - Annotations.of(node).addWarning(message); + Annotations.of(node).addWarningV2(node.node.id, message); } protected error(node: IConstruct, message: string): void { Annotations.of(node).addError(message); } -} \ No newline at end of file +} diff --git a/packages/aws-cdk-lib/aws-apigateway/lib/method.ts b/packages/aws-cdk-lib/aws-apigateway/lib/method.ts index 5b4ca0428f86f..4c453350f0c34 100644 --- a/packages/aws-cdk-lib/aws-apigateway/lib/method.ts +++ b/packages/aws-cdk-lib/aws-apigateway/lib/method.ts @@ -288,7 +288,7 @@ export class Method extends Resource { public addMethodResponse(methodResponse: MethodResponse): void { const mr = this.methodResponses.find((x) => x.statusCode === methodResponse.statusCode); if (mr) { - Annotations.of(this).addWarning(`addMethodResponse called multiple times with statusCode=${methodResponse.statusCode}, deployment will be nondeterministic. Use a single addMethodResponse call to configure the entire response.`); + Annotations.of(this).addWarningV2('@aws-cdk/aws-apigateway:duplicateStatusCodes', `addMethodResponse called multiple times with statusCode=${methodResponse.statusCode}, deployment will be nondeterministic. Use a single addMethodResponse call to configure the entire response.`); } this.methodResponses.push(methodResponse); } @@ -512,4 +512,4 @@ export enum AuthorizationType { function pathForArn(path: string): string { return path.replace(/\{[^\}]*\}/g, '*'); // replace path parameters (like '{bookId}') with asterisk -} \ No newline at end of file +} diff --git a/packages/aws-cdk-lib/aws-applicationautoscaling/lib/schedule.ts b/packages/aws-cdk-lib/aws-applicationautoscaling/lib/schedule.ts index fb448d42bc732..82c73291993ed 100644 --- a/packages/aws-cdk-lib/aws-applicationautoscaling/lib/schedule.ts +++ b/packages/aws-cdk-lib/aws-applicationautoscaling/lib/schedule.ts @@ -63,7 +63,7 @@ export abstract class Schedule { public readonly expressionString: string = `cron(${minute} ${hour} ${day} ${month} ${weekDay} ${year})`; public _bind(scope: Construct) { if (!options.minute) { - Annotations.of(scope).addWarning('cron: If you don\'t pass \'minute\', by default the event runs every minute. Pass \'minute: \'*\'\' if that\'s what you intend, or \'minute: 0\' to run once per hour instead.'); + Annotations.of(scope).addWarningV2('@aws-cdk/aws-applicationautoscaling:defaultRunEveryMinute', 'cron: If you don\'t pass \'minute\', by default the event runs every minute. Pass \'minute: \'*\'\' if that\'s what you intend, or \'minute: 0\' to run once per hour instead.'); } return new LiteralSchedule(this.expressionString); } diff --git a/packages/aws-cdk-lib/aws-autoscaling/lib/aspects/require-imdsv2-aspect.ts b/packages/aws-cdk-lib/aws-autoscaling/lib/aspects/require-imdsv2-aspect.ts index 158ccf9b5b094..17389e2f34d20 100644 --- a/packages/aws-cdk-lib/aws-autoscaling/lib/aspects/require-imdsv2-aspect.ts +++ b/packages/aws-cdk-lib/aws-autoscaling/lib/aspects/require-imdsv2-aspect.ts @@ -34,6 +34,6 @@ export class AutoScalingGroupRequireImdsv2Aspect implements cdk.IAspect { * @param message The warning message. */ protected warn(node: IConstruct, message: string) { - cdk.Annotations.of(node).addWarning(`${AutoScalingGroupRequireImdsv2Aspect.name} failed on node ${node.node.id}: ${message}`); + cdk.Annotations.of(node).addWarningV2(`@aws-cdk/aws-autoscaling:imdsv2${AutoScalingGroupRequireImdsv2Aspect.name}`, `${AutoScalingGroupRequireImdsv2Aspect.name} failed on node ${node.node.id}: ${message}`); } } diff --git a/packages/aws-cdk-lib/aws-autoscaling/lib/auto-scaling-group.ts b/packages/aws-cdk-lib/aws-autoscaling/lib/auto-scaling-group.ts index 3edd4e3bb6c98..c803fd5f2349e 100644 --- a/packages/aws-cdk-lib/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/aws-cdk-lib/aws-autoscaling/lib/auto-scaling-group.ts @@ -1380,7 +1380,7 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements }); if (desiredCapacity !== undefined) { - Annotations.of(this).addWarning('desiredCapacity has been configured. Be aware this will reset the size of your AutoScalingGroup on every deployment. See https://github.com/aws/aws-cdk/issues/5215'); + Annotations.of(this).addWarningV2('@aws-cdk/aws-autoscaling:desiredCapacitySet', 'desiredCapacity has been configured. Be aware this will reset the size of your AutoScalingGroup on every deployment. See https://github.com/aws/aws-cdk/issues/5215'); } this.maxInstanceLifetime = props.maxInstanceLifetime; @@ -2296,7 +2296,7 @@ function synthesizeBlockDeviceMappings(construct: Construct, blockDevices: Block throw new Error('iops property is required with volumeType: EbsDeviceVolumeType.IO1'); } } else if (volumeType !== EbsDeviceVolumeType.IO1) { - Annotations.of(construct).addWarning('iops will be ignored without volumeType: EbsDeviceVolumeType.IO1'); + Annotations.of(construct).addWarningV2('@aws-cdk/aws-autoscaling:iopsIgnored', 'iops will be ignored without volumeType: EbsDeviceVolumeType.IO1'); } } diff --git a/packages/aws-cdk-lib/aws-autoscaling/lib/schedule.ts b/packages/aws-cdk-lib/aws-autoscaling/lib/schedule.ts index e0eb783f43519..7f532e65ac62a 100644 --- a/packages/aws-cdk-lib/aws-autoscaling/lib/schedule.ts +++ b/packages/aws-cdk-lib/aws-autoscaling/lib/schedule.ts @@ -33,7 +33,7 @@ export abstract class Schedule { public readonly expressionString: string = `${minute} ${hour} ${day} ${month} ${weekDay}`; public _bind(scope: Construct) { if (!options.minute) { - Annotations.of(scope).addWarning('cron: If you don\'t pass \'minute\', by default the event runs every minute. Pass \'minute: \'*\'\' if that\'s what you intend, or \'minute: 0\' to run once per hour instead.'); + Annotations.of(scope).addWarningV2('@aws-cdk/aws-autoscaling:scheduleDefaultRunsEveryMinute', 'cron: If you don\'t pass \'minute\', by default the event runs every minute. Pass \'minute: \'*\'\' if that\'s what you intend, or \'minute: 0\' to run once per hour instead.'); } return new LiteralSchedule(this.expressionString); } diff --git a/packages/aws-cdk-lib/aws-autoscaling/test/auto-scaling-group.test.ts b/packages/aws-cdk-lib/aws-autoscaling/test/auto-scaling-group.test.ts index 2b9124b9d3cd0..5ab8dc9cb1c97 100644 --- a/packages/aws-cdk-lib/aws-autoscaling/test/auto-scaling-group.test.ts +++ b/packages/aws-cdk-lib/aws-autoscaling/test/auto-scaling-group.test.ts @@ -1235,7 +1235,7 @@ describe('auto scaling group', () => { }); // THEN - Annotations.fromStack(stack).hasWarning('/Default/MyStack', 'iops will be ignored without volumeType: EbsDeviceVolumeType.IO1'); + Annotations.fromStack(stack).hasWarning('/Default/MyStack', 'iops will be ignored without volumeType: EbsDeviceVolumeType.IO1 [ack: @aws-cdk/aws-autoscaling:iopsIgnored]'); }); test('warning if iops and volumeType !== IO1', () => { @@ -1259,7 +1259,7 @@ describe('auto scaling group', () => { }); // THEN - Annotations.fromStack(stack).hasWarning('/Default/MyStack', 'iops will be ignored without volumeType: EbsDeviceVolumeType.IO1'); + Annotations.fromStack(stack).hasWarning('/Default/MyStack', 'iops will be ignored without volumeType: EbsDeviceVolumeType.IO1 [ack: @aws-cdk/aws-autoscaling:iopsIgnored]'); }); test('step scaling on metric', () => { diff --git a/packages/aws-cdk-lib/aws-autoscaling/test/scheduled-action.test.ts b/packages/aws-cdk-lib/aws-autoscaling/test/scheduled-action.test.ts index 10fbdfe7066b6..943939c09febb 100644 --- a/packages/aws-cdk-lib/aws-autoscaling/test/scheduled-action.test.ts +++ b/packages/aws-cdk-lib/aws-autoscaling/test/scheduled-action.test.ts @@ -133,7 +133,7 @@ describeDeprecated('scheduled action', () => { }); // THEN - Annotations.fromStack(stack).hasWarning('/Default/ASG/ScheduledActionScaleOutInTheMorning', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead."); + Annotations.fromStack(stack).hasWarning('/Default/ASG/ScheduledActionScaleOutInTheMorning', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead. [ack: @aws-cdk/aws-autoscaling:scheduleDefaultRunsEveryMinute]"); }); test('scheduled scaling shows no warning when minute is * in cron', () => { diff --git a/packages/aws-cdk-lib/aws-cloudwatch/lib/alarm.ts b/packages/aws-cdk-lib/aws-cloudwatch/lib/alarm.ts index 301f669151aae..aaecc5475d28c 100644 --- a/packages/aws-cdk-lib/aws-cloudwatch/lib/alarm.ts +++ b/packages/aws-cdk-lib/aws-cloudwatch/lib/alarm.ts @@ -222,8 +222,8 @@ export class Alarm extends AlarmBase { value: props.threshold, }; - for (const w of this.metric.warnings ?? []) { - Annotations.of(this).addWarning(w); + for (const [i, message] of Object.entries(this.metric.warningsV2 ?? {})) { + Annotations.of(this).addWarningV2(i, message); } } diff --git a/packages/aws-cdk-lib/aws-cloudwatch/lib/dashboard.ts b/packages/aws-cdk-lib/aws-cloudwatch/lib/dashboard.ts index 494a44d54de83..2cf844c734ca3 100644 --- a/packages/aws-cdk-lib/aws-cloudwatch/lib/dashboard.ts +++ b/packages/aws-cdk-lib/aws-cloudwatch/lib/dashboard.ts @@ -179,9 +179,14 @@ export class Dashboard extends Resource { return; } - const warnings = allWidgetsDeep(widgets).flatMap(w => w.warnings ?? []); - for (const w of warnings) { - Annotations.of(this).addWarning(w); + const warnings = allWidgetsDeep(widgets).reduce((prev, curr) => { + return { + ...prev, + ...curr.warningsV2, + }; + }, {} as { [id: string]: string }); + for (const [id, message] of Object.entries(warnings ?? {})) { + Annotations.of(this).addWarningV2(id, message); } const w = widgets.length > 1 ? new Row(...widgets) : widgets[0]; diff --git a/packages/aws-cdk-lib/aws-cloudwatch/lib/metric-types.ts b/packages/aws-cdk-lib/aws-cloudwatch/lib/metric-types.ts index 0fac96a135c1e..7b02013c48a27 100644 --- a/packages/aws-cdk-lib/aws-cloudwatch/lib/metric-types.ts +++ b/packages/aws-cdk-lib/aws-cloudwatch/lib/metric-types.ts @@ -10,9 +10,19 @@ export interface IMetric { * Should be attached to the consuming construct. * * @default - None + * @deprecated - use warningsV2 */ readonly warnings?: string[]; + /** + * Any warnings related to this metric + * + * Should be attached to the consuming construct. + * + * @default - None + */ + readonly warningsV2?: { [id: string]: string }; + /** * Inspect the details of the metric object */ diff --git a/packages/aws-cdk-lib/aws-cloudwatch/lib/metric.ts b/packages/aws-cdk-lib/aws-cloudwatch/lib/metric.ts index cbb7580fb0b29..fc863f8f37870 100644 --- a/packages/aws-cdk-lib/aws-cloudwatch/lib/metric.ts +++ b/packages/aws-cdk-lib/aws-cloudwatch/lib/metric.ts @@ -283,9 +283,15 @@ export class Metric implements IMetric { /** Region which this metric comes from. */ public readonly region?: string; - /** Warnings attached to this metric. */ + /** + * Warnings attached to this metric. + * @deprecated - use warningsV2 + **/ public readonly warnings?: string[]; + /** Warnings attached to this metric. */ + public readonly warningsV2?: { [id: string]: string }; + constructor(props: MetricProps) { this.period = props.period || cdk.Duration.minutes(5); const periodSec = this.period.toSeconds(); @@ -303,11 +309,14 @@ export class Metric implements IMetric { // Unrecognized statistic, do not throw, just warn // There may be a new statistic that this lib does not support yet const label = props.label ? `, label "${props.label}"`: ''; - this.warnings = [ - `Unrecognized statistic "${props.statistic}" for metric with namespace "${props.namespace}"${label} and metric name "${props.metricName}".` + + + const warning = `Unrecognized statistic "${props.statistic}" for metric with namespace "${props.namespace}"${label} and metric name "${props.metricName}".` + ' Preferably use the `aws_cloudwatch.Stats` helper class to specify a statistic.' + - ' You can ignore this warning if your statistic is valid but not yet supported by the `aws_cloudwatch.Stats` helper class.', - ]; + ' You can ignore this warning if your statistic is valid but not yet supported by the `aws_cloudwatch.Stats` helper class.'; + this.warningsV2 = { + 'CloudWatch:Alarm:UnrecognizedStatistic': warning, + }; + this.warnings = [warning]; } this.statistic = normalizeStatistic(parsedStat); @@ -584,9 +593,15 @@ export class MathExpression implements IMetric { /** * Warnings generated by this math expression + * @deprecated - use warningsV2 */ public readonly warnings?: string[]; + /** + * Warnings generated by this math expression + */ + public readonly warningsV2?: { [id: string]: string }; + constructor(props: MathExpressionProps) { this.period = props.period || cdk.Duration.minutes(5); this.expression = props.expression; @@ -609,19 +624,21 @@ export class MathExpression implements IMetric { // we can add warnings. const missingIdentifiers = allIdentifiersInExpression(this.expression).filter(i => !this.usingMetrics[i]); - const warnings: string[] = []; - + const warnings: { [id: string]: string } = {}; if (!this.expression.toUpperCase().match('\\s*SELECT|SEARCH|METRICS\\s.*') && missingIdentifiers.length > 0) { - warnings.push(`Math expression '${this.expression}' references unknown identifiers: ${missingIdentifiers.join(', ')}. Please add them to the 'usingMetrics' map.`); + warnings['CloudWatch:Math:UnknownIdentifier'] = `Math expression '${this.expression}' references unknown identifiers: ${missingIdentifiers.join(', ')}. Please add them to the 'usingMetrics' map.`; } // Also copy warnings from deeper levels so graphs, alarms only have to inspect the top-level objects for (const m of Object.values(this.usingMetrics)) { - warnings.push(...m.warnings ?? []); + for (const [id, message] of Object.entries(m.warningsV2 ?? {})) { + warnings[id] = message; + } } - if (warnings.length > 0) { - this.warnings = warnings; + if (Object.keys(warnings).length > 0) { + this.warnings = Array.from(Object.values(warnings)); + this.warningsV2 = warnings; } } diff --git a/packages/aws-cdk-lib/aws-cloudwatch/lib/widget.ts b/packages/aws-cdk-lib/aws-cloudwatch/lib/widget.ts index a56d7c57d9e5a..c4d2eb0b22f37 100644 --- a/packages/aws-cdk-lib/aws-cloudwatch/lib/widget.ts +++ b/packages/aws-cdk-lib/aws-cloudwatch/lib/widget.ts @@ -21,9 +21,15 @@ export interface IWidget { /** * Any warnings that are produced as a result of putting together this widget + * @deprecated - use warningsV2 */ readonly warnings?: string[]; + /** + * Any warnings that are produced as a result of putting together this widget + */ + readonly warningsV2?: { [id: string]: string }; + /** * Place the widget at a given position */ @@ -47,6 +53,7 @@ export abstract class ConcreteWidget implements IWidget { protected y?: number; public readonly warnings: string[] | undefined = []; + public readonly warningsV2: { [id: string]: string } | undefined = {}; constructor(width: number, height: number) { this.width = width; @@ -68,6 +75,10 @@ export abstract class ConcreteWidget implements IWidget { * Copy the warnings from the given metric */ protected copyMetricWarnings(...ms: IMetric[]) { - this.warnings?.push(...ms.flatMap(m => m.warnings ?? [])); + ms.forEach(m => { + for (const [id, message] of Object.entries(m.warningsV2 ?? {})) { + this.warningsV2![id] = message; + } + }); } } diff --git a/packages/aws-cdk-lib/aws-cloudwatch/test/metric-math.test.ts b/packages/aws-cdk-lib/aws-cloudwatch/test/metric-math.test.ts index 0311f05d0b057..38d2bbd934a52 100644 --- a/packages/aws-cdk-lib/aws-cloudwatch/test/metric-math.test.ts +++ b/packages/aws-cdk-lib/aws-cloudwatch/test/metric-math.test.ts @@ -69,7 +69,9 @@ describe('Metric Math', () => { expression: 'm1 + m2', }); - expect(m.warnings).toContainEqual(expect.stringContaining("'m1 + m2' references unknown identifiers")); + expect(m.warningsV2).toMatchObject({ + 'CloudWatch:Math:UnknownIdentifier': expect.stringContaining("'m1 + m2' references unknown identifiers"), + }); }); test('metrics METRICS expression does not produce warning for unknown identifier', () => { @@ -78,7 +80,7 @@ describe('Metric Math', () => { usingMetrics: {}, }); - expect(m.warnings).toBeUndefined(); + expect(m.warningsV2).toBeUndefined(); }); test('metrics search expression does not produce warning for unknown identifier', () => { @@ -87,7 +89,7 @@ describe('Metric Math', () => { usingMetrics: {}, }); - expect(m.warnings).toBeUndefined(); + expect(m.warningsV2).toBeUndefined(); }); test('metrics insights expression does not produce warning for unknown identifier', () => { @@ -95,7 +97,7 @@ describe('Metric Math', () => { expression: "SELECT AVG(CpuUsage) FROM EC2 WHERE Instance = '123456'", }); - expect(m.warnings).toBeUndefined(); + expect(m.warningsV2).toBeUndefined(); }); test('math expression referring to unknown expressions produces a warning, even when nested', () => { @@ -108,7 +110,9 @@ describe('Metric Math', () => { }, }); - expect(m.warnings).toContainEqual(expect.stringContaining("'m1 + m2' references unknown identifiers")); + expect(m.warningsV2).toMatchObject({ + 'CloudWatch:Math:UnknownIdentifier': expect.stringContaining("'m1 + m2' references unknown identifiers"), + }); }); describe('in graphs', () => { diff --git a/packages/aws-cdk-lib/aws-dynamodb/test/dynamodb.test.ts b/packages/aws-cdk-lib/aws-dynamodb/test/dynamodb.test.ts index 731143a15fb00..edab404c22f61 100644 --- a/packages/aws-cdk-lib/aws-dynamodb/test/dynamodb.test.ts +++ b/packages/aws-cdk-lib/aws-dynamodb/test/dynamodb.test.ts @@ -1548,7 +1548,7 @@ test('scheduled scaling shows warning when minute is not defined in cron', () => }); // THEN - Annotations.fromStack(stack).hasWarning('/Default/MyTable/ReadScaling/Target', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead."); + Annotations.fromStack(stack).hasWarning('/Default/MyTable/ReadScaling/Target', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead. [ack: @aws-cdk/aws-applicationautoscaling:defaultRunEveryMinute]"); }); test('scheduled scaling shows no warning when minute is * in cron', () => { diff --git a/packages/aws-cdk-lib/aws-ec2/lib/aspects/require-imdsv2-aspect.ts b/packages/aws-cdk-lib/aws-ec2/lib/aspects/require-imdsv2-aspect.ts index 9f73da2fd710e..4710db22d13e2 100644 --- a/packages/aws-cdk-lib/aws-ec2/lib/aspects/require-imdsv2-aspect.ts +++ b/packages/aws-cdk-lib/aws-ec2/lib/aspects/require-imdsv2-aspect.ts @@ -37,7 +37,7 @@ abstract class RequireImdsv2Aspect implements cdk.IAspect { */ protected warn(node: IConstruct, message: string) { if (this.suppressWarnings !== true) { - cdk.Annotations.of(node).addWarning(`${RequireImdsv2Aspect.name} failed on node ${node.node.id}: ${message}`); + cdk.Annotations.of(node).addWarningV2(`@aws-cdk/aws-ec2:imdsv2${RequireImdsv2Aspect.name}`, `${RequireImdsv2Aspect.name} failed on node ${node.node.id}: ${message}`); } } } diff --git a/packages/aws-cdk-lib/aws-ec2/lib/private/ebs-util.ts b/packages/aws-cdk-lib/aws-ec2/lib/private/ebs-util.ts index 3152fdb26974a..51a020fa1036f 100644 --- a/packages/aws-cdk-lib/aws-ec2/lib/private/ebs-util.ts +++ b/packages/aws-cdk-lib/aws-ec2/lib/private/ebs-util.ts @@ -32,7 +32,7 @@ function synthesizeBlockDeviceMappings(construct: Construct, blockDevic throw new Error('iops property is required with volumeType: EbsDeviceVolumeType.IO1 and EbsDeviceVolumeType.IO2'); } } else if (volumeType !== EbsDeviceVolumeType.IO1 && volumeType !== EbsDeviceVolumeType.IO2 && volumeType !== EbsDeviceVolumeType.GP3) { - Annotations.of(construct).addWarning('iops will be ignored without volumeType: IO1, IO2, or GP3'); + Annotations.of(construct).addWarningV2('@aws-cdk/aws-ec2:iopsIgnored', 'iops will be ignored without volumeType: IO1, IO2, or GP3'); } /** diff --git a/packages/aws-cdk-lib/aws-ec2/lib/security-group.ts b/packages/aws-cdk-lib/aws-ec2/lib/security-group.ts index 1951d632ebf49..125ce5c0e18ba 100644 --- a/packages/aws-cdk-lib/aws-ec2/lib/security-group.ts +++ b/packages/aws-cdk-lib/aws-ec2/lib/security-group.ts @@ -553,7 +553,7 @@ export class SecurityGroup extends SecurityGroupBase { // is only one rule which allows all traffic and that subsumes any other // rule. if (!remoteRule) { // Warn only if addEgressRule() was explicitely called - Annotations.of(this).addWarning('Ignoring Egress rule since \'allowAllOutbound\' is set to true; To add customized rules, set allowAllOutbound=false on the SecurityGroup'); + Annotations.of(this).addWarningV2('@aws-cdk/aws-ec2:ipv4IgnoreEgressRule', 'Ignoring Egress rule since \'allowAllOutbound\' is set to true; To add customized rules, set allowAllOutbound=false on the SecurityGroup'); } return; } else if (!isIpv6 && !this.allowAllOutbound) { @@ -568,7 +568,7 @@ export class SecurityGroup extends SecurityGroupBase { // is only one rule which allows all traffic and that subsumes any other // rule. if (!remoteRule) { // Warn only if addEgressRule() was explicitely called - Annotations.of(this).addWarning('Ignoring Egress rule since \'allowAllIpv6Outbound\' is set to true; To add customized rules, set allowAllIpv6Outbound=false on the SecurityGroup'); + Annotations.of(this).addWarningV2('@aws-cdk/aws-ec2:ipv6IgnoreEgressRule', 'Ignoring Egress rule since \'allowAllIpv6Outbound\' is set to true; To add customized rules, set allowAllIpv6Outbound=false on the SecurityGroup'); } return; } diff --git a/packages/aws-cdk-lib/aws-ec2/lib/vpc.ts b/packages/aws-cdk-lib/aws-ec2/lib/vpc.ts index 21f4549208330..a02ac3f726363 100644 --- a/packages/aws-cdk-lib/aws-ec2/lib/vpc.ts +++ b/packages/aws-cdk-lib/aws-ec2/lib/vpc.ts @@ -641,7 +641,7 @@ abstract class VpcBase extends Resource implements IVpc { if (placement.subnetGroupName !== undefined) { throw new Error('Please use only \'subnetGroupName\' (\'subnetName\' is deprecated and has the same behavior)'); } else { - Annotations.of(this).addWarning('Usage of \'subnetName\' in SubnetSelection is deprecated, use \'subnetGroupName\' instead'); + Annotations.of(this).addWarningV2('@aws-cdk/aws-ec2:subnetNameDeprecated', 'Usage of \'subnetName\' in SubnetSelection is deprecated, use \'subnetGroupName\' instead'); } placement = { ...placement, subnetGroupName: placement.subnetName }; } @@ -2192,7 +2192,7 @@ class ImportedVpc extends VpcBase { // None of the values may be unresolved list tokens for (const k of Object.keys(props) as Array) { if (Array.isArray(props[k]) && Token.isUnresolved(props[k])) { - Annotations.of(this).addWarning(`fromVpcAttributes: '${k}' is a list token: the imported VPC will not work with constructs that require a list of subnets at synthesis time. Use 'Vpc.fromLookup()' or 'Fn.importListValue' instead.`); + Annotations.of(this).addWarningV2(`@aws-cdk/aws-ec2:vpcAttributeIsListToken${k}`, `fromVpcAttributes: '${k}' is a list token: the imported VPC will not work with constructs that require a list of subnets at synthesis time. Use 'Vpc.fromLookup()' or 'Fn.importListValue' instead.`); } } @@ -2353,7 +2353,7 @@ class ImportedSubnet extends Resource implements ISubnet, IPublicSubnet, IPrivat ? `at '${Node.of(scope).path}/${id}'` : `'${attrs.subnetId}'`; // eslint-disable-next-line max-len - Annotations.of(this).addWarning(`No routeTableId was provided to the subnet ${ref}. Attempting to read its .routeTable.routeTableId will return null/undefined. (More info: https://github.com/aws/aws-cdk/pull/3171)`); + Annotations.of(this).addWarningV2('@aws-cdk/aws-ec2:noSubnetRouteTableId', `No routeTableId was provided to the subnet ${ref}. Attempting to read its .routeTable.routeTableId will return null/undefined. (More info: https://github.com/aws/aws-cdk/pull/3171)`); } this._ipv4CidrBlock = attrs.ipv4CidrBlock; diff --git a/packages/aws-cdk-lib/aws-ec2/test/instance.test.ts b/packages/aws-cdk-lib/aws-ec2/test/instance.test.ts index 82cf2031a9bd5..3dcde47e3b5a8 100644 --- a/packages/aws-cdk-lib/aws-ec2/test/instance.test.ts +++ b/packages/aws-cdk-lib/aws-ec2/test/instance.test.ts @@ -365,7 +365,7 @@ describe('instance', () => { }); // THEN - Annotations.fromStack(stack).hasWarning('/Default/Instance', 'iops will be ignored without volumeType: IO1, IO2, or GP3'); + Annotations.fromStack(stack).hasWarning('/Default/Instance', 'iops will be ignored without volumeType: IO1, IO2, or GP3 [ack: @aws-cdk/aws-ec2:iopsIgnored]'); }); test('warning if iops and invalid volumeType', () => { @@ -385,7 +385,7 @@ describe('instance', () => { }); // THEN - Annotations.fromStack(stack).hasWarning('/Default/Instance', 'iops will be ignored without volumeType: IO1, IO2, or GP3'); + Annotations.fromStack(stack).hasWarning('/Default/Instance', 'iops will be ignored without volumeType: IO1, IO2, or GP3 [ack: @aws-cdk/aws-ec2:iopsIgnored]'); }); }); diff --git a/packages/aws-cdk-lib/aws-ec2/test/vpc.test.ts b/packages/aws-cdk-lib/aws-ec2/test/vpc.test.ts index c62567345a929..90b90ac87cdec 100644 --- a/packages/aws-cdk-lib/aws-ec2/test/vpc.test.ts +++ b/packages/aws-cdk-lib/aws-ec2/test/vpc.test.ts @@ -1911,7 +1911,7 @@ describe('vpc', () => { subnetIds: { 'Fn::Split': [',', { 'Fn::ImportValue': 'myPublicSubnetIds' }] }, }); - Annotations.fromStack(stack).hasWarning('/TestStack/VPC', "fromVpcAttributes: 'availabilityZones' is a list token: the imported VPC will not work with constructs that require a list of subnets at synthesis time. Use 'Vpc.fromLookup()' or 'Fn.importListValue' instead."); + Annotations.fromStack(stack).hasWarning('/TestStack/VPC', "fromVpcAttributes: 'availabilityZones' is a list token: the imported VPC will not work with constructs that require a list of subnets at synthesis time. Use 'Vpc.fromLookup()' or 'Fn.importListValue' instead. [ack: @aws-cdk/aws-ec2:vpcAttributeIsListTokenavailabilityZones]"); }); test('fromVpcAttributes using fixed-length list tokens', () => { diff --git a/packages/aws-cdk-lib/aws-ecr-assets/lib/image-asset.ts b/packages/aws-cdk-lib/aws-ecr-assets/lib/image-asset.ts index 663f1306a936f..c8475ce037873 100644 --- a/packages/aws-cdk-lib/aws-ecr-assets/lib/image-asset.ts +++ b/packages/aws-cdk-lib/aws-ecr-assets/lib/image-asset.ts @@ -460,7 +460,7 @@ export class DockerImageAsset extends Construct implements IAsset { exclude.push(cdkout); if (props.repositoryName) { - Annotations.of(this).addWarning('DockerImageAsset.repositoryName is deprecated. Override "core.Stack.addDockerImageAsset" to control asset locations'); + Annotations.of(this).addWarningV2('@aws-cdk/aws-ecr-assets:repositoryNameDeprecated', 'DockerImageAsset.repositoryName is deprecated. Override "core.Stack.addDockerImageAsset" to control asset locations'); } // include build context in "extra" so it will impact the hash diff --git a/packages/aws-cdk-lib/aws-ecr/lib/repository.ts b/packages/aws-cdk-lib/aws-ecr/lib/repository.ts index 9f5cf978d633d..cddc3e1e42f4b 100644 --- a/packages/aws-cdk-lib/aws-ecr/lib/repository.ts +++ b/packages/aws-cdk-lib/aws-ecr/lib/repository.ts @@ -738,7 +738,7 @@ export class Repository extends RepositoryBase { */ public addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult { if (statement.resources.length) { - Annotations.of(this).addWarning('ECR resource policy does not allow resource statements.'); + Annotations.of(this).addWarningV2('@aws-cdk/aws-ecr:noResourceStatements', 'ECR resource policy does not allow resource statements.'); } if (this.policyDocument === undefined) { this.policyDocument = new iam.PolicyDocument(); diff --git a/packages/aws-cdk-lib/aws-ecr/test/repository.test.ts b/packages/aws-cdk-lib/aws-ecr/test/repository.test.ts index 019148bba20d2..32c79d8d16978 100644 --- a/packages/aws-cdk-lib/aws-ecr/test/repository.test.ts +++ b/packages/aws-cdk-lib/aws-ecr/test/repository.test.ts @@ -396,7 +396,7 @@ describe('repository', () => { })); // THEN - Annotations.fromStack(stack).hasWarning('*', 'ECR resource policy does not allow resource statements.'); + Annotations.fromStack(stack).hasWarning('*', 'ECR resource policy does not allow resource statements. [ack: @aws-cdk/aws-ecr:noResourceStatements]'); }); test('does not warn if repository policy does not have resources', () => { @@ -412,7 +412,7 @@ describe('repository', () => { })); // THEN - Annotations.fromStack(stack).hasNoWarning('*', 'ECR resource policy does not allow resource statements.'); + Annotations.fromStack(stack).hasNoWarning('*', 'ECR resource policy does not allow resource statements. [ack: @aws-cdk/aws-ecr:noResourceStatements]'); }); test('default encryption configuration', () => { 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 ec6d1dd1ebd2b..c7725538ca7e2 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 @@ -89,16 +89,16 @@ export class ScheduledFargateTask extends ScheduledTaskBase { } if (props.taskDefinition) { - Annotations.of(this).addWarning('Property \'taskDefinition\' is ignored, use \'scheduledFargateTaskDefinitionOptions\' or \'scheduledFargateTaskImageOptions\' instead.'); + Annotations.of(this).addWarningV2('@aws-cdk/aws-ecs-patterns:propertyIgnored', 'Property \'taskDefinition\' is ignored, use \'scheduledFargateTaskDefinitionOptions\' or \'scheduledFargateTaskImageOptions\' instead.'); } if (props.cpu) { - Annotations.of(this).addWarning('Property \'cpu\' is ignored, use \'scheduledFargateTaskImageOptions.cpu\' instead.'); + Annotations.of(this).addWarningV2('@aws-cdk/aws-ecs-patterns:propertyIgnored', 'Property \'cpu\' is ignored, use \'scheduledFargateTaskImageOptions.cpu\' instead.'); } if (props.memoryLimitMiB) { - Annotations.of(this).addWarning('Property \'memoryLimitMiB\' is ignored, use \'scheduledFargateTaskImageOptions.memoryLimitMiB\' instead.'); + Annotations.of(this).addWarningV2('@aws-cdk/aws-ecs-patterns:propertyIgnored', 'Property \'memoryLimitMiB\' is ignored, use \'scheduledFargateTaskImageOptions.memoryLimitMiB\' instead.'); } if (props.runtimePlatform) { - Annotations.of(this).addWarning('Property \'runtimePlatform\' is ignored.'); + Annotations.of(this).addWarningV2('@aws-cdk/aws-ecs-patterns:propertyIgnored', 'Property \'runtimePlatform\' is ignored.'); } // Use the EcsTask as the target of the EventRule diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/test/ec2/scheduled-ecs-task.test.ts b/packages/aws-cdk-lib/aws-ecs-patterns/test/ec2/scheduled-ecs-task.test.ts index 56baf1a4d46b0..764d99d4216c0 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/test/ec2/scheduled-ecs-task.test.ts +++ b/packages/aws-cdk-lib/aws-ecs-patterns/test/ec2/scheduled-ecs-task.test.ts @@ -364,7 +364,7 @@ test('Scheduled Ec2 Task shows warning when minute is not defined in cron', () = }); // THEN - Annotations.fromStack(stack).hasWarning('/Default', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead."); + Annotations.fromStack(stack).hasWarning('/Default', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead. [ack: @aws-cdk/aws-events:scheduleWillRunEveryMinute]"); }); test('Scheduled Ec2 Task shows no warning when minute is * in cron', () => { diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/test/fargate/scheduled-fargate-task.test.ts b/packages/aws-cdk-lib/aws-ecs-patterns/test/fargate/scheduled-fargate-task.test.ts index e18588fc10932..8b318fd8b1167 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/test/fargate/scheduled-fargate-task.test.ts +++ b/packages/aws-cdk-lib/aws-ecs-patterns/test/fargate/scheduled-fargate-task.test.ts @@ -451,7 +451,7 @@ test('Scheduled Fargate Task shows warning when minute is not defined in cron', }); // THEN - Annotations.fromStack(stack).hasWarning('/Default', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead."); + Annotations.fromStack(stack).hasWarning('/Default', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead. [ack: @aws-cdk/aws-events:scheduleWillRunEveryMinute]"); }); test('Scheduled Fargate Task shows no warning when minute is * in cron', () => { 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 11f1b1bb9199f..51dea56f85d39 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 @@ -620,7 +620,7 @@ export abstract class BaseService extends Resource this.node.addDependency(this.taskDefinition.taskRole); if (props.deploymentController?.type === DeploymentControllerType.EXTERNAL) { - Annotations.of(this).addWarning('taskDefinition and launchType are blanked out when using external deployment controller.'); + Annotations.of(this).addWarningV2('@aws-cdk/aws-ecs:externalDeploymentController', 'taskDefinition and launchType are blanked out when using external deployment controller.'); } if (props.circuitBreaker diff --git a/packages/aws-cdk-lib/aws-ecs/lib/images/repository.ts b/packages/aws-cdk-lib/aws-ecs/lib/images/repository.ts index bbfc2a7dc6a95..2a371804177b6 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/images/repository.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/images/repository.ts @@ -37,7 +37,7 @@ export class RepositoryImage extends ContainerImage { public bind(scope: Construct, containerDefinition: ContainerDefinition): ContainerImageConfig { // name could be a Token - in that case, skip validation altogether if (!Token.isUnresolved(this.imageName) && ECR_IMAGE_REGEX.test(this.imageName)) { - Annotations.of(scope).addWarning("Proper policies need to be attached before pulling from ECR repository, or use 'fromEcrRepository'."); + Annotations.of(scope).addWarningV2('@aws-cdk/aws-ecs:ecrImageRequiresPolicy', "Proper policies need to be attached before pulling from ECR repository, or use 'fromEcrRepository'."); } if (this.props.credentials) { diff --git a/packages/aws-cdk-lib/aws-ecs/test/ec2/ec2-service.test.ts b/packages/aws-cdk-lib/aws-ecs/test/ec2/ec2-service.test.ts index a289b75162b20..e50017ad80e91 100644 --- a/packages/aws-cdk-lib/aws-ecs/test/ec2/ec2-service.test.ts +++ b/packages/aws-cdk-lib/aws-ecs/test/ec2/ec2-service.test.ts @@ -1216,7 +1216,7 @@ describe('ec2 service', () => { }); // THEN - Annotations.fromStack(stack).hasWarning('/Default/Ec2Service', 'taskDefinition and launchType are blanked out when using external deployment controller.'); + Annotations.fromStack(stack).hasWarning('/Default/Ec2Service', 'taskDefinition and launchType are blanked out when using external deployment controller. [ack: @aws-cdk/aws-ecs:externalDeploymentController]'); Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { Cluster: { Ref: 'EcsCluster97242B84', @@ -1233,7 +1233,8 @@ describe('ec2 service', () => { test('add warning to annotations if circuitBreaker is specified with a non-ECS DeploymentControllerType', () => { // GIVEN - const stack = new cdk.Stack(); + const app = new cdk.App(); + const stack = new cdk.Stack(app); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); addDefaultCapacityProvider(cluster, stack, vpc); @@ -1252,10 +1253,12 @@ describe('ec2 service', () => { }, circuitBreaker: { rollback: true }, }); + app.synth(); // THEN - expect(service.node.metadata[0].data).toEqual('taskDefinition and launchType are blanked out when using external deployment controller.'); + expect(service.node.metadata[0].data).toEqual('taskDefinition and launchType are blanked out when using external deployment controller. [ack: @aws-cdk/aws-ecs:externalDeploymentController]'); expect(service.node.metadata[1].data).toEqual('Deployment circuit breaker requires the ECS deployment controller.'); + }); test('errors if daemon and desiredCount both specified', () => { diff --git a/packages/aws-cdk-lib/aws-ecs/test/ec2/ec2-task-definition.test.ts b/packages/aws-cdk-lib/aws-ecs/test/ec2/ec2-task-definition.test.ts index 34544438e6c7b..0f6b316dd8f26 100644 --- a/packages/aws-cdk-lib/aws-ecs/test/ec2/ec2-task-definition.test.ts +++ b/packages/aws-cdk-lib/aws-ecs/test/ec2/ec2-task-definition.test.ts @@ -688,7 +688,7 @@ describe('ec2 task definition', () => { }); // THEN - Annotations.fromStack(stack).hasWarning('/Default/Ec2TaskDef/web', "Proper policies need to be attached before pulling from ECR repository, or use 'fromEcrRepository'."); + Annotations.fromStack(stack).hasWarning('/Default/Ec2TaskDef/web', "Proper policies need to be attached before pulling from ECR repository, or use 'fromEcrRepository'. [ack: @aws-cdk/aws-ecs:ecrImageRequiresPolicy]"); }); test('warns when setting containers from ECR repository by creating a RepositoryImage class', () => { @@ -706,7 +706,7 @@ describe('ec2 task definition', () => { }); // THEN - Annotations.fromStack(stack).hasWarning('/Default/Ec2TaskDef/web', "Proper policies need to be attached before pulling from ECR repository, or use 'fromEcrRepository'."); + Annotations.fromStack(stack).hasWarning('/Default/Ec2TaskDef/web', "Proper policies need to be attached before pulling from ECR repository, or use 'fromEcrRepository'. [ack: @aws-cdk/aws-ecs:ecrImageRequiresPolicy]"); }); test('correctly sets containers from asset using all props', () => { diff --git a/packages/aws-cdk-lib/aws-ecs/test/external/external-service.test.ts b/packages/aws-cdk-lib/aws-ecs/test/external/external-service.test.ts index a9285cf375b6e..5e64a502b225c 100644 --- a/packages/aws-cdk-lib/aws-ecs/test/external/external-service.test.ts +++ b/packages/aws-cdk-lib/aws-ecs/test/external/external-service.test.ts @@ -553,7 +553,8 @@ describe('external service', () => { test('add warning to annotations if circuitBreaker is specified with a non-ECS DeploymentControllerType', () => { // GIVEN - const stack = new cdk.Stack(); + const app = new cdk.App(); + const stack = new cdk.Stack(app); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); addDefaultCapacityProvider(cluster, stack, vpc); @@ -572,10 +573,12 @@ describe('external service', () => { }, circuitBreaker: { rollback: true }, }); + app.synth(); // THEN - expect(service.node.metadata[0].data).toEqual('taskDefinition and launchType are blanked out when using external deployment controller.'); - expect(service.node.metadata[1].data).toEqual('Deployment circuit breaker requires the ECS deployment controller.'); - + expect(service.node.metadata.map((m) => m.data)).toEqual([ + 'taskDefinition and launchType are blanked out when using external deployment controller. [ack: @aws-cdk/aws-ecs:externalDeploymentController]', + 'Deployment circuit breaker requires the ECS deployment controller.', + ]); }); }); diff --git a/packages/aws-cdk-lib/aws-ecs/test/external/external-task-definition.test.ts b/packages/aws-cdk-lib/aws-ecs/test/external/external-task-definition.test.ts index 681e525c1b8f0..a78872cfbba2b 100644 --- a/packages/aws-cdk-lib/aws-ecs/test/external/external-task-definition.test.ts +++ b/packages/aws-cdk-lib/aws-ecs/test/external/external-task-definition.test.ts @@ -578,7 +578,7 @@ describe('external task definition', () => { }); // THEN - Annotations.fromStack(stack).hasWarning('/Default/ExternalTaskDef/web', "Proper policies need to be attached before pulling from ECR repository, or use 'fromEcrRepository'."); + Annotations.fromStack(stack).hasWarning('/Default/ExternalTaskDef/web', "Proper policies need to be attached before pulling from ECR repository, or use 'fromEcrRepository'. [ack: @aws-cdk/aws-ecs:ecrImageRequiresPolicy]"); }); test('correctly sets volumes', () => { diff --git a/packages/aws-cdk-lib/aws-ecs/test/fargate/fargate-service.test.ts b/packages/aws-cdk-lib/aws-ecs/test/fargate/fargate-service.test.ts index 4470d8c785ea3..aec9f09bff889 100644 --- a/packages/aws-cdk-lib/aws-ecs/test/fargate/fargate-service.test.ts +++ b/packages/aws-cdk-lib/aws-ecs/test/fargate/fargate-service.test.ts @@ -620,7 +620,7 @@ describe('fargate service', () => { }); // THEN - Annotations.fromStack(stack).hasWarning('/Default/FargateService', 'taskDefinition and launchType are blanked out when using external deployment controller.'); + Annotations.fromStack(stack).hasWarning('/Default/FargateService', 'taskDefinition and launchType are blanked out when using external deployment controller. [ack: @aws-cdk/aws-ecs:externalDeploymentController]'); Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { Cluster: { Ref: 'EcsCluster97242B84', @@ -638,7 +638,8 @@ describe('fargate service', () => { test('add warning to annotations if circuitBreaker is specified with a non-ECS DeploymentControllerType', () => { // GIVEN - const stack = new cdk.Stack(); + const app = new cdk.App(); + const stack = new cdk.Stack(app); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); @@ -655,10 +656,11 @@ describe('fargate service', () => { }, circuitBreaker: { rollback: true }, }); + app.synth(); // THEN - expect(service.node.metadata[0].data).toEqual('taskDefinition and launchType are blanked out when using external deployment controller.'); expect(service.node.metadata[1].data).toEqual('Deployment circuit breaker requires the ECS deployment controller.'); + expect(service.node.metadata[0].data).toEqual('taskDefinition and launchType are blanked out when using external deployment controller. [ack: @aws-cdk/aws-ecs:externalDeploymentController]'); }); @@ -2491,7 +2493,7 @@ describe('fargate service', () => { }); // THEN - Annotations.fromStack(stack).hasWarning('/Default/Service/TaskCount/Target', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead."); + Annotations.fromStack(stack).hasWarning('/Default/Service/TaskCount/Target', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead. [ack: @aws-cdk/aws-applicationautoscaling:defaultRunEveryMinute]"); }); test('scheduled scaling shows no warning when minute is * in cron', () => { diff --git a/packages/aws-cdk-lib/aws-eks/lib/cluster.ts b/packages/aws-cdk-lib/aws-eks/lib/cluster.ts index 545eb31bcadeb..6cb1e811a6078 100644 --- a/packages/aws-cdk-lib/aws-eks/lib/cluster.ts +++ b/packages/aws-cdk-lib/aws-eks/lib/cluster.ts @@ -1157,7 +1157,7 @@ abstract class ClusterBase extends Resource implements ICluster { let mapRole = options.mapRole ?? true; if (mapRole && !(this instanceof Cluster)) { // do the mapping... - Annotations.of(autoScalingGroup).addWarning('Auto-mapping aws-auth role for imported cluster is not supported, please map role manually'); + Annotations.of(autoScalingGroup).addWarningV2('@aws-cdk/aws-eks:clusterUnsupportedAutoMappingAwsAutoRole', 'Auto-mapping aws-auth role for imported cluster is not supported, please map role manually'); mapRole = false; } if (mapRole) { @@ -1471,7 +1471,7 @@ export class Cluster extends ClusterBase { const kubectlVersion = new semver.SemVer(`${props.version.version}.0`); if (semver.gte(kubectlVersion, '1.22.0') && !props.kubectlLayer) { - Annotations.of(this).addWarning(`You created a cluster with Kubernetes Version ${props.version.version} without specifying the kubectlLayer property. This may cause failures as the kubectl version provided with aws-cdk-lib is 1.20, which is only guaranteed to be compatible with Kubernetes versions 1.19-1.21. Please provide a kubectlLayer from @aws-cdk/lambda-layer-kubectl-v${kubectlVersion.minor}.`); + Annotations.of(this).addWarningV2('@aws-cdk/aws-eks:clusterKubectlLayerNotSpecified', `You created a cluster with Kubernetes Version ${props.version.version} without specifying the kubectlLayer property. This may cause failures as the kubectl version provided with aws-cdk-lib is 1.20, which is only guaranteed to be compatible with Kubernetes versions 1.19-1.21. Please provide a kubectlLayer from @aws-cdk/lambda-layer-kubectl-v${kubectlVersion.minor}.`); }; this.version = props.version; @@ -1975,7 +1975,7 @@ export class Cluster extends ClusterBase { // message (if token): "could not auto-tag public/private subnet with tag..." // message (if not token): "count not auto-tag public/private subnet xxxxx with tag..." const subnetID = Token.isUnresolved(subnet.subnetId) || Token.isUnresolved([subnet.subnetId]) ? '' : ` ${subnet.subnetId}`; - Annotations.of(this).addWarning(`Could not auto-tag ${type} subnet${subnetID} with "${tag}=1", please remember to do this manually`); + Annotations.of(this).addWarningV2('@aws-cdk/aws-eks:clusterMustManuallyTagSubnet', `Could not auto-tag ${type} subnet${subnetID} with "${tag}=1", please remember to do this manually`); continue; } diff --git a/packages/aws-cdk-lib/aws-eks/lib/fargate-profile.ts b/packages/aws-cdk-lib/aws-eks/lib/fargate-profile.ts index 13c760cdd4d1a..cebe05debe77d 100644 --- a/packages/aws-cdk-lib/aws-eks/lib/fargate-profile.ts +++ b/packages/aws-cdk-lib/aws-eks/lib/fargate-profile.ts @@ -155,7 +155,7 @@ export class FargateProfile extends Construct implements ITaggable { this.podExecutionRole.grantPassRole(props.cluster.adminRole); if (props.subnetSelection && !props.vpc) { - Annotations.of(this).addWarning('Vpc must be defined to use a custom subnet selection. All private subnets belonging to the EKS cluster will be used by default'); + Annotations.of(this).addWarningV2('@aws-cdk/aws-eks:fargateProfileDefaultToPrivateSubnets', 'Vpc must be defined to use a custom subnet selection. All private subnets belonging to the EKS cluster will be used by default'); } let subnets: string[] | undefined; diff --git a/packages/aws-cdk-lib/aws-eks/lib/managed-nodegroup.ts b/packages/aws-cdk-lib/aws-eks/lib/managed-nodegroup.ts index c3c9519571423..c388778661ff1 100644 --- a/packages/aws-cdk-lib/aws-eks/lib/managed-nodegroup.ts +++ b/packages/aws-cdk-lib/aws-eks/lib/managed-nodegroup.ts @@ -370,7 +370,7 @@ export class Nodegroup extends Resource implements INodegroup { } if (props.instanceType) { - Annotations.of(this).addWarning('"instanceType" is deprecated and will be removed in the next major version. please use "instanceTypes" instead'); + Annotations.of(this).addWarningV2('@aws-cdk/aws-eks:managedNodeGroupDeprecatedInstanceType', '"instanceType" is deprecated and will be removed in the next major version. please use "instanceTypes" instead'); } const instanceTypes = props.instanceTypes ?? (props.instanceType ? [props.instanceType] : undefined); let possibleAmiTypes: NodegroupAmiType[] = []; diff --git a/packages/aws-cdk-lib/aws-eks/test/cluster.test.ts b/packages/aws-cdk-lib/aws-eks/test/cluster.test.ts index 545d2ae8cfb59..368f9f2e0cf5e 100644 --- a/packages/aws-cdk-lib/aws-eks/test/cluster.test.ts +++ b/packages/aws-cdk-lib/aws-eks/test/cluster.test.ts @@ -3066,7 +3066,7 @@ describe('cluster', () => { return [ `You created a cluster with Kubernetes Version 1.${version} without specifying the kubectlLayer property.`, 'This may cause failures as the kubectl version provided with aws-cdk-lib is 1.20, which is only guaranteed to be compatible with Kubernetes versions 1.19-1.21.', - `Please provide a kubectlLayer from @aws-cdk/lambda-layer-kubectl-v${version}.`, + `Please provide a kubectlLayer from @aws-cdk/lambda-layer-kubectl-v${version}. [ack: @aws-cdk/aws-eks:clusterKubectlLayerNotSpecified]`, ].join(' '); } @@ -3207,4 +3207,4 @@ describe('cluster', () => { }, }); }); -}); \ No newline at end of file +}); diff --git a/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts b/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts index c9627e18688c8..47c7fbac64969 100644 --- a/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts +++ b/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts @@ -306,7 +306,7 @@ export class ApplicationListenerRule extends Construct { // Instead, signal this through a warning. // @deprecate: upon the next major version bump, replace this with a `throw` if (this.action) { - cdk.Annotations.of(this).addWarning('An Action already existed on this ListenerRule and was replaced. Configure exactly one default Action.'); + cdk.Annotations.of(this).addWarningV2('@aws-cdk/aws-elbv2:albListnerRuleDefaultActionReplaced', 'An Action already existed on this ListenerRule and was replaced. Configure exactly one default Action.'); } action.bind(this, this.listener, this); diff --git a/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts b/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts index 5d1b99f6fbc37..59ab4e1b6e38f 100644 --- a/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts +++ b/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts @@ -643,11 +643,11 @@ class ImportedApplicationTargetGroup extends ImportedTargetGroupBase implements public registerListener(_listener: IApplicationListener, _associatingConstruct?: IConstruct) { // Nothing to do, we know nothing of our members - Annotations.of(this).addWarning('Cannot register listener on imported target group -- security groups might need to be updated manually'); + Annotations.of(this).addWarningV2('@aws-cdk/aws-elbv2:albTargetGroupCannotRegisterListener', 'Cannot register listener on imported target group -- security groups might need to be updated manually'); } public registerConnectable(_connectable: ec2.IConnectable, _portRange?: ec2.Port | undefined): void { - Annotations.of(this).addWarning('Cannot register connectable on imported target group -- security groups might need to be updated manually'); + Annotations.of(this).addWarningV2('@aws-cdk/aws-elbv2:albTargetGroupCannotRegisterConnectable', 'Cannot register connectable on imported target group -- security groups might need to be updated manually'); } public addTarget(...targets: IApplicationLoadBalancerTarget[]) { diff --git a/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/shared/base-listener.ts b/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/shared/base-listener.ts index 9c2aa911a247e..72dad641eb2d7 100644 --- a/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/shared/base-listener.ts +++ b/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/shared/base-listener.ts @@ -153,7 +153,7 @@ export abstract class BaseListener extends Resource implements IListener { // Instead, signal this through a warning. // @deprecate: upon the next major version bump, replace this with a `throw` if (this.defaultAction) { - Annotations.of(this).addWarning('A default Action already existed on this Listener and was replaced. Configure exactly one default Action.'); + Annotations.of(this).addWarningV2('@aws-cdk/aws-elbv2:listenerExistingDefaultActionReplaced', 'A default Action already existed on this Listener and was replaced. Configure exactly one default Action.'); } this.defaultAction = action; diff --git a/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts b/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts index 5ca71c8b5e528..024f6303336a7 100644 --- a/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts +++ b/packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts @@ -328,7 +328,7 @@ export abstract class TargetGroupBase extends Construct implements ITargetGroup const ret = new Array(); if (this.targetType === undefined && this.targetsJson.length === 0) { - cdk.Annotations.of(this).addWarning("When creating an empty TargetGroup, you should specify a 'targetType' (this warning may become an error in the future)."); + cdk.Annotations.of(this).addWarningV2('@aws-cdk/aws-elbv2:targetGroupSpecifyTargetTypeForEmptyTargetGroup', "When creating an empty TargetGroup, you should specify a 'targetType' (this warning may become an error in the future)."); } if (this.targetType !== TargetType.LAMBDA && this.vpc === undefined) { diff --git a/packages/aws-cdk-lib/aws-events-targets/lib/aws-api.ts b/packages/aws-cdk-lib/aws-events-targets/lib/aws-api.ts index 028e48fc224d5..165198643a4fe 100644 --- a/packages/aws-cdk-lib/aws-events-targets/lib/aws-api.ts +++ b/packages/aws-cdk-lib/aws-events-targets/lib/aws-api.ts @@ -129,7 +129,7 @@ export class AwsApi implements events.IRuleTarget { function checkServiceExists(service: string, handler: lambda.SingletonFunction) { const sdkService = awsSdkMetadata[service.toLowerCase()]; if (!sdkService) { - Annotations.of(handler).addWarning(`Service ${service} does not exist in the AWS SDK. Check the list of available \ + Annotations.of(handler).addWarningV2(`@aws-cdk/aws-events-targets:${service}DoesNotExist`, `Service ${service} does not exist in the AWS SDK. Check the list of available \ services and actions from https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/index.html`); } } diff --git a/packages/aws-cdk-lib/aws-events-targets/lib/ecs-task.ts b/packages/aws-cdk-lib/aws-events-targets/lib/ecs-task.ts index a4391ffcc2490..9cf0f430b9ac6 100644 --- a/packages/aws-cdk-lib/aws-events-targets/lib/ecs-task.ts +++ b/packages/aws-cdk-lib/aws-events-targets/lib/ecs-task.ts @@ -185,7 +185,7 @@ export class EcsTask implements events.IRuleTarget { // Security groups are only configurable with the "awsvpc" network mode. if (this.taskDefinition.networkMode !== ecs.NetworkMode.AWS_VPC) { if (props.securityGroup !== undefined || props.securityGroups !== undefined) { - cdk.Annotations.of(this.taskDefinition).addWarning('security groups are ignored when network mode is not awsvpc'); + cdk.Annotations.of(this.taskDefinition).addWarningV2('@aws-cdk/aws-events-targets:ecsTaskSecurityGroupIgnored', 'security groups are ignored when network mode is not awsvpc'); } return; } diff --git a/packages/aws-cdk-lib/aws-events-targets/lib/util.ts b/packages/aws-cdk-lib/aws-events-targets/lib/util.ts index c42e8942df31c..ab279d03f4b60 100644 --- a/packages/aws-cdk-lib/aws-events-targets/lib/util.ts +++ b/packages/aws-cdk-lib/aws-events-targets/lib/util.ts @@ -133,7 +133,7 @@ export function addToDeadLetterQueueResourcePolicy(rule: events.IRule, queue: sq }, })); } else { - Annotations.of(rule).addWarning(`Cannot add a resource policy to your dead letter queue associated with rule ${rule.ruleName} because the queue is in a different account. You must add the resource policy manually to the dead letter queue in account ${queue.env.account}.`); + Annotations.of(rule).addWarningV2('@aws-cdk/aws-events-targets:manuallyAddDLQResourcePolicy', `Cannot add a resource policy to your dead letter queue associated with rule ${rule.ruleName} because the queue is in a different account. You must add the resource policy manually to the dead letter queue in account ${queue.env.account}.`); } } diff --git a/packages/aws-cdk-lib/aws-events-targets/test/aws-api/aws-api.test.ts b/packages/aws-cdk-lib/aws-events-targets/test/aws-api/aws-api.test.ts index 425a49d4e246a..22ca924fc33b9 100644 --- a/packages/aws-cdk-lib/aws-events-targets/test/aws-api/aws-api.test.ts +++ b/packages/aws-cdk-lib/aws-events-targets/test/aws-api/aws-api.test.ts @@ -163,5 +163,5 @@ test('with service not in AWS SDK', () => { rule.addTarget(awsApi); // THEN - Annotations.fromStack(stack).hasWarning('*', 'Service no-such-service does not exist in the AWS SDK. Check the list of available services and actions from https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/index.html'); + Annotations.fromStack(stack).hasWarning('*', 'Service no-such-service does not exist in the AWS SDK. Check the list of available services and actions from https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/index.html [ack: @aws-cdk/aws-events-targets:no-such-serviceDoesNotExist]'); }); diff --git a/packages/aws-cdk-lib/aws-events/lib/rule.ts b/packages/aws-cdk-lib/aws-events/lib/rule.ts index 4b4c60f7a3517..7b3f512e692e8 100644 --- a/packages/aws-cdk-lib/aws-events/lib/rule.ts +++ b/packages/aws-cdk-lib/aws-events/lib/rule.ts @@ -448,7 +448,7 @@ export class Rule extends Resource implements IRule { private sameEnvDimension(dim1: string, dim2: string) { switch (Token.compareStrings(dim1, dim2)) { case TokenComparison.ONE_UNRESOLVED: - Annotations.of(this).addWarning('Either the Event Rule or target has an unresolved environment. \n \ + Annotations.of(this).addWarningV2('@aws-cdk/aws-events:ruleUnresolvedEnvironment', 'Either the Event Rule or target has an unresolved environment. \n \ If they are being used in a cross-environment setup you need to specify the environment for both.'); return true; case TokenComparison.BOTH_UNRESOLVED: diff --git a/packages/aws-cdk-lib/aws-events/lib/schedule.ts b/packages/aws-cdk-lib/aws-events/lib/schedule.ts index d9e27642fc8e0..94b3ecbf7f1e0 100644 --- a/packages/aws-cdk-lib/aws-events/lib/schedule.ts +++ b/packages/aws-cdk-lib/aws-events/lib/schedule.ts @@ -62,7 +62,7 @@ export abstract class Schedule { public readonly expressionString: string = `cron(${minute} ${hour} ${day} ${month} ${weekDay} ${year})`; public _bind(scope: Construct) { if (!options.minute) { - Annotations.of(scope).addWarning('cron: If you don\'t pass \'minute\', by default the event runs every minute. Pass \'minute: \'*\'\' if that\'s what you intend, or \'minute: 0\' to run once per hour instead.'); + Annotations.of(scope).addWarningV2('@aws-cdk/aws-events:scheduleWillRunEveryMinute', 'cron: If you don\'t pass \'minute\', by default the event runs every minute. Pass \'minute: \'*\'\' if that\'s what you intend, or \'minute: 0\' to run once per hour instead.'); } return new LiteralSchedule(this.expressionString); } diff --git a/packages/aws-cdk-lib/aws-events/test/rule.test.ts b/packages/aws-cdk-lib/aws-events/test/rule.test.ts index 537859aa9b4f0..6cca0b11ba804 100644 --- a/packages/aws-cdk-lib/aws-events/test/rule.test.ts +++ b/packages/aws-cdk-lib/aws-events/test/rule.test.ts @@ -38,7 +38,7 @@ describe('rule', () => { }), }); - Annotations.fromStack(stack).hasWarning('/Default/MyRule', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead."); + Annotations.fromStack(stack).hasWarning('/Default/MyRule', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead. [ack: @aws-cdk/aws-events:scheduleWillRunEveryMinute]"); }); test('rule does not display warning when minute is set to * in cron', () => { diff --git a/packages/aws-cdk-lib/aws-iam/lib/group.ts b/packages/aws-cdk-lib/aws-iam/lib/group.ts index 6346d216596ae..985746176b252 100644 --- a/packages/aws-cdk-lib/aws-iam/lib/group.ts +++ b/packages/aws-cdk-lib/aws-iam/lib/group.ts @@ -218,7 +218,7 @@ export class Group extends GroupBase { private managedPoliciesExceededWarning() { if (this.managedPolicies.length > 10) { - Annotations.of(this).addWarning(`You added ${this.managedPolicies.length} to IAM Group ${this.physicalName}. The maximum number of managed policies attached to an IAM group is 10.`); + Annotations.of(this).addWarningV2('@aws-cdk/aws-iam:groupMaxPoliciesExceeded', `You added ${this.managedPolicies.length} to IAM Group ${this.physicalName}. The maximum number of managed policies attached to an IAM group is 10.`); } } } diff --git a/packages/aws-cdk-lib/aws-iam/lib/private/imported-role.ts b/packages/aws-cdk-lib/aws-iam/lib/private/imported-role.ts index d7bb14da16ccf..6573696da4e2a 100644 --- a/packages/aws-cdk-lib/aws-iam/lib/private/imported-role.ts +++ b/packages/aws-cdk-lib/aws-iam/lib/private/imported-role.ts @@ -68,7 +68,7 @@ export class ImportedRole extends Resource implements IRole, IComparablePrincipa } public addManagedPolicy(policy: IManagedPolicy): void { - Annotations.of(this).addWarning(`Not adding managed policy: ${policy.managedPolicyArn} to imported role: ${this.roleName}`); + Annotations.of(this).addWarningV2('@aws-cdk/aws-iam:importedRoleManagedPolicyNotAdded', `Not adding managed policy: ${policy.managedPolicyArn} to imported role: ${this.roleName}`); } public grantPassRole(identity: IPrincipal): Grant { diff --git a/packages/aws-cdk-lib/aws-iam/lib/role.ts b/packages/aws-cdk-lib/aws-iam/lib/role.ts index b2e38505cc967..5ed1302543d47 100644 --- a/packages/aws-cdk-lib/aws-iam/lib/role.ts +++ b/packages/aws-cdk-lib/aws-iam/lib/role.ts @@ -652,9 +652,9 @@ export class Role extends Resource implements IRole { const mpCount = this.managedPolicies.length + (splitOffDocs.size - 1); if (mpCount > 20) { - Annotations.of(this).addWarning(`Policy too large: ${mpCount} exceeds the maximum of 20 managed policies attached to a Role`); + Annotations.of(this).addWarningV2('@aws-cdk/aws-iam:rolePolicyTooLarge', `Policy too large: ${mpCount} exceeds the maximum of 20 managed policies attached to a Role`); } else if (mpCount > 10) { - Annotations.of(this).addWarning(`Policy large: ${mpCount} exceeds 10 managed policies attached to a Role, this requires a quota increase`); + Annotations.of(this).addWarningV2('@aws-cdk/aws-iam:rolePolicyLarge', `Policy large: ${mpCount} exceeds 10 managed policies attached to a Role, this requires a quota increase`); } // Create the managed policies and fix up the dependencies @@ -790,4 +790,4 @@ Object.defineProperty(Role.prototype, IAM_ROLE_SYMBOL, { value: true, enumerable: false, writable: false, -}); \ No newline at end of file +}); diff --git a/packages/aws-cdk-lib/aws-iam/lib/unknown-principal.ts b/packages/aws-cdk-lib/aws-iam/lib/unknown-principal.ts index 49bab6a23244f..d79258d6612a4 100644 --- a/packages/aws-cdk-lib/aws-iam/lib/unknown-principal.ts +++ b/packages/aws-cdk-lib/aws-iam/lib/unknown-principal.ts @@ -41,7 +41,7 @@ export class UnknownPrincipal implements IPrincipal { public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult { const stack = Stack.of(this.resource); const repr = JSON.stringify(stack.resolve(statement)); - Annotations.of(this.resource).addWarning(`Add statement to this resource's role: ${repr}`); + Annotations.of(this.resource).addWarningV2('@aws-cdk/aws-iam:unknownPrincipalAddStatementToRole', `Add statement to this resource's role: ${repr}`); // Pretend we did the work. The human will do it for us, eventually. return { statementAdded: true, policyDependable: new DependencyGroup() }; } @@ -49,4 +49,4 @@ export class UnknownPrincipal implements IPrincipal { public addToPolicy(statement: PolicyStatement): boolean { return this.addToPrincipalPolicy(statement).statementAdded; } -} \ No newline at end of file +} diff --git a/packages/aws-cdk-lib/aws-iam/test/group.test.ts b/packages/aws-cdk-lib/aws-iam/test/group.test.ts index 466b4f902d906..37595b0ae6dbb 100644 --- a/packages/aws-cdk-lib/aws-iam/test/group.test.ts +++ b/packages/aws-cdk-lib/aws-iam/test/group.test.ts @@ -126,7 +126,7 @@ test('throw warning if attached managed policies exceed 10 in constructor', () = ], }); - Annotations.fromStack(stack).hasWarning('*', 'You added 11 to IAM Group MyGroup. The maximum number of managed policies attached to an IAM group is 10.'); + Annotations.fromStack(stack).hasWarning('*', 'You added 11 to IAM Group MyGroup. The maximum number of managed policies attached to an IAM group is 10. [ack: @aws-cdk/aws-iam:groupMaxPoliciesExceeded]'); }); test('throw warning if attached managed policies exceed 10 when calling `addManagedPolicy`', () => { @@ -142,5 +142,5 @@ test('throw warning if attached managed policies exceed 10 when calling `addMana group.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName(i.toString())); } - Annotations.fromStack(stack).hasWarning('/Default/MyGroup', 'You added 11 to IAM Group MyGroup. The maximum number of managed policies attached to an IAM group is 10.'); + Annotations.fromStack(stack).hasWarning('/Default/MyGroup', 'You added 12 to IAM Group MyGroup. The maximum number of managed policies attached to an IAM group is 10. [ack: @aws-cdk/aws-iam:groupMaxPoliciesExceeded]'); }); diff --git a/packages/aws-cdk-lib/aws-lambda-event-sources/lib/sqs.ts b/packages/aws-cdk-lib/aws-lambda-event-sources/lib/sqs.ts index ff23e77b6357f..6b175bc18cd25 100644 --- a/packages/aws-cdk-lib/aws-lambda-event-sources/lib/sqs.ts +++ b/packages/aws-cdk-lib/aws-lambda-event-sources/lib/sqs.ts @@ -103,7 +103,7 @@ export class SqsEventSource implements lambda.IEventSource { if (target.role) { this.queue.grantConsumeMessages(target); } else { - Annotations.of(target).addWarning(`Function '${target.node.path}' was imported without an IAM role `+ + Annotations.of(target).addWarningV2('@aws-cdk/aws-lambda-event-sources:sqsFunctionImportWithoutRole', `Function '${target.node.path}' was imported without an IAM role `+ `so it was not granted access to consume messages from '${this.queue.node.path}'`); } } diff --git a/packages/aws-cdk-lib/aws-lambda-nodejs/lib/bundling.ts b/packages/aws-cdk-lib/aws-lambda-nodejs/lib/bundling.ts index 3c87c4db08227..7245eac6c5640 100644 --- a/packages/aws-cdk-lib/aws-lambda-nodejs/lib/bundling.ts +++ b/packages/aws-cdk-lib/aws-lambda-nodejs/lib/bundling.ts @@ -137,15 +137,15 @@ export class Bundling implements cdk.BundlingOptions { // Warn users if they are trying to rely on global versions of the SDK that aren't available in // their environment. if (isV2Runtime && externals.some((pkgName) => pkgName.startsWith('@aws-sdk/'))) { - cdk.Annotations.of(scope).addWarning('If you are relying on AWS SDK v3 to be present in the Lambda environment already, please explicitly configure a NodeJS runtime of Node 18 or higher.'); + cdk.Annotations.of(scope).addWarningV2('@aws-cdk/aws-lambda-nodejs:sdkV3NotInRuntime', 'If you are relying on AWS SDK v3 to be present in the Lambda environment already, please explicitly configure a NodeJS runtime of Node 18 or higher.'); } else if (externals.includes('aws-sdk')) { - cdk.Annotations.of(scope).addWarning('If you are relying on AWS SDK v2 to be present in the Lambda environment already, please explicitly configure a NodeJS runtime of Node 16 or lower.'); + cdk.Annotations.of(scope).addWarningV2('@aws-cdk/aws-lambda-nodejs:sdkV2NotInRuntime', 'If you are relying on AWS SDK v2 to be present in the Lambda environment already, please explicitly configure a NodeJS runtime of Node 16 or lower.'); } // Warn users if they are using a runtime that may change and are excluding any dependencies from // bundling. if (externals.length && props.runtime?.isVariable) { - cdk.Annotations.of(scope).addWarning('When using NODEJS_LATEST the runtime version may change as new runtimes are released, this may affect the availability of packages shipped with the environment. Ensure that any external dependencies are available through layers or specify a specific runtime version.'); + cdk.Annotations.of(scope).addWarningV2('@aws-cdk/aws-lambda-nodejs:variableRuntimeExternals', 'When using NODEJS_LATEST the runtime version may change as new runtimes are released, this may affect the availability of packages shipped with the environment. Ensure that any external dependencies are available through layers or specify a specific runtime version.'); } this.externals = [ diff --git a/packages/aws-cdk-lib/aws-lambda-nodejs/test/bundling.test.ts b/packages/aws-cdk-lib/aws-lambda-nodejs/test/bundling.test.ts index d865c8be34a0a..e7aa97b4c6a8d 100644 --- a/packages/aws-cdk-lib/aws-lambda-nodejs/test/bundling.test.ts +++ b/packages/aws-cdk-lib/aws-lambda-nodejs/test/bundling.test.ts @@ -878,7 +878,7 @@ test('bundling with <= Node16 warns when sdk v3 is external', () => { }); Annotations.fromStack(stack).hasWarning('*', - 'If you are relying on AWS SDK v3 to be present in the Lambda environment already, please explicitly configure a NodeJS runtime of Node 18 or higher.', + 'If you are relying on AWS SDK v3 to be present in the Lambda environment already, please explicitly configure a NodeJS runtime of Node 18 or higher. [ack: @aws-cdk/aws-lambda-nodejs:sdkV3NotInRuntime]', ); }); @@ -893,7 +893,7 @@ test('bundling with >= Node18 warns when sdk v3 is external', () => { }); Annotations.fromStack(stack).hasWarning('*', - 'If you are relying on AWS SDK v2 to be present in the Lambda environment already, please explicitly configure a NodeJS runtime of Node 16 or lower.', + 'If you are relying on AWS SDK v2 to be present in the Lambda environment already, please explicitly configure a NodeJS runtime of Node 16 or lower. [ack: @aws-cdk/aws-lambda-nodejs:sdkV2NotInRuntime]', ); }); @@ -908,7 +908,7 @@ test('bundling with NODEJS_LATEST warns when any dependencies are external', () }); Annotations.fromStack(stack).hasWarning('*', - 'When using NODEJS_LATEST the runtime version may change as new runtimes are released, this may affect the availability of packages shipped with the environment. Ensure that any external dependencies are available through layers or specify a specific runtime version.', + 'When using NODEJS_LATEST the runtime version may change as new runtimes are released, this may affect the availability of packages shipped with the environment. Ensure that any external dependencies are available through layers or specify a specific runtime version. [ack: @aws-cdk/aws-lambda-nodejs:variableRuntimeExternals]', ); }); diff --git a/packages/aws-cdk-lib/aws-lambda/lib/function-base.ts b/packages/aws-cdk-lib/aws-lambda/lib/function-base.ts index a251b27ffcd54..3822c2747d8cc 100644 --- a/packages/aws-cdk-lib/aws-lambda/lib/function-base.ts +++ b/packages/aws-cdk-lib/aws-lambda/lib/function-base.ts @@ -325,7 +325,7 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC } protected warnInvokeFunctionPermissions(scope: Construct): void { - Annotations.of(scope).addWarning([ + Annotations.of(scope).addWarningV2('@aws-cdk/aws-lambda:addPermissionsToVersionOrAlias', [ "AWS Lambda has changed their authorization strategy, which may cause client invocations using the 'Qualifier' parameter of the lambda function to fail with Access Denied errors.", "If you are using a lambda Version or Alias, make sure to call 'grantInvoke' or 'addPermission' on the Version or Alias, not the underlying Function", 'See: https://github.com/aws/aws-cdk/issues/19273', diff --git a/packages/aws-cdk-lib/aws-lambda/test/alias.test.ts b/packages/aws-cdk-lib/aws-lambda/test/alias.test.ts index 739a756559ec3..50bab5959c6aa 100644 --- a/packages/aws-cdk-lib/aws-lambda/test/alias.test.ts +++ b/packages/aws-cdk-lib/aws-lambda/test/alias.test.ts @@ -602,7 +602,7 @@ describe('alias', () => { }); // THEN - Annotations.fromStack(stack).hasWarning('/Default/Alias/AliasScaling/Target', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead."); + Annotations.fromStack(stack).hasWarning('/Default/Alias/AliasScaling/Target', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead. [ack: @aws-cdk/aws-applicationautoscaling:defaultRunEveryMinute]"); }); test('scheduled scaling shows no warning when minute is * in cron', () => { diff --git a/packages/aws-cdk-lib/aws-rds/lib/cluster.ts b/packages/aws-cdk-lib/aws-rds/lib/cluster.ts index 467df41b2fa21..fe87818884720 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/cluster.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/cluster.ts @@ -717,23 +717,27 @@ abstract class DatabaseClusterNew extends DatabaseClusterBase { const hasOnlyServerlessReaders = hasServerlessReader && !hasProvisionedReader; if (hasOnlyServerlessReaders) { if (noFailoverTierInstances) { - Annotations.of(this).addWarning( + Annotations.of(this).addWarningV2( + '@aws-cdk/aws-rds:noFailoverServerlessReaders', `Cluster ${this.node.id} only has serverless readers and no reader is in promotion tier 0-1.`+ 'Serverless readers in promotion tiers >= 2 will NOT scale with the writer, which can lead to '+ 'availability issues if a failover event occurs. It is recommended that at least one reader '+ 'has `scaleWithWriter` set to true', ); + } } else { if (serverlessInHighestTier && highestTier > 1) { - Annotations.of(this).addWarning( + Annotations.of(this).addWarningV2( + '@aws-cdk/aws-rds:serverlessInHighestTier2-15', `There are serverlessV2 readers in tier ${highestTier}. Since there are no instances in a higher tier, `+ 'any instance in this tier is a failover target. Since this tier is > 1 the serverless reader will not scale '+ 'with the writer which could lead to availability issues during failover.', ); } if (someProvisionedReadersDontMatchWriter.length > 0 && writer.type === InstanceType.PROVISIONED) { - Annotations.of(this).addWarning( + Annotations.of(this).addWarningV2( + '@aws-cdk/aws-rds:provisionedReadersDontMatchWriter', `There are provisioned readers in the highest promotion tier ${highestTier} that do not have the same `+ 'InstanceSize as the writer. Any of these instances could be chosen as the new writer in the event '+ 'of a failover.\n'+ @@ -752,7 +756,7 @@ abstract class DatabaseClusterNew extends DatabaseClusterBase { if (writer.type === InstanceType.PROVISIONED) { if (reader.type === InstanceType.SERVERLESS_V2) { if (!instanceSizeSupportedByServerlessV2(writer.instanceSize!, this.serverlessV2MaxCapacity)) { - Annotations.of(this).addWarning( + Annotations.of(this).addWarningV2('@aws-cdk/aws-rds:serverlessInstanceCantScaleWithWriter', 'For high availability any serverless instances in promotion tiers 0-1 '+ 'should be able to scale to match the provisioned instance capacity.\n'+ `Serverless instance ${reader.node.id} is in promotion tier ${reader.tier},\n`+ @@ -1138,10 +1142,10 @@ export class DatabaseClusterFromSnapshot extends DatabaseClusterNew { super(scope, id, props); if (props.credentials && !props.credentials.password && !props.credentials.secret) { - Annotations.of(this).addWarning('Use `snapshotCredentials` to modify password of a cluster created from a snapshot.'); + Annotations.of(this).addWarningV2('@aws-cdk/aws-rds:useSnapshotCredentials', 'Use `snapshotCredentials` to modify password of a cluster created from a snapshot.'); } if (!props.credentials && !props.snapshotCredentials) { - Annotations.of(this).addWarning('Generated credentials will not be applied to cluster. Use `snapshotCredentials` instead. `addRotationSingleUser()` and `addRotationMultiUser()` cannot be used on this cluster.'); + Annotations.of(this).addWarningV2('@aws-cdk/aws-rds:generatedCredsNotApplied', 'Generated credentials will not be applied to cluster. Use `snapshotCredentials` instead. `addRotationSingleUser()` and `addRotationMultiUser()` cannot be used on this cluster.'); } const deprecatedCredentials = renderCredentials(this, props.engine, props.credentials); diff --git a/packages/aws-cdk-lib/aws-rds/test/cluster.test.ts b/packages/aws-cdk-lib/aws-rds/test/cluster.test.ts index 174301cba0e2a..22e40c0b2ec51 100644 --- a/packages/aws-cdk-lib/aws-rds/test/cluster.test.ts +++ b/packages/aws-cdk-lib/aws-rds/test/cluster.test.ts @@ -5,7 +5,7 @@ import * as kms from '../../aws-kms'; import * as logs from '../../aws-logs'; import * as s3 from '../../aws-s3'; import * as cdk from '../../core'; -import { RemovalPolicy, Stack } from '../../core'; +import { RemovalPolicy, Stack, Annotations as CoreAnnotations } from '../../core'; import { AuroraEngineVersion, AuroraMysqlEngineVersion, AuroraPostgresEngineVersion, CfnDBCluster, Credentials, DatabaseCluster, DatabaseClusterEngine, DatabaseClusterFromSnapshot, ParameterGroup, PerformanceInsightRetention, SubnetGroup, DatabaseSecret, @@ -507,6 +507,89 @@ describe('cluster new api', () => { }); Annotations.fromStack(stack).hasWarning('*', + `Cluster ${cluster.node.id} only has serverless readers and no reader is in promotion tier 0-1.`+ + 'Serverless readers in promotion tiers >= 2 will NOT scale with the writer, which can lead to '+ + 'availability issues if a failover event occurs. It is recommended that at least one reader '+ + 'has `scaleWithWriter` set to true [ack: @aws-cdk/aws-rds:noFailoverServerlessReaders]', + ); + }); + + test('serverless reader in promotion tier 2 does not throws', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const cluster = new DatabaseCluster(stack, 'Database', { + engine: DatabaseClusterEngine.AURORA, + vpc, + writer: ClusterInstance.provisioned('writer'), + readers: [ClusterInstance.serverlessV2('reader')], + iamAuthentication: true, + }); + + CoreAnnotations.of(stack).acknowledgeWarning('RDSNoFailoverServerlessReaders'); + // THEN + const template = Template.fromStack(stack); + template.resourceCountIs('AWS::RDS::DBInstance', 2); + template.hasResourceProperties('AWS::RDS::DBInstance', { + DBClusterIdentifier: { Ref: 'DatabaseB269D8BB' }, + DBInstanceClass: 'db.t3.medium', + PromotionTier: 0, + }); + + template.hasResourceProperties('AWS::RDS::DBInstance', { + DBClusterIdentifier: { Ref: 'DatabaseB269D8BB' }, + DBInstanceClass: 'db.serverless', + PromotionTier: 2, + }); + + Annotations.fromStack(stack).hasNoWarning('*', + `Cluster ${cluster.node.id} only has serverless readers and no reader is in promotion tier 0-1.`+ + 'Serverless readers in promotion tiers >= 2 will NOT scale with the writer, which can lead to '+ + 'availability issues if a failover event occurs. It is recommended that at least one reader '+ + 'has `scaleWithWriter` set to true', + ); + }); + + test('serverless reader in promotion tier 2 does not throws with root context', () => { + // GIVEN + const app = new cdk.App({ + context: { + ACKNOWLEDGEMENTS_CONTEXT_KEY: { + RDSNoFailoverServerlessReaders: ['Default/Database'], + + }, + }, + }); + const stack = testStack(app); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const cluster = new DatabaseCluster(stack, 'Database', { + engine: DatabaseClusterEngine.AURORA, + vpc, + writer: ClusterInstance.provisioned('writer'), + readers: [ClusterInstance.serverlessV2('reader')], + iamAuthentication: true, + }); + + // THEN + const template = Template.fromStack(stack); + template.resourceCountIs('AWS::RDS::DBInstance', 2); + template.hasResourceProperties('AWS::RDS::DBInstance', { + DBClusterIdentifier: { Ref: 'DatabaseB269D8BB' }, + DBInstanceClass: 'db.t3.medium', + PromotionTier: 0, + }); + + template.hasResourceProperties('AWS::RDS::DBInstance', { + DBClusterIdentifier: { Ref: 'DatabaseB269D8BB' }, + DBInstanceClass: 'db.serverless', + PromotionTier: 2, + }); + + Annotations.fromStack(stack).hasNoWarning('*', `Cluster ${cluster.node.id} only has serverless readers and no reader is in promotion tier 0-1.`+ 'Serverless readers in promotion tiers >= 2 will NOT scale with the writer, which can lead to '+ 'availability issues if a failover event occurs. It is recommended that at least one reader '+ @@ -591,7 +674,7 @@ describe('cluster new api', () => { 'For high availability any serverless instances in promotion tiers 0-1 '+ 'should be able to scale to match the provisioned instance capacity.\n'+ 'Serverless instance reader is in promotion tier 1,\n'+ - `But can not scale to match the provisioned writer instance (${instanceType.toString()})`, + `But can not scale to match the provisioned writer instance (${instanceType.toString()}) [ack: @aws-cdk/aws-rds:serverlessInstanceCantScaleWithWriter]`, ); }); }); @@ -674,7 +757,7 @@ describe('cluster new api', () => { 'InstanceSize as the writer. Any of these instances could be chosen as the new writer in the event '+ 'of a failover.\n'+ 'Writer InstanceSize: m5.24xlarge\n'+ - 'Reader InstanceSizes: t3.medium, m5.xlarge', + 'Reader InstanceSizes: t3.medium, m5.xlarge [ack: @aws-cdk/aws-rds:provisionedReadersDontMatchWriter]', ); }); @@ -808,7 +891,7 @@ describe('cluster new api', () => { Annotations.fromStack(stack).hasWarning('*', 'There are serverlessV2 readers in tier 2. Since there are no instances in a higher tier, '+ 'any instance in this tier is a failover target. Since this tier is > 1 the serverless reader will not scale '+ - 'with the writer which could lead to availability issues during failover.', + 'with the writer which could lead to availability issues during failover. [ack: @aws-cdk/aws-rds:serverlessInHighestTier2-15]', ); Annotations.fromStack(stack).hasWarning('*', @@ -816,7 +899,7 @@ describe('cluster new api', () => { 'InstanceSize as the writer. Any of these instances could be chosen as the new writer in the event '+ 'of a failover.\n'+ 'Writer InstanceSize: m5.24xlarge\n'+ - 'Reader InstanceSizes: m5.xlarge', + 'Reader InstanceSizes: m5.xlarge [ack: @aws-cdk/aws-rds:provisionedReadersDontMatchWriter]', ); }); }); diff --git a/packages/aws-cdk-lib/aws-s3-notifications/lib/sqs.ts b/packages/aws-cdk-lib/aws-s3-notifications/lib/sqs.ts index 4206d067e6ecc..41bb67ee9983a 100644 --- a/packages/aws-cdk-lib/aws-s3-notifications/lib/sqs.ts +++ b/packages/aws-cdk-lib/aws-s3-notifications/lib/sqs.ts @@ -32,7 +32,7 @@ export class SqsDestination implements s3.IBucketNotificationDestination { }); const addResult = this.queue.encryptionMasterKey.addToResourcePolicy(statement, /* allowNoOp */ true); if (!addResult.statementAdded) { - Annotations.of(this.queue.encryptionMasterKey).addWarning(`Can not change key policy of imported kms key. Ensure that your key policy contains the following permissions: \n${JSON.stringify(statement.toJSON(), null, 2)}`); + Annotations.of(this.queue.encryptionMasterKey).addWarningV2('@aws-cdk/aws-s3-notifications:sqsKMSPermissionsNotAdded', `Can not change key policy of imported kms key. Ensure that your key policy contains the following permissions: \n${JSON.stringify(statement.toJSON(), null, 2)}`); } } diff --git a/packages/aws-cdk-lib/aws-s3-notifications/test/queue.test.ts b/packages/aws-cdk-lib/aws-s3-notifications/test/queue.test.ts index 2433404fc7e2a..8b6e9dcb46260 100644 --- a/packages/aws-cdk-lib/aws-s3-notifications/test/queue.test.ts +++ b/packages/aws-cdk-lib/aws-s3-notifications/test/queue.test.ts @@ -119,5 +119,5 @@ test('if the queue is encrypted with a imported kms key, printout warning', () = Service: 's3.amazonaws.com', }, Resource: '*', - }, null, 2)}`); + }, null, 2)} [ack: @aws-cdk/aws-s3-notifications:sqsKMSPermissionsNotAdded]`); }); diff --git a/packages/aws-cdk-lib/aws-s3/lib/bucket.ts b/packages/aws-cdk-lib/aws-s3/lib/bucket.ts index 6812e73bb0af5..349e0e6916b51 100644 --- a/packages/aws-cdk-lib/aws-s3/lib/bucket.ts +++ b/packages/aws-cdk-lib/aws-s3/lib/bucket.ts @@ -1893,7 +1893,7 @@ export class Bucket extends BucketBase { } else if (props.serverAccessLogsBucket) { // A `serverAccessLogsBucket` was provided but it is not a concrete `Bucket` and it // may not be possible to configure the ACLs or bucket policy as required. - Annotations.of(this).addWarning( + Annotations.of(this).addWarningV2('@aws-cdk/aws-s3:accessLogsPolicyNotAdded', `Unable to add necessary logging permissions to imported target bucket: ${props.serverAccessLogsBucket}`, ); } diff --git a/packages/aws-cdk-lib/aws-servicecatalog/lib/private/product-stack-synthesizer.ts b/packages/aws-cdk-lib/aws-servicecatalog/lib/private/product-stack-synthesizer.ts index 214097fef3344..72056d174de21 100644 --- a/packages/aws-cdk-lib/aws-servicecatalog/lib/private/product-stack-synthesizer.ts +++ b/packages/aws-cdk-lib/aws-servicecatalog/lib/private/product-stack-synthesizer.ts @@ -75,7 +75,7 @@ export class ProductStackSynthesizer extends cdk.StackSynthesizer { if (!this.bucketDeployment) { if (!cdk.Resource.isOwnedResource(this.assetBucket)) { - cdk.Annotations.of(this.parentStack).addWarning('[WARNING] Bucket Policy Permissions cannot be added to' + + cdk.Annotations.of(this.parentStack).addWarningV2('@aws-cdk/aws-servicecatalog:assetsManuallyAddBucketPermissions', '[WARNING] Bucket Policy Permissions cannot be added to' + ' referenced Bucket. Please make sure your bucket has the correct permissions'); } this.bucketDeployment = new BucketDeployment(this.parentStack, 'AssetsBucketDeployment', { diff --git a/packages/aws-cdk-lib/aws-ses-actions/lib/lambda.ts b/packages/aws-cdk-lib/aws-ses-actions/lib/lambda.ts index 6917184924a74..ca9e961ea528e 100644 --- a/packages/aws-cdk-lib/aws-ses-actions/lib/lambda.ts +++ b/packages/aws-cdk-lib/aws-ses-actions/lib/lambda.ts @@ -71,7 +71,7 @@ export class Lambda implements ses.IReceiptRuleAction { rule.node.addDependency(permission); } else { // eslint-disable-next-line max-len - cdk.Annotations.of(rule).addWarning('This rule is using a Lambda action with an imported function. Ensure permission is given to SES to invoke that function.'); + cdk.Annotations.of(rule).addWarningV2('@aws-cdk/aws-ses-actions:lambdaAddInvokePermissions', 'This rule is using a Lambda action with an imported function. Ensure permission is given to SES to invoke that function.'); } return { diff --git a/packages/aws-cdk-lib/aws-ses-actions/lib/s3.ts b/packages/aws-cdk-lib/aws-ses-actions/lib/s3.ts index 4505090c30826..5fa01ce1c91be 100644 --- a/packages/aws-cdk-lib/aws-ses-actions/lib/s3.ts +++ b/packages/aws-cdk-lib/aws-ses-actions/lib/s3.ts @@ -65,7 +65,7 @@ export class S3 implements ses.IReceiptRuleAction { if (policy) { // The bucket could be imported rule.node.addDependency(policy); } else { - cdk.Annotations.of(rule).addWarning('This rule is using a S3 action with an imported bucket. Ensure permission is given to SES to write to that bucket.'); + cdk.Annotations.of(rule).addWarningV2('@aws-cdk/s3:AddBucketPermissions', 'This rule is using a S3 action with an imported bucket. Ensure permission is given to SES to write to that bucket.'); } // Allow SES to use KMS master key diff --git a/packages/aws-cdk-lib/core/README.md b/packages/aws-cdk-lib/core/README.md index 707d70d56e13d..691a11aa39278 100644 --- a/packages/aws-cdk-lib/core/README.md +++ b/packages/aws-cdk-lib/core/README.md @@ -1453,4 +1453,40 @@ add it to the `postinstall` [script](https://docs.npmjs.com/cli/v9/using-npm/scripts) in the `package.json` file. +## Annotations + +Construct authors can add annotations to constructs to report at three different +levels: `ERROR`, `WARN`, `INFO`. + +Typically warnings are added for things that are important for the user to be +aware of, but will not cause deployment errors in all cases. Some common +scenarios are (non-exhaustive list): + +- Warn when the user needs to take a manual action, e.g. IAM policy should be + added to an referenced resource. +- Warn if the user configuration might not follow best practices (but is still + valid) +- Warn if the user is using a deprecated API + +### Acknowledging Warnings + +If you would like to run with `--strict` mode enabled (warnings will throw +errors) it is possible to `acknowledge` warnings to make the warning go away. + +For example, if > 10 IAM managed policies are added to an IAM Group, a warning +will be created: + +``` +IAM:Group:MaxPoliciesExceeded: You added 11 to IAM Group my-group. The maximum number of managed policies attached to an IAM group is 10. +``` + +If you have requested a [quota increase](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html#reference_iam-quotas-entities) +you may have the ability to add > 10 managed policies which means that this +warning does not apply to you. You can acknowledge this by `acknowledging` the +warning by the `id`. + +```ts +Annotations.of(this).acknowledgeWarning('IAM:Group:MaxPoliciesExceeded', 'Account has quota increased to 20'); +``` + diff --git a/packages/aws-cdk-lib/core/lib/adr/acknowledge-warnings.md b/packages/aws-cdk-lib/core/lib/adr/acknowledge-warnings.md new file mode 100644 index 0000000000000..a672c7fade958 --- /dev/null +++ b/packages/aws-cdk-lib/core/lib/adr/acknowledge-warnings.md @@ -0,0 +1,148 @@ +# Acknowledge Warnings + +## Status + +accepted + +## Context + +Construct authors can add messages that will be printed at synthesis. It +possible to add three types of warnings: + +- `INFO`: Prints the info on synthesis +- `WARN`: Prints the warning on synthesis. Can also throw an error if the `--strict` + argument is used +- `ERROR`: Throws an error with the message + +Warning messages are typically used for two types of messages: + +1. Things that are errors (and will error on deployment) in some cases, but not all. For example, a warning is + used when > 10 managed policies are added to an IAM Role because that is the + initial limit for AWS accounts. An error is not used because it is possible + to increase the limit. +2. Things that are not errors (and will deploy successfully), but might be + sub-optimal configuration. For example, a warning is used when an Serverless + V2 Aurora cluster is created and no reader instances are created in tiers + 0-1. This is a valid configuration, but could cause availability issues in + certain scenarios. + +Users who are developing CDK applications may want to always use the `--strict` +CLI argument to turn all warnings into errors. Currently this is not +possible since as soon as one warning is not applicable they can no longer use +`--strict`. Ideally they should be able to `acknowledge` warnings (similar to +what they can do with notices) indicating that the warning is not applicable to +them/they acknowledge the risk. + +```ts +Annotations.of(this).acknowledgeWarning( + '@aws-cdk/aws-iam:maxPoliciesExceeded', + 'A limit increase has been submitted', +); +``` + +This would allow acknowledgements to live alongside the code so all developers +on the code base would have the information readily available. This also allows +the acknowledgement to be added at a certain `scope` and apply to all child +constructs. Users would be able to acknowledge certain warnings for the entire +app, or for a specific scope. + +## Constraints + +Warnings are currently added with the `Annotations` API. + +```ts +Annotations.of(scope).addWarning('This is a warning'); +``` + +`Annotations.of(scope)` creates a new instance of `Annotations` every time so +there is no way to keep track of warnings added inside this object. + +Currently the storage mechanism for `Annotations` is the `Node` metadata. The +warning is added to the node as node metadata which is read by the CLI after +synthesis to print out the messages. This means that Annotations can be added +during `synthesis`. + +Another constraint is that currently you add a warning with only the message. +There is no unique identifier. This means that to add an acknowledgement the +user would need to copy the entire message. + +## Decision + +We will deprecate the `addWarning` method and add a new method `addWarningV2` + +```ts + /** + * @param id the unique identifier for the warning. This can be used to acknowledge the warning + * @param message The warning message. + */ + public addWarningV2(id: string, message: string): void +``` + +We will add a new method `acknowledgeWarning` that will allow users to +acknowledge a specific warning by `id`. + +```ts + /** + * @param id - the id of the warning message to acknowledge + * @param message optional message to explain the reason for acknowledgement + */ + public acknowledgeWarning(id: string, message?: string): void +``` + +We want warning acknowledgement to work both before and after the warning is actually emitted. + +We therefore do the following: + +- When a warning is acknowledged on a construct, we iterate over the already + added warnings and remove matching warnings. At the same time, we record + that this warning has been acknowledged on this construct tree. +- When a warning is added, we only add it if it hasn't been acknowledged in + that location. + +## Alternatives + +### Filter at synthesis time + +Right now, we are filtering in every method call. We could defer the filtering +to when we translate construct tree metadata to cloud assembly metadata. + +Right now, removing existing metadata entries requires accessing private APIs +of the `constructs` library, which is not an ideal situation. + +At the same time, implementing it there would allow us to generate a suppression +report. We can always still do this. + +### Use context and remove metadata + +Another alternative that was considered was to use context to set +acknowledgements. This would look something like this: + +```ts +public acknowledgeWarning(id: string, message?: string) { + this.scope.node.setContext(id, message ?? true); // can't do this today + this.scope.node.removeMetadata(id); // this method does not exist +} +public addWarningV2(id: string, message: string) { + if (this.scope.node.tryGetContext(id) === undefined) { + this.addMessage(...); + } +} +``` + +There are two issues with this alternative. + +1. It is currently not possible to `node.setContext` once children are added. We + would have to first remove that limitation. I think this could lead to a lot + of issues where users have to keep track of _when_ context is added. +2. We would have to implement the ability to `removeMetadata` from a node. This + functionality doesn't make much sense outside of this context (can't find + where anybody has asked for it). It also may require updating the metadata + types since currently there is no unique identifier for a given metadata + entry (other than the message). + + +### Acknowledge via context + +There is currently no way to configure suppressed warnings via context, which +might be useful to do at the application level (using `cdk.json`). We can always +add this feature in the future if desired. diff --git a/packages/aws-cdk-lib/core/lib/annotations.ts b/packages/aws-cdk-lib/core/lib/annotations.ts index 45ce2f1b8fd06..405c3891f0953 100644 --- a/packages/aws-cdk-lib/core/lib/annotations.ts +++ b/packages/aws-cdk-lib/core/lib/annotations.ts @@ -1,4 +1,5 @@ -import { IConstruct } from 'constructs'; +import { IConstruct, MetadataEntry } from 'constructs'; +import { App } from './app'; import * as cxschema from '../../cloud-assembly-schema'; import * as cxapi from '../../cx-api'; @@ -24,6 +25,53 @@ export class Annotations { this.stackTraces = !disableTrace; } + /** + * Acknowledge a warning. When a warning is acknowledged for a scope + * all warnings that match the id will be ignored. + * + * The acknowledgement will apply to all child scopes + * + * @example + * declare const myConstruct: Construct; + * Annotations.of(myConstruct).acknowledgeWarning('SomeWarningId', 'This warning can be ignored because...'); + * + * @param id - the id of the warning message to acknowledge + * @param message optional message to explain the reason for acknowledgement + */ + public acknowledgeWarning(id: string, message?: string): void { + Acknowledgements.of(this.scope).add(this.scope, id); + + // We don't use message currently, but encouraging people to supply it is good for documentation + // purposes, and we can always add a report on it in the future. + void(message); + + // Iterate over the construct and remove any existing instances of this warning + // (addWarningV2 will prevent future instances of it) + removeWarningDeep(this.scope, id); + } + + /** + * Adds an acknowledgeable warning metadata entry to this construct. + * + * The CLI will display the warning when an app is synthesized, or fail if run + * in `--strict` mode. + * + * If the warning is acknowledged using `acknowledgeWarning()`, it will not be shown by + * the CLI, and will not cause `--strict` mode to fail synthesis. + * + * @example + * declare const myConstruct: Construct; + * Annotations.of(myConstruct).addWarningV2('my-library:Construct.someWarning', 'Some message explaining the warning'); + * + * @param id the unique identifier for the warning. This can be used to acknowledge the warning + * @param message The warning message. + */ + public addWarningV2(id: string, message: string) { + if (!Acknowledgements.of(this.scope).has(this.scope, id)) { + this.addMessage(cxschema.ArtifactMetadataEntryType.WARN, `${message} ${ackTag(id)}`); + } + } + /** * Adds a warning metadata entry to this construct. * @@ -31,6 +79,7 @@ export class Annotations { * in --strict mode. * * @param message The warning message. + * @deprecated - use addWarningV2 instead */ public addWarning(message: string) { this.addMessage(cxschema.ArtifactMetadataEntryType.WARN, message); @@ -78,7 +127,7 @@ export class Annotations { throw new Error(`${this.scope.node.path}: ${text}`); } - this.addWarning(text); + this.addWarningV2(`Deprecated:${api}`, text); } /** @@ -95,3 +144,116 @@ export class Annotations { } } } + +/** + * Class to keep track of acknowledgements + * + * There is a singleton instance for every `App` instance, which can be obtained by + * calling `Acknowledgements.of(...)`. + */ +class Acknowledgements { + public static of(scope: IConstruct): Acknowledgements { + const app = App.of(scope); + if (!app) { + return new Acknowledgements(); + } + + const existing = (app as any)[Acknowledgements.ACKNOWLEDGEMENTS_SYM]; + if (existing) { + return existing as Acknowledgements; + } + + const fresh = new Acknowledgements(); + (app as any)[Acknowledgements.ACKNOWLEDGEMENTS_SYM] = fresh; + return fresh; + } + + private static ACKNOWLEDGEMENTS_SYM = Symbol.for('@aws-cdk/core.Acknowledgements'); + + private readonly acks = new Map>(); + + private constructor() {} + + public add(node: string | IConstruct, ack: string) { + const nodePath = this.nodePath(node); + + let arr = this.acks.get(nodePath); + if (!arr) { + arr = new Set(); + this.acks.set(nodePath, arr); + } + arr.add(ack); + } + + public has(node: string | IConstruct, ack: string): boolean { + for (const candidate of this.searchPaths(this.nodePath(node))) { + if (this.acks.get(candidate)?.has(ack)) { + return true; + } + } + return false; + } + + private nodePath(node: string | IConstruct) { + // Normalize, remove leading / if it exists + return (typeof node === 'string' ? node : node.node.path).replace(/^\//, ''); + } + + /** + * Given 'a/b/c', return ['a/b/c', 'a/b', 'a'] + */ + private searchPaths(path: string) { + const ret = new Array(); + let start = 0; + while (start < path.length) { + let i = path.indexOf('/', start); + if (i !== -1) { + ret.push(path.substring(0, i)); + start = i + 1; + } else { + start = path.length; + } + } + return ret.reverse(); + } +} + +/** + * Remove warning metadata from all constructs in a given scope + * + * No recursion to avoid blowing out the stack. + */ +function removeWarningDeep(construct: IConstruct, id: string) { + const stack = [construct]; + + while (stack.length > 0) { + const next = stack.pop()!; + removeWarning(next, id); + stack.push(...next.node.children); + } +} + +/** + * Remove metadata from a construct node. + * + * This uses private APIs for now; we could consider adding this functionality + * to the constructs library itself. + */ +function removeWarning(construct: IConstruct, id: string) { + const meta: MetadataEntry[] | undefined = (construct.node as any)._metadata; + if (!meta) { return; } + + let i = 0; + while (i < meta.length) { + const m = meta[i]; + if (m.type === cxschema.ArtifactMetadataEntryType.WARN && (m.data as string).includes(ackTag(id))) { + meta.splice(i, 1); + } else { + i += 1; + } + } +} + +function ackTag(id: string) { + return `[ack: ${id}]`; +} \ No newline at end of file diff --git a/packages/aws-cdk-lib/core/lib/cfn-resource.ts b/packages/aws-cdk-lib/core/lib/cfn-resource.ts index 43afeb56cd804..ad8435951c416 100644 --- a/packages/aws-cdk-lib/core/lib/cfn-resource.ts +++ b/packages/aws-cdk-lib/core/lib/cfn-resource.ts @@ -156,7 +156,7 @@ export class CfnResource extends CfnRefElement { if (FeatureFlags.of(this).isEnabled(cxapi.VALIDATE_SNAPSHOT_REMOVAL_POLICY) ) { throw new Error(`${this.cfnResourceType} does not support snapshot removal policy`); } else { - Annotations.of(this).addWarning(`${this.cfnResourceType} does not support snapshot removal policy. This policy will be ignored.`); + Annotations.of(this).addWarningV2(`@aws-cdk/core:${this.cfnResourceType}SnapshotRemovalPolicyIgnored`, `${this.cfnResourceType} does not support snapshot removal policy. This policy will be ignored.`); } } diff --git a/packages/aws-cdk-lib/core/lib/private/synthesis.ts b/packages/aws-cdk-lib/core/lib/private/synthesis.ts index 7ad67d959ec27..3526991880735 100644 --- a/packages/aws-cdk-lib/core/lib/private/synthesis.ts +++ b/packages/aws-cdk-lib/core/lib/private/synthesis.ts @@ -238,7 +238,7 @@ function invokeAspects(root: IConstruct) { // if an aspect was added to the node while invoking another aspect it will not be invoked, emit a warning // the `nestedAspectWarning` flag is used to prevent the warning from being emitted for every child if (!nestedAspectWarning && nodeAspectsCount !== aspects.all.length) { - Annotations.of(construct).addWarning('We detected an Aspect was added via another Aspect, and will not be applied'); + Annotations.of(construct).addWarningV2('@aws-cdk/core:ignoredAspect', 'We detected an Aspect was added via another Aspect, and will not be applied'); nestedAspectWarning = true; } diff --git a/packages/aws-cdk-lib/core/lib/private/tree-metadata.ts b/packages/aws-cdk-lib/core/lib/private/tree-metadata.ts index 24e6bb3f526b1..f844136a636d5 100644 --- a/packages/aws-cdk-lib/core/lib/private/tree-metadata.ts +++ b/packages/aws-cdk-lib/core/lib/private/tree-metadata.ts @@ -37,7 +37,7 @@ export class TreeMetadata extends Construct { try { return visit(c); } catch (e) { - Annotations.of(this).addWarning(`Failed to render tree metadata for node [${c.node.id}]. Reason: ${e}`); + Annotations.of(this).addWarningV2(`@aws-cdk/core:failedToRenderTreeMetadata-${c.node.id}`, `Failed to render tree metadata for node [${c.node.id}]. Reason: ${e}`); return undefined; } }); diff --git a/packages/aws-cdk-lib/core/lib/stack.ts b/packages/aws-cdk-lib/core/lib/stack.ts index 61edc8f8625c2..61c85fc110677 100644 --- a/packages/aws-cdk-lib/core/lib/stack.ts +++ b/packages/aws-cdk-lib/core/lib/stack.ts @@ -1087,7 +1087,7 @@ export class Stack extends Construct implements ITaggable { const message = `Template size ${verb} limit: ${templateData.length}/${TEMPLATE_BODY_MAXIMUM_SIZE}. ${advice}.`; - Annotations.of(this).addWarning(message); + Annotations.of(this).addWarningV2('@aws-cdk/core:Stack.templateSize', message); } fs.writeFileSync(outPath, templateData); @@ -1342,7 +1342,7 @@ export class Stack extends Construct implements ITaggable { if (this.templateOptions.transform) { // eslint-disable-next-line max-len - Annotations.of(this).addWarning('This stack is using the deprecated `templateOptions.transform` property. Consider switching to `addTransform()`.'); + Annotations.of(this).addWarningV2('@aws-cdk/core:stackDeprecatedTransform', 'This stack is using the deprecated `templateOptions.transform` property. Consider switching to `addTransform()`.'); this.addTransform(this.templateOptions.transform); } diff --git a/packages/aws-cdk-lib/core/test/annotations.test.ts b/packages/aws-cdk-lib/core/test/annotations.test.ts index fc5c7430d22a8..c6cc2fb9420b8 100644 --- a/packages/aws-cdk-lib/core/test/annotations.test.ts +++ b/packages/aws-cdk-lib/core/test/annotations.test.ts @@ -24,7 +24,7 @@ describe('annotations', () => { expect(getWarnings(app.synth())).toEqual([ { path: '/MyStack/Hello', - message: 'The API @aws-cdk/core.Construct.node is deprecated: use @aws-Construct.construct instead. This API will be removed in the next major release', + message: 'The API @aws-cdk/core.Construct.node is deprecated: use @aws-Construct.construct instead. This API will be removed in the next major release [ack: Deprecated:@aws-cdk/core.Construct.node]', }, ]); }); @@ -51,15 +51,15 @@ describe('annotations', () => { expect(getWarnings(app.synth())).toEqual([ { path: '/MyStack1/Hello', - message: 'The API @aws-cdk/core.Construct.node is deprecated: use @aws-Construct.construct instead. This API will be removed in the next major release', + message: 'The API @aws-cdk/core.Construct.node is deprecated: use @aws-Construct.construct instead. This API will be removed in the next major release [ack: Deprecated:@aws-cdk/core.Construct.node]', }, { path: '/MyStack1/World', - message: 'The API @aws-cdk/core.Construct.node is deprecated: use @aws-Construct.construct instead. This API will be removed in the next major release', + message: 'The API @aws-cdk/core.Construct.node is deprecated: use @aws-Construct.construct instead. This API will be removed in the next major release [ack: Deprecated:@aws-cdk/core.Construct.node]', }, { path: '/MyStack2/FooBar', - message: 'The API @aws-cdk/core.Construct.node is deprecated: use @aws-Construct.construct instead. This API will be removed in the next major release', + message: 'The API @aws-cdk/core.Construct.node is deprecated: use @aws-Construct.construct instead. This API will be removed in the next major release [ack: Deprecated:@aws-cdk/core.Construct.node]', }, ]); }); @@ -79,18 +79,54 @@ describe('annotations', () => { const app = new App(); const stack = new Stack(app, 'S1'); const c1 = new Construct(stack, 'C1'); - Annotations.of(c1).addWarning('You should know this!'); - Annotations.of(c1).addWarning('You should know this!'); - Annotations.of(c1).addWarning('You should know this!'); - Annotations.of(c1).addWarning('You should know this, too!'); - expect(getWarnings(app.synth())).toEqual([{ - path: '/S1/C1', - message: 'You should know this!', - }, - { - path: '/S1/C1', - message: 'You should know this, too!', - }], - ); + Annotations.of(c1).addWarningV2('warning1', 'You should know this!'); + Annotations.of(c1).addWarningV2('warning1', 'You should know this!'); + Annotations.of(c1).addWarningV2('warning1', 'You should know this!'); + Annotations.of(c1).addWarningV2('warning2', 'You should know this, too!'); + expect(getWarnings(app.synth())).toEqual([ + { + path: '/S1/C1', + message: 'You should know this! [ack: warning1]', + }, + { + path: '/S1/C1', + message: 'You should know this, too! [ack: warning2]', + }, + ]); + }); + + test('acknowledgeWarning removes warning', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'S1'); + const c1 = new Construct(stack, 'C1'); + + // WHEN + Annotations.of(c1).addWarningV2('MESSAGE1', 'You should know this!'); + Annotations.of(c1).addWarningV2('MESSAGE2', 'You Should know this too!'); + Annotations.of(c1).acknowledgeWarning('MESSAGE2', 'I Ack this'); + + // THEN + expect(getWarnings(app.synth())).toEqual([ + { + path: '/S1/C1', + message: 'You should know this! [ack: MESSAGE1]', + }, + ]); + }); + + test('acknowledgeWarning removes warning on children', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'S1'); + const c1 = new Construct(stack, 'C1'); + const c2 = new Construct(c1, 'C2'); + + // WHEN + Annotations.of(c2).addWarningV2('MESSAGE2', 'You Should know this too!'); + Annotations.of(c1).acknowledgeWarning('MESSAGE2', 'I Ack this'); + + // THEN + expect(getWarnings(app.synth())).toEqual([]); }); -}); +}); \ No newline at end of file diff --git a/packages/aws-cdk-lib/core/test/app.test.ts b/packages/aws-cdk-lib/core/test/app.test.ts index b647c08c67d99..49654658ef8d4 100644 --- a/packages/aws-cdk-lib/core/test/app.test.ts +++ b/packages/aws-cdk-lib/core/test/app.test.ts @@ -35,8 +35,8 @@ function synth(context?: { [key: string]: any }): cxapi.CloudAssembly { // add some metadata stack1.node.addMetadata('meta', 111); - Annotations.of(r2).addWarning('warning1'); - Annotations.of(r2).addWarning('warning2'); + Annotations.of(r2).addWarningV2('warning1', 'warning1'); + Annotations.of(r2).addWarningV2('warning2', 'warning2'); c1.node.addMetadata('meta', { key: 'value' }); app.node.addMetadata('applevel', 123); // apps can also have metadata }); @@ -78,8 +78,8 @@ describe('app', () => { '/stack1/s1c1': [{ type: 'aws:cdk:logicalId', data: 's1c1' }], '/stack1/s1c2': [{ type: 'aws:cdk:logicalId', data: 's1c2' }, - { type: 'aws:cdk:warning', data: 'warning1' }, - { type: 'aws:cdk:warning', data: 'warning2' }], + { type: 'aws:cdk:warning', data: 'warning1 [ack: warning1]' }, + { type: 'aws:cdk:warning', data: 'warning2 [ack: warning2]' }], }); const stack2 = response.stacks[1]; diff --git a/packages/aws-cdk-lib/core/test/aspect.test.ts b/packages/aws-cdk-lib/core/test/aspect.test.ts index bc716e5843ac1..c050f4be0ccaa 100644 --- a/packages/aws-cdk-lib/core/test/aspect.test.ts +++ b/packages/aws-cdk-lib/core/test/aspect.test.ts @@ -50,7 +50,7 @@ describe('aspect', () => { }); app.synth(); expect(root.node.metadata[0].type).toEqual(cxschema.ArtifactMetadataEntryType.WARN); - expect(root.node.metadata[0].data).toEqual('We detected an Aspect was added via another Aspect, and will not be applied'); + expect(root.node.metadata[0].data).toEqual('We detected an Aspect was added via another Aspect, and will not be applied [ack: @aws-cdk/core:ignoredAspect]'); // warning is not added to child construct expect(child.node.metadata.length).toEqual(0); }); diff --git a/packages/aws-cdk-lib/core/test/cfn-resource.test.ts b/packages/aws-cdk-lib/core/test/cfn-resource.test.ts index 7d6d08787c6fe..d8c8fab44d5b5 100644 --- a/packages/aws-cdk-lib/core/test/cfn-resource.test.ts +++ b/packages/aws-cdk-lib/core/test/cfn-resource.test.ts @@ -119,7 +119,7 @@ describe('cfn resource', () => { expect(getWarnings(app.synth())).toEqual([ { path: '/Default/Resource', - message: 'AWS::Lambda::Function does not support snapshot removal policy. This policy will be ignored.', + message: 'AWS::Lambda::Function does not support snapshot removal policy. This policy will be ignored. [ack: @aws-cdk/core:AWS::Lambda::FunctionSnapshotRemovalPolicyIgnored]', }, ]); }); diff --git a/packages/aws-cdk-lib/core/test/construct.test.ts b/packages/aws-cdk-lib/core/test/construct.test.ts index 2dd6c299fe2ec..8db1f906a4280 100644 --- a/packages/aws-cdk-lib/core/test/construct.test.ts +++ b/packages/aws-cdk-lib/core/test/construct.test.ts @@ -2,7 +2,7 @@ import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import { Construct, ConstructOrder, IConstruct } from 'constructs'; import { reEnableStackTraceCollection, restoreStackTraceColection } from './util'; import * as cxschema from '../../cloud-assembly-schema'; -import { Names } from '../lib'; +import { App, Names } from '../lib'; import { Annotations } from '../lib/annotations'; /* eslint-disable @typescript-eslint/naming-convention */ @@ -247,13 +247,14 @@ describe('construct', () => { test('addWarning(message) can be used to add a "WARNING" message entry to the construct', () => { const previousValue = reEnableStackTraceCollection(); - const root = new Root(); + const root = new App(); const con = new Construct(root, 'MyConstruct'); - Annotations.of(con).addWarning('This construct is deprecated, use the other one instead'); + Annotations.of(con).addWarningV2('WARNING1', 'This construct is deprecated, use the other one instead'); restoreStackTraceColection(previousValue); + root.synth(); expect(con.node.metadata[0].type).toEqual(cxschema.ArtifactMetadataEntryType.WARN); - expect(con.node.metadata[0].data).toEqual('This construct is deprecated, use the other one instead'); + expect(con.node.metadata[0].data).toEqual('This construct is deprecated, use the other one instead [ack: WARNING1]'); expect(con.node.metadata[0].trace && con.node.metadata[0].trace.length > 0).toEqual(true); }); diff --git a/packages/aws-cdk-lib/core/test/fn.test.ts b/packages/aws-cdk-lib/core/test/fn.test.ts index 8356e14cfcd0a..a69b71f27a4fc 100644 --- a/packages/aws-cdk-lib/core/test/fn.test.ts +++ b/packages/aws-cdk-lib/core/test/fn.test.ts @@ -18,7 +18,7 @@ function asyncTest(cb: () => Promise): () => void { }; } -const nonEmptyString = fc.string({ minLength: 1, maxLength: 16 }); +const nonEmptyString = fc.string({ minLength: 1, maxLength: 16 }).filter((x) => x !== '__proto__'); const tokenish = fc.array(nonEmptyString, { minLength: 2, maxLength: 2 }).map(arr => ({ [arr[0]]: arr[1] })); const anyValue = fc.oneof(nonEmptyString, tokenish); @@ -122,7 +122,7 @@ describe('fn', () => { _.isEqual(stack.resolve(Fn.join(delimiter, [...prefix, stringToken(obj), ...suffix])), { 'Fn::Join': [delimiter, [prefix.join(delimiter), obj, suffix.join(delimiter)]] }), ), - { verbose: true, seed: 1539874645005, path: '0:0:0:0:0:0:0:0:0' }, + { verbose: true }, ); })); diff --git a/packages/aws-cdk-lib/core/test/util.ts b/packages/aws-cdk-lib/core/test/util.ts index 6c5c2eba6dbad..594acc55a5134 100644 --- a/packages/aws-cdk-lib/core/test/util.ts +++ b/packages/aws-cdk-lib/core/test/util.ts @@ -1,4 +1,4 @@ -import { CloudAssembly } from '../../cx-api'; +import { CloudArtifact, CloudAssembly, SynthesisMessageLevel } from '../../cx-api'; import { Stack } from '../lib'; import { CDK_DEBUG } from '../lib/debug'; import { synthesize } from '../lib/private/synthesis'; @@ -35,13 +35,12 @@ export function restoreStackTraceColection(previousValue: string | undefined): v export function getWarnings(casm: CloudAssembly) { const result = new Array<{ path: string, message: string }>(); for (const stack of Object.values(casm.manifest.artifacts ?? {})) { - for (const [path, md] of Object.entries(stack.metadata ?? {})) { - for (const x of md) { - if (x.type === 'aws:cdk:warning') { - result.push({ path, message: x.data as string }); - } + const artifact = CloudArtifact.fromManifest(casm, 'art', stack); + artifact?.messages.forEach(message => { + if (message.level === SynthesisMessageLevel.WARNING) { + result.push({ path: message.id, message: message.entry.data as string }); } - } + }); } return result; } diff --git a/packages/aws-cdk-lib/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts b/packages/aws-cdk-lib/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts index e85f12b59d81e..c084cfb94d57c 100644 --- a/packages/aws-cdk-lib/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts +++ b/packages/aws-cdk-lib/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts @@ -453,7 +453,7 @@ export class AwsCustomResource extends Construct implements iam.IGrantable { if (installLatestAwsSdk && props.installLatestAwsSdk === undefined) { // This is dangerous. Add a warning. - Annotations.of(this).addWarning([ + Annotations.of(this).addWarningV2('@aws-cdk/custom-resources:installLatestAwsSdkNotSpecified', [ 'installLatestAwsSdk was not specified, and defaults to true. You probably do not want this.', `Set the global context flag \'${cxapi.AWS_CUSTOM_RESOURCE_LATEST_SDK_DEFAULT}\' to false to switch this behavior off project-wide,`, 'or set the property explicitly to true if you know you need to call APIs that are not in Lambda\'s built-in SDK version.', diff --git a/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md b/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md index 32f485172e01f..d97798ee21c88 100644 --- a/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md +++ b/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md @@ -56,8 +56,8 @@ Flags come in three types: | [@aws-cdk/core:includePrefixInUniqueNameGeneration](#aws-cdkcoreincludeprefixinuniquenamegeneration) | Include the stack prefix in the stack name generation process | 2.84.0 | (fix) | | [@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig](#aws-cdkaws-autoscalinggeneratelaunchtemplateinsteadoflaunchconfig) | Generate a launch template when creating an AutoScalingGroup | 2.88.0 | (fix) | | [@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby](#aws-cdkaws-opensearchserviceenableopensearchmultiazwithstandby) | Enables support for Multi-AZ with Standby deployment for opensearch domains | 2.88.0 | (default) | -| [@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion](#aws-cdkaws-lambda-nodejsuselatestruntimeversion) | Enables aws-lambda-nodejs.Function to use the latest available NodeJs runtime as the default | V2NEXT | (default) | | [@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId](#aws-cdkaws-efsmounttargetorderinsensitivelogicalid) | When enabled, mount targets will have a stable logicalId that is linked to the associated subnet. | V2NEXT | (fix) | +| [@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion](#aws-cdkaws-lambda-nodejsuselatestruntimeversion) | Enables aws-lambda-nodejs.Function to use the latest available NodeJs runtime as the default | V2NEXT | (default) | @@ -1056,14 +1056,16 @@ multi-az with standby enabled. **Compatibility with old behavior:** Pass `capacity.multiAzWithStandbyEnabled: false` to `Domain` construct to restore the old behavior. -### @aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion +### @aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId -*Enables aws-lambda-nodejs.Function to use the latest available NodeJs runtime as the default* (default) +*When enabled, mount targets will have a stable logicalId that is linked to the associated subnet.* (fix) -If this is set, and a `runtime` prop is not passed to, Lambda NodeJs -functions will us the latest version of the runtime provided by the Lambda -service. Do not use this if you your lambda function is reliant on dependencies -shipped as part of the runtime environment. +When this feature flag is enabled, each mount target will have a stable +logicalId that is linked to the associated subnet. If the flag is set to +false then the logicalIds of the mount targets can change if the number of +subnets changes. + +Set this flag to false for existing mount targets. | Since | Default | Recommended | @@ -1071,19 +1073,15 @@ shipped as part of the runtime environment. | (not in v1) | | | | V2NEXT | `false` | `true` | -**Compatibility with old behavior:** Pass `runtime: lambda.Runtime.NODEJS_16_X` to `Function` construct to restore the previous behavior. - - -### @aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId -*When enabled, mount targets will have a stable logicalId that is linked to the associated subnet.* (fix) +### @aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion -When this feature flag is enabled, each mount target will have a stable -logicalId that is linked to the associated subnet. If the flag is set to -false then the logicalIds of the mount targets can change if the number of -subnets changes. +*Enables aws-lambda-nodejs.Function to use the latest available NodeJs runtime as the default* (default) -Set this flag to false for existing mount targets. +If this is set, and a `runtime` prop is not passed to, Lambda NodeJs +functions will us the latest version of the runtime provided by the Lambda +service. Do not use this if you your lambda function is reliant on dependencies +shipped as part of the runtime environment. | Since | Default | Recommended | @@ -1091,5 +1089,7 @@ Set this flag to false for existing mount targets. | (not in v1) | | | | V2NEXT | `false` | `true` | +**Compatibility with old behavior:** Pass `runtime: lambda.Runtime.NODEJS_16_X` to `Function` construct to restore the previous behavior. + diff --git a/packages/aws-cdk-lib/pipelines/lib/legacy/pipeline.ts b/packages/aws-cdk-lib/pipelines/lib/legacy/pipeline.ts index b1f23e2e22788..ea87b74b2f4de 100644 --- a/packages/aws-cdk-lib/pipelines/lib/legacy/pipeline.ts +++ b/packages/aws-cdk-lib/pipelines/lib/legacy/pipeline.ts @@ -437,7 +437,7 @@ export class CdkPipeline extends Construct { const depAction = stackActions.find(s => s.stackArtifactId === depId); if (depAction === undefined) { - Annotations.of(this).addWarning(`Stack '${stackAction.stackName}' depends on stack ` + + Annotations.of(this).addWarningV2('@aws-cdk/pipelines:dependencyOnNonPipelineStack', `Stack '${stackAction.stackName}' depends on stack ` + `'${depId}', but that dependency is not deployed through the pipeline!`); } else if (!(depAction.executeRunOrder < stackAction.prepareRunOrder)) { yield `Stack '${stackAction.stackName}' depends on stack ` + diff --git a/packages/aws-cdk-lib/rosetta/default.ts-fixture b/packages/aws-cdk-lib/rosetta/default.ts-fixture index f15b52668477f..4b594b61d4233 100644 --- a/packages/aws-cdk-lib/rosetta/default.ts-fixture +++ b/packages/aws-cdk-lib/rosetta/default.ts-fixture @@ -11,6 +11,7 @@ import * as sns from 'aws-cdk-lib/aws-sns'; import * as sqs from 'aws-cdk-lib/aws-sqs'; import * as s3 from 'aws-cdk-lib/aws-s3'; import { + Annotations, App, Aws, CfnCondition,