Skip to content

feat(toolkit-lib): report hotswap messages into a message span #247

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 18, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -1,4 +1,30 @@
import type { PropertyDifference, Resource } from '@aws-cdk/cloudformation-diff';
import type * as cxapi from '@aws-cdk/cx-api';

/**
* A resource affected by a change
*/
export interface AffectedResource {
/**
* The logical ID of the affected resource in the template
*/
readonly logicalId: string;
/**
* The CloudFormation type of the resource
* This could be a custom type.
*/
readonly resourceType: string;
/**
* The friendly description of the affected resource
*/
readonly description?: string;
/**
* The physical name of the resource when deployed.
*
* A physical name is not always available, e.g. new resources will not have one until after the deployment
*/
readonly physicalName?: string;
}

/**
* Represents a change in a resource
Expand All @@ -22,9 +48,28 @@ export interface ResourceChange {
readonly propertyUpdates: Record<string, PropertyDifference<unknown>>;
}

/**
* A change that can be hotswapped
*/
export interface HotswappableChange {
/**
* The resource change that is causing the hotswap.
*/
readonly cause: ResourceChange;
}

/**
* Information about a hotswap deployment
*/
export interface HotswapDeployment {
/**
* The stack that's currently being deployed
*/
readonly stack: cxapi.CloudFormationStackArtifact;

/**
* The mode the hotswap deployment was initiated with.
*/
readonly mode: 'hotswap-only' | 'fall-back';
}

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { BootstrapEnvironmentProgress } from '../payloads/bootstrap-environ
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 { HotswapDeployment } from '../payloads/hotswap';
import type { StackDetailsPayload } from '../payloads/list';
import type { CloudWatchLogEvent, CloudWatchLogMonitorControlEvent } from '../payloads/logs-monitor';
import type { StackRollbackProgress } from '../payloads/rollback';
Expand Down Expand Up @@ -188,6 +189,16 @@ export const IO = {
}),

// Hotswap (54xx)
CDK_TOOLKIT_I5400: make.trace<HotswapDeployment>({
code: 'CDK_TOOLKIT_I5400',
description: 'Starting a hotswap deployment',
interface: 'HotswapDeployment',
}),
CDK_TOOLKIT_I5410: make.info<Duration>({
code: 'CDK_TOOLKIT_I5410',
description: 'Hotswap deployment has ended, a full deployment might still follow if needed',
interface: 'Duration',
}),

