Skip to content

Commit 30a2a29

Browse files
mrgrainiliapolo
andauthored
feat(toolkit-lib): structured messages for hotswap (#260)
Adds structured data to hotswap messages. Includes a new trace method that contains information about the hotswap after details have been computed, but before execution. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Co-authored-by: Eli Polonsky <[email protected]>
1 parent a3b405a commit 30a2a29

File tree

4 files changed

+77
-44
lines changed

4 files changed

+77
-44
lines changed

packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap.ts

+18-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { PropertyDifference, Resource } from '@aws-cdk/cloudformation-diff';
22
import type * as cxapi from '@aws-cdk/cx-api';
3+
import type { Duration } from './types';
34
import type { ResourceMetadata } from '../../resource-metadata/resource-metadata';
45

56
/**
@@ -176,10 +177,7 @@ export interface NonHotswappableChange {
176177
readonly description: string;
177178
}
178179

179-
/**
180-
* Information about a hotswap deployment
181-
*/
182-
export interface HotswapDeployment {
180+
export interface HotswapDeploymentAttempt {
183181
/**
184182
* The stack that's currently being deployed
185183
*/
@@ -192,23 +190,18 @@ export interface HotswapDeployment {
192190
}
193191

194192
/**
195-
* The result of an attempted hotswap deployment
193+
* Information about a hotswap deployment
196194
*/
197-
export interface HotswapResult {
195+
export interface HotswapDeploymentDetails {
198196
/**
199-
* The stack that was hotswapped
197+
* The stack that's currently being deployed
200198
*/
201199
readonly stack: cxapi.CloudFormationStackArtifact;
200+
202201
/**
203202
* The mode the hotswap deployment was initiated with.
204203
*/
205204
readonly mode: 'hotswap-only' | 'fall-back';
206-
/**
207-
* Whether hotswapping happened or not.
208-
*
209-
* `false` indicates that the deployment could not be hotswapped and full deployment may be attempted as fallback.
210-
*/
211-
readonly hotswapped: boolean;
212205
/**
213206
* The changes that were deemed hotswappable
214207
*/
@@ -218,3 +211,15 @@ export interface HotswapResult {
218211
*/
219212
readonly nonHotswappableChanges: NonHotswappableChange[];
220213
}
214+
215+
/**
216+
* The result of an attempted hotswap deployment
217+
*/
218+
export interface HotswapResult extends Duration, HotswapDeploymentDetails {
219+
/**
220+
* Whether hotswapping happened or not.
221+
*
222+
* `false` indicates that the deployment could not be hotswapped and full deployment may be attempted as fallback.
223+
*/
224+
readonly hotswapped: boolean;
225+
}

packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/private/messages.ts

+21-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { BootstrapEnvironmentProgress } from '../payloads/bootstrap-environ
55
import type { MissingContext, UpdatedContext } from '../payloads/context';
66
import type { BuildAsset, DeployConfirmationRequest, PublishAsset, StackDeployProgress, SuccessfulDeployStackResult } from '../payloads/deploy';
77
import type { StackDestroy, StackDestroyProgress } from '../payloads/destroy';
8-
import type { HotswapDeployment } from '../payloads/hotswap';
8+
import type { HotswapDeploymentDetails, HotswapDeploymentAttempt, HotswappableChange, HotswapResult } from '../payloads/hotswap';
99
import type { StackDetailsPayload } from '../payloads/list';
1010
import type { CloudWatchLogEvent, CloudWatchLogMonitorControlEvent } from '../payloads/logs-monitor';
1111
import type { StackRollbackProgress } from '../payloads/rollback';
@@ -197,15 +197,30 @@ export const IO = {
197197
}),
198198

199199
// Hotswap (54xx)
200-
CDK_TOOLKIT_I5400: make.trace<HotswapDeployment>({
200+
CDK_TOOLKIT_I5400: make.trace<HotswapDeploymentAttempt>({
201201
code: 'CDK_TOOLKIT_I5400',
202-
description: 'Starting a hotswap deployment',
203-
interface: 'HotswapDeployment',
202+
description: 'Attempting a hotswap deployment',
203+
interface: 'HotswapDeploymentAttempt',
204204
}),
205-
CDK_TOOLKIT_I5410: make.info<Duration>({
205+
CDK_TOOLKIT_I5401: make.trace<HotswapDeploymentDetails>({
206+
code: 'CDK_TOOLKIT_I5401',
207+
description: 'Computed details for the hotswap deployment',
208+
interface: 'HotswapDeploymentDetails',
209+
}),
210+
CDK_TOOLKIT_I5402: make.info<HotswappableChange>({
211+
code: 'CDK_TOOLKIT_I5402',
212+
description: 'A hotswappable change is processed as part of a hotswap deployment',
213+
interface: 'HotswappableChange',
214+
}),
215+
CDK_TOOLKIT_I5403: make.info<HotswappableChange>({
216+
code: 'CDK_TOOLKIT_I5403',
217+
description: 'The hotswappable change has completed processing',
218+
interface: 'HotswappableChange',
219+
}),
220+
CDK_TOOLKIT_I5410: make.info<HotswapResult>({
206221
code: 'CDK_TOOLKIT_I5410',
207222
description: 'Hotswap deployment has ended, a full deployment might still follow if needed',
208-
interface: 'Duration',
223+
interface: 'HotswapResult',
209224
}),
210225

211226
// Stack Monitor (55xx)

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,11 @@ group: Documents
4040
| `CDK_TOOLKIT_I5313` | File event detected during active deployment, changes are queued | `info` | {@link FileWatchEvent} |
4141
| `CDK_TOOLKIT_I5314` | Initial watch deployment started | `info` | n/a |
4242
| `CDK_TOOLKIT_I5315` | Queued watch deployment started | `info` | n/a |
43-
| `CDK_TOOLKIT_I5400` | Starting a hotswap deployment | `trace` | {@link HotswapDeployment} |
44-
| `CDK_TOOLKIT_I5410` | Hotswap deployment has ended, a full deployment might still follow if needed | `info` | {@link Duration} |
43+
| `CDK_TOOLKIT_I5400` | Attempting a hotswap deployment | `trace` | {@link HotswapDeploymentAttempt} |
44+
| `CDK_TOOLKIT_I5401` | Computed details for the hotswap deployment | `trace` | {@link HotswapDeploymentDetails} |
45+
| `CDK_TOOLKIT_I5402` | A hotswappable change is processed as part of a hotswap deployment | `info` | {@link HotswappableChange} |
46+
| `CDK_TOOLKIT_I5403` | The hotswappable change has completed processing | `info` | {@link HotswappableChange} |
47+
| `CDK_TOOLKIT_I5410` | Hotswap deployment has ended, a full deployment might still follow if needed | `info` | {@link HotswapResult} |
4548
| `CDK_TOOLKIT_I5501` | Stack Monitoring: Start monitoring of a single stack | `info` | {@link StackMonitoringControlEvent} |
4649
| `CDK_TOOLKIT_I5502` | Stack Monitoring: Activity event for a single stack | `info` | {@link StackActivity} |
4750
| `CDK_TOOLKIT_I5503` | Stack Monitoring: Finished monitoring of a single stack | `info` | {@link StackMonitoringControlEvent} |

packages/aws-cdk/lib/api/deployments/hotswap-deployments.ts

+33-23
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export async function tryHotswapDeployment(
110110
hotswapPropertyOverrides,
111111
);
112112

113-
await hotswapSpan.end();
113+
await hotswapSpan.end(result);
114114

115115
if (result?.hotswapped === true) {
116116
return {
@@ -135,7 +135,7 @@ async function hotswapDeployment(
135135
stack: cxapi.CloudFormationStackArtifact,
136136
hotswapMode: HotswapMode,
137137
hotswapPropertyOverrides: HotswapPropertyOverrides,
138-
): Promise<HotswapResult> {
138+
): Promise<Omit<HotswapResult, 'duration'>> {
139139
// resolve the environment, so we can substitute things like AWS::Region in CFN expressions
140140
const resolvedEnv = await sdkProvider.resolveEnvironment(stack.environment);
141141
// create a new SDK using the CLI credentials, because the default one will not work for new-style synthesis -
@@ -162,11 +162,18 @@ async function hotswapDeployment(
162162
currentTemplate.nestedStacks, hotswapPropertyOverrides,
163163
);
164164

165-
await logNonHotswappableChanges(ioSpan, nonHotswappable, hotswapMode);
165+
await logRejectedChanges(ioSpan, nonHotswappable, hotswapMode);
166166

167167
const hotswappableChanges = hotswappable.map(o => o.change);
168168
const nonHotswappableChanges = nonHotswappable.map(n => n.change);
169169

170+
await ioSpan.notify(IO.CDK_TOOLKIT_I5401.msg('Hotswap plan created', {
171+
stack,
172+
mode: hotswapMode,
173+
hotswappableChanges,
174+
nonHotswappableChanges,
175+
}));
176+
170177
// preserve classic hotswap behavior
171178
if (hotswapMode === 'fall-back') {
172179
if (nonHotswappableChanges.length > 0) {
@@ -181,7 +188,7 @@ async function hotswapDeployment(
181188
}
182189

183190
// apply the short-circuitable changes
184-
await applyAllHotswappableChanges(sdk, ioSpan, hotswappable);
191+
await applyAllHotswapOperations(sdk, ioSpan, hotswappable);
185192

186193
return {
187194
stack,
@@ -489,27 +496,29 @@ function isCandidateForHotswapping(
489496
};
490497
}
491498

492-
async function applyAllHotswappableChanges(sdk: SDK, ioSpan: IMessageSpan<any>, hotswappableChanges: HotswapOperation[]): Promise<void[]> {
493-
if (hotswappableChanges.length > 0) {
494-
await ioSpan.notify(IO.DEFAULT_TOOLKIT_INFO.msg(`\n${ICON} hotswapping resources:`));
499+
async function applyAllHotswapOperations(sdk: SDK, ioSpan: IMessageSpan<any>, hotswappableChanges: HotswapOperation[]): Promise<void[]> {
500+
if (hotswappableChanges.length === 0) {
501+
return Promise.resolve([]);
495502
}
503+
504+
await ioSpan.notify(IO.DEFAULT_TOOLKIT_INFO.msg(`\n${ICON} hotswapping resources:`));
496505
const limit = pLimit(10);
497506
// eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism
498507
return Promise.all(hotswappableChanges.map(hotswapOperation => limit(() => {
499-
return applyHotswappableChange(sdk, ioSpan, hotswapOperation);
508+
return applyHotswapOperation(sdk, ioSpan, hotswapOperation);
500509
})));
501510
}
502511

503-
async function applyHotswappableChange(sdk: SDK, ioSpan: IMessageSpan<any>, hotswapOperation: HotswapOperation): Promise<void> {
512+
async function applyHotswapOperation(sdk: SDK, ioSpan: IMessageSpan<any>, hotswapOperation: HotswapOperation): Promise<void> {
504513
// note the type of service that was successfully hotswapped in the User-Agent
505514
const customUserAgent = `cdk-hotswap/success-${hotswapOperation.service}`;
506515
sdk.appendCustomUserAgent(customUserAgent);
507-
508516
const resourceText = (r: AffectedResource) => r.description ?? `${r.resourceType} '${r.physicalName ?? r.logicalId}'`;
509517

510-
for (const resource of hotswapOperation.change.resources) {
511-
await ioSpan.notify(IO.DEFAULT_TOOLKIT_INFO.msg(format(` ${ICON} %s`, chalk.bold(resourceText(resource)))));
512-
}
518+
await ioSpan.notify(IO.CDK_TOOLKIT_I5402.msg(
519+
hotswapOperation.change.resources.map(r => format(` ${ICON} %s`, chalk.bold(resourceText(r)))).join('\n'),
520+
hotswapOperation.change,
521+
));
513522

514523
// if the SDK call fails, an error will be thrown by the SDK
515524
// and will prevent the green 'hotswapped!' text from being displayed
@@ -525,9 +534,10 @@ async function applyHotswappableChange(sdk: SDK, ioSpan: IMessageSpan<any>, hots
525534
throw e;
526535
}
527536

528-
for (const resource of hotswapOperation.change.resources) {
529-
await ioSpan.notify(IO.DEFAULT_TOOLKIT_INFO.msg(format(`${ICON} %s %s`, chalk.bold(resourceText(resource)), chalk.green('hotswapped!'))));
530-
}
537+
await ioSpan.notify(IO.CDK_TOOLKIT_I5403.msg(
538+
hotswapOperation.change.resources.map(r => format(` ${ICON} %s %s`, chalk.bold(resourceText(r)), chalk.green('hotswapped!'))).join('\n'),
539+
hotswapOperation.change,
540+
));
531541

532542
sdk.removeCustomUserAgent(customUserAgent);
533543
}
@@ -550,12 +560,12 @@ function formatWaiterErrorResult(result: WaiterResult) {
550560
return main;
551561
}
552562

553-
async function logNonHotswappableChanges(
563+
async function logRejectedChanges(
554564
ioSpan: IMessageSpan<any>,
555-
nonHotswappableChanges: RejectedChange[],
565+
rejectedChanges: RejectedChange[],
556566
hotswapMode: HotswapMode,
557567
): Promise<void> {
558-
if (nonHotswappableChanges.length === 0) {
568+
if (rejectedChanges.length === 0) {
559569
return;
560570
}
561571
/**
@@ -566,9 +576,9 @@ async function logNonHotswappableChanges(
566576
* This logic prevents us from logging that change as non-hotswappable when we hotswap it.
567577
*/
568578
if (hotswapMode === 'hotswap-only') {
569-
nonHotswappableChanges = nonHotswappableChanges.filter((change) => change.hotswapOnlyVisible === true);
579+
rejectedChanges = rejectedChanges.filter((change) => change.hotswapOnlyVisible === true);
570580

571-
if (nonHotswappableChanges.length === 0) {
581+
if (rejectedChanges.length === 0) {
572582
return;
573583
}
574584
}
@@ -581,8 +591,8 @@ async function logNonHotswappableChanges(
581591
messages.push(format('%s %s', chalk.red('⚠️'), chalk.red('The following non-hotswappable changes were found:')));
582592
}
583593

584-
for (const rejection of nonHotswappableChanges) {
585-
messages.push(' ' + nonHotswappableChangeMessage(rejection.change));
594+
for (const { change } of rejectedChanges) {
595+
messages.push(' ' + nonHotswappableChangeMessage(change));
586596
}
587597
messages.push(''); // newline
588598

0 commit comments

Comments
 (0)