Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,8 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab
return new Deployments({
sdkProvider: await this.sdkProvider(action),
toolkitStackName: this.toolkitStackName,
ioHost: this.ioHost as any, // @todo temporary while we have to separate IIoHost interfaces
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we have separate IIoHost interfaces?

Copy link
Contributor Author

@mrgrain mrgrain Feb 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because currently the toolkit depends on the CLI package (and therefore the CLI cannot depend on the toolkit). Once the repo split is complete, we will introduce a new private intermediate package that both toolkit and CLI can depend on.

action,
});
}

Expand Down
66 changes: 43 additions & 23 deletions packages/aws-cdk/lib/api/deployments/asset-publishing.ts
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kaizencc special attention

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
type ISecretsManagerClient,
} from 'cdk-assets';
import type { SDK } from '..';
import { debug, error, info } from '../../logging';
import { formatMessage } from '../../cli/messages';
import { IIoHost, IoMessageLevel, ToolkitAction } from '../../toolkit/cli-io-host';
import { ToolkitError } from '../../toolkit/error';
import type { SdkProvider } from '../aws-auth';
import { Mode } from '../plugin';
Expand Down Expand Up @@ -56,7 +57,7 @@ export async function publishAssets(

const publisher = new AssetPublishing(manifest, {
aws: new PublishingAws(sdk, targetEnv),
progressListener: new PublishingProgressListener(),
progressListener: new PublishingProgressListener(ioHost, action),
throwOnError: false,
publishInParallel: options.parallel ?? true,
buildAssets: true,
Expand Down Expand Up @@ -163,30 +164,49 @@ export class PublishingAws implements IAws {
}
}

function ignore() {
}

export const EVENT_TO_LOGGER: Record<EventType, (x: string) => void> = {
build: debug,
cached: debug,
check: debug,
debug,
fail: error,
found: debug,
start: info,
success: info,
upload: debug,
shell_open: debug,
shell_stderr: ignore,
shell_stdout: ignore,
shell_close: ignore,
export const EVENT_TO_LEVEL: Record<EventType, IoMessageLevel | false> = {
build: 'debug',
cached: 'debug',
check: 'debug',
debug: 'debug',
fail: 'error',
found: 'debug',
start: 'info',
success: 'info',
upload: 'debug',
shell_open: 'debug',
shell_stderr: false,
shell_stdout: false,
shell_close: false,
};

class PublishingProgressListener implements IPublishProgressListener {
constructor() {}
export abstract class BasePublishProgressListener implements IPublishProgressListener {
protected readonly ioHost: IIoHost;
protected readonly action: ToolkitAction;

constructor(ioHost: IIoHost, action: ToolkitAction) {
this.ioHost = ioHost;
this.action = action;
}

protected abstract getMessage(type: EventType, event: IPublishProgress): string;

public onPublishEvent(type: EventType, event: IPublishProgress): void {
const handler = EVENT_TO_LOGGER[type];
handler(`[${event.percentComplete}%] ${type}: ${event.message}`);
const level = EVENT_TO_LEVEL[type];
if (level) {
void this.ioHost.notify(
formatMessage({
level,
action: this.action,
message: this.getMessage(type, event),
}),
);
}
}
}

class PublishingProgressListener extends BasePublishProgressListener {
protected getMessage(type: EventType, event: IPublishProgress): string {
return `[${event.percentComplete}%] ${type}: ${event.message}`;
}
}
29 changes: 17 additions & 12 deletions packages/aws-cdk/lib/api/deployments/deploy-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import {
import { ChangeSetDeploymentMethod, DeploymentMethod } from './deployment-method';
import { DeployStackResult, SuccessfulDeployStackResult } from './deployment-result';
import { tryHotswapDeployment } from './hotswap-deployments';
import { debug, info, warning } from '../../logging';
import { debug } from '../../cli/messages';
import { IIoHost, ToolkitAction } from '../../toolkit/cli-io-host';
import { ToolkitError } from '../../toolkit/error';
import { formatErrorMessage } from '../../util/error';
import type { SDK, SdkProvider, ICloudFormationClient } from '../aws-auth';
Expand Down Expand Up @@ -381,6 +382,8 @@ class FullCloudFormationDeployment {
private readonly stackArtifact: cxapi.CloudFormationStackArtifact,
private readonly stackParams: ParameterValues,
private readonly bodyParameter: TemplateBodyParameter,
private readonly ioHost: IIoHost,
private readonly action: ToolkitAction,
) {
this.cfn = options.sdk.cloudFormation();
this.stackName = options.deployName ?? stackArtifact.stackName;
Expand Down Expand Up @@ -713,13 +716,15 @@ async function canSkipDeploy(
deployStackOptions: DeployStackOptions,
cloudFormationStack: CloudFormationStack,
parameterChanges: ParameterChanges,
ioHost: IIoHost,
action: ToolkitAction,
): Promise<boolean> {
const deployName = deployStackOptions.deployName || deployStackOptions.stack.stackName;
debug(`${deployName}: checking if we can skip deploy`);
await ioHost.notify(debug(action, `${deployName}: checking if we can skip deploy`));

// Forced deploy
if (deployStackOptions.force) {
debug(`${deployName}: forced deployment`);
await ioHost.notify(debug(action, `${deployName}: forced deployment`));
return false;
}

Expand All @@ -728,53 +733,53 @@ async function canSkipDeploy(
deployStackOptions.deploymentMethod?.method === 'change-set' &&
deployStackOptions.deploymentMethod.execute === false
) {
debug(`${deployName}: --no-execute, always creating change set`);
await ioHost.notify(debug(action, `${deployName}: --no-execute, always creating change set`));
return false;
}

// No existing stack
if (!cloudFormationStack.exists) {
debug(`${deployName}: no existing stack`);
await ioHost.notify(debug(action, `${deployName}: no existing stack`));
return false;
}

// Template has changed (assets taken into account here)
if (JSON.stringify(deployStackOptions.stack.template) !== JSON.stringify(await cloudFormationStack.template())) {
debug(`${deployName}: template has changed`);
await ioHost.notify(debug(action, `${deployName}: template has changed`));
return false;
}

// Tags have changed
if (!compareTags(cloudFormationStack.tags, deployStackOptions.tags ?? [])) {
debug(`${deployName}: tags have changed`);
await ioHost.notify(debug(action, `${deployName}: tags have changed`));
return false;
}

// Notification arns have changed
if (!arrayEquals(cloudFormationStack.notificationArns, deployStackOptions.notificationArns ?? [])) {
debug(`${deployName}: notification arns have changed`);
await ioHost.notify(debug(action, `${deployName}: notification arns have changed`));
return false;
}

// Termination protection has been updated
if (!!deployStackOptions.stack.terminationProtection !== !!cloudFormationStack.terminationProtection) {
debug(`${deployName}: termination protection has been updated`);
await ioHost.notify(debug(action, `${deployName}: termination protection has been updated`));
return false;
}

// Parameters have changed
if (parameterChanges) {
if (parameterChanges === 'ssm') {
debug(`${deployName}: some parameters come from SSM so we have to assume they may have changed`);
await ioHost.notify(debug(action, `${deployName}: some parameters come from SSM so we have to assume they may have changed`));
} else {
debug(`${deployName}: parameters have changed`);
await ioHost.notify(debug(action, `${deployName}: parameters have changed`));
}
return false;
}

// Existing stack is in a failed state
if (cloudFormationStack.stackStatus.isFailure) {
debug(`${deployName}: stack is in a failure state`);
await ioHost.notify(debug(action, `${deployName}: stack is in a failure state`));
return false;
}

Expand Down
54 changes: 32 additions & 22 deletions packages/aws-cdk/lib/api/deployments/deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as cdk_assets from 'cdk-assets';
import * as chalk from 'chalk';
import { AssetManifestBuilder } from './asset-manifest-builder';
import {
EVENT_TO_LOGGER,
BasePublishProgressListener,
PublishingAws,
} from './asset-publishing';
import { determineAllowCrossAccountAssetPublishing } from './checks';
Expand All @@ -24,7 +24,8 @@ import {
loadCurrentTemplateWithNestedStacks,
type RootTemplateWithNestedStacks,
} from './nested-stack-helpers';
import { debug, warning } from '../../logging';
import { debug, warn } from '../../cli/messages';
import { IIoHost, ToolkitAction } from '../../toolkit/cli-io-host';
import { ToolkitError } from '../../toolkit/error';
import { formatErrorMessage } from '../../util/error';
import type { SdkProvider } from '../aws-auth/sdk-provider';
Expand Down Expand Up @@ -322,9 +323,10 @@ export interface StackExistsOptions {
}

export interface DeploymentsProps {
sdkProvider: SdkProvider;
readonly sdkProvider: SdkProvider;
readonly toolkitStackName?: string;
readonly quiet?: boolean;
readonly ioHost: IIoHost;
readonly action: ToolkitAction;
}

/**
Expand Down Expand Up @@ -358,10 +360,16 @@ export class Deployments {
private readonly publisherCache = new Map<cdk_assets.AssetManifest, cdk_assets.AssetPublishing>();

private _allowCrossAccountAssetPublishing: boolean | undefined;

private readonly ioHost: IIoHost;
private readonly action: ToolkitAction;

constructor(private readonly props: DeploymentsProps) {
this.assetSdkProvider = props.sdkProvider;
this.deployStackSdkProvider = props.sdkProvider;
this.envs = new EnvironmentAccess(props.sdkProvider, props.toolkitStackName ?? DEFAULT_TOOLKIT_STACK_NAME);
this.ioHost = props.ioHost;
this.action = props.action;
}

/**
Expand All @@ -380,15 +388,15 @@ export class Deployments {
}

public async readCurrentTemplate(stackArtifact: cxapi.CloudFormationStackArtifact): Promise<Template> {
debug(`Reading existing template for stack ${stackArtifact.displayName}.`);
await this.ioHost.notify(debug(this.action, `Reading existing template for stack ${stackArtifact.displayName}.`));
const env = await this.envs.accessStackForLookupBestEffort(stackArtifact);
return loadCurrentTemplate(stackArtifact, env.sdk);
}

public async resourceIdentifierSummaries(
stackArtifact: cxapi.CloudFormationStackArtifact,
): Promise<ResourceIdentifierSummaries> {
debug(`Retrieving template summary for stack ${stackArtifact.displayName}.`);
await this.ioHost.notify(debug(this.action, `Retrieving template summary for stack ${stackArtifact.displayName}.`));
// Currently, needs to use `deploy-role` since it may need to read templates in the staging
// bucket which have been encrypted with a KMS key (and lookup-role may not read encrypted things)
const env = await this.envs.accessStackForReadOnlyStackOperations(stackArtifact);
Expand Down Expand Up @@ -418,7 +426,7 @@ export class Deployments {

const response = await cfn.getTemplateSummary(cfnParam);
if (!response.ResourceIdentifierSummaries) {
debug('GetTemplateSummary API call did not return "ResourceIdentifierSummaries"');
await this.ioHost.notify(debug(this.action, 'GetTemplateSummary API call did not return "ResourceIdentifierSummaries"'));
}
return response.ResourceIdentifierSummaries ?? [];
}
Expand Down Expand Up @@ -506,11 +514,11 @@ export class Deployments {

switch (cloudFormationStack.stackStatus.rollbackChoice) {
case RollbackChoice.NONE:
warning(`Stack ${deployName} does not need a rollback: ${cloudFormationStack.stackStatus}`);
await this.ioHost.notify(warn(this.action, `Stack ${deployName} does not need a rollback: ${cloudFormationStack.stackStatus}`));
return { notInRollbackableState: true };

case RollbackChoice.START_ROLLBACK:
debug(`Initiating rollback of stack ${deployName}`);
await this.ioHost.notify(debug(this.action, `Initiating rollback of stack ${deployName}`));
await cfn.rollbackStack({
StackName: deployName,
RoleARN: executionRoleArn,
Expand All @@ -536,7 +544,7 @@ export class Deployments {
}

const skipDescription = resourcesToSkip.length > 0 ? ` (orphaning: ${resourcesToSkip.join(', ')})` : '';
warning(`Continuing rollback of stack ${deployName}${skipDescription}`);
await this.ioHost.notify(warn(this.action, `Continuing rollback of stack ${deployName}${skipDescription}`));
await cfn.continueUpdateRollback({
StackName: deployName,
ClientRequestToken: randomUUID(),
Expand All @@ -546,9 +554,10 @@ export class Deployments {
break;

case RollbackChoice.ROLLBACK_FAILED:
warning(
await this.ioHost.notify(warn(
this.action,
`Stack ${deployName} failed creation and rollback. This state cannot be rolled back. You can recreate this stack by running 'cdk deploy'.`,
);
));
return { notInRollbackableState: true };

default:
Expand Down Expand Up @@ -725,7 +734,7 @@ export class Deployments {
// The AssetPublishing class takes care of role assuming etc, so it's okay to
// give it a direct `SdkProvider`.
aws: new PublishingAws(this.assetSdkProvider, env),
progressListener: new ParallelSafeAssetProgress(prefix, this.props.quiet ?? false),
progressListener: new ParallelSafeAssetProgress(prefix, this.ioHost, this.action),
});
this.publisherCache.set(assetManifest, publisher);
return publisher;
Expand All @@ -735,15 +744,16 @@ export class Deployments {
/**
* Asset progress that doesn't do anything with percentages (currently)
*/
class ParallelSafeAssetProgress implements cdk_assets.IPublishProgressListener {
constructor(
private readonly prefix: string,
private readonly quiet: boolean,
) {}

public onPublishEvent(type: cdk_assets.EventType, event: cdk_assets.IPublishProgress): void {
const handler = this.quiet && type !== 'fail' ? debug : EVENT_TO_LOGGER[type];
handler(`${this.prefix}${type}: ${event.message}`);
class ParallelSafeAssetProgress extends BasePublishProgressListener {
private readonly prefix: string;

constructor(prefix: string, ioHost: IIoHost, action: ToolkitAction) {
super(ioHost, action);
this.prefix = prefix;
}

protected getMessage(type: cdk_assets.EventType, event: cdk_assets.IPublishProgress): string {
return `${this.prefix}${type}: ${event.message}`;
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/aws-cdk/lib/api/resource-import/importer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as promptly from 'promptly';
import { error, info, warn } from '../../cli/messages';
import { IIoHost, ToolkitAction } from '../../toolkit/cli-io-host';
import { ToolkitError } from '../../toolkit/error';
import { assertIsSuccessfulDeployStackResult, Deployments, DeploymentMethod, ResourceIdentifierProperties, ResourcesToImport } from '../deployments';
import { assertIsSuccessfulDeployStackResult, type Deployments, DeploymentMethod, ResourceIdentifierProperties, ResourcesToImport } from '../deployments';
import { Tag } from '../tags';
import { StackActivityProgress } from '../util/cloudformation/stack-activity-monitor';

Expand Down
2 changes: 1 addition & 1 deletion packages/aws-cdk/lib/api/resource-import/migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ImportDeploymentOptions, ResourceImporter } from './importer';
import { info } from '../../cli/messages';
import type { IIoHost, ToolkitAction } from '../../toolkit/cli-io-host';
import { StackCollection } from '../cxapp/cloud-assembly';
import { Deployments, ResourcesToImport } from '../deployments';
import type { Deployments, ResourcesToImport } from '../deployments';
import { formatTime } from '../util/string-manipulation';

export interface ResourceMigratorProps {
Expand Down
2 changes: 1 addition & 1 deletion packages/aws-cdk/lib/cli/cdk-toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from '../api/cxapp/cloud-assembly';
import { CloudExecutable } from '../api/cxapp/cloud-executable';
import { environmentsFromDescriptors, globEnvironmentsFromStacks, looksLikeGlob } from '../api/cxapp/environments';
import { Deployments, DeploymentMethod, SuccessfulDeployStackResult, createDiffChangeSet } from '../api/deployments';
import { type Deployments, DeploymentMethod, SuccessfulDeployStackResult, createDiffChangeSet } from '../api/deployments';
import { GarbageCollector } from '../api/garbage-collection/garbage-collector';
import { HotswapMode, HotswapPropertyOverrides, EcsHotswapProperties } from '../api/hotswap/common';
import { findCloudWatchLogGroups } from '../api/logs/find-cloudwatch-logs';
Expand Down
7 changes: 6 additions & 1 deletion packages/aws-cdk/lib/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,12 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
const toolkitStackName: string = ToolkitInfo.determineName(configuration.settings.get(['toolkitStackName']));
debug(`Toolkit stack: ${chalk.bold(toolkitStackName)}`);

const cloudFormation = new Deployments({ sdkProvider, toolkitStackName });
const cloudFormation = new Deployments({
sdkProvider,
toolkitStackName,
ioHost,
action: ioHost.currentAction,
});

if (args.all && args.STACKS) {
throw new ToolkitError('You must either specify a list of Stacks or the `--all` argument');
Expand Down
2 changes: 1 addition & 1 deletion packages/aws-cdk/lib/cli/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type SimplifiedMessage<T> = Omit<IoMessage<T>, 'time'>;
* Handles string interpolation, format strings, and object parameter styles.
* Applies optional styling and prepares the final message for logging.
*/
function formatMessage<T>(msg: Optional<SimplifiedMessage<T>, 'code'>, category: IoMessageCodeCategory = 'TOOLKIT'): IoMessage<T> {
export function formatMessage<T>(msg: Optional<SimplifiedMessage<T>, 'code'>, category: IoMessageCodeCategory = 'TOOLKIT'): IoMessage<T> {
return {
time: new Date(),
level: msg.level,
Expand Down
Loading
Loading