Skip to content

Commit 1c52125

Browse files
committed
refactor(toolkit-lib)!: hotswap is now a deployment method
1 parent c96c4f0 commit 1c52125

File tree

12 files changed

+183
-136
lines changed

12 files changed

+183
-136
lines changed

packages/@aws-cdk/toolkit-lib/lib/actions/deploy/index.ts

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import type { BaseDeployOptions } from './private/deploy-options';
22
import type { Tag } from '../../api/tags';
33

4-
export type DeploymentMethod = DirectDeploymentMethod | ChangeSetDeploymentMethod;
4+
export type DeploymentMethod = DirectDeployment | ChangeSetDeployment | HotswapDeployment;
55

6-
export interface DirectDeploymentMethod {
7-
/**
8-
* Use stack APIs to the deploy stack changes
9-
*/
6+
/**
7+
* Use stack APIs to the deploy stack changes
8+
*/
9+
export interface DirectDeployment {
1010
readonly method: 'direct';
1111
}
1212

13-
export interface ChangeSetDeploymentMethod {
14-
/**
15-
* Use change-set APIS to deploy a stack changes
16-
*/
13+
/**
14+
* Use change-set APIs to deploy a stack changes
15+
*/
16+
export interface ChangeSetDeployment {
1717
readonly method: 'change-set';
1818

1919
/**
@@ -37,6 +37,31 @@ export interface ChangeSetDeploymentMethod {
3737
readonly importExistingResources?: boolean;
3838
}
3939

40+
/**
41+
* Perform a 'hotswap' deployment to deploy a stack changes
42+
*
43+
* A 'hotswap' deployment will attempt to short-circuit CloudFormation
44+
* and update the affected resources like Lambda functions directly.
45+
*/
46+
export interface HotswapDeployment {
47+
readonly method: 'hotswap';
48+
49+
/**
50+
* Represents configuration property overrides for hotswap deployments.
51+
* Currently only supported by ECS.
52+
*
53+
* @default - no overrides
54+
*/
55+
readonly properties?: HotswapProperties;
56+
57+
/**
58+
* Fall back to a CloudFormation deployment when a non-hotswappable change is detected
59+
*
60+
* @default - do not fall back to a CloudFormation deployment
61+
*/
62+
readonly fallback?: DirectDeployment | ChangeSetDeployment;
63+
}
64+
4065
/**
4166
* When to build assets
4267
*/
@@ -55,23 +80,6 @@ export enum AssetBuildTime {
5580
JUST_IN_TIME = 'just-in-time',
5681
}
5782

58-
export enum HotswapMode {
59-
/**
60-
* Will fall back to CloudFormation when a non-hotswappable change is detected
61-
*/
62-
FALL_BACK = 'fall-back',
63-
64-
/**
65-
* Will not fall back to CloudFormation when a non-hotswappable change is detected
66-
*/
67-
HOTSWAP_ONLY = 'hotswap-only',
68-
69-
/**
70-
* Will not attempt to hotswap anything and instead go straight to CloudFormation
71-
*/
72-
FULL_DEPLOYMENT = 'full-deployment',
73-
}
74-
7583
export class StackParameters {
7684
/**
7785
* Use only existing parameters on the stack.
@@ -143,14 +151,6 @@ export interface DeployOptions extends BaseDeployOptions {
143151
* @default AssetBuildTime.ALL_BEFORE_DEPLOY
144152
*/
145153
readonly assetBuildTime?: AssetBuildTime;
146-
147-
/**
148-
* Represents configuration property overrides for hotswap deployments.
149-
* Currently only supported by ECS.
150-
*
151-
* @default - no overrides
152-
*/
153-
readonly hotswapProperties?: HotswapProperties;
154154
}
155155

156156
/**

packages/@aws-cdk/toolkit-lib/lib/actions/deploy/private/deploy-options.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { DeploymentMethod, DeployOptions, HotswapMode } from '..';
1+
import type { DeploymentMethod, DeployOptions } from '..';
22
import type { StackSelector } from '../../../api/cloud-assembly';
33
import type { CloudWatchLogEventMonitor } from '../../../api/logs-monitor/logs-monitor';
44

@@ -24,17 +24,10 @@ export interface BaseDeployOptions {
2424

2525
/**
2626
* Deployment method
27-
*/
28-
readonly deploymentMethod?: DeploymentMethod;
29-
30-
/**
31-
* Whether to perform a 'hotswap' deployment.
32-
* A 'hotswap' deployment will attempt to short-circuit CloudFormation
33-
* and update the affected resources like Lambda functions directly.
3427
*
35-
* @default - no hotswap
28+
* @default ChangeSetDeployment
3629
*/
37-
readonly hotswap?: HotswapMode;
30+
readonly deploymentMethod?: DeploymentMethod;
3831

3932
/**
4033
* Rollback failed deployments

packages/@aws-cdk/toolkit-lib/lib/actions/deploy/private/helpers.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import type { DeployOptions, HotswapProperties } from '..';
1+
import type { DeployOptions } from '..';
22
import type { Deployments } from '../../../api/deployments';
3-
import { EcsHotswapProperties, HotswapPropertyOverrides } from '../../../api/hotswap';
43
import type { WorkGraph } from '../../../api/work-graph';
54

65
export function buildParameterMap(parameters?: Map<string, string | undefined>): { [name: string]: { [name: string]: string | undefined } } {
@@ -35,13 +34,3 @@ export async function removePublishedAssetsFromWorkGraph(graph: WorkGraph, deplo
3534
stackName: assetNode.parentStack.stackName,
3635
}));
3736
}
38-
39-
/**
40-
* Create the HotswapPropertyOverrides class out of the Interface exposed to users
41-
*/
42-
export function createHotswapPropertyOverrides(hotswapProperties: HotswapProperties): HotswapPropertyOverrides {
43-
return new HotswapPropertyOverrides(new EcsHotswapProperties(
44-
hotswapProperties.ecs?.minimumHealthyPercent,
45-
hotswapProperties.ecs?.maximumHealthyPercent,
46-
));
47-
}

packages/@aws-cdk/toolkit-lib/lib/actions/watch/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { DeploymentMethod } from '../deploy';
12
import type { BaseDeployOptions } from '../deploy/private';
23

34
export interface WatchOptions extends BaseDeployOptions {
@@ -21,4 +22,11 @@ export interface WatchOptions extends BaseDeployOptions {
2122
* @default process.cwd()
2223
*/
2324
readonly watchDir?: string;
25+
26+
/**
27+
* Deployment method
28+
*
29+
* @default HotswapDeployment
30+
*/
31+
readonly deploymentMethod?: DeploymentMethod;
2432
}

packages/@aws-cdk/toolkit-lib/lib/api/deployments/deploy-stack.ts

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@ import {
2626
} from './cfn-api';
2727
import { determineAllowCrossAccountAssetPublishing } from './checks';
2828
import type { DeployStackResult, SuccessfulDeployStackResult } from './deployment-result';
29-
import type { ChangeSetDeploymentMethod, DeploymentMethod } from '../../actions/deploy';
29+
import type { ChangeSetDeployment, DeploymentMethod, DirectDeployment } from '../../actions/deploy';
3030
import { ToolkitError } from '../../toolkit/toolkit-error';
3131
import { formatErrorMessage } from '../../util';
3232
import type { SDK, SdkProvider, ICloudFormationClient } from '../aws-auth/private';
3333
import type { TemplateBodyParameter } from '../cloudformation';
3434
import { makeBodyParameter, CfnEvaluationException, CloudFormationStack } from '../cloudformation';
3535
import type { EnvironmentResources, StringWithoutPlaceholders } from '../environment';
36-
import { HotswapMode, HotswapPropertyOverrides, ICON } from '../hotswap/common';
36+
import { HotswapPropertyOverrides, HotswapMode, ICON, createHotswapPropertyOverrides } from '../hotswap/common';
3737
import { tryHotswapDeployment } from '../hotswap/hotswap-deployments';
3838
import { IO, type IoHelper } from '../io/private';
3939
import type { ResourcesToImport } from '../resource-import';
@@ -158,17 +158,21 @@ export interface DeployStackOptions {
158158
*/
159159
readonly rollback?: boolean;
160160

161-
/*
161+
/**
162162
* Whether to perform a 'hotswap' deployment.
163163
* A 'hotswap' deployment will attempt to short-circuit CloudFormation
164164
* and update the affected resources like Lambda functions directly.
165165
*
166166
* @default - `HotswapMode.FULL_DEPLOYMENT` for regular deployments, `HotswapMode.HOTSWAP_ONLY` for 'watch' deployments
167+
*
168+
* @deprecated Use 'deploymentMethod' instead
167169
*/
168170
readonly hotswap?: HotswapMode;
169171

170172
/**
171173
* Extra properties that configure hotswap behavior
174+
*
175+
* @deprecated Use 'deploymentMethod' instead
172176
*/
173177
readonly hotswapPropertyOverrides?: HotswapPropertyOverrides;
174178

@@ -202,9 +206,31 @@ export interface DeployStackOptions {
202206

203207
export async function deployStack(options: DeployStackOptions, ioHelper: IoHelper): Promise<DeployStackResult> {
204208
const stackArtifact = options.stack;
205-
206209
const stackEnv = options.resolvedEnvironment;
207210

211+
let deploymentMethod = options.deploymentMethod ?? { method: 'change-set' };
212+
// Honour old hotswap option
213+
// @TODO remove when we don't care about this export anymore
214+
if (options.hotswap && deploymentMethod?.method !== 'hotswap') {
215+
switch (options.hotswap) {
216+
case HotswapMode.HOTSWAP_ONLY:
217+
deploymentMethod = {
218+
method: 'hotswap',
219+
properties: options.hotswapPropertyOverrides,
220+
};
221+
break;
222+
case HotswapMode.FALL_BACK:
223+
deploymentMethod = {
224+
method: 'hotswap',
225+
properties: options.hotswapPropertyOverrides,
226+
fallback: deploymentMethod,
227+
};
228+
break;
229+
case HotswapMode.FULL_DEPLOYMENT:
230+
break;
231+
}
232+
}
233+
208234
options.sdk.appendCustomUserAgent(options.extraUserAgent);
209235
const cfn = options.sdk.cloudFormation();
210236
const deployName = options.deployName || stackArtifact.stackName;
@@ -246,14 +272,11 @@ export async function deployStack(options: DeployStackOptions, ioHelper: IoHelpe
246272
? templateParams.updateExisting(finalParameterValues, cloudFormationStack.parameters)
247273
: templateParams.supplyAll(finalParameterValues);
248274

249-
const hotswapMode = options.hotswap ?? HotswapMode.FULL_DEPLOYMENT;
250-
const hotswapPropertyOverrides = options.hotswapPropertyOverrides ?? new HotswapPropertyOverrides();
251-
252275
if (await canSkipDeploy(options, cloudFormationStack, stackParams.hasChanges(cloudFormationStack.parameters), ioHelper)) {
253276
await ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`${deployName}: skipping deployment (use --force to override)`));
254277
// if we can skip deployment and we are performing a hotswap, let the user know
255278
// that no hotswap deployment happened
256-
if (hotswapMode !== HotswapMode.FULL_DEPLOYMENT) {
279+
if (deploymentMethod?.method === 'hotswap') {
257280
await ioHelper.notify(IO.DEFAULT_TOOLKIT_INFO.msg(
258281
format(
259282
`\n ${ICON} %s\n`,
@@ -290,16 +313,21 @@ export async function deployStack(options: DeployStackOptions, ioHelper: IoHelpe
290313
allowCrossAccount: await determineAllowCrossAccountAssetPublishing(options.sdk, ioHelper, bootstrapStackName),
291314
}, ioHelper);
292315

293-
if (hotswapMode !== HotswapMode.FULL_DEPLOYMENT) {
294-
// attempt to short-circuit the deployment if possible
316+
// attempt to short-circuit the deployment if possible
317+
if (deploymentMethod?.method === 'hotswap') {
295318
try {
319+
const hotswapModeNew = deploymentMethod?.fallback ? 'fall-back' : 'hotswap-only';
320+
const hotswapPropertyOverrides = deploymentMethod.properties
321+
? createHotswapPropertyOverrides(deploymentMethod.properties)
322+
: new HotswapPropertyOverrides();
323+
296324
const hotswapDeploymentResult = await tryHotswapDeployment(
297325
options.sdkProvider,
298326
ioHelper,
299327
stackParams.values,
300328
cloudFormationStack,
301329
stackArtifact,
302-
hotswapMode,
330+
hotswapModeNew,
303331
hotswapPropertyOverrides,
304332
);
305333

@@ -321,9 +349,10 @@ export async function deployStack(options: DeployStackOptions, ioHelper: IoHelpe
321349
)));
322350
}
323351

324-
if (hotswapMode === HotswapMode.FALL_BACK) {
352+
if (deploymentMethod.fallback) {
325353
await ioHelper.notify(IO.DEFAULT_TOOLKIT_INFO.msg('Falling back to doing a full deployment'));
326354
options.sdk.appendCustomUserAgent('cdk-hotswap/fallback');
355+
deploymentMethod = deploymentMethod.fallback;
327356
} else {
328357
return {
329358
type: 'did-deploy-stack',
@@ -336,6 +365,7 @@ export async function deployStack(options: DeployStackOptions, ioHelper: IoHelpe
336365

337366
// could not short-circuit the deployment, perform a full CFN deploy instead
338367
const fullDeployment = new FullCloudFormationDeployment(
368+
deploymentMethod,
339369
options,
340370
cloudFormationStack,
341371
stackArtifact,
@@ -364,6 +394,7 @@ class FullCloudFormationDeployment {
364394
private readonly uuid: string;
365395

366396
constructor(
397+
private readonly deploymentMethod: DirectDeployment | ChangeSetDeployment,
367398
private readonly options: DeployStackOptions,
368399
private readonly cloudFormationStack: CloudFormationStack,
369400
private readonly stackArtifact: cxapi.CloudFormationStackArtifact,
@@ -380,9 +411,7 @@ class FullCloudFormationDeployment {
380411
}
381412

382413
public async performDeployment(): Promise<DeployStackResult> {
383-
const deploymentMethod = this.options.deploymentMethod ?? {
384-
method: 'change-set',
385-
};
414+
const deploymentMethod = this.deploymentMethod ?? { method: 'change-set' };
386415

387416
if (deploymentMethod.method === 'direct' && this.options.resourcesToImport) {
388417
throw new ToolkitError('Importing resources requires a changeset deployment');
@@ -397,7 +426,7 @@ class FullCloudFormationDeployment {
397426
}
398427
}
399428

400-
private async changeSetDeployment(deploymentMethod: ChangeSetDeploymentMethod): Promise<DeployStackResult> {
429+
private async changeSetDeployment(deploymentMethod: ChangeSetDeployment): Promise<DeployStackResult> {
401430
const changeSetName = deploymentMethod.changeSetName ?? 'cdk-deploy-change-set';
402431
const execute = deploymentMethod.execute ?? true;
403432
const importExistingResources = deploymentMethod.importExistingResources ?? false;

packages/@aws-cdk/toolkit-lib/lib/api/deployments/deployments.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,17 +136,21 @@ export interface DeployStackOptions {
136136
*/
137137
readonly rollback?: boolean;
138138

139-
/*
139+
/**
140140
* Whether to perform a 'hotswap' deployment.
141141
* A 'hotswap' deployment will attempt to short-circuit CloudFormation
142142
* and update the affected resources like Lambda functions directly.
143143
*
144144
* @default - `HotswapMode.FULL_DEPLOYMENT` for regular deployments, `HotswapMode.HOTSWAP_ONLY` for 'watch' deployments
145+
*
146+
* @deprecated Use 'deploymentMethod' instead
145147
*/
146148
readonly hotswap?: HotswapMode;
147149

148150
/**
149151
* Properties that configure hotswap behavior
152+
*
153+
* @deprecated Use 'deploymentMethod' instead
150154
*/
151155
readonly hotswapPropertyOverrides?: HotswapPropertyOverrides;
152156

@@ -395,6 +399,8 @@ export class Deployments {
395399

396400
public async deployStack(options: DeployStackOptions): Promise<DeployStackResult> {
397401
let deploymentMethod = options.deploymentMethod;
402+
// @deprecated changeSetName and execute
403+
// @TODO remove when we don't care about this export anymore
398404
if (options.changeSetName || options.execute !== undefined) {
399405
if (deploymentMethod) {
400406
throw new ToolkitError(

packages/@aws-cdk/toolkit-lib/lib/api/hotswap/common.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,17 @@ export class EcsHotswapProperties implements IEcsHotswapProperties {
120120
}
121121
}
122122

123+
/**
124+
* Create the HotswapPropertyOverrides class out of the Interface exposed to users
125+
*/
126+
export function createHotswapPropertyOverrides(props: HotswapProperties): HotswapPropertyOverrides {
127+
return new HotswapPropertyOverrides(new EcsHotswapProperties(
128+
props.ecs?.minimumHealthyPercent,
129+
props.ecs?.maximumHealthyPercent,
130+
props.ecs?.stabilizationTimeoutSeconds,
131+
));
132+
}
133+
123134
type PropDiffs = Record<string, PropertyDifference<any>>;
124135

125136
class ClassifiedChanges {

0 commit comments

Comments
 (0)