Skip to content
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
Expand Up @@ -70,6 +70,45 @@ export interface HotswappableChange {
readonly resources: AffectedResource[];
}

export enum NonHotswappableReason {
/**
* Tags are not hotswappable
*/
TAGS = 'tags',
/**
* Changed resource properties are not hotswappable on this resource type
*/
PROPERTIES = 'properties',
/**
* A stack output has changed
*/
OUTPUT = 'output',
/**
* A dependant resource is not hotswappable
*/
DEPENDENCY_UNSUPPORTED = 'dependency-unsupported',
/**
* The resource type is not hotswappable
*/
RESOURCE_UNSUPPORTED = 'resource-unsupported',
/**
* The resource is created in the deployment
*/
RESOURCE_CREATION = 'resource-creation',
/**
* The resource is removed in the deployment
*/
RESOURCE_DELETION = 'resource-deletion',
/**
* The resource identified by the logical id has its type changed
*/
RESOURCE_TYPE_CHANGED = 'resource-type-changed',
/**
* The nested stack is created in the deployment
*/
NESTED_STACK_CREATION = 'nested-stack-creation',
}

/**
* Information about a hotswap deployment
*/
Expand Down
27 changes: 19 additions & 8 deletions packages/aws-cdk/lib/api/deployments/hotswap-deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type * as cxapi from '@aws-cdk/cx-api';
import type { WaiterResult } from '@smithy/util-waiter';
import * as chalk from 'chalk';
import type { AffectedResource, ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads';
import { NonHotswappableReason } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads';
import type { IMessageSpan, IoHelper } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private';
import { IO, SPAN } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private';
import type { SDK, SdkProvider } from '../aws-auth';
Expand Down Expand Up @@ -77,7 +78,11 @@ const RESOURCE_DETECTORS: { [key: string]: HotswapDetector } = {
return [];
}

return reportNonHotswappableResource(change, 'This resource type is not supported for hotswap deployments');
return reportNonHotswappableResource(
change,
NonHotswappableReason.RESOURCE_UNSUPPORTED,
'This resource type is not supported for hotswap deployments',
);
},

