diff --git a/.projenrc.ts b/.projenrc.ts index ffe588ec9..a1b86da95 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1249,7 +1249,7 @@ toolkitLib.package.addField('exports', { './package.json': './package.json', }); -toolkitLib.postCompileTask.exec('ts-node scripts/gen-code-registry.ts'); +toolkitLib.postCompileTask.exec('ts-node --prefer-ts-exts scripts/gen-code-registry.ts'); toolkitLib.postCompileTask.exec('node build-tools/bundle.mjs'); // Smoke test built JS files toolkitLib.postCompileTask.exec('node ./lib/index.js >/dev/null 2>/dev/null extends MessageInfo { /** * Produce an IoMessageMaker for the provided level and code info. */ -function generic(level: IoMessageLevel, details: CodeInfo): IoMessageMaker { - const msg = (message: string, data: T) => ({ +function message(level: IoMessageLevel, details: CodeInfo): IoMessageMaker { + const maker = (text: string, data: T) => ({ time: new Date(), level, code: details.code, - message: message, - data: data, + message: text, + data, } as ActionLessMessage); return { ...details, level, - msg: msg as any, + msg: maker as any, }; } +/** + * A type that is impossible for a user to replicate + * This is used to ensure that results always have a proper type generic declared. + */ +declare const privateKey: unique symbol; +export type ImpossibleType = { + readonly [privateKey]: typeof privateKey; +}; + // Create `IoMessageMaker`s for a given level and type check that calls with payload are using the correct interface type CodeInfoMaybeInterface = [T] extends [never] ? Omit : Required; -export const trace = (details: CodeInfoMaybeInterface) => generic('trace', details); -export const debug = (details: CodeInfoMaybeInterface) => generic('debug', details); -export const info = (details: CodeInfoMaybeInterface) => generic('info', details); -export const warn = (details: CodeInfoMaybeInterface) => generic('warn', details); -export const error = (details: CodeInfoMaybeInterface) => generic('error', details); -export const result = (details: Required) => generic('result', details); +export const trace = (details: CodeInfoMaybeInterface) => message('trace', details); +export const debug = (details: CodeInfoMaybeInterface) => message('debug', details); +export const info = (details: CodeInfoMaybeInterface) => message('info', details); +export const warn = (details: CodeInfoMaybeInterface) => message('warn', details); +export const error = (details: CodeInfoMaybeInterface) => message('error', details); +export const result = (details: Required) => message('result', details); + +interface RequestInfo extends CodeInfo { + readonly defaultResponse: U; +} + +/** + * An interface that can produce requests for a specific code. + */ +export interface IoRequestMaker extends MessageInfo { + /** + * Create a message for this code, with or without payload. + */ + req: [T] extends [never] ? (message: string) => ActionLessMessage : (message: string, data: T) => ActionLessRequest; +} + +/** + * Produce an IoRequestMaker for the provided level and request info. + */ +function request(level: IoMessageLevel, details: RequestInfo): IoRequestMaker { + const maker = (text: string, data: T) => ({ + time: new Date(), + level, + code: details.code, + message: text, + data, + defaultResponse: details.defaultResponse, + } as ActionLessRequest); + + return { + ...details, + level, + req: maker as any, + }; +} + +/** + * A request that is a simple yes/no question, with the expectation that 'yes' is the default. + */ +export const confirm = (details: Required, 'defaultResponse'>>) => request('info', { + ...details, + defaultResponse: true, +}); diff --git a/packages/@aws-cdk/toolkit-lib/.projen/tasks.json b/packages/@aws-cdk/toolkit-lib/.projen/tasks.json index f6146f244..46c25133f 100644 --- a/packages/@aws-cdk/toolkit-lib/.projen/tasks.json +++ b/packages/@aws-cdk/toolkit-lib/.projen/tasks.json @@ -171,7 +171,7 @@ "description": "Runs after successful compilation", "steps": [ { - "exec": "ts-node scripts/gen-code-registry.ts" + "exec": "ts-node --prefer-ts-exts scripts/gen-code-registry.ts" }, { "exec": "node build-tools/bundle.mjs" diff --git a/packages/@aws-cdk/toolkit-lib/CODE_REGISTRY.md b/packages/@aws-cdk/toolkit-lib/CODE_REGISTRY.md index a9590b32b..58850766e 100644 --- a/packages/@aws-cdk/toolkit-lib/CODE_REGISTRY.md +++ b/packages/@aws-cdk/toolkit-lib/CODE_REGISTRY.md @@ -2,6 +2,9 @@ | Code | Description | Level | Data Interface | |------|-------------|-------|----------------| +| CDK_TOOLKIT_I0000 | Default info messages emitted from the Toolkit | info | n/a | +| CDK_TOOLKIT_I0000 | Default debug messages emitted from the Toolkit | debug | n/a | +| CDK_TOOLKIT_W0000 | Default warning messages emitted from the Toolkit | warn | n/a | | CDK_TOOLKIT_I1000 | Provides synthesis times. | info | [Duration](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/Duration.html) | | CDK_TOOLKIT_I1901 | Provides stack data | result | [StackAndAssemblyData](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/StackAndAssemblyData.html) | | CDK_TOOLKIT_I1902 | Successfully deployed stacks | result | [AssemblyData](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/AssemblyData.html) | @@ -10,29 +13,52 @@ | CDK_TOOLKIT_I5000 | Provides deployment times | info | [Duration](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/Duration.html) | | CDK_TOOLKIT_I5001 | Provides total time in deploy action, including synth and rollback | info | [Duration](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/Duration.html) | | CDK_TOOLKIT_I5002 | Provides time for resource migration | info | n/a | +| CDK_TOOLKIT_W5021 | Empty non-existent stack, deployment is skipped | warn | n/a | +| CDK_TOOLKIT_W5022 | Empty existing stack, stack will be destroyed | warn | n/a | | CDK_TOOLKIT_I5031 | Informs about any log groups that are traced as part of the deployment | info | n/a | -| CDK_TOOLKIT_I5050 | Confirm rollback during deployment | info | n/a | -| CDK_TOOLKIT_I5060 | Confirm deploy security sensitive changes | info | n/a | +| CDK_TOOLKIT_I5050 | Confirm rollback during deployment | info | [ConfirmationRequest](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/ConfirmationRequest.html) | +| CDK_TOOLKIT_I5060 | Confirm deploy security sensitive changes | info | [ConfirmationRequest](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/ConfirmationRequest.html) | +| CDK_TOOLKIT_I5100 | Stack deploy progress | info | [StackDeployProgress](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/StackDeployProgress.html) | +| CDK_TOOLKIT_I5310 | The computed settings used for file watching | debug | [WatchSettings](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/WatchSettings.html) | +| CDK_TOOLKIT_I5311 | File watching started | info | [FileWatchEvent](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/FileWatchEvent.html) | +| CDK_TOOLKIT_I5312 | File event detected, starting deployment | info | [FileWatchEvent](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/FileWatchEvent.html) | +| CDK_TOOLKIT_I5313 | File event detected during active deployment, changes are queued | info | [FileWatchEvent](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/FileWatchEvent.html) | +| CDK_TOOLKIT_I5314 | Initial watch deployment started | info | n/a | +| CDK_TOOLKIT_I5315 | Queued watch deployment started | info | n/a | | CDK_TOOLKIT_I5501 | Stack Monitoring: Start monitoring of a single stack | info | [StackMonitoringControlEvent](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/StackMonitoringControlEvent.html) | | CDK_TOOLKIT_I5502 | Stack Monitoring: Activity event for a single stack | info | [StackActivity](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/StackActivity.html) | | CDK_TOOLKIT_I5503 | Stack Monitoring: Finished monitoring of a single stack | info | [StackMonitoringControlEvent](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/StackMonitoringControlEvent.html) | | CDK_TOOLKIT_I5900 | Deployment results on success | result | [SuccessfulDeployStackResult](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/SuccessfulDeployStackResult.html) | +| CDK_TOOLKIT_I5901 | Generic deployment success messages | info | n/a | +| CDK_TOOLKIT_W5400 | Hotswap disclosure message | warn | n/a | | CDK_TOOLKIT_E5001 | No stacks found | error | n/a | | CDK_TOOLKIT_E5500 | Stack Monitoring error | error | [ErrorPayload](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/ErrorPayload.html) | | CDK_TOOLKIT_I6000 | Provides rollback times | info | [Duration](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/Duration.html) | +| CDK_TOOLKIT_I6100 | Stack rollback progress | info | [StackRollbackProgress](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/StackRollbackProgress.html) | | CDK_TOOLKIT_E6001 | No stacks found | error | n/a | -| CDK_TOOLKIT_E6900 | Rollback failed | error | n/a | +| CDK_TOOLKIT_E6900 | Rollback failed | error | [ErrorPayload](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/ErrorPayload.html) | | CDK_TOOLKIT_I7000 | Provides destroy times | info | [Duration](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/Duration.html) | -| CDK_TOOLKIT_I7010 | Confirm destroy stacks | info | n/a | +| CDK_TOOLKIT_I7010 | Confirm destroy stacks | info | [ConfirmationRequest](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/ConfirmationRequest.html) | +| CDK_TOOLKIT_I7100 | Stack destroy progress | info | [StackDestroyProgress](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/StackDestroyProgress.html) | +| CDK_TOOLKIT_I7900 | Stack deletion succeeded | result | [cxapi.CloudFormationStackArtifact](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/cxapi.CloudFormationStackArtifact.html) | | CDK_TOOLKIT_E7010 | Action was aborted due to negative confirmation of request | error | n/a | -| CDK_TOOLKIT_E7900 | Stack deletion failed | error | n/a | +| CDK_TOOLKIT_E7900 | Stack deletion failed | error | [ErrorPayload](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/ErrorPayload.html) | | CDK_TOOLKIT_I9000 | Provides bootstrap times | info | [Duration](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/Duration.html) | -| CDK_TOOLKIT_I9900 | Bootstrap results on success | info | n/a | -| CDK_TOOLKIT_E9900 | Bootstrap failed | error | n/a | +| CDK_TOOLKIT_I9100 | Bootstrap progress | info | [BootstrapEnvironmentProgress](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/BootstrapEnvironmentProgress.html) | +| CDK_TOOLKIT_I9900 | Bootstrap results on success | result | [cxapi.Environment](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/cxapi.Environment.html) | +| CDK_TOOLKIT_E9900 | Bootstrap failed | error | [ErrorPayload](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/ErrorPayload.html) | +| CDK_ASSEMBLY_I0010 | Generic environment preparation debug messages | debug | n/a | +| CDK_ASSEMBLY_W0010 | Emitted if the found framework version does not support context overflow | warn | n/a | | CDK_ASSEMBLY_I0042 | Writing updated context | debug | [UpdatedContext](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/UpdatedContext.html) | -| CDK_ASSEMBLY_I0241 | Fetching missing context | debug | [MissingContext](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/MissingContext.html) | +| CDK_ASSEMBLY_I0240 | Context lookup was stopped as no further progress was made. | debug | [MissingContext](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/MissingContext.html) | +| CDK_ASSEMBLY_I0241 | Fetching missing context. This is an iterative message that may appear multiple times with different missing keys. | debug | [MissingContext](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/MissingContext.html) | | CDK_ASSEMBLY_I1000 | Cloud assembly output starts | debug | n/a | | CDK_ASSEMBLY_I1001 | Output lines emitted by the cloud assembly to stdout | info | n/a | | CDK_ASSEMBLY_E1002 | Output lines emitted by the cloud assembly to stderr | error | n/a | | CDK_ASSEMBLY_I1003 | Cloud assembly output finished | info | n/a | | CDK_ASSEMBLY_E1111 | Incompatible CDK CLI version. Upgrade needed. | error | [ErrorPayload](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/ErrorPayload.html) | +| CDK_ASSEMBLY_I0150 | Indicates the use of a pre-synthesized cloud assembly directory | debug | n/a | +| CDK_ASSEMBLY_I9999 | Annotations emitted by the cloud assembly | info | [cxapi.SynthesisMessage](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/cxapi.SynthesisMessage.html) | +| CDK_ASSEMBLY_W9999 | Warnings emitted by the cloud assembly | warn | [cxapi.SynthesisMessage](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/cxapi.SynthesisMessage.html) | +| CDK_ASSEMBLY_E9999 | Errors emitted by the cloud assembly | error | [cxapi.SynthesisMessage](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/cxapi.SynthesisMessage.html) | +| CDK_SDK_I0100 | An SDK trace. SDK traces are emitted as traces to the IoHost, but contain the original SDK logging level. | trace | [SdkTrace](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/SdkTrace.html) | diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts index 6c4915128..a44fb6bb9 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts @@ -227,3 +227,20 @@ export class BootstrapSource { }; } } + +export interface BootstrapEnvironmentProgress { + /** + * The total number of environments being deployed + */ + readonly total: number; + /** + * The count of the environment currently bootstrapped + * + * This is counting value, not an identifier. + */ + readonly current: number; + /** + * The environment that's currently being bootstrapped + */ + readonly environment: cxapi.Environment; +} diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/deploy/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/deploy/index.ts index d129838a7..7c33cac3f 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/deploy/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/deploy/index.ts @@ -1,3 +1,4 @@ +import { CloudFormationStackArtifact } from '@aws-cdk/cx-api'; import type { BaseDeployOptions } from './private/deploy-options'; import type { Tag } from '../../api/aws-cdk'; @@ -210,3 +211,20 @@ export interface HotswapProperties { */ readonly ecs: EcsHotswapProperties; } + +export interface StackDeployProgress { + /** + * The total number of stacks being deployed + */ + readonly total: number; + /** + * The count of the stack currently attempted to be deployed + * + * This is counting value, not an identifier. + */ + readonly current: number; + /** + * The stack that's currently being deployed + */ + readonly stack: CloudFormationStackArtifact; +} diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/destroy/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/destroy/index.ts index 3a9862048..ce6f90226 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/destroy/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/destroy/index.ts @@ -1,3 +1,4 @@ +import type { CloudFormationStackArtifact } from '@aws-cdk/cx-api'; import type { StackSelector } from '../../api/cloud-assembly'; export interface DestroyOptions { @@ -18,3 +19,20 @@ export interface DestroyOptions { */ readonly ci?: boolean; } + +export interface StackDestroyProgress { + /** + * The total number of stacks being destroyed + */ + readonly total: number; + /** + * The count of the stack currently attempted to be destroyed + * + * This is counting value, not an identifier. + */ + readonly current: number; + /** + * The stack that's currently being destroyed + */ + readonly stack: CloudFormationStackArtifact; +} diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/rollback/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/rollback/index.ts index 7f5f8e5ae..ef38edaf6 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/rollback/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/rollback/index.ts @@ -1,3 +1,4 @@ +import type { CloudFormationStackArtifact } from '@aws-cdk/cx-api'; import type { StackSelector } from '../../api/cloud-assembly'; export interface RollbackOptions { @@ -42,3 +43,20 @@ export interface RollbackOptions { */ readonly validateBootstrapStackVersion?: boolean; } + +export interface StackRollbackProgress { + /** + * The total number of stacks being rolled back + */ + readonly total: number; + /** + * The count of the stack currently attempted to be rolled back + * + * This is counting value, not an identifier. + */ + readonly current: number; + /** + * The stack that's currently being rolled back + */ + readonly stack: CloudFormationStackArtifact; +} diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/watch/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/watch/index.ts index 25824c017..8887b5647 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/watch/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/watch/index.ts @@ -38,3 +38,32 @@ export interface WatchOptions extends BaseDeployOptions { */ readonly outdir?: string; } + +/** + * The computed file watch settings + */ +export interface WatchSettings { + /** + * The directory observed for file changes + */ + readonly watchDir: string; + /** + * List of include patterns for watching files + */ + readonly includes: string[]; + /** + * List of excludes patterns for watching files + */ + readonly excludes: string[]; +} + +export interface FileWatchEvent { + /** + * The change to the path + */ + readonly event: string; + /** + * The path that has an observed event + */ + readonly path?: string; +} diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts b/packages/@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts index 7113d2a41..2c5a5e583 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts @@ -1,7 +1,7 @@ /* eslint-disable import/no-restricted-paths */ // APIs -export { formatSdkLoggerContent, SdkProvider } from '../../../../aws-cdk/lib/api/aws-auth'; +export { SdkProvider } from '../../../../aws-cdk/lib/api/aws-auth'; export { Context, PROJECT_CONTEXT } from '../../../../aws-cdk/lib/api/context'; export { Deployments, type SuccessfulDeployStackResult } from '../../../../aws-cdk/lib/api/deployments'; export { Settings } from '../../../../aws-cdk/lib/api/settings'; diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/context-aware-source.ts b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/context-aware-source.ts index eb35d2a56..4846bc4e8 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/context-aware-source.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/context-aware-source.ts @@ -2,7 +2,7 @@ import type { MissingContext } from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; import { ToolkitServices } from '../../../toolkit/private'; import { type Context, contextproviders, PROJECT_CONTEXT } from '../../aws-cdk'; -import { CODES, debug } from '../../io/private'; +import { CODES } from '../../io/private'; import { ActionAwareIoHost } from '../../shared-private'; import { ToolkitError } from '../../shared-public'; import { ICloudAssemblySource } from '../types'; @@ -65,27 +65,26 @@ export class ContextAwareCloudAssembly implements ICloudAssemblySource { const assembly = await this.source.produce(); if (assembly.manifest.missing && assembly.manifest.missing.length > 0) { - const missingKeys = missingContextKeys(assembly.manifest.missing); + const missingKeysSet = missingContextKeys(assembly.manifest.missing); + const missingKeys = Array.from(missingKeysSet); if (!this.canLookup) { throw new ToolkitError( 'Context lookups have been disabled. ' + 'Make sure all necessary context is already in \'cdk.context.json\' by running \'cdk synth\' on a machine with sufficient AWS credentials and committing the result. ' - + `Missing context keys: '${Array.from(missingKeys).join(', ')}'`); + + `Missing context keys: '${missingKeys.join(', ')}'`); } let tryLookup = true; - if (previouslyMissingKeys && equalSets(missingKeys, previouslyMissingKeys)) { - await this.ioHost.notify(debug('Not making progress trying to resolve environmental context. Giving up.')); + if (previouslyMissingKeys && equalSets(missingKeysSet, previouslyMissingKeys)) { + await this.ioHost.notify(CODES.CDK_ASSEMBLY_I0240.msg('Not making progress trying to resolve environmental context. Giving up.', { missingKeys })); tryLookup = false; } - previouslyMissingKeys = missingKeys; + previouslyMissingKeys = missingKeysSet; if (tryLookup) { - await this.ioHost.notify(CODES.CDK_ASSEMBLY_I0241.msg('Some context information is missing. Fetching...', { - missingKeys: Array.from(missingKeys), - })); + await this.ioHost.notify(CODES.CDK_ASSEMBLY_I0241.msg('Some context information is missing. Fetching...', { missingKeys })); await contextproviders.provideContextValues( assembly.manifest.missing, this.context, diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/prepare-source.ts b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/prepare-source.ts index 39889ac32..dddb9fae2 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/prepare-source.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/prepare-source.ts @@ -1,5 +1,6 @@ import * as os from 'node:os'; import * as path from 'node:path'; +import { format } from 'node:util'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs-extra'; @@ -7,7 +8,7 @@ import { lte } from 'semver'; import { prepareDefaultEnvironment as oldPrepare, prepareContext, spaceAvailableForContext, Settings, loadTree, some, versionNumber } from '../../../api/aws-cdk'; import { splitBySize } from '../../../private/util'; import { ToolkitServices } from '../../../toolkit/private'; -import { asLogger, CODES } from '../../io/private'; +import { CODES } from '../../io/private'; import { ActionAwareIoHost } from '../../shared-private'; import { ToolkitError } from '../../shared-public'; import type { AppSynthOptions } from '../source-builder'; @@ -37,7 +38,7 @@ export function determineOutputDirectory(outdir?: string) { * @param context The context key/value bash. */ export async function prepareDefaultEnvironment(services: ToolkitServices, props: { outdir?: string } = {}): Promise { - const logFn = asLogger(services.ioHost, 'ASSEMBLY').debug; + const logFn = (msg: string, ...args: any) => services.ioHost.notify(CODES.CDK_ASSEMBLY_I0010.msg(format(msg, ...args))); const env = await oldPrepare(services.sdkProvider, logFn); if (props.outdir) { @@ -130,7 +131,6 @@ export async function withContext( * @param assembly the assembly to check */ export async function checkContextOverflowSupport(assembly: cxapi.CloudAssembly, ioHost: ActionAwareIoHost): Promise { - const logFn = asLogger(ioHost, 'ASSEMBLY').warn; const tree = loadTree(assembly); const frameworkDoesNotSupportContextOverflow = some(tree, node => { const fqn = node.constructInfo?.fqn; @@ -142,7 +142,7 @@ export async function checkContextOverflowSupport(assembly: cxapi.CloudAssembly, // We're dealing with an old version of the framework here. It is unaware of the temporary // file, which means that it will ignore the context overflow. if (frameworkDoesNotSupportContextOverflow) { - await logFn('Part of the context could not be sent to the application. Please update the AWS CDK library to the latest version.'); + await ioHost.notify(CODES.CDK_ASSEMBLY_W0010.msg('Part of the context could not be sent to the application. Please update the AWS CDK library to the latest version.')); } } diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/source-builder.ts b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/source-builder.ts index 906d1dd6e..3250992da 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/source-builder.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/source-builder.ts @@ -6,7 +6,7 @@ import { execInChildProcess } from './exec'; import { assemblyFromDirectory, changeDir, determineOutputDirectory, guessExecutable, prepareDefaultEnvironment, withContext, withEnv } from './prepare-source'; import { ToolkitServices } from '../../../toolkit/private'; import { Context, ILock, RWLock, Settings } from '../../aws-cdk'; -import { CODES, debug } from '../../io/private'; +import { CODES } from '../../io/private'; import { ToolkitError } from '../../shared-public'; import { AssemblyBuilder } from '../source-builder'; @@ -76,7 +76,7 @@ export abstract class CloudAssemblySourceBuilder { { produce: async () => { // @todo build - await services.ioHost.notify(debug('--app points to a cloud assembly, so we bypass synth')); + await services.ioHost.notify(CODES.CDK_ASSEMBLY_I0150.msg('--app points to a cloud assembly, so we bypass synth')); return assemblyFromDirectory(directory, services.ioHost); }, }, diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts index fa84de3f6..fad7848c7 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts @@ -1,7 +1,14 @@ -import { StackDetailsPayload } from '../../../actions/list'; -import { AssemblyData, Duration, ErrorPayload, StackAndAssemblyData } from '../../../toolkit/types'; -import { StackActivity, StackMonitoringControlEvent } from '../../aws-cdk'; -import { MissingContext, UpdatedContext } from '../../cloud-assembly/context'; +import type * as cxapi from '@aws-cdk/cx-api'; +import type { SdkTrace } from './sdk-logger'; +import type { BootstrapEnvironmentProgress } from '../../../actions/bootstrap'; +import type { StackDeployProgress } from '../../../actions/deploy'; +import type { StackDestroyProgress } from '../../../actions/destroy'; +import type { StackDetailsPayload } from '../../../actions/list'; +import type { StackRollbackProgress } from '../../../actions/rollback'; +import type { FileWatchEvent, WatchSettings } from '../../../actions/watch'; +import type { AssemblyData, ConfirmationRequest, Duration, ErrorPayload, StackAndAssemblyData, SuccessfulDeployStackResult } from '../../../toolkit/types'; +import type { StackActivity, StackMonitoringControlEvent } from '../../aws-cdk'; +import type { MissingContext, UpdatedContext } from '../../cloud-assembly/context'; import * as make from '../../shared-private'; /** @@ -11,6 +18,20 @@ import * as make from '../../shared-private'; * - X900-X999 are reserved for results */ export const CODES = { + // Defaults + DEFAULT_TOOLKIT_INFO: make.info({ + code: 'CDK_TOOLKIT_I0000', + description: 'Default info messages emitted from the Toolkit', + }), + DEFAULT_TOOLKIT_DEBUG: make.debug({ + code: 'CDK_TOOLKIT_I0000', + description: 'Default debug messages emitted from the Toolkit', + }), + DEFAULT_TOOLKIT_WARN: make.warn({ + code: 'CDK_TOOLKIT_W0000', + description: 'Default warning messages emitted from the Toolkit', + }), + // 1: Synth CDK_TOOLKIT_I1000: make.info({ code: 'CDK_TOOLKIT_I1000', @@ -58,19 +79,66 @@ export const CODES = { code: 'CDK_TOOLKIT_I5002', description: 'Provides time for resource migration', }), + CDK_TOOLKIT_W5021: make.warn({ + code: 'CDK_TOOLKIT_W5021', + description: 'Empty non-existent stack, deployment is skipped', + }), + CDK_TOOLKIT_W5022: make.warn({ + code: 'CDK_TOOLKIT_W5022', + description: 'Empty existing stack, stack will be destroyed', + }), CDK_TOOLKIT_I5031: make.info({ code: 'CDK_TOOLKIT_I5031', description: 'Informs about any log groups that are traced as part of the deployment', }), - CDK_TOOLKIT_I5050: make.info({ + CDK_TOOLKIT_I5050: make.confirm({ code: 'CDK_TOOLKIT_I5050', description: 'Confirm rollback during deployment', + interface: 'ConfirmationRequest', }), - CDK_TOOLKIT_I5060: make.info({ + CDK_TOOLKIT_I5060: make.confirm({ code: 'CDK_TOOLKIT_I5060', description: 'Confirm deploy security sensitive changes', + interface: 'ConfirmationRequest', + }), + + CDK_TOOLKIT_I5100: make.info({ + code: 'CDK_TOOLKIT_I5100', + description: 'Stack deploy progress', + interface: 'StackDeployProgress', }), + // Watch + CDK_TOOLKIT_I5310: make.debug({ + code: 'CDK_TOOLKIT_I5310', + description: 'The computed settings used for file watching', + interface: 'WatchSettings', + }), + CDK_TOOLKIT_I5311: make.info({ + code: 'CDK_TOOLKIT_I5311', + description: 'File watching started', + interface: 'FileWatchEvent', + }), + CDK_TOOLKIT_I5312: make.info({ + code: 'CDK_TOOLKIT_I5312', + description: 'File event detected, starting deployment', + interface: 'FileWatchEvent', + }), + CDK_TOOLKIT_I5313: make.info({ + code: 'CDK_TOOLKIT_I5313', + description: 'File event detected during active deployment, changes are queued', + interface: 'FileWatchEvent', + }), + CDK_TOOLKIT_I5314: make.info({ + code: 'CDK_TOOLKIT_I5314', + description: 'Initial watch deployment started', + }), + CDK_TOOLKIT_I5315: make.info({ + code: 'CDK_TOOLKIT_I5315', + description: 'Queued watch deployment started', + }), + + // Stack Monitor CDK_TOOLKIT_I5501: make.info({ code: 'CDK_TOOLKIT_I5501', description: 'Stack Monitoring: Start monitoring of a single stack', @@ -87,12 +155,22 @@ export const CODES = { interface: 'StackMonitoringControlEvent', }), - CDK_TOOLKIT_I5900: make.result({ + // Success + CDK_TOOLKIT_I5900: make.result({ code: 'CDK_TOOLKIT_I5900', description: 'Deployment results on success', interface: 'SuccessfulDeployStackResult', }), + CDK_TOOLKIT_I5901: make.info({ + code: 'CDK_TOOLKIT_I5901', + description: 'Generic deployment success messages', + }), + CDK_TOOLKIT_W5400: make.warn({ + code: 'CDK_TOOLKIT_W5400', + description: 'Hotswap disclosure message', + }), + // errors CDK_TOOLKIT_E5001: make.error({ code: 'CDK_TOOLKIT_E5001', description: 'No stacks found', @@ -109,14 +187,20 @@ export const CODES = { description: 'Provides rollback times', interface: 'Duration', }), + CDK_TOOLKIT_I6100: make.info({ + code: 'CDK_TOOLKIT_I6100', + description: 'Stack rollback progress', + interface: 'StackRollbackProgress', + }), CDK_TOOLKIT_E6001: make.error({ code: 'CDK_TOOLKIT_E6001', description: 'No stacks found', }), - CDK_TOOLKIT_E6900: make.error({ + CDK_TOOLKIT_E6900: make.error({ code: 'CDK_TOOLKIT_E6900', description: 'Rollback failed', + interface: 'ErrorPayload', }), // 7: Destroy @@ -125,18 +209,31 @@ export const CODES = { description: 'Provides destroy times', interface: 'Duration', }), - CDK_TOOLKIT_I7010: make.info({ + CDK_TOOLKIT_I7010: make.confirm({ code: 'CDK_TOOLKIT_I7010', description: 'Confirm destroy stacks', + interface: 'ConfirmationRequest', + }), + CDK_TOOLKIT_I7100: make.info({ + code: 'CDK_TOOLKIT_I7100', + description: 'Stack destroy progress', + interface: 'StackDestroyProgress', + }), + + CDK_TOOLKIT_I7900: make.result({ + code: 'CDK_TOOLKIT_I7900', + description: 'Stack deletion succeeded', + interface: 'cxapi.CloudFormationStackArtifact', }), CDK_TOOLKIT_E7010: make.error({ code: 'CDK_TOOLKIT_E7010', description: 'Action was aborted due to negative confirmation of request', }), - CDK_TOOLKIT_E7900: make.error({ + CDK_TOOLKIT_E7900: make.error({ code: 'CDK_TOOLKIT_E7900', description: 'Stack deletion failed', + interface: 'ErrorPayload', }), // 9: Bootstrap @@ -145,24 +242,45 @@ export const CODES = { description: 'Provides bootstrap times', interface: 'Duration', }), - CDK_TOOLKIT_I9900: make.info({ + CDK_TOOLKIT_I9100: make.info({ + code: 'CDK_TOOLKIT_I9100', + description: 'Bootstrap progress', + interface: 'BootstrapEnvironmentProgress', + }), + + CDK_TOOLKIT_I9900: make.result<{ environment: cxapi.Environment }>({ code: 'CDK_TOOLKIT_I9900', description: 'Bootstrap results on success', + interface: 'cxapi.Environment', }), - CDK_TOOLKIT_E9900: make.error({ + CDK_TOOLKIT_E9900: make.error({ code: 'CDK_TOOLKIT_E9900', description: 'Bootstrap failed', + interface: 'ErrorPayload', }), // Assembly codes + CDK_ASSEMBLY_I0010: make.debug({ + code: 'CDK_ASSEMBLY_I0010', + description: 'Generic environment preparation debug messages', + }), + CDK_ASSEMBLY_W0010: make.warn({ + code: 'CDK_ASSEMBLY_W0010', + description: 'Emitted if the found framework version does not support context overflow', + }), CDK_ASSEMBLY_I0042: make.debug({ code: 'CDK_ASSEMBLY_I0042', description: 'Writing updated context', interface: 'UpdatedContext', }), + CDK_ASSEMBLY_I0240: make.debug({ + code: 'CDK_ASSEMBLY_I0240', + description: 'Context lookup was stopped as no further progress was made. ', + interface: 'MissingContext', + }), CDK_ASSEMBLY_I0241: make.debug({ code: 'CDK_ASSEMBLY_I0241', - description: 'Fetching missing context', + description: 'Fetching missing context. This is an iterative message that may appear multiple times with different missing keys.', interface: 'MissingContext', }), CDK_ASSEMBLY_I1000: make.debug({ @@ -186,4 +304,33 @@ export const CODES = { description: 'Incompatible CDK CLI version. Upgrade needed.', interface: 'ErrorPayload', }), + + CDK_ASSEMBLY_I0150: make.debug({ + code: 'CDK_ASSEMBLY_I0150', + description: 'Indicates the use of a pre-synthesized cloud assembly directory', + }), + + // Assembly Annotations + CDK_ASSEMBLY_I9999: make.info({ + code: 'CDK_ASSEMBLY_I9999', + description: 'Annotations emitted by the cloud assembly', + interface: 'cxapi.SynthesisMessage', + }), + CDK_ASSEMBLY_W9999: make.warn({ + code: 'CDK_ASSEMBLY_W9999', + description: 'Warnings emitted by the cloud assembly', + interface: 'cxapi.SynthesisMessage', + }), + CDK_ASSEMBLY_E9999: make.error({ + code: 'CDK_ASSEMBLY_E9999', + description: 'Errors emitted by the cloud assembly', + interface: 'cxapi.SynthesisMessage', + }), + + // SDK codes + CDK_SDK_I0100: make.trace({ + code: 'CDK_SDK_I0100', + description: 'An SDK trace. SDK traces are emitted as traces to the IoHost, but contain the original SDK logging level.', + interface: 'SdkTrace', + }), }; diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/index.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/index.ts index c017db196..3338ca1e6 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/index.ts @@ -1,5 +1,5 @@ export * from './codes'; +export * from './io-host-wrappers'; export * from './level-priority'; -export * from './logger'; -export * from './messages'; export * from './timer'; +export * from './sdk-logger'; diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/io-host-wrappers.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/io-host-wrappers.ts new file mode 100644 index 000000000..6c7570c12 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/io-host-wrappers.ts @@ -0,0 +1,74 @@ +import type { IoMessage, IoRequest, IIoHost } from '../'; + +/** + * An IoHost wrapper that strips out ANSI colors and styles from the message before + * sending the message to the given IoHost + */ +export function withoutColor(ioHost: IIoHost): IIoHost { + return { + notify: async (msg: IoMessage) => { + await ioHost.notify({ + ...msg, + message: stripColor(msg.message), + }); + }, + requestResponse: async (msg: IoRequest) => { + return ioHost.requestResponse({ + ...msg, + message: stripColor(msg.message), + }); + }, + }; +} + +function stripColor(msg: string): string { + return msg.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ''); +} + +/** + * An IoHost wrapper that strips out emojis from the message before + * sending the message to the given IoHost + */ +export function withoutEmojis(ioHost: IIoHost): IIoHost { + return { + notify: async (msg: IoMessage) => { + await ioHost.notify({ + ...msg, + message: stripEmojis(msg.message), + }); + }, + requestResponse: async (msg: IoRequest) => { + return ioHost.requestResponse({ + ...msg, + message: stripEmojis(msg.message), + }); + }, + }; +} + +function stripEmojis(msg: string): string { + // https://www.unicode.org/reports/tr51/#def_emoji_presentation + return msg.replace(/\p{Emoji_Presentation}/gu, ''); +} + +/** + * An IoHost wrapper that trims whitespace at the beginning and end of messages. + * This is required, since after removing emojis and ANSI colors, + * we might end up with floating whitespace at either end. + */ +export function withTrimmedWhitespace(ioHost: IIoHost): IIoHost { + return { + notify: async (msg: IoMessage) => { + await ioHost.notify({ + ...msg, + message: msg.message.trim(), + }); + }, + requestResponse: async (msg: IoRequest) => { + return ioHost.requestResponse({ + ...msg, + message: msg.message.trim(), + }); + }, + }; +} diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/logger.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/logger.ts deleted file mode 100644 index 72de270b8..000000000 --- a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/logger.ts +++ /dev/null @@ -1,197 +0,0 @@ -import * as util from 'node:util'; -import type { Logger } from '@smithy/types'; -import type { IoMessage, IoMessageLevel, IoRequest, IIoHost } from '../'; -import { debug, error, info, defaultMessageCode, trace, warn } from './messages'; -import { formatSdkLoggerContent } from '../../aws-cdk'; -import type { ActionAwareIoHost, IoMessageCodeCategory } from '../../shared-private'; -import type { ToolkitAction } from '../../shared-public'; - -/** - * An IoHost wrapper that strips out ANSI colors and styles from the message before - * sending the message to the given IoHost - */ -export function withoutColor(ioHost: IIoHost): IIoHost { - return { - notify: async (msg: IoMessage) => { - await ioHost.notify({ - ...msg, - message: stripColor(msg.message), - }); - }, - requestResponse: async (msg: IoRequest) => { - return ioHost.requestResponse({ - ...msg, - message: stripColor(msg.message), - }); - }, - }; -} - -function stripColor(msg: string): string { - return msg.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ''); -} - -/** - * An IoHost wrapper that strips out emojis from the message before - * sending the message to the given IoHost - */ -export function withoutEmojis(ioHost: IIoHost): IIoHost { - return { - notify: async (msg: IoMessage) => { - await ioHost.notify({ - ...msg, - message: stripEmojis(msg.message), - }); - }, - requestResponse: async (msg: IoRequest) => { - return ioHost.requestResponse({ - ...msg, - message: stripEmojis(msg.message), - }); - }, - }; -} - -function stripEmojis(msg: string): string { - // https://www.unicode.org/reports/tr51/#def_emoji_presentation - return msg.replace(/\p{Emoji_Presentation}/gu, ''); -} - -/** - * An IoHost wrapper that trims whitespace at the beginning and end of messages. - * This is required, since after removing emojis and ANSI colors, - * we might end up with floating whitespace at either end. - */ -export function withTrimmedWhitespace(ioHost: IIoHost): IIoHost { - return { - notify: async (msg: IoMessage) => { - await ioHost.notify({ - ...msg, - message: msg.message.trim(), - }); - }, - requestResponse: async (msg: IoRequest) => { - return ioHost.requestResponse({ - ...msg, - message: msg.message.trim(), - }); - }, - }; -} - -// @todo these cannot be awaited WTF -export function asSdkLogger(ioHost: IIoHost, action: ToolkitAction): Logger { - return new class implements Logger { - // This is too much detail for our logs - public trace(..._content: any[]) { - } - public debug(..._content: any[]) { - } - - /** - * Info is called mostly (exclusively?) for successful API calls - * - * Payload: - * - * (Note the input contains entire CFN templates, for example) - * - * ``` - * { - * clientName: 'S3Client', - * commandName: 'GetBucketLocationCommand', - * input: { - * Bucket: '.....', - * ExpectedBucketOwner: undefined - * }, - * output: { LocationConstraint: 'eu-central-1' }, - * metadata: { - * httpStatusCode: 200, - * requestId: '....', - * extendedRequestId: '...', - * cfId: undefined, - * attempts: 1, - * totalRetryDelay: 0 - * } - * } - * ``` - */ - public info(...content: any[]) { - void ioHost.notify({ - action, - ...trace(`[sdk info] ${formatSdkLoggerContent(content)}`), - data: { - sdkLevel: 'info', - content, - }, - }); - } - - public warn(...content: any[]) { - void ioHost.notify({ - action, - ...trace(`[sdk warn] ${formatSdkLoggerContent(content)}`), - data: { - sdkLevel: 'warn', - content, - }, - }); - } - - /** - * Error is called mostly (exclusively?) for failing API calls - * - * Payload (input would be the entire API call arguments). - * - * ``` - * { - * clientName: 'STSClient', - * commandName: 'GetCallerIdentityCommand', - * input: {}, - * error: AggregateError [ECONNREFUSED]: - * at internalConnectMultiple (node:net:1121:18) - * at afterConnectMultiple (node:net:1688:7) { - * code: 'ECONNREFUSED', - * '$metadata': { attempts: 3, totalRetryDelay: 600 }, - * [errors]: [ [Error], [Error] ] - * }, - * metadata: { attempts: 3, totalRetryDelay: 600 } - * } - * ``` - */ - public error(...content: any[]) { - void ioHost.notify({ - action, - ...trace(`[sdk error] ${formatSdkLoggerContent(content)}`), - data: { - sdkLevel: 'error', - content, - }, - }); - } - }; -} - -/** - * Turn an ActionAwareIoHost into a logger that is compatible with older code, but doesn't support data - */ -export function asLogger(ioHost: ActionAwareIoHost, category?: IoMessageCodeCategory) { - const code = (level: IoMessageLevel) => defaultMessageCode(level, category); - - return { - trace: async (msg: string, ...args: any[]) => { - await ioHost.notify(trace(util.format(msg, args), code('trace'))); - }, - debug: async (msg: string, ...args: any[]) => { - await ioHost.notify(debug(util.format(msg, args), code('debug'))); - }, - info: async (msg: string, ...args: any[]) => { - await ioHost.notify(info(util.format(msg, args), code('info'))); - }, - warn: async (msg: string, ...args: any[]) => { - await ioHost.notify(warn(util.format(msg, args), code('warn'))); - }, - error: async (msg: string, ...args: any[]) => { - await ioHost.notify(error(util.format(msg, args), code('error'))); - }, - }; -} diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/messages.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/messages.ts deleted file mode 100644 index 4fa638621..000000000 --- a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/messages.ts +++ /dev/null @@ -1,175 +0,0 @@ -import * as chalk from 'chalk'; -import type { IoMessageCode, IoMessageLevel } from '../'; -import type { ActionLessMessage, ActionLessRequest, Optional, SimplifiedMessage, IoMessageCodeCategory } from '../../shared-private'; - -interface CodeInfo { - code: IoMessageCode; - level: IoMessageLevel; -} - -/** - * Internal helper that processes log inputs into a consistent format. - * Handles string interpolation, format strings, and object parameter styles. - * Applies optional styling and prepares the final message for logging. - */ -function formatMessage(msg: Optional, 'code'>, category: IoMessageCodeCategory = 'TOOLKIT'): ActionLessMessage { - return { - time: new Date(), - level: msg.level, - code: msg.code ?? defaultMessageCode(msg.level, category).code, - message: msg.message, - data: msg.data, - }; -} - -/** - * Build a message code from level and category. The code must be valid for this function to pass. - * Otherwise it returns a ToolkitError. - */ -export function defaultMessageCode(level: IoMessageLevel, category: IoMessageCodeCategory = 'TOOLKIT'): CodeInfo { - const levelIndicator = level === 'error' ? 'E' : - level === 'warn' ? 'W' : - 'I'; - const code = `CDK_${category}_${levelIndicator}0000` as IoMessageCode; - return { - code, - level, - }; -} - -/** - * Requests a yes/no confirmation from the IoHost. - */ -export const confirm = ( - code: CodeInfo, - question: string, - motivation: string, - defaultResponse: boolean, - concurrency?: number, -): ActionLessRequest<{ - motivation: string; - concurrency?: number; -}, boolean> => { - return prompt(code, `${chalk.cyan(question)} (y/n)?`, defaultResponse, { - motivation, - concurrency, - }); -}; - -/** - * Prompt for a response from the IoHost. - */ -export const prompt = (code: CodeInfo, message: string, defaultResponse: U, payload?: T): ActionLessRequest => { - return { - defaultResponse, - ...formatMessage({ - level: code.level, - code: code.code, - message, - data: payload, - }), - }; -}; - -/** - * Creates an error level message. - * Errors must always have a unique code. - */ -export const error = (message: string, code: CodeInfo, payload?: T) => { - return formatMessage({ - level: 'error', - code: code.code, - message, - data: payload, - }); -}; - -/** - * Creates a result level message and represents the most important message for a given action. - * - * They should be used sparsely, with an action usually having no or exactly one result. - * However actions that operate on Cloud Assemblies might include a result per Stack. - * Unlike other messages, results must always have a code and a payload. - */ -export const result = (message: string, code: CodeInfo, payload: T) => { - return formatMessage({ - level: 'result', - code: code.code, - message, - data: payload, - }); -}; - -/** - * Creates a warning level message. - */ -export const warn = (message: string, code?: CodeInfo, payload?: T) => { - return formatMessage({ - level: 'warn', - code: code?.code, - message, - data: payload, - }); -}; - -/** - * Creates an info level message. - */ -export const info = (message: string, code?: CodeInfo, payload?: T) => { - return formatMessage({ - level: 'info', - code: code?.code, - message, - data: payload, - }); -}; - -/** - * Creates a debug level message. - */ -export const debug = (message: string, code?: CodeInfo, payload?: T) => { - return formatMessage({ - level: 'debug', - code: code?.code, - message, - data: payload, - }); -}; - -/** - * Creates a trace level message. - */ -export const trace = (message: string, code?: CodeInfo, payload?: T) => { - return formatMessage({ - level: 'trace', - code: code?.code, - message, - data: payload, - }); -}; - -/** - * Creates an info level success message in green text. - * @deprecated - */ -export const success = (message: string, code?: CodeInfo, payload?: T) => { - return formatMessage({ - level: 'info', - code: code?.code, - message: chalk.green(message), - data: payload, - }); -}; - -/** - * Creates an info level message in bold text. - * @deprecated - */ -export const highlight = (message: string, code?: CodeInfo, payload?: T) => { - return formatMessage({ - level: 'info', - code: code?.code, - message: chalk.bold(message), - data: payload, - }); -}; diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/sdk-logger.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/sdk-logger.ts new file mode 100644 index 000000000..f444598c5 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/sdk-logger.ts @@ -0,0 +1,174 @@ + +import { inspect } from 'util'; +import type { Logger } from '@smithy/types'; +import { CODES } from './codes'; +import { replacerBufferWithInfo } from '../../../private/util'; +import type { ActionAwareIoHost } from '../../shared-private'; + +/** + * An SDK logging trace. + * + * Only info, warn and error level messages are emitted. + * SDK traces are emitted as traces to the IoHost, but contain the original SDK logging level. + */ +export interface SdkTrace { + /** + * The level the SDK has emitted the original message with + */ + readonly sdkLevel: 'info' | 'warn' | 'error'; + + /** + * The content of the SDK trace + * + * This will include the request and response data for API calls, including potentially sensitive information. + * + * @see https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/logging-sdk-calls.html + */ + readonly content: any; +} + +export function asSdkLogger(ioHost: ActionAwareIoHost): Logger { + return new class implements Logger { + // This is too much detail for our logs + public trace(..._content: any[]) { + } + public debug(..._content: any[]) { + } + + /** + * Info is called mostly (exclusively?) for successful API calls + * + * Payload: + * + * (Note the input contains entire CFN templates, for example) + * + * ``` + * { + * clientName: 'S3Client', + * commandName: 'GetBucketLocationCommand', + * input: { + * Bucket: '.....', + * ExpectedBucketOwner: undefined + * }, + * output: { LocationConstraint: 'eu-central-1' }, + * metadata: { + * httpStatusCode: 200, + * requestId: '....', + * extendedRequestId: '...', + * cfId: undefined, + * attempts: 1, + * totalRetryDelay: 0 + * } + * } + * ``` + */ + public info(...content: any[]) { + void ioHost.notify(CODES.CDK_SDK_I0100.msg(`[sdk info] ${formatSdkLoggerContent(content)}`, { + sdkLevel: 'info', + content, + })); + } + + public warn(...content: any[]) { + void ioHost.notify(CODES.CDK_SDK_I0100.msg(`[sdk warn] ${formatSdkLoggerContent(content)}`, { + sdkLevel: 'warn', + content, + })); + } + + /** + * Error is called mostly (exclusively?) for failing API calls + * + * Payload (input would be the entire API call arguments). + * + * ``` + * { + * clientName: 'STSClient', + * commandName: 'GetCallerIdentityCommand', + * input: {}, + * error: AggregateError [ECONNREFUSED]: + * at internalConnectMultiple (node:net:1121:18) + * at afterConnectMultiple (node:net:1688:7) { + * code: 'ECONNREFUSED', + * '$metadata': { attempts: 3, totalRetryDelay: 600 }, + * [errors]: [ [Error], [Error] ] + * }, + * metadata: { attempts: 3, totalRetryDelay: 600 } + * } + * ``` + */ + public error(...content: any[]) { + void ioHost.notify(CODES.CDK_SDK_I0100.msg(`[sdk error] ${formatSdkLoggerContent(content)}`, { + sdkLevel: 'error', + content, + })); + } + }; +} + +/** + * This can be anything. + * + * For debug, it seems to be mostly strings. + * For info, it seems to be objects. + * + * Stringify and join without separator. + */ +function formatSdkLoggerContent(content: any[]) { + if (content.length === 1) { + const apiFmt = formatApiCall(content[0]); + if (apiFmt) { + return apiFmt; + } + } + return content.map((x) => typeof x === 'string' ? x : inspect(x)).join(''); +} + +function formatApiCall(content: any): string | undefined { + if (!isSdkApiCallSuccess(content) && !isSdkApiCallError(content)) { + return undefined; + } + + const service = content.clientName.replace(/Client$/, ''); + const api = content.commandName.replace(/Command$/, ''); + + const parts = []; + if ((content.metadata?.attempts ?? 0) > 1) { + parts.push(`[${content.metadata?.attempts} attempts, ${content.metadata?.totalRetryDelay}ms retry]`); + } + + parts.push(`${service}.${api}(${JSON.stringify(content.input, replacerBufferWithInfo)})`); + + if (isSdkApiCallSuccess(content)) { + parts.push('-> OK'); + } else { + parts.push(`-> ${content.error}`); + } + + return parts.join(' '); +} + +interface SdkApiCallBase { + clientName: string; + commandName: string; + input: Record; + metadata?: { + httpStatusCode?: number; + requestId?: string; + extendedRequestId?: string; + cfId?: string; + attempts?: number; + totalRetryDelay?: number; + }; +} + +type SdkApiCallSuccess = SdkApiCallBase & { output: Record }; +type SdkApiCallError = SdkApiCallBase & { error: Error }; + +function isSdkApiCallSuccess(x: any): x is SdkApiCallSuccess { + return x && typeof x === 'object' && x.commandName && x.output; +} + +function isSdkApiCallError(x: any): x is SdkApiCallError { + return x && typeof x === 'object' && x.commandName && x.error; +} diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 9340f80a5..0d7b94d2d 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -19,8 +19,8 @@ import { type SdkConfig } from '../api/aws-auth'; import { DEFAULT_TOOLKIT_STACK_NAME, Bootstrapper, SdkProvider, SuccessfulDeployStackResult, StackCollection, Deployments, HotswapMode, ResourceMigrator, tagsForStack, CliIoHost, Concurrency, WorkGraphBuilder, AssetBuildNode, AssetPublishNode, StackNode, CloudWatchLogEventMonitor, findCloudWatchLogGroups, StackDetails } from '../api/aws-cdk'; import { ICloudAssemblySource, StackSelectionStrategy } from '../api/cloud-assembly'; import { ALL_STACKS, CloudAssemblySourceBuilder, IdentityCloudAssemblySource, StackAssembly } from '../api/cloud-assembly/private'; -import { IIoHost, IoMessageCode, IoMessageLevel } from '../api/io'; -import { asSdkLogger, Timer, confirm, error, info, success, warn, debug, result, withoutEmojis, withoutColor, withTrimmedWhitespace, CODES } from '../api/io/private'; +import { IIoHost, IoMessageLevel } from '../api/io'; +import { Timer, CODES, asSdkLogger, withoutColor, withoutEmojis, withTrimmedWhitespace } from '../api/io/private'; import { ActionAwareIoHost, withAction } from '../api/shared-private'; import { ToolkitAction, ToolkitError } from '../api/shared-public'; import { obscureTemplate, serializeStructure, validateSnsTopicArn, formatTime, formatErrorMessage } from '../private/util'; @@ -123,7 +123,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab if (!this._sdkProvider) { this._sdkProvider = await SdkProvider.withAwsCliCompatibleDefaults({ ...this.props.sdkConfig, - logger: asSdkLogger(this.ioHost, action), + logger: asSdkLogger(withAction(this.ioHost, action)), }); } @@ -153,8 +153,12 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab const limit = pLimit(20); // eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism - await Promise.all(bootstrapEnvironments.map((environment: cxapi.Environment) => limit(async () => { - await ioHost.notify(info(`${chalk.bold(environment.name)}: bootstrapping...`)); + await Promise.all(bootstrapEnvironments.map((environment: cxapi.Environment, currentIdx) => limit(async () => { + await ioHost.notify(CODES.CDK_TOOLKIT_I9100.msg(`${chalk.bold(environment.name)}: bootstrapping...`, { + total: bootstrapEnvironments.length, + current: currentIdx+1, + environment, + })); const bootstrapTimer = Timer.start(); try { @@ -173,10 +177,10 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab ? ` ✅ ${environment.name} (no changes)` : ` ✅ ${environment.name}`; - await ioHost.notify(result(chalk.green('\n' + message), CODES.CDK_TOOLKIT_I9900, { environment })); + await ioHost.notify(CODES.CDK_TOOLKIT_I9900.msg(chalk.green('\n' + message), { environment })); await bootstrapTimer.endAs(ioHost, 'bootstrap'); - } catch (e) { - await ioHost.notify(error(`\n ❌ ${chalk.bold(environment.name)} failed: ${formatErrorMessage(e)}`, CODES.CDK_TOOLKIT_E9900)); + } catch (e: any) { + await ioHost.notify(CODES.CDK_TOOLKIT_E9900.msg(`\n ❌ ${chalk.bold(environment.name)} failed: ${formatErrorMessage(e)}`, { error: e })); throw e; } }))); @@ -218,8 +222,8 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab })); } else { // not outputting template to stdout, let's explain things to the user a little bit... - await ioHost.notify(result(chalk.green(message), CODES.CDK_TOOLKIT_I1902, assemblyData)); - await ioHost.notify(info(`Supply a stack id (${stacks.stackArtifacts.map((s) => chalk.green(s.hierarchicalId)).join(', ')}) to display its template.`)); + await ioHost.notify(CODES.CDK_TOOLKIT_I1902.msg(chalk.green(message), assemblyData)); + await ioHost.notify(CODES.DEFAULT_TOOLKIT_INFO.msg(`Supply a stack id (${stacks.stackArtifacts.map((s) => chalk.green(s.hierarchicalId)).join(', ')}) to display its template.`)); } return new IdentityCloudAssemblySource(assembly.assembly); @@ -265,7 +269,6 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab const synthDuration = await synthTimer.endAs(ioHost, 'synth'); if (stackCollection.stackCount === 0) { - await ioHost.notify(error('This app contains no stacks', CODES.CDK_TOOLKIT_E5001)); await ioHost.notify(CODES.CDK_TOOLKIT_E5001.msg('This app contains no stacks')); return; } @@ -281,7 +284,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab const hotswapMode = options.hotswap ?? HotswapMode.FULL_DEPLOYMENT; if (hotswapMode !== HotswapMode.FULL_DEPLOYMENT) { - await ioHost.notify(warn([ + await ioHost.notify(CODES.CDK_TOOLKIT_W5400.msg([ '⚠️ The --hotswap and --hotswap-fallback flags deliberately introduce CloudFormation drift to speed up deployments', '⚠️ They should only be used for development - never use them for your production Stacks!', ].join('\n'))); @@ -315,7 +318,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab const deployStack = async (stackNode: StackNode) => { const stack = stackNode.stack; if (stackCollection.stackCount !== 1) { - await ioHost.notify(info(chalk.bold(stack.displayName))); + await ioHost.notify(CODES.DEFAULT_TOOLKIT_INFO.msg(chalk.bold(stack.displayName))); } if (!stack.environment) { @@ -329,11 +332,11 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab // stack is empty and doesn't exist => do nothing const stackExists = await deployments.stackExists({ stack }); if (!stackExists) { - return ioHost.notify(warn(`${chalk.bold(stack.displayName)}: stack has no resources, skipping deployment.`)); + return ioHost.notify(CODES.CDK_TOOLKIT_W5021.msg(`${chalk.bold(stack.displayName)}: stack has no resources, skipping deployment.`)); } // stack is empty, but exists => delete - await ioHost.notify(warn(`${chalk.bold(stack.displayName)}: stack has no resources, deleting existing stack.`)); + await ioHost.notify(CODES.CDK_TOOLKIT_W5022.msg(`${chalk.bold(stack.displayName)}: stack has no resources, deleting existing stack.`)); await this._destroy(assembly, 'deploy', { stacks: { patterns: [stack.hierarchicalId], strategy: StackSelectionStrategy.PATTERN_MUST_MATCH_SINGLE }, roleArn: options.roleArn, @@ -348,7 +351,10 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab if (diffRequiresApproval(currentTemplate, stack, requireApproval)) { const motivation = '"--require-approval" is enabled and stack includes security-sensitive updates.'; const question = `${motivation}\nDo you wish to deploy these changes`; - const confirmed = await ioHost.requestResponse(confirm(CODES.CDK_TOOLKIT_I5060, question, motivation, true, concurrency)); + const confirmed = await ioHost.requestResponse(CODES.CDK_TOOLKIT_I5060.req(question, { + motivation, + concurrency, + })); if (!confirmed) { throw new ToolkitError('Aborted by user'); } @@ -371,9 +377,11 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab } const stackIndex = stacks.indexOf(stack) + 1; - await ioHost.notify( - info(`${chalk.bold(stack.displayName)}: deploying... [${stackIndex}/${stackCollection.stackCount}]`), - ); + await ioHost.notify(CODES.CDK_TOOLKIT_I5100.msg(`${chalk.bold(stack.displayName)}: deploying... [${stackIndex}/${stackCollection.stackCount}]`, { + total: stackCollection.stackCount, + current: stackIndex, + stack, + })); const deployTimer = Timer.start(); let tags = options.tags; @@ -423,9 +431,12 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab const question = `${motivation}. Perform a regular deployment`; if (options.force) { - await ioHost.notify(warn(`${motivation}. Rolling back first (--force).`)); + await ioHost.notify(CODES.DEFAULT_TOOLKIT_WARN.msg(`${motivation}. Rolling back first (--force).`)); } else { - const confirmed = await ioHost.requestResponse(confirm(CODES.CDK_TOOLKIT_I5050, question, motivation, true, concurrency)); + const confirmed = await ioHost.requestResponse(CODES.CDK_TOOLKIT_I5050.req(question, { + motivation, + concurrency, + })); if (!confirmed) { throw new ToolkitError('Aborted by user'); } @@ -448,9 +459,12 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab // @todo no force here if (options.force) { - await ioHost.notify(warn(`${motivation}. Proceeding with regular deployment (--force).`)); + await ioHost.notify(CODES.DEFAULT_TOOLKIT_WARN.msg(`${motivation}. Proceeding with regular deployment (--force).`)); } else { - const confirmed = await ioHost.requestResponse(confirm(CODES.CDK_TOOLKIT_I5050, question, motivation, true, concurrency)); + const confirmed = await ioHost.requestResponse(CODES.CDK_TOOLKIT_I5050.req(question, { + motivation, + concurrency, + })); if (!confirmed) { throw new ToolkitError('Aborted by user'); } @@ -470,7 +484,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab ? ` ✅ ${stack.displayName} (no changes)` : ` ✅ ${stack.displayName}`; - await ioHost.notify(result(chalk.green('\n' + message), CODES.CDK_TOOLKIT_I5900, deployResult)); + await ioHost.notify(CODES.CDK_TOOLKIT_I5900.msg(chalk.green('\n' + message), deployResult)); deployDuration = await deployTimer.endAs(ioHost, 'deploy'); if (Object.keys(deployResult.outputs).length > 0) { @@ -481,9 +495,9 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab const value = deployResult.outputs[name]; buffer.push(`${chalk.cyan(stack.id)}.${chalk.cyan(name)} = ${chalk.underline(chalk.cyan(value))}`); } - await ioHost.notify(info(buffer.join('\n'))); + await ioHost.notify(CODES.CDK_TOOLKIT_I5901.msg(buffer.join('\n'))); } - await ioHost.notify(info(`Stack ARN:\n${deployResult.stackArn}`)); + await ioHost.notify(CODES.CDK_TOOLKIT_I5901.msg(`Stack ARN:\n${deployResult.stackArn}`)); } catch (e: any) { // It has to be exactly this string because an integration test tests for // "bold(stackname) failed: ResourceNotReady: " @@ -500,7 +514,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab foundLogGroupsResult.sdk, foundLogGroupsResult.logGroupNames, ); - await ioHost.notify(info(`The following log groups are added: ${foundLogGroupsResult.logGroupNames}`, CODES.CDK_TOOLKIT_I5031)); + await ioHost.notify(CODES.CDK_TOOLKIT_I5031.msg(`The following log groups are added: ${foundLogGroupsResult.logGroupNames}`)); } // If an outputs file has been specified, create the file path and write stack outputs to it once. @@ -515,7 +529,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab } } const duration = synthDuration.asMs + (deployDuration?.asMs ?? 0); - await ioHost.notify(info(`\n✨ Total time: ${formatTime(duration)}s\n`, CODES.CDK_TOOLKIT_I5001, { duration })); + await ioHost.notify(CODES.CDK_TOOLKIT_I5001.msg(`\n✨ Total time: ${formatTime(duration)}s\n`, { duration })); }; const assetBuildTime = options.assetBuildTime ?? AssetBuildTime.ALL_BEFORE_DEPLOY; @@ -556,7 +570,6 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab const assembly = await assemblyFromSource(cx, false); const ioHost = withAction(this.ioHost, 'watch'); const rootDir = options.watchDir ?? process.cwd(); - await ioHost.notify(debug(`root directory used for 'watch' is: ${rootDir}`)); if (options.include === undefined && options.exclude === undefined) { throw new ToolkitError( @@ -574,7 +587,6 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab rootDir, returnRootDirIfEmpty: true, }); - await ioHost.notify(debug(`'include' patterns for 'watch': ${JSON.stringify(watchIncludes)}`)); // For the "exclude" subkey under the "watch" key, // the behavior is to add some default excludes in addition to the ones specified by the user: @@ -587,7 +599,17 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab rootDir, returnRootDirIfEmpty: false, }).concat(`${outdir}/**`, '**/.*', '**/.*/**', '**/node_modules/**'); - await ioHost.notify(debug(`'exclude' patterns for 'watch': ${JSON.stringify(watchExcludes)}`)); + + // Print some debug information on computed settings + await ioHost.notify(CODES.CDK_TOOLKIT_I5310.msg([ + `root directory used for 'watch' is: ${rootDir}`, + `'include' patterns for 'watch': ${JSON.stringify(watchIncludes)}`, + `'exclude' patterns for 'watch': ${JSON.stringify(watchExcludes)}`, + ].join('\n'), { + watchDir: rootDir, + includes: watchIncludes, + excludes: watchExcludes, + })); // Since 'cdk deploy' is a relatively slow operation for a 'watch' process, // introduce a concurrency latch that tracks the state. @@ -600,22 +622,23 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab // | pre-ready | -------------> | open | | deploying | | queued | | // | | | | <------------------ | | <------------------ | | <-------------| // -------------- -------- 'cdk deploy' done -------------- 'cdk deploy' done -------------- - let latch: 'pre-ready' | 'open' | 'deploying' | 'queued' = 'pre-ready'; + type LatchState = 'pre-ready' | 'open' | 'deploying' | 'queued'; + let latch: LatchState = 'pre-ready'; const cloudWatchLogMonitor = options.traceLogs ? new CloudWatchLogEventMonitor() : undefined; const deployAndWatch = async () => { - latch = 'deploying'; + latch = 'deploying' as LatchState; cloudWatchLogMonitor?.deactivate(); await this.invokeDeployFromWatch(assembly, options, cloudWatchLogMonitor); // If latch is still 'deploying' after the 'await', that's fine, // but if it's 'queued', that means we need to deploy again - while ((latch as 'deploying' | 'queued') === 'queued') { + while (latch === 'queued') { // TypeScript doesn't realize latch can change between 'awaits', // and thinks the above 'while' condition is always 'false' without the cast latch = 'deploying'; - await ioHost.notify(info("Detected file changes during deployment. Invoking 'cdk deploy' again")); + await ioHost.notify(CODES.CDK_TOOLKIT_I5315.msg("Detected file changes during deployment. Invoking 'cdk deploy' again")); await this.invokeDeployFromWatch(assembly, options, cloudWatchLogMonitor); } latch = 'open'; @@ -629,21 +652,26 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab }) .on('ready', async () => { latch = 'open'; - await ioHost.notify(debug("'watch' received the 'ready' event. From now on, all file changes will trigger a deployment")); - await ioHost.notify(info("Triggering initial 'cdk deploy'")); + await ioHost.notify(CODES.DEFAULT_TOOLKIT_DEBUG.msg("'watch' received the 'ready' event. From now on, all file changes will trigger a deployment")); + await ioHost.notify(CODES.CDK_TOOLKIT_I5314.msg("Triggering initial 'cdk deploy'")); await deployAndWatch(); }) - .on('all', async (event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', filePath?: string) => { + .on('all', async (event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', filePath: string) => { + const watchEvent = { + event, + path: filePath, + }; if (latch === 'pre-ready') { - await ioHost.notify(info(`'watch' is observing ${event === 'addDir' ? 'directory' : 'the file'} '${filePath}' for changes`)); + await ioHost.notify(CODES.CDK_TOOLKIT_I5311.msg(`'watch' is observing ${event === 'addDir' ? 'directory' : 'the file'} '${filePath}' for changes`, watchEvent)); } else if (latch === 'open') { - await ioHost.notify(info(`Detected change to '${filePath}' (type: ${event}). Triggering 'cdk deploy'`)); + await ioHost.notify(CODES.CDK_TOOLKIT_I5312.msg(`Detected change to '${filePath}' (type: ${event}). Triggering 'cdk deploy'`, watchEvent)); await deployAndWatch(); } else { // this means latch is either 'deploying' or 'queued' latch = 'queued'; - await ioHost.notify(info( + await ioHost.notify(CODES.CDK_TOOLKIT_I5313.msg( `Detected change to '${filePath}' (type: ${event}) while 'cdk deploy' is still running. Will queue for another deployment after this one finishes'`, + watchEvent, )); } }); @@ -670,14 +698,18 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab await synthTimer.endAs(ioHost, 'synth'); if (stacks.stackCount === 0) { - await ioHost.notify(error('No stacks selected', CODES.CDK_TOOLKIT_E6001)); + await ioHost.notify(CODES.CDK_TOOLKIT_E6001.msg('No stacks selected')); return; } let anyRollbackable = false; - for (const stack of stacks.stackArtifacts) { - await ioHost.notify(info(`Rolling back ${chalk.bold(stack.displayName)}`)); + for (const [index, stack] of stacks.stackArtifacts.entries()) { + await ioHost.notify(CODES.CDK_TOOLKIT_I6100.msg(`Rolling back ${chalk.bold(stack.displayName)}`, { + total: stacks.stackCount, + current: index + 1, + stack, + })); const rollbackTimer = Timer.start(); const deployments = await this.deploymentsForAction('rollback'); try { @@ -694,7 +726,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab } await rollbackTimer.endAs(ioHost, 'rollback'); } catch (e: any) { - await ioHost.notify(error(`\n ❌ ${chalk.bold(stack.displayName)} failed: ${formatErrorMessage(e)}`, CODES.CDK_TOOLKIT_E6900)); + await ioHost.notify(CODES.CDK_TOOLKIT_E6900.msg(`\n ❌ ${chalk.bold(stack.displayName)} failed: ${formatErrorMessage(e)}`, { error: e })); throw new ToolkitError('Rollback failed (use --force to orphan failing resources)'); } } @@ -725,15 +757,19 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab const motivation = 'Destroying stacks is an irreversible action'; const question = `Are you sure you want to delete: ${chalk.red(stacks.hierarchicalIds.join(', '))}`; - const confirmed = await ioHost.requestResponse(confirm(CODES.CDK_TOOLKIT_I7010, question, motivation, true)); + const confirmed = await ioHost.requestResponse(CODES.CDK_TOOLKIT_I7010.req(question, { motivation })); if (!confirmed) { - return ioHost.notify(error('Aborted by user', CODES.CDK_TOOLKIT_E7010)); + return ioHost.notify(CODES.CDK_TOOLKIT_E7010.msg('Aborted by user')); } const destroyTimer = Timer.start(); try { for (const [index, stack] of stacks.stackArtifacts.entries()) { - await ioHost.notify(success(`${chalk.blue(stack.displayName)}: destroying... [${index + 1}/${stacks.stackCount}]`)); + await ioHost.notify(CODES.CDK_TOOLKIT_I7100.msg(chalk.green(`${chalk.blue(stack.displayName)}: destroying... [${index + 1}/${stacks.stackCount}]`), { + total: stacks.stackCount, + current: index + 1, + stack, + })); try { const deployments = await this.deploymentsForAction(action); await deployments.destroyStack({ @@ -741,9 +777,9 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab deployName: stack.stackName, roleArn: options.roleArn, }); - await ioHost.notify(success(`\n ✅ ${chalk.blue(stack.displayName)}: ${action}ed`)); - } catch (e) { - await ioHost.notify(error(`\n ❌ ${chalk.blue(stack.displayName)}: ${action} failed ${e}`, CODES.CDK_TOOLKIT_E7900)); + await ioHost.notify(CODES.CDK_TOOLKIT_I7900.msg(chalk.green(`\n ✅ ${chalk.blue(stack.displayName)}: ${action}ed`), stack)); + } catch (e: any) { + await ioHost.notify(CODES.CDK_TOOLKIT_E7900.msg(`\n ❌ ${chalk.blue(stack.displayName)}: ${action} failed ${e}`, { error: e })); throw e; } } @@ -756,21 +792,17 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab * Validate the stacks for errors and warnings according to the CLI's current settings */ private async validateStacksMetadata(stacks: StackCollection, ioHost: ActionAwareIoHost) { - // @TODO define these somewhere central - const code = (level: IoMessageLevel): IoMessageCode => { + const builder = (level: IoMessageLevel) => { switch (level) { - case 'error': return 'CDK_ASSEMBLY_E9999'; - case 'warn': return 'CDK_ASSEMBLY_W9999'; - default: return 'CDK_ASSEMBLY_I9999'; + case 'error': return CODES.CDK_ASSEMBLY_E9999; + case 'warn': return CODES.CDK_ASSEMBLY_W9999; + default: return CODES.CDK_ASSEMBLY_I9999; } }; - await stacks.validateMetadata(this.props.assemblyFailureAt, async (level, msg) => ioHost.notify({ - time: new Date(), - level, - code: code(level), - message: `[${level} at ${msg.id}] ${msg.entry.data}`, - data: msg, - })); + await stacks.validateMetadata( + this.props.assemblyFailureAt, + async (level, msg) => ioHost.notify(builder(level).msg(`[${level} at ${msg.id}] ${msg.entry.data}`, msg)), + ); } /** diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts index 4baedf29b..9b5fd4363 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts @@ -86,3 +86,24 @@ export interface ErrorPayload { */ readonly error: Error; } + +/** + * Generic payload of a simple yes/no question. + * + * The expectation is that 'yes' means moving on, + * and 'no' means aborting the current action. + */ +export interface ConfirmationRequest { + /** + * Some additional motivation for the confirmation that may be used as context for the user. + */ + readonly motivation: string; + /** + * Number of on-going concurrent operations + * If more than one operations is on-going, a client might decide that asking the user + * for input is too complex, as the confirmation might not easily be attributed to a specific request. + * + * @default - no concurrency + */ + readonly concurrency?: number; +} diff --git a/packages/@aws-cdk/toolkit-lib/scripts/gen-code-registry.ts b/packages/@aws-cdk/toolkit-lib/scripts/gen-code-registry.ts index 59e44b84c..3460355b9 100644 --- a/packages/@aws-cdk/toolkit-lib/scripts/gen-code-registry.ts +++ b/packages/@aws-cdk/toolkit-lib/scripts/gen-code-registry.ts @@ -11,7 +11,8 @@ function codesToMarkdownTable(codes: Record { - if (key !== code.code) { + // we allow DEFAULT_* as special case here + if (key !== code.code && !key.startsWith('DEFAULT_')) { throw new Error(`Code key ${key} does not match code.code ${code.code}. This is probably a typo.`); } table += `| ${code.code} | ${code.description} | ${code.level} | ${code.interface ? linkInterface(code.interface) : 'n/a'} |\n`; diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/destroy.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/destroy.test.ts index 0964053c7..3e2317c75 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/destroy.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/destroy.test.ts @@ -57,12 +57,12 @@ describe('destroy', () => { // THEN expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ action: 'destroy', - level: 'info', + level: 'result', message: expect.stringContaining(`${chalk.blue('Stack2')}${chalk.green(': destroyed')}`), })); expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ action: 'destroy', - level: 'info', + level: 'result', message: expect.stringContaining(`${chalk.blue('Stack1')}${chalk.green(': destroyed')}`), })); }); @@ -91,7 +91,11 @@ describe('destroy', () => { function successfulDestroy() { expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ action: 'destroy', - level: 'info', + level: 'result', + code: 'CDK_TOOLKIT_I7900', message: expect.stringContaining('destroyed'), + data: expect.objectContaining({ + displayName: expect.any(String), + }), })); }