Skip to content

Commit fb1792c

Browse files
authored
feat(core): addInfoV2 method for suppressible info Annotations (#34872)
### Issue # (if applicable) Closes #34871 ### Reason for this change > This feature request proposes add Annotations.addInfoV2() method to provide CDK developers the way to suppressable info message. This allows the CDK users to suppress the unnecessary info annotation message. > > I propose adding addInfoV2() -- the Info-level counterpart to addWarningV2() -- which would let builders acknowledge an Info message once and prevent it from re-appearing in subsequent runs. ### Description of changes This PR adds the following new features to the `Annotations` class: 1. `addInfoV2` method - A method to add acknowledgeable info messages 2. `acknowledgeInfo` method - A method to mark specific info messages as acknowledged These changes follow the same pattern as the existing warning message functionality (`addWarningV2`/`acknowledgeWarning`), maintaining API consistency. ### Describe any new or updated permissions being added This PR does not add any new IAM permissions. ### Description of how you validated changes The following tests were added to validate the functionality: 1. Tests confirming that `addInfoV2` correctly adds info messages 2. Tests verifying that `acknowledgeInfo` correctly marks info messages as acknowledged and they are no longer displayed ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 0eb5155 commit fb1792c

File tree

4 files changed

+203
-1
lines changed

4 files changed

+203
-1
lines changed

packages/aws-cdk-lib/core/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1800,6 +1800,17 @@ warning by the `id`.
18001800
Annotations.of(this).acknowledgeWarning('IAM:Group:MaxPoliciesExceeded', 'Account has quota increased to 20');
18011801
```
18021802

1803+
### Acknowledging Infos
1804+
1805+
Informational messages can also be emitted and acknowledged. Use `addInfoV2()`
1806+
to add an info message that can later be suppressed with `acknowledgeInfo()`.
1807+
Unlike warnings, info messages are not affected by the `--strict` mode and will never cause synthesis to fail.
1808+
1809+
```ts
1810+
Annotations.of(this).addInfoV2('my-lib:Construct.someInfo', 'Some message explaining the info');
1811+
Annotations.of(this).acknowledgeInfo('my-lib:Construct.someInfo', 'This info can be ignored');
1812+
```
1813+
18031814
## RemovalPolicies
18041815

18051816
The `RemovalPolicies` class provides a convenient way to manage removal policies for AWS CDK resources within a construct scope. It allows you to apply removal policies to multiple resources at once, with options to include or exclude specific resource types.

packages/aws-cdk-lib/core/lib/annotations.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,51 @@ export class Annotations {
8989
this.addMessage(cxschema.ArtifactMetadataEntryType.WARN, message);
9090
}
9191

92+
/**
93+
* Acknowledge a info. When a info is acknowledged for a scope
94+
* all infos that match the id will be ignored.
95+
*
96+
* The acknowledgement will apply to all child scopes
97+
*
98+
* @example
99+
* declare const myConstruct: Construct;
100+
* Annotations.of(myConstruct).acknowledgeInfo('SomeInfoId', 'This info can be ignored because...');
101+
*
102+
* @param id - the id of the info message to acknowledge
103+
* @param message optional message to explain the reason for acknowledgement
104+
*/
105+
public acknowledgeInfo(id: string, message?: string): void {
106+
Acknowledgements.of(this.scope).add(this.scope, id);
107+
108+
// We don't use message currently, but encouraging people to supply it is good for documentation
109+
// purposes, and we can always add a report on it in the future.
110+
void(message);
111+
112+
// Iterate over the construct and remove any existing instances of this info
113+
// (addInfoV2 will prevent future instances of it)
114+
removeInfoDeep(this.scope, id);
115+
}
116+
117+
/**
118+
* Adds an acknowledgeable info metadata entry to this construct.
119+
*
120+
* The CLI will display the info when an app is synthesized.
121+
*
122+
* If the info is acknowledged using `acknowledgeInfo()`, it will not be shown by the CLI.
123+
*
124+
* @example
125+
* declare const myConstruct: Construct;
126+
* Annotations.of(myConstruct).addInfoV2('my-library:Construct.someInfo', 'Some message explaining the info');
127+
*
128+
* @param id the unique identifier for the info. This can be used to acknowledge the info
129+
* @param message The info message.
130+
*/
131+
public addInfoV2(id: string, message: string) {
132+
if (!Acknowledgements.of(this.scope).has(this.scope, id)) {
133+
this.addMessage(cxschema.ArtifactMetadataEntryType.INFO, `${message} ${ackTag(id)}`);
134+
}
135+
}
136+
92137
/**
93138
* Adds an info metadata entry to this construct.
94139
*
@@ -259,6 +304,42 @@ function removeWarning(construct: IConstruct, id: string) {
259304
}
260305
}
261306

307+
/**
308+
* Remove info metadata from all constructs in a given scope
309+
*
310+
* No recursion to avoid blowing out the stack.
311+
*/
312+
function removeInfoDeep(construct: IConstruct, id: string) {
313+
const stack = [construct];
314+
315+
while (stack.length > 0) {
316+
const next = stack.pop()!;
317+
removeInfo(next, id);
318+
stack.push(...next.node.children);
319+
}
320+
}
321+
322+
/**
323+
* Remove metadata from a construct node.
324+
*
325+
* This uses private APIs for now; we could consider adding this functionality
326+
* to the constructs library itself.
327+
*/
328+
function removeInfo(construct: IConstruct, id: string) {
329+
const meta: MetadataEntry[] | undefined = (construct.node as any)._metadata;
330+
if (!meta) { return; }
331+
332+
let i = 0;
333+
while (i < meta.length) {
334+
const m = meta[i];
335+
if (m.type === cxschema.ArtifactMetadataEntryType.INFO && (m.data as string).includes(ackTag(id))) {
336+
meta.splice(i, 1);
337+
} else {
338+
i += 1;
339+
}
340+
}
341+
}
342+
262343
function ackTag(id: string) {
263344
return `[ack: ${id}]`;
264345
}

packages/aws-cdk-lib/core/test/annotations.test.ts

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Construct } from 'constructs';
2-
import { getWarnings } from './util';
2+
import { getWarnings, getInfos } from './util';
33
import { App, Stack } from '../lib';
44
import { Annotations } from '../lib/annotations';
55

@@ -147,4 +147,101 @@ describe('annotations', () => {
147147
},
148148
]);
149149
});
150+
151+
test('addInfoV2 adds an acknowledgeable info message', () => {
152+
const app = new App();
153+
const stack = new Stack(app, 'S1');
154+
const c1 = new Construct(stack, 'C1');
155+
Annotations.of(c1).addInfoV2('info1', 'This is an info message');
156+
Annotations.of(c1).addInfoV2('info1', 'This is an info message');
157+
Annotations.of(c1).addInfoV2('info1', 'This is an info message');
158+
Annotations.of(c1).addInfoV2('info2', 'This is another info message');
159+
expect(getInfos(app.synth())).toEqual([
160+
{
161+
path: '/S1/C1',
162+
message: 'This is an info message [ack: info1]',
163+
},
164+
{
165+
path: '/S1/C1',
166+
message: 'This is another info message [ack: info2]',
167+
},
168+
]);
169+
});
170+
171+
test('acknowledgeInfo removes info message', () => {
172+
// GIVEN
173+
const app = new App();
174+
const stack = new Stack(app, 'S1');
175+
const c1 = new Construct(stack, 'C1');
176+
177+
// WHEN
178+
Annotations.of(c1).addInfoV2('INFO1', 'This is an info message');
179+
Annotations.of(c1).addInfoV2('INFO2', 'This is another info message');
180+
Annotations.of(c1).acknowledgeInfo('INFO2', 'I acknowledge this info');
181+
182+
// THEN
183+
expect(getInfos(app.synth())).toEqual([
184+
{
185+
path: '/S1/C1',
186+
message: 'This is an info message [ack: INFO1]',
187+
},
188+
]);
189+
});
190+
191+
test('acknowledgeInfo removes info message on children', () => {
192+
// GIVEN
193+
const app = new App();
194+
const stack = new Stack(app, 'S1');
195+
const c1 = new Construct(stack, 'C1');
196+
const c2 = new Construct(c1, 'C2');
197+
198+
// WHEN
199+
Annotations.of(c2).addInfoV2('INFO2', 'This is an info message for child');
200+
Annotations.of(c1).acknowledgeInfo('INFO2', 'I acknowledge this info');
201+
202+
// THEN
203+
expect(getInfos(app.synth())).toEqual([]);
204+
});
205+
206+
test('don\'t resolve the info message if tokens are included', () => {
207+
// GIVEN
208+
const app = new App();
209+
const stack = new Stack(app, 'S1');
210+
const c1 = new Construct(stack, 'C1');
211+
212+
// WHEN
213+
Annotations.of(c1).addInfoV2('INFO', `stackId: ${stack.stackId}`);
214+
215+
// THEN
216+
expect(getInfos(app.synth())).toEqual([
217+
{
218+
path: '/S1/C1',
219+
message: expect.stringMatching(/stackId: \${Token\[AWS::StackId\.\d+\]} \[ack: INFO\]/),
220+
},
221+
]);
222+
});
223+
224+
test('messages with same ID are treated separately across different levels', () => {
225+
const app = new App();
226+
const stack = new Stack(app, 'S1');
227+
const c1 = new Construct(stack, 'C1');
228+
229+
// WHEN
230+
Annotations.of(c1).addWarningV2('message1', 'This is a message');
231+
Annotations.of(c1).addInfoV2('message1', 'This is another message');
232+
233+
// THEN
234+
expect(getWarnings(app.synth())).toEqual([
235+
{
236+
path: '/S1/C1',
237+
message: 'This is a message [ack: message1]',
238+
},
239+
]);
240+
expect(getInfos(app.synth())).toEqual([
241+
{
242+
path: '/S1/C1',
243+
message: 'This is another message [ack: message1]',
244+
},
245+
]);
246+
});
150247
});

packages/aws-cdk-lib/core/test/util.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,16 @@ export function getWarnings(casm: CloudAssembly) {
4444
}
4545
return result;
4646
}
47+
48+
export function getInfos(casm: CloudAssembly) {
49+
const result = new Array<{ path: string; message: string }>();
50+
for (const stack of Object.values(casm.manifest.artifacts ?? {})) {
51+
const artifact = CloudArtifact.fromManifest(casm, 'art', stack);
52+
artifact?.messages.forEach(message => {
53+
if (message.level === SynthesisMessageLevel.INFO) {
54+
result.push({ path: message.id, message: message.entry.data as string });
55+
}
56+
});
57+
}
58+
return result;
59+
}

0 commit comments

Comments
 (0)