packages/@aws-cdk/toolkit-lib/lib/api/hotswap/common.ts (127 lines of code) (raw):
import type { PropertyDifference } from '@aws-cdk/cloudformation-diff';
import type { HotswappableChange, NonHotswappableChange, ResourceChange } from '../../payloads/hotswap';
import { NonHotswappableReason } from '../../payloads/hotswap';
import type { SDK } from '../aws-auth/private';
import { ToolkitError } from '../toolkit-error';
export const ICON = '✨';
export interface HotswapOperation {
/**
* Marks the operation as hotswappable
*/
readonly hotswappable: true;
/**
* The name of the service being hotswapped.
* Used to set a custom User-Agent for SDK calls.
*/
readonly service: string;
/**
* Description of the change that is applied as part of the operation
*/
readonly change: HotswappableChange;
/**
* Applies the hotswap operation
*/
readonly apply: (sdk: SDK) => Promise<void>;
}
export interface RejectedChange {
/**
* Marks the change as not hotswappable
*/
readonly hotswappable: false;
/**
* The change that got rejected
*/
readonly change: NonHotswappableChange;
/**
* Whether or not to show this change when listing non-hotswappable changes in HOTSWAP_ONLY mode. Does not affect
* listing in FALL_BACK mode.
*
* @default true
*/
readonly hotswapOnlyVisible?: boolean;
}
export type HotswapChange = HotswapOperation | RejectedChange;
export enum HotswapMode {
/**
* Will fall back to CloudFormation when a non-hotswappable change is detected
*/
FALL_BACK = 'fall-back',
/**
* Will not fall back to CloudFormation when a non-hotswappable change is detected
*/
HOTSWAP_ONLY = 'hotswap-only',
/**
* Will not attempt to hotswap anything and instead go straight to CloudFormation
*/
FULL_DEPLOYMENT = 'full-deployment',
}
/**
* Represents configuration property overrides for hotswap deployments
*/
export class HotswapPropertyOverrides {
// Each supported resource type will have its own properties. Currently this is ECS
ecsHotswapProperties?: EcsHotswapProperties;
public constructor (ecsHotswapProperties?: EcsHotswapProperties) {
this.ecsHotswapProperties = ecsHotswapProperties;
}
}
/**
* Represents configuration properties for ECS hotswap deployments
*/
export class EcsHotswapProperties {
// The lower limit on the number of your service's tasks that must remain in the RUNNING state during a deployment, as a percentage of the desiredCount
readonly minimumHealthyPercent?: number;
// The upper limit on the number of your service's tasks that are allowed in the RUNNING or PENDING state during a deployment, as a percentage of the desiredCount
readonly maximumHealthyPercent?: number;
public constructor (minimumHealthyPercent?: number, maximumHealthyPercent?: number) {
if (minimumHealthyPercent !== undefined && minimumHealthyPercent < 0 ) {
throw new ToolkitError('hotswap-ecs-minimum-healthy-percent can\'t be a negative number');
}
if (maximumHealthyPercent !== undefined && maximumHealthyPercent < 0 ) {
throw new ToolkitError('hotswap-ecs-maximum-healthy-percent can\'t be a negative number');
}
// In order to preserve the current behaviour, when minimumHealthyPercent is not defined, it will be set to the currently default value of 0
if (minimumHealthyPercent == undefined) {
this.minimumHealthyPercent = 0;
} else {
this.minimumHealthyPercent = minimumHealthyPercent;
}
this.maximumHealthyPercent = maximumHealthyPercent;
}
/**
* Check if any hotswap properties are defined
* @returns true if all properties are undefined, false otherwise
*/
public isEmpty(): boolean {
return this.minimumHealthyPercent === 0 && this.maximumHealthyPercent === undefined;
}
}
type PropDiffs = Record<string, PropertyDifference<any>>;
class ClassifiedChanges {
public constructor(
public readonly change: ResourceChange,
public readonly hotswappableProps: PropDiffs,
public readonly nonHotswappableProps: PropDiffs,
) {
}
public reportNonHotswappablePropertyChanges(ret: HotswapChange[]): void {
const nonHotswappablePropNames = Object.keys(this.nonHotswappableProps);
if (nonHotswappablePropNames.length > 0) {
const tagOnlyChange = nonHotswappablePropNames.length === 1 && nonHotswappablePropNames[0] === 'Tags';
const reason = tagOnlyChange ? NonHotswappableReason.TAGS : NonHotswappableReason.PROPERTIES;
const description = tagOnlyChange ? 'Tags are not hotswappable' : `resource properties '${nonHotswappablePropNames}' are not hotswappable on this resource type`;
ret.push(nonHotswappableChange(
this.change,
reason,
description,
this.nonHotswappableProps,
));
}
}
public get namesOfHotswappableProps(): string[] {
return Object.keys(this.hotswappableProps);
}
}
export function classifyChanges(xs: ResourceChange, hotswappablePropNames: string[]): ClassifiedChanges {
const hotswappableProps: PropDiffs = {};
const nonHotswappableProps: PropDiffs = {};
for (const [name, propDiff] of Object.entries(xs.propertyUpdates)) {
if (hotswappablePropNames.includes(name)) {
hotswappableProps[name] = propDiff;
} else {
nonHotswappableProps[name] = propDiff;
}
}
return new ClassifiedChanges(xs, hotswappableProps, nonHotswappableProps);
}
export function nonHotswappableChange(
change: ResourceChange,
reason: NonHotswappableReason,
description: string,
nonHotswappableProps?: PropDiffs,
hotswapOnlyVisible: boolean = true,
): RejectedChange {
return {
hotswappable: false,
hotswapOnlyVisible,
change: {
reason,
description,
subject: {
type: 'Resource',
logicalId: change.logicalId,
resourceType: change.newValue.Type,
rejectedProperties: Object.keys(nonHotswappableProps ?? change.propertyUpdates),
metadata: change.metadata,
},
},
};
}
export function nonHotswappableResource(change: ResourceChange): RejectedChange {
return {
hotswappable: false,
change: {
reason: NonHotswappableReason.RESOURCE_UNSUPPORTED,
description: 'This resource type is not supported for hotswap deployments',
subject: {
type: 'Resource',
logicalId: change.logicalId,
resourceType: change.newValue.Type,
rejectedProperties: Object.keys(change.propertyUpdates),
metadata: change.metadata,
},
},
};
}