Skip to content

Commit 4ae4df8

Browse files
authored
feat(cli): hotswap deployments for CodeBuild projects (#18161)
This extends the `cdk deploy --hotswap` command to support CodeBuild projects. This supports all changes to the `Source`, `SourceVersion`, and `Environment` attributes of the AWS::CodeBuild::Project cloudformation resource. The possible changes supported on the [Project](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-codebuild.Project.html) L2 Construct will be changes to the [buildSpec](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-codebuild.Project.html#buildspec), [environment](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-codebuild.Project.html#environment), [environmentVariables](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-codebuild.Project.html#environmentvariables), and [source](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-codebuild.Project.html#source) constructor props. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent caa5178 commit 4ae4df8

File tree

9 files changed

+731
-20
lines changed

9 files changed

+731
-20
lines changed

packages/aws-cdk/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ Hotswapping is currently supported for the following changes
366366
- Definition changes of AWS Step Functions State Machines.
367367
- Container asset changes of AWS ECS Services.
368368
- Website asset changes of AWS S3 Bucket Deployments.
369+
- Source and Environment changes of AWS CodeBuild Projects.
369370

370371
**⚠ Note #1**: This command deliberately introduces drift in CloudFormation stacks in order to speed up deployments.
371372
For this reason, only use it for development purposes.

packages/aws-cdk/lib/api/aws-auth/sdk.ts

+5
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export interface ISDK {
6161
secretsManager(): AWS.SecretsManager;
6262
kms(): AWS.KMS;
6363
stepFunctions(): AWS.StepFunctions;
64+
codeBuild(): AWS.CodeBuild
6465
}
6566

6667
/**
@@ -180,6 +181,10 @@ export class SDK implements ISDK {
180181
return this.wrapServiceErrorHandling(new AWS.StepFunctions(this.config));
181182
}
182183

184+
public codeBuild(): AWS.CodeBuild {
185+
return this.wrapServiceErrorHandling(new AWS.CodeBuild(this.config));
186+
}
187+
183188
public async currentAccount(): Promise<Account> {
184189
// Get/refresh if necessary before we can access `accessKeyId`
185190
await this.forceCredentialRetrieval();

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

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as colors from 'colors/safe';
55
import { print } from '../logging';
66
import { ISDK, Mode, SdkProvider } from './aws-auth';
77
import { DeployStackResult } from './deploy-stack';
8+
import { isHotswappableCodeBuildProjectChange } from './hotswap/code-build-projects';
89
import { ICON, ChangeHotswapImpact, ChangeHotswapResult, HotswapOperation, HotswappableChangeCandidate, ListStackResources } from './hotswap/common';
910
import { isHotswappableEcsServiceChange } from './hotswap/ecs-services';
1011
import { EvaluateCloudFormationTemplate } from './hotswap/evaluate-cloudformation-template';
@@ -77,6 +78,7 @@ async function findAllHotswappableChanges(
7778
isHotswappableStateMachineChange(logicalId, resourceHotswapEvaluation, evaluateCfnTemplate),
7879
isHotswappableEcsServiceChange(logicalId, resourceHotswapEvaluation, evaluateCfnTemplate),
7980
isHotswappableS3BucketDeploymentChange(logicalId, resourceHotswapEvaluation, evaluateCfnTemplate),
81+
isHotswappableCodeBuildProjectChange(logicalId, resourceHotswapEvaluation, evaluateCfnTemplate),
8082
]);
8183
}
8284
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import * as AWS from 'aws-sdk';
2+
import { ISDK } from '../aws-auth';
3+
import { ChangeHotswapImpact, ChangeHotswapResult, establishResourcePhysicalName, HotswapOperation, HotswappableChangeCandidate, lowerCaseFirstCharacter, transformObjectKeys } from './common';
4+
import { EvaluateCloudFormationTemplate } from './evaluate-cloudformation-template';
5+
6+
export async function isHotswappableCodeBuildProjectChange(
7+
logicalId: string, change: HotswappableChangeCandidate, evaluateCfnTemplate: EvaluateCloudFormationTemplate,
8+
): Promise<ChangeHotswapResult> {
9+
if (change.newValue.Type !== 'AWS::CodeBuild::Project') {
10+
return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT;
11+
}
12+
13+
const updateProjectInput: AWS.CodeBuild.UpdateProjectInput = {
14+
name: '',
15+
};
16+
for (const updatedPropName in change.propertyUpdates) {
17+
const updatedProp = change.propertyUpdates[updatedPropName];
18+
switch (updatedPropName) {
19+
case 'Source':
20+
updateProjectInput.source = transformObjectKeys(
21+
await evaluateCfnTemplate.evaluateCfnExpression(updatedProp.newValue),
22+
convertSourceCloudformationKeyToSdkKey,
23+
);
24+
break;
25+
case 'Environment':
26+
updateProjectInput.environment = await transformObjectKeys(
27+
await evaluateCfnTemplate.evaluateCfnExpression(updatedProp.newValue),
28+
lowerCaseFirstCharacter,
29+
);
30+
break;
31+
case 'SourceVersion':
32+
updateProjectInput.sourceVersion = await evaluateCfnTemplate.evaluateCfnExpression(updatedProp.newValue);
33+
break;
34+
default:
35+
return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT;
36+
}
37+
}
38+
39+
const projectName = await establishResourcePhysicalName(logicalId, change.newValue.Properties?.Name, evaluateCfnTemplate);
40+
if (!projectName) {
41+
return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT;
42+
}
43+
updateProjectInput.name = projectName;
44+
return new ProjectHotswapOperation(updateProjectInput);
45+
}
46+
47+
class ProjectHotswapOperation implements HotswapOperation {
48+
public readonly service = 'codebuild'
49+
public readonly resourceNames: string[];
50+
51+
constructor(
52+
private readonly updateProjectInput: AWS.CodeBuild.UpdateProjectInput,
53+
) {
54+
this.resourceNames = [updateProjectInput.name];
55+
}
56+
57+
public async apply(sdk: ISDK): Promise<any> {
58+
return sdk.codeBuild().updateProject(this.updateProjectInput).promise();
59+
}
60+
}
61+
62+
function convertSourceCloudformationKeyToSdkKey(key: string): string {
63+
if (key.toLowerCase() === 'buildspec') {
64+
return key.toLowerCase();
65+
}
66+
return lowerCaseFirstCharacter(key);
67+
}

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

+28
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,31 @@ export async function establishResourcePhysicalName(
8282
}
8383
return evaluateCfnTemplate.findPhysicalNameFor(logicalId);
8484
}
85+
86+
/**
87+
* This function transforms all keys (recursively) in the provided `val` object.
88+
*
89+
* @param val The object whose keys need to be transformed.
90+
* @param transform The function that will be applied to each key.
91+
* @returns A new object with the same values as `val`, but with all keys transformed according to `transform`.
92+
*/
93+
export function transformObjectKeys(val: any, transform: (str: string) => string): any {
94+
if (val == null || typeof val !== 'object') {
95+
return val;
96+
}
97+
if (Array.isArray(val)) {
98+
return val.map((input: any) => transformObjectKeys(input, transform));
99+
}
100+
const ret: { [k: string]: any; } = {};
101+
for (const [k, v] of Object.entries(val)) {
102+
ret[transform(k)] = transformObjectKeys(v, transform);
103+
}
104+
return ret;
105+
}
106+
107+
/**
108+
* This function lower cases the first character of the string provided.
109+
*/
110+
export function lowerCaseFirstCharacter(str: string): string {
111+
return str.length > 0 ? `${str[0].toLowerCase()}${str.substr(1)}` : str;
112+
}

packages/aws-cdk/lib/api/hotswap/ecs-services.ts

+2-20
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as AWS from 'aws-sdk';
22
import { ISDK } from '../aws-auth';
3-
import { ChangeHotswapImpact, ChangeHotswapResult, establishResourcePhysicalName, HotswapOperation, HotswappableChangeCandidate } from './common';
3+
import { ChangeHotswapImpact, ChangeHotswapResult, establishResourcePhysicalName, HotswapOperation, HotswappableChangeCandidate, lowerCaseFirstCharacter, transformObjectKeys } from './common';
44
import { EvaluateCloudFormationTemplate } from './evaluate-cloudformation-template';
55

66
export async function isHotswappableEcsServiceChange(
@@ -90,7 +90,7 @@ class EcsServiceHotswapOperation implements HotswapOperation {
9090
// Step 1 - update the changed TaskDefinition, creating a new TaskDefinition Revision
9191
// we need to lowercase the evaluated TaskDef from CloudFormation,
9292
// as the AWS SDK uses lowercase property names for these
93-
const lowercasedTaskDef = lowerCaseFirstCharacterOfObjectKeys(this.taskDefinitionResource);
93+
const lowercasedTaskDef = transformObjectKeys(this.taskDefinitionResource, lowerCaseFirstCharacter);
9494
const registerTaskDefResponse = await sdk.ecs().registerTaskDefinition(lowercasedTaskDef).promise();
9595
const taskDefRevArn = registerTaskDefResponse.taskDefinition?.taskDefinitionArn;
9696

@@ -172,21 +172,3 @@ class EcsServiceHotswapOperation implements HotswapOperation {
172172
}));
173173
}
174174
}
175-
176-
function lowerCaseFirstCharacterOfObjectKeys(val: any): any {
177-
if (val == null || typeof val !== 'object') {
178-
return val;
179-
}
180-
if (Array.isArray(val)) {
181-
return val.map(lowerCaseFirstCharacterOfObjectKeys);
182-
}
183-
const ret: { [k: string]: any; } = {};
184-
for (const [k, v] of Object.entries(val)) {
185-
ret[lowerCaseFirstCharacter(k)] = lowerCaseFirstCharacterOfObjectKeys(v);
186-
}
187-
return ret;
188-
}
189-
190-
function lowerCaseFirstCharacter(str: string): string {
191-
return str.length > 0 ? `${str[0].toLowerCase()}${str.substr(1)}` : str;
192-
}

0 commit comments

Comments
 (0)