'AWS::CDK::Metadata': async () => [],
Expand Down Expand Up @@ -210,7 +215,8 @@ async function classifyResourceChanges(
for (const logicalId of Object.keys(stackChanges.outputs.changes)) {
nonHotswappableResources.push({
hotswappable: false,
reason: 'output was changed',
reason: NonHotswappableReason.OUTPUT,
description: 'output was changed',
logicalId,
rejectedChanges: [],
resourceType: 'Stack Output',
Expand Down Expand Up @@ -253,6 +259,7 @@ async function classifyResourceChanges(
reportNonHotswappableChange(
nonHotswappableResources,
hotswappableChangeCandidate,
NonHotswappableReason.RESOURCE_UNSUPPORTED,
undefined,
'This resource type is not supported for hotswap deployments',
);
Expand Down Expand Up @@ -350,7 +357,8 @@ async function findNestedHotswappableChanges(
{
hotswappable: false,
logicalId,
reason: `physical name for AWS::CloudFormation::Stack '${logicalId}' could not be found in CloudFormation, so this is a newly created nested stack and cannot be hotswapped`,
reason: NonHotswappableReason.NESTED_STACK_CREATION,
description: `physical name for AWS::CloudFormation::Stack '${logicalId}' could not be found in CloudFormation, so this is a newly created nested stack and cannot be hotswapped`,
rejectedChanges: [],
resourceType: 'AWS::CloudFormation::Stack',
},
Expand Down Expand Up @@ -425,15 +433,17 @@ function isCandidateForHotswapping(
resourceType: change.newValue!.Type,
logicalId,
rejectedChanges: [],
reason: `resource '${logicalId}' was created by this deployment`,
reason: NonHotswappableReason.RESOURCE_CREATION,
description: `resource '${logicalId}' was created by this deployment`,
};
} else if (!change.newValue) {
return {
hotswappable: false,
resourceType: change.oldValue!.Type,
logicalId,
rejectedChanges: [],
reason: `resource '${logicalId}' was destroyed by this deployment`,
reason: NonHotswappableReason.RESOURCE_DELETION,
description: `resource '${logicalId}' was destroyed by this deployment`,
};
}

Expand All @@ -444,7 +454,8 @@ function isCandidateForHotswapping(
resourceType: change.newValue?.Type,
logicalId,
rejectedChanges: [],
reason: `resource '${logicalId}' had its type changed from '${change.oldValue?.Type}' to '${change.newValue?.Type}'`,
reason: NonHotswappableReason.RESOURCE_TYPE_CHANGED,
description: `resource '${logicalId}' had its type changed from '${change.oldValue?.Type}' to '${change.newValue?.Type}'`,
};
}

Expand Down Expand Up @@ -555,14 +566,14 @@ async function logNonHotswappableChanges(
chalk.bold(change.logicalId),
chalk.bold(change.resourceType),
chalk.bold(change.rejectedChanges),
chalk.red(change.reason),
chalk.red(change.description),
));
} else {
messages.push(format(
' logicalID: %s, type: %s, reason: %s',
chalk.bold(change.logicalId),
chalk.bold(change.resourceType),
chalk.red(change.reason),
chalk.red(change.description),
));
}
}
Expand Down
33 changes: 20 additions & 13 deletions packages/aws-cdk/lib/api/hotswap/common.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { PropertyDifference } from '@aws-cdk/cloudformation-diff';
import type { CloudFormationStackArtifact } from '@aws-cdk/cx-api';
import type { HotswappableChange, ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap';
import { NonHotswappableReason } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap';
import { ToolkitError } from '../../toolkit/error';
import type { SDK } from '../aws-auth';

Expand Down Expand Up @@ -58,11 +59,15 @@ export interface NonHotswappableChange {
readonly resourceType: string;
readonly rejectedChanges: Array<string>;
readonly logicalId: string;
/**
* Why was this change was deemed non-hotswappable
*/
readonly reason: NonHotswappableReason;
/**
* Tells the user exactly why this change was deemed non-hotswappable and what its logical ID is.
* If not specified, `reason` will be autofilled to state that the properties listed in `rejectedChanges` are not hotswappable.
* If not specified, `displayReason` default to state that the properties listed in `rejectedChanges` are not hotswappable.
*/
readonly reason?: string;
readonly description?: string;
/**
* Whether or not to show this change when listing non-hotswappable changes in HOTSWAP_ONLY mode. Does not affect
* listing in FALL_BACK mode.
Expand Down Expand Up @@ -195,13 +200,15 @@ export class ClassifiedChanges {
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`;

reportNonHotswappableChange(
ret,
this.change,
reason,
this.nonHotswappableProps,
tagOnlyChange
? 'Tags are not hotswappable'
: `resource properties '${nonHotswappablePropNames}' are not hotswappable on this resource type`,
description,
);
}
}
Expand Down Expand Up @@ -229,27 +236,26 @@ export function classifyChanges(xs: ResourceChange, hotswappablePropNames: strin
export function reportNonHotswappableChange(
ret: ChangeHotswapResult,
change: ResourceChange,
reason: NonHotswappableReason,
nonHotswappableProps?: PropDiffs,
reason?: string,
hotswapOnlyVisible?: boolean,
description?: string,
hotswapOnlyVisible: boolean = true,
): void {
let hotswapOnlyVisibility = true;
if (hotswapOnlyVisible === false) {
hotswapOnlyVisibility = false;
}
ret.push({
hotswappable: false,
rejectedChanges: Object.keys(nonHotswappableProps ?? change.propertyUpdates),
logicalId: change.logicalId,
resourceType: change.newValue.Type,
reason,
hotswapOnlyVisible: hotswapOnlyVisibility,
description: description,
hotswapOnlyVisible,
});
}

export function reportNonHotswappableResource(
change: ResourceChange,
reason?: string,
reason: NonHotswappableReason,
description?: string,
): ChangeHotswapResult {
return [
{
Expand All @@ -258,6 +264,7 @@ export function reportNonHotswappableResource(
logicalId: change.logicalId,
resourceType: change.newValue.Type,
reason,
description: description,
},
];
}
21 changes: 17 additions & 4 deletions packages/aws-cdk/lib/api/hotswap/ecs-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
reportNonHotswappableChange,
transformObjectKeys,
} from './common';
import type { ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap';
import { NonHotswappableReason, type ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap';
import type { SDK } from '../aws-auth';
import type { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template';

Expand Down Expand Up @@ -48,9 +48,21 @@ export async function isHotswappableEcsServiceChange(
}
}
if (ecsServicesReferencingTaskDef.length === 0) {
// if there are no resources referencing the TaskDefinition,
// hotswap is not possible in FALL_BACK mode
reportNonHotswappableChange(ret, change, undefined, 'No ECS services reference the changed task definition', false);
/**
* ECS Services can have a task definition that doesn't refer to the task definition being updated.
* We have to log this as a non-hotswappable change to the task definition, but when we do,
* we wind up hotswapping the task definition and logging it as a non-hotswappable change.
*
* This logic prevents us from logging that change as non-hotswappable when we hotswap it.
*/
reportNonHotswappableChange(
ret,
change,
NonHotswappableReason.DEPENDENCY_UNSUPPORTED,
undefined,
'No ECS services reference the changed task definition',
false,
);
}
if (resourcesReferencingTaskDef.length > ecsServicesReferencingTaskDef.length) {
// if something besides an ECS Service is referencing the TaskDefinition,
Expand All @@ -60,6 +72,7 @@ export async function isHotswappableEcsServiceChange(
reportNonHotswappableChange(
ret,
change,
NonHotswappableReason.DEPENDENCY_UNSUPPORTED,
undefined,
`A resource '${taskRef.LogicalId}' with Type '${taskRef.Type}' that is not an ECS Service was found referencing the changed TaskDefinition '${logicalId}'`,
);
Expand Down
Loading