diff --git a/.projenrc.ts b/.projenrc.ts index bf8c8d83f..6c98f9a23 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -705,6 +705,7 @@ const tmpToolkitHelpers = configureProject( 'archiver', 'glob', 'semver', + 'uuid', 'yaml@^1', ], tsconfig: { diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/.projen/deps.json b/packages/@aws-cdk/tmp-toolkit-helpers/.projen/deps.json index a728e3640..5033e4172 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/.projen/deps.json +++ b/packages/@aws-cdk/tmp-toolkit-helpers/.projen/deps.json @@ -116,6 +116,10 @@ "name": "semver", "type": "runtime" }, + { + "name": "uuid", + "type": "runtime" + }, { "name": "yaml", "version": "^1", diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/.projen/tasks.json b/packages/@aws-cdk/tmp-toolkit-helpers/.projen/tasks.json index 4263e6770..b534ad6ab 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/.projen/tasks.json +++ b/packages/@aws-cdk/tmp-toolkit-helpers/.projen/tasks.json @@ -37,7 +37,7 @@ }, "steps": [ { - "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --no-deprecated --dep=dev,peer,prod,optional --filter=@cdklabs/eslint-plugin,@stylistic/eslint-plugin,@types/archiver,@types/jest,@types/semver,eslint-config-prettier,eslint-import-resolver-typescript,eslint-plugin-import,eslint-plugin-jest,eslint-plugin-jsdoc,eslint-plugin-prettier,fast-check,jest,projen,ts-jest,archiver,glob,semver" + "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --no-deprecated --dep=dev,peer,prod,optional --filter=@cdklabs/eslint-plugin,@stylistic/eslint-plugin,@types/archiver,@types/jest,@types/semver,eslint-config-prettier,eslint-import-resolver-typescript,eslint-plugin-import,eslint-plugin-jest,eslint-plugin-jsdoc,eslint-plugin-prettier,fast-check,jest,projen,ts-jest,archiver,glob,semver,uuid" } ] }, diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/package.json b/packages/@aws-cdk/tmp-toolkit-helpers/package.json index bc4033479..1cc255a69 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/package.json +++ b/packages/@aws-cdk/tmp-toolkit-helpers/package.json @@ -59,6 +59,7 @@ "archiver": "^7.0.1", "glob": "^11.0.1", "semver": "^7.7.1", + "uuid": "^11.1.0", "yaml": "^1" }, "keywords": [ diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/cloud-assembly/index.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/cloud-assembly/index.ts new file mode 100644 index 000000000..99fd72d8d --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/cloud-assembly/index.ts @@ -0,0 +1 @@ +export * from './stack-selector'; diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/cloud-assembly/stack-selector.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/cloud-assembly/stack-selector.ts new file mode 100644 index 000000000..ee50183e5 --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/cloud-assembly/stack-selector.ts @@ -0,0 +1,100 @@ +/** + * Which stacks should be selected from a cloud assembly + */ +export enum StackSelectionStrategy { + /** + * Returns all stacks in the app regardless of patterns, + * including stacks inside nested assemblies. + */ + ALL_STACKS = 'all-stacks', + + /** + * Returns all stacks in the main (top level) assembly only. + */ + MAIN_ASSEMBLY = 'main-assembly', + + /** + * If the assembly includes a single stack, returns it. + * Otherwise throws an exception. + */ + ONLY_SINGLE = 'only-single', + + /** + * Return stacks matched by patterns. + * If no stacks are found, execution is halted successfully. + * Most likely you don't want to use this but `StackSelectionStrategy.MUST_MATCH_PATTERN` + */ + PATTERN_MATCH = 'pattern-match', + + /** + * Return stacks matched by patterns. + * Throws an exception if the patterns don't match at least one stack in the assembly. + */ + PATTERN_MUST_MATCH = 'pattern-must-match', + + /** + * Returns if exactly one stack is matched by the pattern(s). + * Throws an exception if no stack, or more than exactly one stack are matched. + */ + PATTERN_MUST_MATCH_SINGLE = 'pattern-must-match-single', +} + +/** + * When selecting stacks, what other stacks to include because of dependencies + */ +export enum ExpandStackSelection { + /** + * Don't select any extra stacks + */ + NONE = 'none', + + /** + * Include stacks that this stack depends on + */ + UPSTREAM = 'upstream', + + /** + * Include stacks that depend on this stack + */ + DOWNSTREAM = 'downstream', + + /** + * @TODO + * Include both directions. + * I.e. stacks that this stack depends on, and stacks that depend on this stack. + */ + // FULL = 'full', +} + +/** + * A specification of which stacks should be selected + */ +export interface StackSelector { + /** + * The behavior if if no selectors are provided. + */ + strategy: StackSelectionStrategy; + + /** + * A list of patterns to match the stack hierarchical ids + * Only used with `PATTERN_*` selection strategies. + */ + patterns?: string[]; + + /** + * Expand the selection to upstream/downstream stacks. + * @default ExpandStackSelection.None only select the specified/matched stacks + */ + expand?: ExpandStackSelection; + + /** + * By default, we throw an exception if the assembly contains no stacks. + * Set to `false`, to halt execution for empty assemblies without error. + * + * Note that actions can still throw if a stack selection result is empty, + * but the assembly contains stacks in principle. + * + * @default true + */ + failOnEmpty?: boolean; +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/index.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/index.ts index 44fa3fca3..f7878ba07 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/index.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/index.ts @@ -1,2 +1,3 @@ +export * from './cloud-assembly'; export * from './io'; export * from './toolkit-error'; diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/index.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/index.ts index 556994ed7..e74eadf55 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/index.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/index.ts @@ -2,4 +2,3 @@ export * from './io-host'; export * from './io-message'; export * from './toolkit-action'; export * from './payloads'; -export * from './messages'; diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/io-message.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/io-message.ts index 19c896580..546beeec9 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/io-message.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/io-message.ts @@ -59,6 +59,16 @@ export interface IoMessage { */ readonly message: string; + /** + * Identifies the message span, this message belongs to. + * + * A message span, groups multiple messages together that semantically related to the same operation. + * This is an otherwise meaningless identifier. + * + * A message without a `spanId`, does not belong to a span. + */ + readonly span?: string; + /** * The data attached to the message. */ diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/deploy.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/deploy.ts index b3b7bf364..47eb25211 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/deploy.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/deploy.ts @@ -1,4 +1,5 @@ import type { CloudFormationStackArtifact } from '@aws-cdk/cx-api'; +import type { IManifestEntry } from 'cdk-assets'; import type { PermissionChangeType } from './diff'; import type { ConfirmationRequest } from './types'; @@ -55,3 +56,46 @@ export interface NeedRollbackFirstDeployStackResult { export interface ReplacementRequiresRollbackStackResult { readonly type: 'replacement-requires-rollback'; } + +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; +} + +/** + * Payload for a yes/no confirmation in deploy. Includes information on + * what kind of change is being made. + */ +export interface DeployConfirmationRequest extends ConfirmationRequest { + /** + * The type of change being made to the IAM permissions. + */ + readonly permissionChangeType: PermissionChangeType; +} + +export interface BuildAsset { + /** + * The asset that is build + */ + readonly asset: IManifestEntry; +} + +export interface PublishAsset { + + /** + * The asset that is published + */ + readonly asset: IManifestEntry; +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/destroy.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/destroy.ts index 3aa0eb328..ebf1ec098 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/destroy.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/destroy.ts @@ -1,5 +1,12 @@ import type { CloudFormationStackArtifact } from '@aws-cdk/cx-api'; +export interface StackDestroy { + /** + * The stacks that will be destroyed + */ + readonly stacks: CloudFormationStackArtifact[]; +} + export interface StackDestroyProgress { /** * The total number of stacks being destroyed diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/index.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/index.ts index ec206a970..ae8a4d77b 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/index.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/index.ts @@ -6,6 +6,7 @@ export * from './sdk-trace'; export * from './context'; export * from './rollback'; export * from './stack-activity'; +export * from './synth'; export * from './types'; export * from './progress'; export * from './watch'; diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/stack-activity.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/stack-activity.ts index 1740601ce..e194d13f4 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/stack-activity.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/stack-activity.ts @@ -1,5 +1,5 @@ -import { type MetadataEntry } from '@aws-cdk/cloud-assembly-schema'; -import { type CloudFormationStackArtifact } from '@aws-cdk/cx-api'; +import type { MetadataEntry } from '@aws-cdk/cloud-assembly-schema'; +import type { CloudFormationStackArtifact } from '@aws-cdk/cx-api'; import type { StackEvent } from '@aws-sdk/client-cloudformation'; import type { StackProgress } from './progress'; diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/synth.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/synth.ts new file mode 100644 index 000000000..fbe368722 --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/synth.ts @@ -0,0 +1,8 @@ +import type { StackSelector } from '../../cloud-assembly/stack-selector'; + +export interface StackSelectionDetails { + /** + * The selected stacks, if any + */ + readonly stacks: StackSelector; +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/watch.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/watch.ts index efcdab095..507c86d30 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/watch.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/watch.ts @@ -1,4 +1,3 @@ - /** * The computed file watch settings */ diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/private/index.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/private/index.ts index 547552a4d..1ebdccec9 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/private/index.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/private/index.ts @@ -1,4 +1,6 @@ export * from './io-helper'; export * from './level-priority'; +export * from './span'; export * from './message-maker'; +export * from './messages'; export * from './types'; diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/private/io-helper.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/private/io-helper.ts index e1ded895f..fef8c06df 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/private/io-helper.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/private/io-helper.ts @@ -1,39 +1,59 @@ import type { IIoHost } from '../io-host'; import type { IoMessage, IoRequest } from '../io-message'; import type { ToolkitAction } from '../toolkit-action'; +import type { SpanEnd, SpanDefinition } from './span'; +import { SpanMaker } from './span'; -export type Optional = Pick, K> & Omit; -export type SimplifiedMessage = Pick, 'level' | 'code' | 'message' | 'data'>; export type ActionLessMessage = Omit, 'action'>; export type ActionLessRequest = Omit, 'action'>; /** - * Helper for IO messaging. - * - * Wraps a client provided IoHost and provides additional features & services to toolkit internal classes. + * A class containing helper tools to interact with IoHost */ -export interface IoHelper extends IIoHost { - notify(msg: ActionLessMessage): Promise; - notify(msg: ActionLessMessage): Promise; - requestResponse(msg: ActionLessRequest): Promise; +export class IoHelper implements IIoHost { + public static fromIoHost(ioHost: IIoHost, action: ToolkitAction) { + return new IoHelper(ioHost, action); + } + + private readonly ioHost: IIoHost; + private readonly action: ToolkitAction; + + private constructor(ioHost: IIoHost, action: ToolkitAction) { + this.ioHost = ioHost; + this.action = action; + } + + /** + * Forward a message to the IoHost, while injection the current action + */ + public notify(msg: ActionLessMessage): Promise { + return this.ioHost.notify({ + ...msg, + action: this.action, + }); + } + + /** + * Forward a request to the IoHost, while injection the current action + */ + public requestResponse(msg: ActionLessRequest): Promise { + return this.ioHost.requestResponse({ + ...msg, + action: this.action, + }); + } + + /** + * Create a new marker from a given registry entry + */ + public span(definition: SpanDefinition) { + return new SpanMaker(this, definition); + } } /** * Wraps an IoHost and creates an IoHelper from it */ export function asIoHelper(ioHost: IIoHost, action: ToolkitAction): IoHelper { - return { - notify: async (msg: Omit, 'action'>) => { - await ioHost.notify({ - ...msg, - action, - }); - }, - requestResponse: async (msg: Omit, 'action'>) => { - return ioHost.requestResponse({ - ...msg, - action, - }); - }, - }; + return IoHelper.fromIoHost(ioHost, action); } diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/messages.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/private/messages.ts similarity index 77% rename from packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/messages.ts rename to packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/private/messages.ts index ff52fa60c..41d153270 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/messages.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/private/messages.ts @@ -1,15 +1,17 @@ import type * as cxapi from '@aws-cdk/cx-api'; -import type { BootstrapEnvironmentProgress } from './payloads/bootstrap-environment-progress'; -import type { MissingContext, UpdatedContext } from './payloads/context'; -import type { DeployConfirmationRequest, StackDeployProgress, SuccessfulDeployStackResult } from './payloads/deploy'; -import type { StackDestroyProgress } from './payloads/destroy'; -import type { StackDetailsPayload } from './payloads/list'; -import type { StackRollbackProgress } from './payloads/rollback'; -import type { SdkTrace } from './payloads/sdk-trace'; -import type { StackActivity, StackMonitoringControlEvent } from './payloads/stack-activity'; -import type { AssemblyData, ConfirmationRequest, Duration, ErrorPayload, StackAndAssemblyData } from './payloads/types'; -import type { FileWatchEvent, WatchSettings } from './payloads/watch'; -import * as make from './private'; +import * as make from './message-maker'; +import type { SpanDefinition } from './span'; +import type { BootstrapEnvironmentProgress } from '../payloads/bootstrap-environment-progress'; +import type { MissingContext, UpdatedContext } from '../payloads/context'; +import type { BuildAsset, DeployConfirmationRequest, PublishAsset, StackDeployProgress, SuccessfulDeployStackResult } from '../payloads/deploy'; +import type { StackDestroy, StackDestroyProgress } from '../payloads/destroy'; +import type { StackDetailsPayload } from '../payloads/list'; +import type { StackRollbackProgress } from '../payloads/rollback'; +import type { SdkTrace } from '../payloads/sdk-trace'; +import type { StackActivity, StackMonitoringControlEvent } from '../payloads/stack-activity'; +import type { StackSelectionDetails } from '../payloads/synth'; +import type { AssemblyData, ConfirmationRequest, Duration, ErrorPayload, StackAndAssemblyData } from '../payloads/types'; +import type { FileWatchEvent, WatchSettings } from '../payloads/watch'; /** * We have a rough system by which we assign message codes: @@ -38,6 +40,11 @@ export const IO = { description: 'Provides synthesis times.', interface: 'Duration', }), + CDK_TOOLKIT_I1001: make.trace({ + code: 'CDK_TOOLKIT_I1001', + description: 'Cloud Assembly synthesis is starting', + interface: 'StackSelectionDetails', + }), CDK_TOOLKIT_I1901: make.result({ code: 'CDK_TOOLKIT_I1901', description: 'Provides stack data', @@ -108,6 +115,29 @@ export const IO = { interface: 'StackDeployProgress', }), + // Assets + CDK_TOOLKIT_I5210: make.trace({ + code: 'CDK_TOOLKIT_I5210', + description: 'Started building a specific asset', + interface: 'BuildAsset', + }), + CDK_TOOLKIT_I5211: make.trace({ + code: 'CDK_TOOLKIT_I5211', + description: 'Building the asset has completed', + interface: 'Duration', + }), + + CDK_TOOLKIT_I5220: make.trace({ + code: 'CDK_TOOLKIT_I5220', + description: 'Started publishing a specific asset', + interface: 'PublishAsset', + }), + CDK_TOOLKIT_I5221: make.trace({ + code: 'CDK_TOOLKIT_I5221', + description: 'Publishing the asset has completed', + interface: 'Duration', + }), + // Watch CDK_TOOLKIT_I5310: make.debug({ code: 'CDK_TOOLKIT_I5310', @@ -209,6 +239,11 @@ export const IO = { description: 'Provides destroy times', interface: 'Duration', }), + CDK_TOOLKIT_I7001: make.trace({ + code: 'CDK_TOOLKIT_I7001', + description: 'Provides destroy time for a single stack', + interface: 'Duration', + }), CDK_TOOLKIT_I7010: make.confirm({ code: 'CDK_TOOLKIT_I7010', description: 'Confirm destroy stacks', @@ -219,6 +254,11 @@ export const IO = { description: 'Stack destroy progress', interface: 'StackDestroyProgress', }), + CDK_TOOLKIT_I7101: make.trace({ + code: 'CDK_TOOLKIT_I7101', + description: 'Start stack destroying', + interface: 'StackDestroy', + }), CDK_TOOLKIT_I7900: make.result({ code: 'CDK_TOOLKIT_I7900', @@ -338,3 +378,48 @@ export const IO = { interface: 'SdkTrace', }), }; + +////////////////////////////////////////////////////////////////////////////////////////// + +export const SPAN = { + SYNTH_ASSEMBLY: { + name: 'Synthesis', + start: IO.CDK_TOOLKIT_I1001, + end: IO.CDK_TOOLKIT_I1000, + }, + DEPLOY_STACK: { + name: 'Deployment', + start: IO.CDK_TOOLKIT_I5100, + end: IO.CDK_TOOLKIT_I5001, + }, + ROLLBACK_STACK: { + name: 'Rollback', + start: IO.CDK_TOOLKIT_I6100, + end: IO.CDK_TOOLKIT_I6000, + }, + DESTROY_STACK: { + name: 'Destroy', + start: IO.CDK_TOOLKIT_I7100, + end: IO.CDK_TOOLKIT_I7001, + }, + DESTROY_ACTION: { + name: 'Destroy', + start: IO.CDK_TOOLKIT_I7101, + end: IO.CDK_TOOLKIT_I7000, + }, + BOOTSTRAP_SINGLE: { + name: 'Bootstrap', + start: IO.CDK_TOOLKIT_I9100, + end: IO.CDK_TOOLKIT_I9000, + }, + BUILD_ASSET: { + name: 'Build Asset', + start: IO.CDK_TOOLKIT_I5210, + end: IO.CDK_TOOLKIT_I5211, + }, + PUBLISH_ASSET: { + name: 'Publish Asset', + start: IO.CDK_TOOLKIT_I5220, + end: IO.CDK_TOOLKIT_I5221, + }, +} satisfies Record>; diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/private/span.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/private/span.ts new file mode 100644 index 000000000..e93f2b707 --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/private/span.ts @@ -0,0 +1,180 @@ +import * as util from 'node:util'; +import * as uuid from 'uuid'; +import type { ActionLessMessage, IoHelper } from './io-helper'; +import type { IoMessageMaker } from './message-maker'; +import { formatTime } from '../../../util'; +import type { Duration } from '../payloads/types'; + +export interface SpanEnd { + readonly duration: number; +} + +/** + * Describes a specific span + * + * A span definition is a pair of `IoMessageMaker`s to create a start and end message of the span respectively. + * It also has a display name, that is used for auto-generated message text when they are not provided. + */ +export interface SpanDefinition { + readonly name: string; + readonly start: IoMessageMaker; + readonly end: IoMessageMaker; +} + +/** + * Used in conditional types to check if a type (e.g. after omitting fields) is an empty object + * This is needed because counter-intuitive neither `object` nor `{}` represent that. + */ +type EmptyObject = { + [index: string | number | symbol]: never; +} + +/** + * Helper type to force a parameter to be not present of the computed type is an empty object + */ +type VoidWhenEmpty = T extends EmptyObject ? void : T + +/** + * Helper type to force a parameter to be an empty object if the computed type is an empty object + * This is weird, but some computed types (e.g. using `Omit`) don't end up enforcing this. + */ +type ForceEmpty = T extends EmptyObject ? EmptyObject : T + +/** + * Make some properties optional + */ +type Optional = Pick, K> & Omit; + +/** + * Ending the span returns the observed duration + */ +interface ElapsedTime { + readonly asMs: number; + readonly asSec: number; +} + +/** + * A message span that can be ended and read times from + */ +export interface IMessageSpan { + /** + * Get the time elapsed since the start + */ + elapsedTime(): Promise; + /** + * Sends a simple, generic message with the current timing + * For more complex intermediate messages, get the `elapsedTime` and use `notify` + */ + timing(maker: IoMessageMaker, message?: string): Promise; + /** + * Sends an arbitrary intermediate message as part of the span + */ + notify(message: ActionLessMessage): Promise; + /** + * End the span with a payload + */ + end(payload: VoidWhenEmpty>): Promise; + /** + * End the span with a payload, overwriting + */ + end(payload: VoidWhenEmpty>): Promise; + /** + * End the span with a message and payload + */ + end(message: string, payload: ForceEmpty>): Promise; +} + +/** + * Helper class to make spans around blocks of work + * + * Blocks are enclosed by a start and end message. + * All messages of the span share a unique id. + * The end message contains the time passed between start and end. + */ +export class SpanMaker { + private readonly definition: SpanDefinition; + private readonly ioHelper: IoHelper; + + public constructor(ioHelper: IoHelper, definition: SpanDefinition) { + this.definition = definition; + this.ioHelper = ioHelper; + } + + /** + * Starts the span and initially notifies the IoHost + * @returns a message span + */ + public async begin(payload: VoidWhenEmpty): Promise>; + public async begin(message: string, payload: S): Promise>; + public async begin(first: any, second?: S): Promise> { + const spanId = uuid.v4(); + const startTime = new Date().getTime(); + + const notify = (msg: ActionLessMessage): Promise => { + return this.ioHelper.notify(withSpanId(spanId, msg)); + }; + + const startMsg = second ? first : 'Starting %s ...'; + const startPayload = second ?? first; + + await notify(this.definition.start.msg( + util.format(startMsg, this.definition.name), + startPayload, + )); + + const timingMsg = '\n✨ %s time: %ds\n'; + const time = () => { + const elapsedTime = new Date().getTime() - startTime; + return { + asMs: elapsedTime, + asSec: formatTime(elapsedTime), + }; + }; + + return { + elapsedTime: async (msg?: ActionLessMessage): Promise => { + if (msg) { + await notify({ + ...msg, + data: msg.data, + }); + } + return time(); + }, + + notify: async(msg: ActionLessMessage): Promise => { + await notify(msg); + }, + + timing: async(maker: IoMessageMaker, message?: string): Promise => { + const duration = time(); + const endMsg = message ? message : timingMsg; + await notify(maker.msg(util.format(endMsg, this.definition.name, duration.asSec), { + duration: duration.asMs, + })); + return duration; + }, + + end: async (a: any, b?: ForceEmpty>): Promise => { + const duration = time(); + const endMsg = b ? a : timingMsg; + const endPayload = b ?? a; + + await notify(this.definition.end.msg( + util.format(endMsg, this.definition.name, duration.asSec), { + duration: duration.asMs, + ...endPayload, + } as E)); + + return duration; + }, + }; + } +} + +function withSpanId(span: string, message: ActionLessMessage): ActionLessMessage { + return { + ...message, + span, + }; +} diff --git a/packages/@aws-cdk/toolkit-lib/build-tools/bundle.mjs b/packages/@aws-cdk/toolkit-lib/build-tools/bundle.mjs index 287195c46..2c3c14dea 100644 --- a/packages/@aws-cdk/toolkit-lib/build-tools/bundle.mjs +++ b/packages/@aws-cdk/toolkit-lib/build-tools/bundle.mjs @@ -30,10 +30,13 @@ const bundleDeclarations = async (entryPoints) => { return Promise.all(files); } +// for the shared public API we also need to bundle the types +const declarations = bundleDeclarations(['lib/api/shared-public.ts']); + // This is a build script, we are fine // eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism -await Promise.all([ +const resources = Promise.all([ copyFromCli(['build-info.json']), copyFromCli(['/db.json.gz']), copyFromCli(['lib', 'index_bg.wasm']), @@ -44,7 +47,7 @@ await Promise.all([ ]); // bundle entrypoints from the library packages -await esbuild.build({ +const bundle = esbuild.build({ outdir: 'lib', entryPoints: [ 'lib/api/aws-cdk.ts', @@ -59,5 +62,9 @@ await esbuild.build({ bundle: true, }); -// for the shared public API we also need to bundle the types -await bundleDeclarations(['lib/api/shared-public.ts']); +// Do all the work in parallel +await Promise.all([ + bundle, + resources, + declarations +]); diff --git a/packages/@aws-cdk/toolkit-lib/docs/message-registry.md b/packages/@aws-cdk/toolkit-lib/docs/message-registry.md index 90b756295..745b7562a 100644 --- a/packages/@aws-cdk/toolkit-lib/docs/message-registry.md +++ b/packages/@aws-cdk/toolkit-lib/docs/message-registry.md @@ -10,6 +10,7 @@ group: Documents | `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` | {@link Duration} | +| `CDK_TOOLKIT_I1001` | Cloud Assembly synthesis is starting | `trace` | {@link StackSelectionDetails} | | `CDK_TOOLKIT_I1901` | Provides stack data | `result` | {@link StackAndAssemblyData} | | `CDK_TOOLKIT_I1902` | Successfully deployed stacks | `result` | {@link AssemblyData} | | `CDK_TOOLKIT_I2901` | Provides details on the selected stacks and their dependencies | `result` | {@link StackDetailsPayload} | @@ -23,6 +24,10 @@ group: Documents | `CDK_TOOLKIT_I5050` | Confirm rollback during deployment | `info` | {@link ConfirmationRequest} | | `CDK_TOOLKIT_I5060` | Confirm deploy security sensitive changes | `info` | {@link DeployConfirmationRequest} | | `CDK_TOOLKIT_I5100` | Stack deploy progress | `info` | {@link StackDeployProgress} | +| `CDK_TOOLKIT_I5210` | Started building a specific asset | `trace` | {@link BuildAsset} | +| `CDK_TOOLKIT_I5211` | Building the asset has completed | `trace` | {@link Duration} | +| `CDK_TOOLKIT_I5220` | Started publishing a specific asset | `trace` | {@link PublishAsset} | +| `CDK_TOOLKIT_I5221` | Publishing the asset has completed | `trace` | {@link Duration} | | `CDK_TOOLKIT_I5310` | The computed settings used for file watching | `debug` | {@link WatchSettings} | | `CDK_TOOLKIT_I5311` | File watching started | `info` | {@link FileWatchEvent} | | `CDK_TOOLKIT_I5312` | File event detected, starting deployment | `info` | {@link FileWatchEvent} | @@ -42,8 +47,10 @@ group: Documents | `CDK_TOOLKIT_E6001` | No stacks found | `error` | n/a | | `CDK_TOOLKIT_E6900` | Rollback failed | `error` | {@link ErrorPayload} | | `CDK_TOOLKIT_I7000` | Provides destroy times | `info` | {@link Duration} | +| `CDK_TOOLKIT_I7001` | Provides destroy time for a single stack | `trace` | {@link Duration} | | `CDK_TOOLKIT_I7010` | Confirm destroy stacks | `info` | {@link ConfirmationRequest} | | `CDK_TOOLKIT_I7100` | Stack destroy progress | `info` | {@link StackDestroyProgress} | +| `CDK_TOOLKIT_I7101` | Start stack destroying | `trace` | {@link StackDestroy} | | `CDK_TOOLKIT_I7900` | Stack deletion succeeded | `result` | [cxapi.CloudFormationStackArtifact](https://docs.aws.amazon.com/cdk/api/v2/docs/@aws-cdk_cx-api.CloudFormationStackArtifact.html) | | `CDK_TOOLKIT_E7010` | Action was aborted due to negative confirmation of request | `error` | n/a | | `CDK_TOOLKIT_E7900` | Stack deletion failed | `error` | {@link ErrorPayload} | diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/index.ts b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/index.ts index 7ab0af5dc..73219a68a 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/index.ts @@ -1,5 +1,4 @@ export * from '../../api/shared-public'; export * from './source-builder'; -export * from './stack-selector'; export * from './types'; diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/stack-selector.ts b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/stack-selector.ts index ee50183e5..e98a64727 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/stack-selector.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/stack-selector.ts @@ -1,100 +1,2 @@ -/** - * Which stacks should be selected from a cloud assembly - */ -export enum StackSelectionStrategy { - /** - * Returns all stacks in the app regardless of patterns, - * including stacks inside nested assemblies. - */ - ALL_STACKS = 'all-stacks', - - /** - * Returns all stacks in the main (top level) assembly only. - */ - MAIN_ASSEMBLY = 'main-assembly', - - /** - * If the assembly includes a single stack, returns it. - * Otherwise throws an exception. - */ - ONLY_SINGLE = 'only-single', - - /** - * Return stacks matched by patterns. - * If no stacks are found, execution is halted successfully. - * Most likely you don't want to use this but `StackSelectionStrategy.MUST_MATCH_PATTERN` - */ - PATTERN_MATCH = 'pattern-match', - - /** - * Return stacks matched by patterns. - * Throws an exception if the patterns don't match at least one stack in the assembly. - */ - PATTERN_MUST_MATCH = 'pattern-must-match', - - /** - * Returns if exactly one stack is matched by the pattern(s). - * Throws an exception if no stack, or more than exactly one stack are matched. - */ - PATTERN_MUST_MATCH_SINGLE = 'pattern-must-match-single', -} - -/** - * When selecting stacks, what other stacks to include because of dependencies - */ -export enum ExpandStackSelection { - /** - * Don't select any extra stacks - */ - NONE = 'none', - - /** - * Include stacks that this stack depends on - */ - UPSTREAM = 'upstream', - - /** - * Include stacks that depend on this stack - */ - DOWNSTREAM = 'downstream', - - /** - * @TODO - * Include both directions. - * I.e. stacks that this stack depends on, and stacks that depend on this stack. - */ - // FULL = 'full', -} - -/** - * A specification of which stacks should be selected - */ -export interface StackSelector { - /** - * The behavior if if no selectors are provided. - */ - strategy: StackSelectionStrategy; - - /** - * A list of patterns to match the stack hierarchical ids - * Only used with `PATTERN_*` selection strategies. - */ - patterns?: string[]; - - /** - * Expand the selection to upstream/downstream stacks. - * @default ExpandStackSelection.None only select the specified/matched stacks - */ - expand?: ExpandStackSelection; - - /** - * By default, we throw an exception if the assembly contains no stacks. - * Set to `false`, to halt execution for empty assemblies without error. - * - * Note that actions can still throw if a stack selection result is empty, - * but the assembly contains stacks in principle. - * - * @default true - */ - failOnEmpty?: boolean; -} +export type { StackSelector } from '../shared-public'; +export { ExpandStackSelection, StackSelectionStrategy } from '../shared-public'; 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 811fb65da..4c85611aa 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,4 +1,4 @@ -export * from '../../shared-public'; +export { IO, SPAN } from '../../shared-private'; export * from './io-host-wrappers'; -export * from './timer'; export * from './sdk-logger'; + 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 index c0d036fa8..976f1026d 100644 --- 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 @@ -3,7 +3,7 @@ import { inspect } from 'util'; import type { Logger } from '@smithy/types'; import { replacerBufferWithInfo } from '../../../private/util'; import type { IoHelper } from '../../shared-private'; -import { IO } from '../../shared-public'; +import { IO } from '../../shared-private'; export function asSdkLogger(ioHost: IoHelper): Logger { return new class implements Logger { diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/timer.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/timer.ts deleted file mode 100644 index 28547a6cc..000000000 --- a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/timer.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { format } from 'util'; -import { formatTime } from '../../../private/util'; -import type { IoHelper } from '../../shared-private'; -import { IO } from '../../shared-public'; - -/** - * Helper class to measure the time of code. - */ -export class Timer { - /** - * Start the timer. - * @return the timer instance - */ - public static start(): Timer { - return new Timer(); - } - - private readonly startTime: number; - - private constructor() { - this.startTime = new Date().getTime(); - } - - /** - * End the current timer. - * @returns the elapsed time - */ - public end() { - const elapsedTime = new Date().getTime() - this.startTime; - return { - asMs: elapsedTime, - asSec: formatTime(elapsedTime), - }; - } - - /** - * Ends the current timer as a specified timing and notifies the IoHost. - * @returns the elapsed time - */ - public async endAs(ioHost: IoHelper, type: 'synth' | 'deploy' | 'rollback' | 'destroy' | 'bootstrap') { - const duration = this.end(); - await ioHost.notify(timerMessage(type, duration)); - return duration; - } -} - -function timerMessage(type: 'synth' | 'deploy' | 'rollback'| 'destroy' | 'bootstrap', duration: { - asMs: number; - asSec: number; -}) { - const message = `\n✨ %s time: ${duration.asSec}s\n`; - const payload = { duration: duration.asMs }; - - switch (type) { - case 'synth': return IO.CDK_TOOLKIT_I1000.msg(format(message, 'Synthesis'), payload); - case 'deploy': return IO.CDK_TOOLKIT_I5000.msg(format(message, 'Deployment'), payload); - case 'rollback': return IO.CDK_TOOLKIT_I6000.msg(format(message, 'Rollback'), payload); - case 'destroy': return IO.CDK_TOOLKIT_I7000.msg(format(message, 'Destroy'), payload); - case 'bootstrap': return IO.CDK_TOOLKIT_I9000.msg(format(message, 'Bootstrap'), payload); - } -} diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index eef1d0f56..1120777eb 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -24,7 +24,7 @@ import { StackSelectionStrategy } from '../api/cloud-assembly'; import type { StackAssembly } from '../api/cloud-assembly/private'; import { ALL_STACKS, CloudAssemblySourceBuilder, IdentityCloudAssemblySource } from '../api/cloud-assembly/private'; import type { IIoHost, IoMessageLevel } from '../api/io'; -import { Timer, IO, asSdkLogger, withoutColor, withoutEmojis, withTrimmedWhitespace } from '../api/io/private'; +import { IO, SPAN, asSdkLogger, withoutColor, withoutEmojis, withTrimmedWhitespace } from '../api/io/private'; import type { IoHelper } from '../api/shared-private'; import { asIoHelper } from '../api/shared-private'; import type { AssemblyData, StackDetails, ToolkitAction } from '../api/shared-public'; @@ -158,12 +158,12 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab // eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism await Promise.all(bootstrapEnvironments.map((environment: cxapi.Environment, currentIdx) => limit(async () => { - await ioHelper.notify(IO.CDK_TOOLKIT_I9100.msg(`${chalk.bold(environment.name)}: bootstrapping...`, { - total: bootstrapEnvironments.length, - current: currentIdx+1, - environment, - })); - const bootstrapTimer = Timer.start(); + const bootstrapSpan = await ioHelper.span(SPAN.BOOTSTRAP_SINGLE) + .begin(`${chalk.bold(environment.name)}: bootstrapping...`, { + total: bootstrapEnvironments.length, + current: currentIdx+1, + environment, + }); try { const bootstrapResult = await bootstrapper.bootstrapEnvironment( @@ -182,7 +182,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab : ` ✅ ${environment.name}`; await ioHelper.notify(IO.CDK_TOOLKIT_I9900.msg(chalk.green('\n' + message), { environment })); - await bootstrapTimer.endAs(ioHelper, 'bootstrap'); + await bootstrapSpan.end(); } catch (e: any) { await ioHelper.notify(IO.CDK_TOOLKIT_E9900.msg(`\n ❌ ${chalk.bold(environment.name)} failed: ${formatErrorMessage(e)}`, { error: e })); throw e; @@ -195,12 +195,13 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab */ public async synth(cx: ICloudAssemblySource, options: SynthOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, 'synth'); - const synthTimer = Timer.start(); + const selectStacks = options.stacks ?? ALL_STACKS; + const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); const assembly = await assemblyFromSource(cx); - const stacks = assembly.selectStacksV2(options.stacks ?? ALL_STACKS); + const stacks = assembly.selectStacksV2(selectStacks); const autoValidateStacks = options.validateStacks ? [assembly.selectStacksForValidation()] : []; await this.validateStacksMetadata(stacks.concat(...autoValidateStacks), ioHelper); - await synthTimer.endAs(ioHelper, 'synth'); + await synthSpan.end(); // if we have a single stack, print it to STDOUT const message = `Successfully synthesized to ${chalk.blue(path.resolve(stacks.assembly.directory))}`; @@ -240,10 +241,11 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab */ public async list(cx: ICloudAssemblySource, options: ListOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, 'list'); - const synthTimer = Timer.start(); + const selectStacks = options.stacks ?? ALL_STACKS; + const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); const assembly = await assemblyFromSource(cx); - const stackCollection = await assembly.selectStacksV2(options.stacks ?? ALL_STACKS); - await synthTimer.endAs(ioHelper, 'synth'); + const stackCollection = await assembly.selectStacksV2(selectStacks); + await synthSpan.end(); const stacks = stackCollection.withDependencies(); const message = stacks.map(s => s.id).join('\n'); @@ -267,10 +269,11 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab */ private async _deploy(assembly: StackAssembly, action: 'deploy' | 'watch', options: ExtendedDeployOptions = {}) { const ioHelper = asIoHelper(this.ioHost, action); - const synthTimer = Timer.start(); - const stackCollection = assembly.selectStacksV2(options.stacks ?? ALL_STACKS); + const selectStacks = options.stacks ?? ALL_STACKS; + const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); + const stackCollection = assembly.selectStacksV2(selectStacks); await this.validateStacksMetadata(stackCollection, ioHelper); - const synthDuration = await synthTimer.endAs(ioHelper, 'synth'); + const synthDuration = await synthSpan.end(); if (stackCollection.stackCount === 0) { await ioHelper.notify(IO.CDK_TOOLKIT_E5001.msg('This app contains no stacks')); @@ -297,6 +300,9 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab const outputsFile = options.outputsFile; const buildAsset = async (assetNode: AssetBuildNode) => { + const buildAssetSpan = await ioHelper.span(SPAN.BUILD_ASSET).begin({ + asset: assetNode.asset, + }); await deployments.buildSingleAsset( assetNode.assetManifestArtifact, assetNode.assetManifest, @@ -307,15 +313,20 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab stackName: assetNode.parentStack.stackName, }, ); + await buildAssetSpan.end(); }; const publishAsset = async (assetNode: AssetPublishNode) => { + const publishAssetSpan = await ioHelper.span(SPAN.PUBLISH_ASSET).begin({ + asset: assetNode.asset, + }); await deployments.publishSingleAsset(assetNode.assetManifest, assetNode.asset, { stack: assetNode.parentStack, roleArn: options.roleArn, stackName: assetNode.parentStack.stackName, forcePublish: options.force, }); + await publishAssetSpan.end(); }; const deployStack = async (stackNode: StackNode) => { @@ -378,12 +389,12 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab } const stackIndex = stacks.indexOf(stack) + 1; - await ioHelper.notify(IO.CDK_TOOLKIT_I5100.msg(`${chalk.bold(stack.displayName)}: deploying... [${stackIndex}/${stackCollection.stackCount}]`, { - total: stackCollection.stackCount, - current: stackIndex, - stack, - })); - const deployTimer = Timer.start(); + const deploySpan = await ioHelper.span(SPAN.DEPLOY_STACK) + .begin(`${chalk.bold(stack.displayName)}: deploying... [${stackIndex}/${stackCollection.stackCount}]`, { + total: stackCollection.stackCount, + current: stackIndex, + stack, + }); let tags = options.tags; if (!tags || tags.length === 0) { @@ -486,7 +497,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab : ` ✅ ${stack.displayName}`; await ioHelper.notify(IO.CDK_TOOLKIT_I5900.msg(chalk.green('\n' + message), deployResult)); - deployDuration = await deployTimer.endAs(ioHelper, 'deploy'); + deployDuration = await deploySpan.timing(IO.CDK_TOOLKIT_I5000); if (Object.keys(deployResult.outputs).length > 0) { const buffer = ['Outputs:']; @@ -530,7 +541,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab } } const duration = synthDuration.asMs + (deployDuration?.asMs ?? 0); - await ioHelper.notify(IO.CDK_TOOLKIT_I5001.msg(`\n✨ Total time: ${formatTime(duration)}s\n`, { duration })); + await deploySpan.end(`\n✨ Total time: ${formatTime(duration)}s\n`, { duration }); }; const assetBuildTime = options.assetBuildTime ?? AssetBuildTime.ALL_BEFORE_DEPLOY; @@ -693,10 +704,10 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab */ private async _rollback(assembly: StackAssembly, action: 'rollback' | 'deploy' | 'watch', options: RollbackOptions): Promise { const ioHelper = asIoHelper(this.ioHost, action); - const synthTimer = Timer.start(); + const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: options.stacks }); const stacks = assembly.selectStacksV2(options.stacks); await this.validateStacksMetadata(stacks, ioHelper); - await synthTimer.endAs(ioHelper, 'synth'); + await synthSpan.end(); if (stacks.stackCount === 0) { await ioHelper.notify(IO.CDK_TOOLKIT_E6001.msg('No stacks selected')); @@ -706,12 +717,11 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab let anyRollbackable = false; for (const [index, stack] of stacks.stackArtifacts.entries()) { - await ioHelper.notify(IO.CDK_TOOLKIT_I6100.msg(`Rolling back ${chalk.bold(stack.displayName)}`, { + const rollbackSpan = await ioHelper.span(SPAN.ROLLBACK_STACK).begin(`Rolling back ${chalk.bold(stack.displayName)}`, { total: stacks.stackCount, current: index + 1, stack, - })); - const rollbackTimer = Timer.start(); + }); const deployments = await this.deploymentsForAction('rollback'); try { const stackResult = await deployments.rollbackStack({ @@ -725,7 +735,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab if (!stackResult.notInRollbackableState) { anyRollbackable = true; } - await rollbackTimer.endAs(ioHelper, 'rollback'); + await rollbackSpan.end(); } catch (e: any) { await ioHelper.notify(IO.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)'); @@ -751,10 +761,10 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab */ private async _destroy(assembly: StackAssembly, action: 'deploy' | 'destroy', options: DestroyOptions): Promise { const ioHelper = asIoHelper(this.ioHost, action); - const synthTimer = Timer.start(); + const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: options.stacks }); // The stacks will have been ordered for deployment, so reverse them for deletion. const stacks = await assembly.selectStacksV2(options.stacks).reversed(); - await synthTimer.endAs(ioHelper, 'synth'); + await synthSpan.end(); const motivation = 'Destroying stacks is an irreversible action'; const question = `Are you sure you want to delete: ${chalk.red(stacks.hierarchicalIds.join(', '))}`; @@ -763,15 +773,18 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab return ioHelper.notify(IO.CDK_TOOLKIT_E7010.msg('Aborted by user')); } - const destroyTimer = Timer.start(); + const destroySpan = await ioHelper.span(SPAN.DESTROY_ACTION).begin({ + stacks: stacks.stackArtifacts, + }); try { for (const [index, stack] of stacks.stackArtifacts.entries()) { - await ioHelper.notify(IO.CDK_TOOLKIT_I7100.msg(chalk.green(`${chalk.blue(stack.displayName)}: destroying... [${index + 1}/${stacks.stackCount}]`), { - total: stacks.stackCount, - current: index + 1, - stack, - })); try { + const singleDestroySpan = await ioHelper.span(SPAN.DESTROY_STACK) + .begin(chalk.green(`${chalk.blue(stack.displayName)}: destroying... [${index + 1}/${stacks.stackCount}]`), { + total: stacks.stackCount, + current: index + 1, + stack, + }); const deployments = await this.deploymentsForAction(action); await deployments.destroyStack({ stack, @@ -779,13 +792,14 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab roleArn: options.roleArn, }); await ioHelper.notify(IO.CDK_TOOLKIT_I7900.msg(chalk.green(`\n ✅ ${chalk.blue(stack.displayName)}: ${action}ed`), stack)); + await singleDestroySpan.end(); } catch (e: any) { await ioHelper.notify(IO.CDK_TOOLKIT_E7900.msg(`\n ❌ ${chalk.blue(stack.displayName)}: ${action} failed ${e}`, { error: e })); throw e; } } } finally { - await destroyTimer.endAs(ioHelper, 'destroy'); + await destroySpan.end(); } } 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 950325d12..57b24e32e 100644 --- a/packages/@aws-cdk/toolkit-lib/scripts/gen-code-registry.ts +++ b/packages/@aws-cdk/toolkit-lib/scripts/gen-code-registry.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as util from 'util'; -import { IO } from '../lib/api/shared-public'; +import { IO } from '../lib/api/shared-private'; function codesToMarkdownTable(codes: Record