Skip to content
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

chore(toolkit): fix various inconsistenncies with hotswap settings #33240

Merged
merged 2 commits into from
Jan 30, 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
16 changes: 0 additions & 16 deletions packages/@aws-cdk/toolkit/lib/actions/deploy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,22 +175,6 @@ export interface BaseDeployOptions {
readonly concurrency?: number;
}

/**
* Deploy options needed by the watch command.
* Intentionally not exported because these options are not
* meant to be public facing.
*
* @internal
*/
export interface ExtendedDeployOptions extends DeployOptions {
/**
* The extra string to append to the User-Agent header when performing AWS SDK calls.
*
* @default - nothing extra is appended to the User-Agent header
*/
readonly extraUserAgent?: string;
}

export interface DeployOptions extends BaseDeployOptions {
/**
* ARNs of SNS topics that CloudFormation will notify with stack related events
Expand Down
16 changes: 16 additions & 0 deletions packages/@aws-cdk/toolkit/lib/actions/deploy/private/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
import { DeployOptions } from '..';

export * from './helpers';

/**
* Deploy options needed by the watch command.
* Intentionally not exported because these options are not
* meant to be public facing.
*/
export interface ExtendedDeployOptions extends DeployOptions {
/**
* The extra string to append to the User-Agent header when performing AWS SDK calls.
*
* @default - nothing extra is appended to the User-Agent header
*/
readonly extraUserAgent?: string;
}
13 changes: 12 additions & 1 deletion packages/@aws-cdk/toolkit/lib/actions/watch/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { BaseDeployOptions } from '../deploy';
import type { BaseDeployOptions, HotswapMode } from '../deploy';

export interface WatchOptions extends BaseDeployOptions {
/**
Expand Down Expand Up @@ -45,6 +45,17 @@ export interface WatchOptions extends BaseDeployOptions {
* @default 'cdk.out'
*/
readonly outdir?: string;

/**
* @TODO can this be part of `DeploymentMethod`
*
* Whether to perform a 'hotswap' deployment.
* A 'hotswap' deployment will attempt to short-circuit CloudFormation
* and update the affected resources like Lambda functions directly.
*
* @default HotswapMode.HOTSWAP_ONLY
*/
readonly hotswap?: HotswapMode;
}

export function patternsArrayForWatch(
Expand Down
40 changes: 19 additions & 21 deletions packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import * as chalk from 'chalk';
import * as chokidar from 'chokidar';
import * as fs from 'fs-extra';
import { ToolkitServices } from './private';
import { AssetBuildTime, DeployOptions, ExtendedDeployOptions, RequireApproval } from '../actions/deploy';
import { buildParameterMap, removePublishedAssets } from '../actions/deploy/private';
import { DestroyOptions } from '../actions/destroy';
import { DiffOptions } from '../actions/diff';
import { AssetBuildTime, type DeployOptions, RequireApproval } from '../actions/deploy';
import { type ExtendedDeployOptions, buildParameterMap, removePublishedAssets } from '../actions/deploy/private';
import { type DestroyOptions } from '../actions/destroy';
import { type DiffOptions } from '../actions/diff';
import { diffRequiresApproval } from '../actions/diff/private';
import { ListOptions } from '../actions/list';
import { RollbackOptions } from '../actions/rollback';
import { SynthOptions } from '../actions/synth';
import { type ListOptions } from '../actions/list';
import { type RollbackOptions } from '../actions/rollback';
import { type SynthOptions } from '../actions/synth';
import { patternsArrayForWatch, WatchOptions } from '../actions/watch';
import { SdkOptions } from '../api/aws-auth';
import { type SdkOptions } from '../api/aws-auth';
import { DEFAULT_TOOLKIT_STACK_NAME, SdkProvider, SuccessfulDeployStackResult, StackCollection, Deployments, HotswapMode, StackActivityProgress, ResourceMigrator, obscureTemplate, serializeStructure, tagsForStack, CliIoHost, validateSnsTopicArn, Concurrency, WorkGraphBuilder, AssetBuildNode, AssetPublishNode, StackNode, formatErrorMessage } from '../api/aws-cdk';
import { CachedCloudAssemblySource, IdentityCloudAssemblySource, StackAssembly, ICloudAssemblySource, StackSelectionStrategy } from '../api/cloud-assembly';
import { ALL_STACKS, CloudAssemblySourceBuilder } from '../api/cloud-assembly/private';
Expand Down Expand Up @@ -234,11 +234,12 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab

const parameterMap = buildParameterMap(options.parameters?.parameters);

if (options.hotswap !== HotswapMode.FULL_DEPLOYMENT) {
await ioHost.notify(warn(
const hotswapMode = options.hotswap ?? HotswapMode.FULL_DEPLOYMENT;
if (hotswapMode !== HotswapMode.FULL_DEPLOYMENT) {
await ioHost.notify(warn([
'⚠️ The --hotswap and --hotswap-fallback flags deliberately introduce CloudFormation drift to speed up deployments',
));
await ioHost.notify(warn('⚠️ They should only be used for development - never use them for your production Stacks!\n'));
'⚠️ They should only be used for development - never use them for your production Stacks!',
].join('\n')));
}

// @TODO
Expand Down Expand Up @@ -367,7 +368,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab
progress,
ci: options.ci,
rollback,
hotswap: options.hotswap,
hotswap: hotswapMode,
extraUserAgent: options.extraUserAgent,
// hotswapPropertyOverrides: hotswapPropertyOverrides,
assetParallelism: options.assetParallelism,
Expand Down Expand Up @@ -769,21 +770,18 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab
assembly: StackAssembly,
options: WatchOptions,
): Promise<void> {
// watch defaults hotswap to enabled
const hotswap = options.hotswap ?? HotswapMode.HOTSWAP_ONLY;
const deployOptions: ExtendedDeployOptions = {
...options,
requireApproval: RequireApproval.NEVER,
// cloudWatchLogMonitor,
hotswap: options.hotswap,
extraUserAgent: `cdk-watch/hotswap-${options.hotswap !== HotswapMode.FALL_BACK ? 'on' : 'off'}`,
concurrency: options.concurrency,
hotswap,
extraUserAgent: `cdk-watch/hotswap-${hotswap === HotswapMode.FULL_DEPLOYMENT ? 'off' : 'on'}`,
};

try {
await this._deploy(
assembly,
'watch',
deployOptions,
);
await this._deploy(assembly, 'watch', deployOptions);
} catch {
// just continue - deploy will show the error
}
Expand Down
91 changes: 91 additions & 0 deletions packages/@aws-cdk/toolkit/test/actions/deploy-hotswap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { HotswapMode } from '../../lib';
import { Toolkit } from '../../lib/toolkit';
import { builderFixture, TestIoHost } from '../_helpers';

const ioHost = new TestIoHost();
const toolkit = new Toolkit({ ioHost });

let mockDeployStack = jest.fn().mockResolvedValue({
type: 'did-deploy-stack',
stackArn: 'arn:aws:cloudformation:region:account:stack/test-stack',
outputs: {},
noOp: false,
});

jest.mock('../../lib/api/aws-cdk', () => {
return {
...jest.requireActual('../../lib/api/aws-cdk'),
Deployments: jest.fn().mockImplementation(() => ({
deployStack: mockDeployStack,
resolveEnvironment: jest.fn().mockResolvedValue({}),
isSingleAssetPublished: jest.fn().mockResolvedValue(true),
readCurrentTemplate: jest.fn().mockResolvedValue({ Resources: {} }),
})),
};
});

beforeEach(() => {
ioHost.notifySpy.mockClear();
ioHost.requestSpy.mockClear();
jest.clearAllMocks();
});

describe('deploy with hotswap', () => {
test('does print hotswap warnings for FALL_BACK mode', async () => {
// WHEN
const cx = await builderFixture(toolkit, 'two-empty-stacks');
await toolkit.deploy(cx, {
hotswap: HotswapMode.FALL_BACK,
});

// THEN
expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({
level: 'warn',
message: expect.stringContaining('hotswap'),
}));
});

test('does print hotswap warnings for HOTSWAP_ONLY mode', async () => {
// WHEN
const cx = await builderFixture(toolkit, 'two-empty-stacks');
await toolkit.deploy(cx, {
hotswap: HotswapMode.HOTSWAP_ONLY,
});

// THEN
expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({
level: 'warn',
message: expect.stringContaining('hotswap'),
}));
});
});

describe('deploy without hotswap', () => {
test('does not print hotswap warnings when mode is undefined', async () => {
// WHEN
const cx = await builderFixture(toolkit, 'two-empty-stacks');
await toolkit.deploy(cx, {
hotswap: undefined,
});

// THEN
expect(ioHost.notifySpy).not.toHaveBeenCalledWith(expect.objectContaining({
level: 'warn',
message: expect.stringContaining('hotswap'),
}));
});

test('does not print hotswap warning for FULL_DEPLOYMENT mode', async () => {
// WHEN
const cx = await builderFixture(toolkit, 'two-empty-stacks');
await toolkit.deploy(cx, {
hotswap: HotswapMode.FULL_DEPLOYMENT,
});

// THEN
expect(ioHost.notifySpy).not.toHaveBeenCalledWith(expect.objectContaining({
level: 'warn',
message: expect.stringContaining('hotswap'),
}));
});
});
26 changes: 24 additions & 2 deletions packages/@aws-cdk/toolkit/test/actions/watch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,11 @@ describe('watch', () => {
}));
});

describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => {
describe.each([
[HotswapMode.FALL_BACK, 'on'],
[HotswapMode.HOTSWAP_ONLY, 'on'],
[HotswapMode.FULL_DEPLOYMENT, 'off'],
])('%p mode', (hotswapMode, userAgent) => {
test('passes through the correct hotswap mode to deployStack()', async () => {
// WHEN
const cx = await builderFixture(toolkit, 'stack-with-role');
Expand All @@ -143,10 +147,28 @@ describe('watch', () => {
// THEN
expect(deploySpy).toHaveBeenCalledWith(expect.anything(), 'watch', expect.objectContaining({
hotswap: hotswapMode,
extraUserAgent: `cdk-watch/hotswap-${hotswapMode !== HotswapMode.FALL_BACK ? 'on' : 'off'}`,
extraUserAgent: `cdk-watch/hotswap-${userAgent}`,
}));
});
});

test('defaults hotswap to HOTSWAP_ONLY', async () => {
// WHEN
const cx = await builderFixture(toolkit, 'stack-with-role');
ioHost.level = 'warn';
await toolkit.watch(cx, {
include: [],
hotswap: undefined, // force the default
});

await fakeChokidarWatcherOn.readyCallback();

// THEN
expect(deploySpy).toHaveBeenCalledWith(expect.anything(), 'watch', expect.objectContaining({
hotswap: HotswapMode.HOTSWAP_ONLY,
extraUserAgent: 'cdk-watch/hotswap-on',
}));
});
});

// @todo unit test watch with file events