Skip to content

Commit b1dfe96

Browse files
authored
feat(toolkit-lib)!: generic messages do not have a code (#526)
In the [original RFC](https://github.com/aws/aws-cdk-rfcs/blob/main/text/0300-programmatic-toolkit.md) we had introduced the concept of "standard" messages for which no backwards compatibility rules won't apply. In the first developer preview release of `toolkit-lib` this was implemented as message with a code ending in `0000` being consider "standard" (or "default") and other messages. With the experience from the developer preview, we have found this way to handle the distinction slightly confusing and putting comparatively complicated work on the user (they need to know and check for the code ending in `0000`). It was also easy to accidentally make a mistake by copy and pasting the message code and not noticing it's specialness. Instead, we are now proposing to change this implementation: Messages and requests with a code will be subject to our backwards compatibility guarantees and can be relied upon by integrators. Standard messages will no longer have a code and must be treated purely informational. Note that all requests and any messages with a payload will always have a code. However some coded messages might not currently have a payload (but may in future according to our backwards compatibility rules). This PR reflects these changes and updates the message registry with the respective documentation. BREAKING CHANGE: The `code` field on messages is now optional. All messages with a code are subject to our backwards compatibility guarantees and can be relied upon by integrators. Messages that previously had a "default code" ending in `0000` no longer have a code and must be treated purely informational. Requests and messages with a payload are always coded. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license
1 parent 841bc03 commit b1dfe96

File tree

18 files changed

+161
-82
lines changed

18 files changed

+161
-82
lines changed

packages/@aws-cdk/toolkit-lib/docs/message-registry.md

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,66 @@
11
---
2-
title: IoMessages Registry
2+
title: Messages and payloads
33
group: Documents
44
---
5-
# IoMessages Registry
5+
# Messages and payloads
66

7-
| Code | Description | Level | Data Interface |
8-
|------|-------------|-------|----------------|
7+
The CDK Toolkit emits *messages* and *requests* to structure interactions.
8+
A *request* is a special *message* that allows the receiver to respond, if no response is returned the toolkit will continue with a default.
9+
Messages are unidirectional and always send from the CDK Toolkit to your integration.
10+
11+
All messages include text that is suitable for display to an end-user or logging.
12+
Some messages also include a unique `code` and a additional payload `data`, providing you with structured information for your integration.
13+
*Requests* always have a `code` and can be addressed explicitly.
14+
See {@link IoMessage} and {@link IoRequest} for a complete list of available fields.
15+
16+
## Levels
17+
18+
Messages have a `level` assigned to them.
19+
Levels are ordered by their importance, with `error` being the most and `trace` being the least important.
20+
21+
| Level | Description |
22+
| ---------- | ---------------------------------------------- |
23+
| `error` | Error messages that may affect operation. |
24+
| `result` | Primary message of an operation. |
25+
| `warn` | Warning messages that don't prevent operation. |
26+
| `info` | General informational messages. |
27+
| `debug` | Detailed messages for troubleshooting. |
28+
| `trace` | Very detailed execution flow information. |
29+
30+
Attached levels are an informal recommendation of what *we* believe is the relevance of a specific message.
31+
Your integration will always receive all messages of all levels.
32+
It is up to you to filter out irrelevant messages.
33+
For standard operations, we recommend to display all messages with level `info` or above.
34+
35+
## Backwards compatibility
36+
37+
Messages and requests are an essential part of the CDK Toolkit's public contract.
38+
We recognize integrators will build critical workflows depending on these structured interactions.
39+
To help integrators build with confidence, we provide clear expectations with regards to backwards compatibility of messages.
40+
41+
**Depend only on messages and requests with a `code`. Treat all other messages as informational only.**
42+
If a message does not have a code, it can change or disappear at any time without notice.
43+
44+
**Only the `code` and `data` properties of a message are in scope for backwards compatibility.**
45+
Payload data can change, but we will only make type-compatible, additive changes.
46+
For example we may add new data, but will not remove information.
47+
48+
For the avoidance of doubt, the following changes are explicitly not considered breaking:
49+
50+
- a change to the message text or level,
51+
- a change to the default response of a request,
52+
- a change to the order messages and requests are emitted in,
53+
- the addition of new messages and requests, and
54+
- the removal of messages without a code
55+
56+
## Registry
57+
58+
This is the complete list of all currently available messages with codes and their respective payload interface.
59+
We are welcoming requests for additional coded messages and data.
60+
Please let us know by [opening an issue](https://github.com/aws/aws-cdk-cli/issues/new/choose).
61+
62+
| Code | Description | Level | Payload data interface |
63+
|------|-------------|-------|------------------------|
964
| `CDK_TOOLKIT_W0100` | Credential plugin warnings | `warn` | n/a |
1065
| `CDK_TOOLKIT_I1000` | Provides synthesis times. | `info` | {@link Duration} |
1166
| `CDK_TOOLKIT_I1001` | Cloud Assembly synthesis is starting | `trace` | {@link StackSelectionDetails} |

packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/awscli-compatible.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export class AwsCliCompatible {
156156

157157
if (!region) {
158158
const usedProfile = !profile ? '' : ` (profile: "${profile}")`;
159-
await this.ioHelper.sdkDefaults.debug(
159+
await this.ioHelper.defaults.debug(
160160
`Unable to determine AWS region from environment or AWS configuration${usedProfile}, defaulting to '${defaultRegion}'`,
161161
);
162162
return defaultRegion;
@@ -173,7 +173,7 @@ export class AwsCliCompatible {
173173
* @returns The region for the instance identity
174174
*/
175175
private async regionFromMetadataService() {
176-
await this.ioHelper.sdkDefaults.debug('Looking up AWS region in the EC2 Instance Metadata Service (IMDS).');
176+
await this.ioHelper.defaults.debug('Looking up AWS region in the EC2 Instance Metadata Service (IMDS).');
177177
try {
178178
const metadataService = new MetadataService({
179179
httpOptions: {
@@ -185,7 +185,7 @@ export class AwsCliCompatible {
185185
const document = await metadataService.request('/latest/dynamic/instance-identity/document', {});
186186
return JSON.parse(document).region;
187187
} catch (e) {
188-
await this.ioHelper.sdkDefaults.debug(`Unable to retrieve AWS region from IMDS: ${e}`);
188+
await this.ioHelper.defaults.debug(`Unable to retrieve AWS region from IMDS: ${e}`);
189189
}
190190
}
191191

@@ -223,7 +223,7 @@ export class AwsCliCompatible {
223223
* Result is send to callback function for SDK to authorize the request
224224
*/
225225
private async tokenCodeFn(deviceArn: string): Promise<string> {
226-
const debugFn = (msg: string, ...args: any[]) => this.ioHelper.sdkDefaults.debug(format(msg, ...args));
226+
const debugFn = (msg: string, ...args: any[]) => this.ioHelper.defaults.debug(format(msg, ...args));
227227
await debugFn('Require MFA token from MFA device with ARN', deviceArn);
228228
try {
229229
const token: string = await this.ioHelper.requestResponse(IO.CDK_SDK_I1100.req(`MFA token for ${deviceArn}`, {

packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/proxy-agent.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@ export class ProxyAgentProvider {
2626
private async tryGetCACert(bundlePath?: string) {
2727
const path = bundlePath || this.caBundlePathFromEnvironment();
2828
if (path) {
29-
await this.ioHelper.sdkDefaults.debug(`Using CA bundle path: ${path}`);
29+
await this.ioHelper.defaults.debug(`Using CA bundle path: ${path}`);
3030
try {
3131
if (!fs.pathExistsSync(path)) {
3232
return undefined;
3333
}
3434
return fs.readFileSync(path, { encoding: 'utf-8' });
3535
} catch (e: any) {
36-
await this.ioHelper.sdkDefaults.debug(String(e));
36+
await this.ioHelper.defaults.debug(String(e));
3737
return undefined;
3838
}
3939
}

packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/sdk-provider.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,10 @@ export class SdkProvider {
173173
// feed the CLI credentials which are sufficient by themselves. Prefer to assume the correct role if we can,
174174
// but if we can't then let's just try with available credentials anyway.
175175
if (baseCreds.source === 'correctDefault' || baseCreds.source === 'plugin') {
176-
await this.ioHelper.sdkDefaults.debug(err.message);
176+
await this.ioHelper.defaults.debug(err.message);
177177

178178
const level = quiet ? 'debug' : 'warn';
179-
await this.ioHelper.sdkDefaults[level](
179+
await this.ioHelper.defaults[level](
180180
`${fmtObtainedCredentials(baseCreds)} could not be used to assume '${options.assumeRoleArn}', but are for the right account. Proceeding anyway.`,
181181
);
182182
return {
@@ -252,13 +252,13 @@ export class SdkProvider {
252252
// they are complaining about if we fail 'cdk synth' on them. We loudly complain in order to show that
253253
// the current situation is probably undesirable, but we don't fail.
254254
if (e.name === 'ExpiredToken') {
255-
await this.ioHelper.sdkDefaults.warn(
255+
await this.ioHelper.defaults.warn(
256256
'There are expired AWS credentials in your environment. The CDK app will synth without current account information.',
257257
);
258258
return undefined;
259259
}
260260

261-
await this.ioHelper.sdkDefaults.debug(`Unable to determine the default AWS account (${e.name}): ${formatErrorMessage(e)}`);
261+
await this.ioHelper.defaults.debug(`Unable to determine the default AWS account (${e.name}): ${formatErrorMessage(e)}`);
262262
return undefined;
263263
}
264264
});
@@ -320,7 +320,7 @@ export class SdkProvider {
320320
additionalOptions?: AssumeRoleAdditionalOptions,
321321
region?: string,
322322
): Promise<SDK> {
323-
await this.ioHelper.sdkDefaults.debug(`Assuming role '${roleArn}'.`);
323+
await this.ioHelper.defaults.debug(`Assuming role '${roleArn}'.`);
324324

325325
region = region ?? this.defaultRegion;
326326

@@ -354,7 +354,7 @@ export class SdkProvider {
354354
throw err;
355355
}
356356

357-
await this.ioHelper.sdkDefaults.debug(`Assuming role failed: ${err.message}`);
357+
await this.ioHelper.defaults.debug(`Assuming role failed: ${err.message}`);
358358
throw new AuthenticationError(
359359
[
360360
'Could not assume role in target account',

packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/sdk.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,7 @@ export class SDK {
593593
ioHelper: IoHelper,
594594
logger?: ISdkLogger,
595595
) {
596-
const debugFn = async (msg: string) => ioHelper.sdkDefaults.debug(msg);
596+
const debugFn = async (msg: string) => ioHelper.defaults.debug(msg);
597597
this.accountCache = new AccountAccessKeyCache(AccountAccessKeyCache.DEFAULT_PATH, debugFn);
598598
this.debug = debugFn;
599599
this.config = {

packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/prepare-source.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export class ExecutionEnvironment implements AsyncDisposable {
6060
) {
6161
this.ioHelper = services.ioHelper;
6262
this.sdkProvider = services.sdkProvider;
63-
this.debugFn = (msg: string) => this.ioHelper.assemblyDefaults.debug(msg);
63+
this.debugFn = (msg: string) => this.ioHelper.defaults.debug(msg);
6464
this.lock = lock;
6565
this.shouldClean = outDirIsTemporary;
6666
}
@@ -228,7 +228,7 @@ export class ExecutionEnvironment implements AsyncDisposable {
228228
* @param assembly the assembly to check
229229
*/
230230
async function checkContextOverflowSupport(assembly: cxapi.CloudAssembly, ioHelper: IoHelper): Promise<void> {
231-
const traceFn = (msg: string) => ioHelper.assemblyDefaults.trace(msg);
231+
const traceFn = (msg: string) => ioHelper.defaults.trace(msg);
232232
const tree = await loadTree(assembly, traceFn);
233233
const frameworkDoesNotSupportContextOverflow = some(tree, node => {
234234
const fqn = node.constructInfo?.fqn;

packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/stack-assembly.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ async function includeDownstreamStacks(
148148
} while (madeProgress);
149149

150150
if (added.length > 0) {
151-
await ioHelper.assemblyDefaults.info(`Including depending stacks: ${chalk.bold(added.join(', '))}`);
151+
await ioHelper.defaults.info(`Including depending stacks: ${chalk.bold(added.join(', '))}`);
152152
}
153153
}
154154

@@ -180,6 +180,6 @@ async function includeUpstreamStacks(
180180
}
181181

182182
if (added.length > 0) {
183-
await ioHelper.assemblyDefaults.info(`Including dependency stacks: ${chalk.bold(added.join(', '))}`);
183+
await ioHelper.defaults.info(`Including dependency stacks: ${chalk.bold(added.join(', '))}`);
184184
}
185185
}

packages/@aws-cdk/toolkit-lib/lib/api/io/io-message.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,25 +36,21 @@ export interface IoMessage<T> {
3636
/**
3737
* A short message code uniquely identifying a message type using the format CDK_[CATEGORY]_[E/W/I][0000-9999].
3838
*
39+
* Every code releates to a message with a specific payload.
40+
* Messages without code are considered generic and do not have a payload.
41+
*
3942
* The level indicator follows these rules:
4043
* - 'E' for error level messages
4144
* - 'W' for warning level messages
4245
* - 'I' for info/debug/trace level messages
4346
*
44-
* Codes ending in 000 0 are generic messages, while codes ending in 0001-9999 are specific to a particular message.
45-
* The following are examples of valid and invalid message codes:
46-
* ```ts
47-
* 'CDK_ASSETS_I0000' // valid: generic assets info message
48-
* 'CDK_TOOLKIT_E0002' // valid: specific toolkit error message
49-
* 'CDK_SDK_W0023' // valid: specific sdk warning message
50-
* ```
51-
*
52-
* @see https://github.com/aws/aws-cdk-cli/blob/main/packages/%40aws-cdk/toolkit-lib/CODE_REGISTRY.md
47+
* @see https://docs.aws.amazon.com/cdk/api/toolkit-lib/message-registry/
5348
*/
54-
readonly code: IoMessageCode;
49+
readonly code?: IoMessageCode;
5550

5651
/**
5752
* The message text.
53+
*
5854
* This is safe to print to an end-user.
5955
*/
6056
readonly message: string;
@@ -83,4 +79,6 @@ export interface IoRequest<T, U> extends IoMessage<T> {
8379
* The default response that will be used if no data is returned.
8480
*/
8581
readonly defaultResponse: U;
82+
83+
readonly code: IoMessageCode;
8684
}

packages/@aws-cdk/toolkit-lib/lib/api/io/private/io-default-messages.ts

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as util from 'util';
2-
import type { ActionLessMessage, ActionLessRequest, IoHelper } from './io-helper';
3-
import type { IoMessageCode, IoMessageLevel } from '../io-message';
2+
import type { ActionLessMessage, IoHelper } from './io-helper';
3+
import type { IoMessageLevel } from '../io-message';
44

55
/**
66
* Helper class to emit standard log messages to an IoHost
@@ -10,24 +10,18 @@ import type { IoMessageCode, IoMessageLevel } from '../io-message';
1010
*/
1111
export class IoDefaultMessages {
1212
private readonly ioHelper: IoHelper;
13-
private readonly category: 'TOOLKIT' | 'ASSEMBLY' | 'SDK';
1413

15-
constructor(ioHelper: IoHelper, category: 'TOOLKIT' | 'ASSEMBLY' | 'SDK') {
14+
constructor(ioHelper: IoHelper) {
1615
this.ioHelper = ioHelper;
17-
this.category = category;
1816
}
1917

2018
public async notify(msg: Omit<ActionLessMessage<unknown>, 'code'>): Promise<void> {
2119
return this.ioHelper.notify({
2220
...msg,
23-
code: levelToCode(this.category, msg.level),
21+
code: undefined,
2422
});
2523
}
2624

27-
public async requestResponse<T, U>(msg: ActionLessRequest<T, U>): Promise<U> {
28-
return this.ioHelper.requestResponse(msg);
29-
}
30-
3125
public async error(input: string, ...args: unknown[]): Promise<void> {
3226
return this.emitMessage('error', input, ...args);
3327
}
@@ -65,7 +59,6 @@ export class IoDefaultMessages {
6559

6660
return {
6761
time: new Date(),
68-
code: levelToCode(this.category, level),
6962
level,
7063
message,
7164
data: undefined,
@@ -76,14 +69,3 @@ export class IoDefaultMessages {
7669
return this.ioHelper.notify(this.msg(level, input, ...args));
7770
}
7871
}
79-
80-
function levelToCode(category: string, level: IoMessageLevel): IoMessageCode {
81-
switch (level) {
82-
case 'error':
83-
return `CDK_${category}_E0000`;
84-
case 'warn':
85-
return `CDK_${category}_W0000`;
86-
default:
87-
return `CDK_${category}_I0000`;
88-
}
89-
}

packages/@aws-cdk/toolkit-lib/lib/api/io/private/io-helper.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,14 @@ export class IoHelper implements IIoHost {
2020
* Simplified access to emit default messages.
2121
*/
2222
public readonly defaults: IoDefaultMessages;
23-
public readonly assemblyDefaults: IoDefaultMessages;
24-
public readonly sdkDefaults: IoDefaultMessages;
2523

2624
private readonly ioHost: IIoHost;
2725
private readonly action: ToolkitAction;
2826

2927
private constructor(ioHost: IIoHost, action: ToolkitAction) {
3028
this.ioHost = ioHost;
3129
this.action = action;
32-
this.defaults = new IoDefaultMessages(this, 'TOOLKIT');
33-
this.assemblyDefaults = new IoDefaultMessages(this, 'ASSEMBLY');
34-
this.sdkDefaults = new IoDefaultMessages(this, 'SDK');
30+
this.defaults = new IoDefaultMessages(this);
3531
}
3632

3733
/**

0 commit comments

Comments
 (0)