// Stack Monitor (55xx)
CDK_TOOLKIT_I5501: make.info<StackMonitoringControlEvent>({
Expand Down Expand Up @@ -443,4 +454,9 @@ export const SPAN = {
start: IO.CDK_TOOLKIT_I5220,
end: IO.CDK_TOOLKIT_I5221,
},
HOTSWAP: {
name: 'hotswap-deployment',
start: IO.CDK_TOOLKIT_I5400,
end: IO.CDK_TOOLKIT_I5410,
},
} satisfies Record<string, SpanDefinition<any, any>>;
2 changes: 2 additions & 0 deletions packages/@aws-cdk/toolkit-lib/docs/message-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ group: Documents
| `CDK_TOOLKIT_I5313` | File event detected during active deployment, changes are queued | `info` | {@link FileWatchEvent} |
| `CDK_TOOLKIT_I5314` | Initial watch deployment started | `info` | n/a |
| `CDK_TOOLKIT_I5315` | Queued watch deployment started | `info` | n/a |
| `CDK_TOOLKIT_I5400` | Starting a hotswap deployment | `trace` | {@link HotswapDeployment} |
| `CDK_TOOLKIT_I5410` | Hotswap deployment has ended, a full deployment might still follow if needed | `info` | {@link Duration} |
| `CDK_TOOLKIT_I5501` | Stack Monitoring: Start monitoring of a single stack | `info` | {@link StackMonitoringControlEvent} |
| `CDK_TOOLKIT_I5502` | Stack Monitoring: Activity event for a single stack | `info` | {@link StackActivity} |
| `CDK_TOOLKIT_I5503` | Stack Monitoring: Finished monitoring of a single stack | `info` | {@link StackMonitoringControlEvent} |
Expand Down
76 changes: 39 additions & 37 deletions packages/aws-cdk/lib/api/deployments/deploy-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ import {
import type { ChangeSetDeploymentMethod, DeploymentMethod } from './deployment-method';
import type { DeployStackResult, SuccessfulDeployStackResult } from './deployment-result';
import { tryHotswapDeployment } from './hotswap-deployments';
import type { IoHelper } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private';
import { debug, info, warn } from '../../cli/messages';
import { IO, type IoHelper } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private';
import { ToolkitError } from '../../toolkit/error';
import { formatErrorMessage } from '../../util';
import type { SDK, SdkProvider, ICloudFormationClient } from '../aws-auth';
Expand Down Expand Up @@ -214,7 +213,7 @@ export async function deployStack(options: DeployStackOptions, ioHelper: IoHelpe
let cloudFormationStack = await CloudFormationStack.lookup(cfn, deployName);

if (cloudFormationStack.stackStatus.isCreationFailure) {
await ioHelper.notify(debug(
await ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(
`Found existing stack ${deployName} that had previously failed creation. Deleting it before attempting to re-create it.`,
));
await cfn.deleteStack({ StackName: deployName });
Expand Down Expand Up @@ -253,11 +252,11 @@ export async function deployStack(options: DeployStackOptions, ioHelper: IoHelpe
const hotswapPropertyOverrides = options.hotswapPropertyOverrides ?? new HotswapPropertyOverrides();

if (await canSkipDeploy(options, cloudFormationStack, stackParams.hasChanges(cloudFormationStack.parameters), ioHelper)) {
await ioHelper.notify(debug(`${deployName}: skipping deployment (use --force to override)`));
await ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`${deployName}: skipping deployment (use --force to override)`));
// if we can skip deployment and we are performing a hotswap, let the user know
// that no hotswap deployment happened
if (hotswapMode !== HotswapMode.FULL_DEPLOYMENT) {
await ioHelper.notify(info(
await ioHelper.notify(IO.DEFAULT_TOOLKIT_INFO.msg(
format(
`\n ${ICON} %s\n`,
chalk.bold('hotswap deployment skipped - no changes were detected (use --force to override)'),
Expand All @@ -271,7 +270,7 @@ export async function deployStack(options: DeployStackOptions, ioHelper: IoHelpe
stackArn: cloudFormationStack.stackId,
};
} else {
await ioHelper.notify(debug(`${deployName}: deploying...`));
await ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`${deployName}: deploying...`));
}

const bodyParameter = await makeBodyParameter(
Expand All @@ -285,7 +284,7 @@ export async function deployStack(options: DeployStackOptions, ioHelper: IoHelpe
try {
bootstrapStackName = (await options.envResources.lookupToolkit()).stackName;
} catch (e) {
await ioHelper.notify(debug(`Could not determine the bootstrap stack name: ${e}`));
await ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`Could not determine the bootstrap stack name: ${e}`));
}
await publishAssets(legacyAssets.toManifest(stackArtifact.assembly.directory), options.sdkProvider, stackEnv, {
parallel: options.assetParallelism,
Expand All @@ -301,27 +300,30 @@ export async function deployStack(options: DeployStackOptions, ioHelper: IoHelpe
stackParams.values,
cloudFormationStack,
stackArtifact,
hotswapMode, hotswapPropertyOverrides,
hotswapMode,
hotswapPropertyOverrides,
);

if (hotswapDeploymentResult) {
return hotswapDeploymentResult;
}
await ioHelper.notify(info(format(

await ioHelper.notify(IO.DEFAULT_TOOLKIT_INFO.msg(format(
'Could not perform a hotswap deployment, as the stack %s contains non-Asset changes',
stackArtifact.displayName,
)));
} catch (e) {
if (!(e instanceof CfnEvaluationException)) {
throw e;
}
await ioHelper.notify(info(format(
await ioHelper.notify(IO.DEFAULT_TOOLKIT_INFO.msg(format(
'Could not perform a hotswap deployment, because the CloudFormation template could not be resolved: %s',
formatErrorMessage(e),
)));
}

if (hotswapMode === HotswapMode.FALL_BACK) {
await ioHelper.notify(info('Falling back to doing a full deployment'));
await ioHelper.notify(IO.DEFAULT_TOOLKIT_INFO.msg('Falling back to doing a full deployment'));
options.sdk.appendCustomUserAgent('cdk-hotswap/fallback');
} else {
return {
Expand Down Expand Up @@ -404,17 +406,17 @@ class FullCloudFormationDeployment {
await this.updateTerminationProtection();

if (changeSetHasNoChanges(changeSetDescription)) {
await this.ioHelper.notify(debug(format('No changes are to be performed on %s.', this.stackName)));
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(format('No changes are to be performed on %s.', this.stackName)));
if (execute) {
await this.ioHelper.notify(debug(format('Deleting empty change set %s', changeSetDescription.ChangeSetId)));
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(format('Deleting empty change set %s', changeSetDescription.ChangeSetId)));
await this.cfn.deleteChangeSet({
StackName: this.stackName,
ChangeSetName: changeSetName,
});
}

if (this.options.force) {
await this.ioHelper.notify(warn(
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_WARN.msg(
[
'You used the --force flag, but CloudFormation reported that the deployment would not make any changes.',
'According to CloudFormation, all resources are already up-to-date with the state in your CDK app.',
Expand All @@ -434,7 +436,7 @@ class FullCloudFormationDeployment {
}

if (!execute) {
await this.ioHelper.notify(info(format(
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_INFO.msg(format(
'Changeset %s created and waiting in review for manual execution (--no-execute)',
changeSetDescription.ChangeSetId,
)));
Expand Down Expand Up @@ -466,8 +468,8 @@ class FullCloudFormationDeployment {
private async createChangeSet(changeSetName: string, willExecute: boolean, importExistingResources: boolean) {
await this.cleanupOldChangeset(changeSetName);

await this.ioHelper.notify(debug(`Attempting to create ChangeSet with name ${changeSetName} to ${this.verb} stack ${this.stackName}`));
await this.ioHelper.notify(info(format('%s: creating CloudFormation changeset...', chalk.bold(this.stackName))));
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`Attempting to create ChangeSet with name ${changeSetName} to ${this.verb} stack ${this.stackName}`));
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_INFO.msg(format('%s: creating CloudFormation changeset...', chalk.bold(this.stackName))));
const changeSet = await this.cfn.createChangeSet({
StackName: this.stackName,
ChangeSetName: changeSetName,
Expand All @@ -479,15 +481,15 @@ class FullCloudFormationDeployment {
...this.commonPrepareOptions(),
});

await this.ioHelper.notify(debug(format('Initiated creation of changeset: %s; waiting for it to finish creating...', changeSet.Id)));
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(format('Initiated creation of changeset: %s; waiting for it to finish creating...', changeSet.Id)));
// Fetching all pages if we'll execute, so we can have the correct change count when monitoring.
return waitForChangeSet(this.cfn, this.ioHelper, this.stackName, changeSetName, {
fetchAll: willExecute,
});
}

private async executeChangeSet(changeSet: DescribeChangeSetCommandOutput): Promise<SuccessfulDeployStackResult> {
await this.ioHelper.notify(debug(format('Initiating execution of changeset %s on stack %s', changeSet.ChangeSetId, this.stackName)));
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(format('Initiating execution of changeset %s on stack %s', changeSet.ChangeSetId, this.stackName)));

await this.cfn.executeChangeSet({
StackName: this.stackName,
Expand All @@ -496,7 +498,7 @@ class FullCloudFormationDeployment {
...this.commonExecuteOptions(),
});

await this.ioHelper.notify(debug(
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(
format(
'Execution of changeset %s on stack %s has started; waiting for the update to complete...',
changeSet.ChangeSetId,
Expand All @@ -513,7 +515,7 @@ class FullCloudFormationDeployment {
if (this.cloudFormationStack.exists) {
// Delete any existing change sets generated by CDK since change set names must be unique.
// The delete request is successful as long as the stack exists (even if the change set does not exist).
await this.ioHelper.notify(debug(`Removing existing change set with name ${changeSetName} if it exists`));
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`Removing existing change set with name ${changeSetName} if it exists`));
await this.cfn.deleteChangeSet({
StackName: this.stackName,
ChangeSetName: changeSetName,
Expand All @@ -525,7 +527,7 @@ class FullCloudFormationDeployment {
// Update termination protection only if it has changed.
const terminationProtection = this.stackArtifact.terminationProtection ?? false;
if (!!this.cloudFormationStack.terminationProtection !== terminationProtection) {
await this.ioHelper.notify(debug(
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(
format (
'Updating termination protection from %s to %s for stack %s',
this.cloudFormationStack.terminationProtection,
Expand All @@ -537,12 +539,12 @@ class FullCloudFormationDeployment {
StackName: this.stackName,
EnableTerminationProtection: terminationProtection,
});
await this.ioHelper.notify(debug(format('Termination protection updated to %s for stack %s', terminationProtection, this.stackName)));
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(format('Termination protection updated to %s for stack %s', terminationProtection, this.stackName)));
}
}

private async directDeployment(): Promise<SuccessfulDeployStackResult> {
await this.ioHelper.notify(info(format('%s: %s stack...', chalk.bold(this.stackName), this.update ? 'updating' : 'creating')));
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_INFO.msg(format('%s: %s stack...', chalk.bold(this.stackName), this.update ? 'updating' : 'creating')));

const startTime = new Date();

Expand All @@ -558,7 +560,7 @@ class FullCloudFormationDeployment {
});
} catch (err: any) {
if (err.message === 'No updates are to be performed.') {
await this.ioHelper.notify(debug(format('No updates are to be performed for stack %s', this.stackName)));
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(format('No updates are to be performed for stack %s', this.stackName)));
return {
type: 'did-deploy-stack',
noOp: true,
Expand Down Expand Up @@ -611,7 +613,7 @@ class FullCloudFormationDeployment {
} finally {
await monitor.stop();
}
debug(format('Stack %s has completed updating', this.stackName));
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(format('Stack %s has completed updating', this.stackName)));
return {
type: 'did-deploy-stack',
noOp: false,
Expand Down Expand Up @@ -709,11 +711,11 @@ async function canSkipDeploy(
ioHelper: IoHelper,
): Promise<boolean> {
const deployName = deployStackOptions.deployName || deployStackOptions.stack.stackName;
await ioHelper.notify(debug(`${deployName}: checking if we can skip deploy`));
await ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`${deployName}: checking if we can skip deploy`));

// Forced deploy
if (deployStackOptions.force) {
await ioHelper.notify(debug(`${deployName}: forced deployment`));
await ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`${deployName}: forced deployment`));
return false;
}

Expand All @@ -722,53 +724,53 @@ async function canSkipDeploy(
deployStackOptions.deploymentMethod?.method === 'change-set' &&
deployStackOptions.deploymentMethod.execute === false
) {
await ioHelper.notify(debug(`${deployName}: --no-execute, always creating change set`));
await ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`${deployName}: --no-execute, always creating change set`));
return false;
}

// No existing stack
if (!cloudFormationStack.exists) {
await ioHelper.notify(debug(`${deployName}: no existing stack`));
await ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`${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())) {
await ioHelper.notify(debug(`${deployName}: template has changed`));
await ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`${deployName}: template has changed`));
return false;
}

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

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

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

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

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

Expand Down
Loading