1
+ import { flatMap } from '../../util' ;
1
2
import { ISDK } from '../aws-auth' ;
2
- import { ChangeHotswapImpact , ChangeHotswapResult , HotswapOperation , HotswappableChangeCandidate , establishResourcePhysicalName } from './common' ;
3
+ import { ChangeHotswapImpact , ChangeHotswapResult , establishResourcePhysicalName , HotswapOperation , HotswappableChangeCandidate } from './common' ;
3
4
import { EvaluateCloudFormationTemplate } from './evaluate-cloudformation-template' ;
4
5
5
6
/**
@@ -11,25 +12,62 @@ import { EvaluateCloudFormationTemplate } from './evaluate-cloudformation-templa
11
12
export async function isHotswappableLambdaFunctionChange (
12
13
logicalId : string , change : HotswappableChangeCandidate , evaluateCfnTemplate : EvaluateCloudFormationTemplate ,
13
14
) : Promise < ChangeHotswapResult > {
15
+ // if the change is for a Lambda Version,
16
+ // ignore it by returning an empty hotswap operation -
17
+ // we will publish a new version when we get to hotswapping the actual Function this Version points to, below
18
+ // (Versions can't be changed in CloudFormation anyway, they're immutable)
19
+ if ( change . newValue . Type === 'AWS::Lambda::Version' ) {
20
+ return ChangeHotswapImpact . IRRELEVANT ;
21
+ }
22
+
23
+ // we handle Aliases specially too
24
+ if ( change . newValue . Type === 'AWS::Lambda::Alias' ) {
25
+ return checkAliasHasVersionOnlyChange ( change ) ;
26
+ }
27
+
14
28
const lambdaCodeChange = await isLambdaFunctionCodeOnlyChange ( change , evaluateCfnTemplate ) ;
15
29
if ( typeof lambdaCodeChange === 'string' ) {
16
30
return lambdaCodeChange ;
17
- } else {
18
- const functionName = await establishResourcePhysicalName ( logicalId , change . newValue . Properties ?. FunctionName , evaluateCfnTemplate ) ;
19
- if ( ! functionName ) {
20
- return ChangeHotswapImpact . REQUIRES_FULL_DEPLOYMENT ;
21
- }
31
+ }
22
32
23
- const functionArn = await evaluateCfnTemplate . evaluateCfnExpression ( {
24
- 'Fn::Sub' : 'arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:' + functionName ,
25
- } ) ;
33
+ const functionName = await establishResourcePhysicalName ( logicalId , change . newValue . Properties ?. FunctionName , evaluateCfnTemplate ) ;
34
+ if ( ! functionName ) {
35
+ return ChangeHotswapImpact . REQUIRES_FULL_DEPLOYMENT ;
36
+ }
37
+
38
+ const functionArn = await evaluateCfnTemplate . evaluateCfnExpression ( {
39
+ 'Fn::Sub' : 'arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:' + functionName ,
40
+ } ) ;
41
+
42
+ // find all Lambda Versions that reference this Function
43
+ const versionsReferencingFunction = evaluateCfnTemplate . findReferencesTo ( logicalId )
44
+ . filter ( r => r . Type === 'AWS::Lambda::Version' ) ;
45
+ // find all Lambda Aliases that reference the above Versions
46
+ const aliasesReferencingVersions = flatMap ( versionsReferencingFunction , v =>
47
+ evaluateCfnTemplate . findReferencesTo ( v . LogicalId ) ) ;
48
+ const aliasesNames = await Promise . all ( aliasesReferencingVersions . map ( a =>
49
+ evaluateCfnTemplate . evaluateCfnExpression ( a . Properties ?. Name ) ) ) ;
50
+
51
+ return new LambdaFunctionHotswapOperation ( {
52
+ physicalName : functionName ,
53
+ functionArn : functionArn ,
54
+ resource : lambdaCodeChange ,
55
+ publishVersion : versionsReferencingFunction . length > 0 ,
56
+ aliasesNames,
57
+ } ) ;
58
+ }
26
59
27
- return new LambdaFunctionHotswapOperation ( {
28
- physicalName : functionName ,
29
- functionArn : functionArn ,
30
- resource : lambdaCodeChange ,
31
- } ) ;
60
+ /**
61
+ * Returns is a given Alias change is only in the 'FunctionVersion' property,
62
+ * and `ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT` is the change is for any other property.
63
+ */
64
+ function checkAliasHasVersionOnlyChange ( change : HotswappableChangeCandidate ) : ChangeHotswapResult {
65
+ for ( const updatedPropName in change . propertyUpdates ) {
66
+ if ( updatedPropName !== 'FunctionVersion' ) {
67
+ return ChangeHotswapImpact . REQUIRES_FULL_DEPLOYMENT ;
68
+ }
32
69
}
70
+ return ChangeHotswapImpact . IRRELEVANT ;
33
71
}
34
72
35
73
/**
@@ -50,7 +88,7 @@ async function isLambdaFunctionCodeOnlyChange(
50
88
}
51
89
52
90
/*
53
- * On first glance, we would want to initialize these using the "previous" values (change.oldValue),
91
+ * At first glance, we would want to initialize these using the "previous" values (change.oldValue),
54
92
* in case only one of them changed, like the key, and the Bucket stayed the same.
55
93
* However, that actually fails for old-style synthesis, which uses CFN Parameters!
56
94
* Because the names of the Parameters depend on the hash of the Asset,
@@ -60,7 +98,6 @@ async function isLambdaFunctionCodeOnlyChange(
60
98
* even if only one of them was actually changed,
61
99
* which means we don't need the "old" values at all, and we can safely initialize these with just `''`.
62
100
*/
63
- // Make sure only the code in the Lambda function changed
64
101
const propertyUpdates = change . propertyUpdates ;
65
102
let code : LambdaFunctionCode | undefined = undefined ;
66
103
let tags : LambdaFunctionTags | undefined = undefined ;
@@ -149,14 +186,22 @@ interface LambdaFunctionResource {
149
186
readonly physicalName : string ;
150
187
readonly functionArn : string ;
151
188
readonly resource : LambdaFunctionChange ;
189
+ readonly publishVersion : boolean ;
190
+ readonly aliasesNames : string [ ] ;
152
191
}
153
192
154
193
class LambdaFunctionHotswapOperation implements HotswapOperation {
155
194
public readonly service = 'lambda-function' ;
156
195
public readonly resourceNames : string [ ] ;
157
196
158
197
constructor ( private readonly lambdaFunctionResource : LambdaFunctionResource ) {
159
- this . resourceNames = [ lambdaFunctionResource . physicalName ] ;
198
+ this . resourceNames = [
199
+ `Lambda Function '${ lambdaFunctionResource . physicalName } '` ,
200
+ // add Version here if we're publishing a new one
201
+ ...( lambdaFunctionResource . publishVersion ? [ `Lambda Version for Function '${ lambdaFunctionResource . physicalName } '` ] : [ ] ) ,
202
+ // add any Aliases that we are hotswapping here
203
+ ...lambdaFunctionResource . aliasesNames . map ( alias => `Lambda Alias '${ alias } ' for Function '${ lambdaFunctionResource . physicalName } '` ) ,
204
+ ] ;
160
205
}
161
206
162
207
public async apply ( sdk : ISDK ) : Promise < any > {
@@ -165,11 +210,44 @@ class LambdaFunctionHotswapOperation implements HotswapOperation {
165
210
const operations : Promise < any > [ ] = [ ] ;
166
211
167
212
if ( resource . code !== undefined ) {
168
- operations . push ( lambda . updateFunctionCode ( {
213
+ const updateFunctionCodePromise = lambda . updateFunctionCode ( {
169
214
FunctionName : this . lambdaFunctionResource . physicalName ,
170
215
S3Bucket : resource . code . s3Bucket ,
171
216
S3Key : resource . code . s3Key ,
172
- } ) . promise ( ) ) ;
217
+ } ) . promise ( ) ;
218
+
219
+ // only if the code changed is there any point in publishing a new Version
220
+ if ( this . lambdaFunctionResource . publishVersion ) {
221
+ // we need to wait for the code update to be done before publishing a new Version
222
+ await updateFunctionCodePromise ;
223
+ // if we don't wait for the Function to finish updating,
224
+ // we can get a "The operation cannot be performed at this time. An update is in progress for resource:"
225
+ // error when publishing a new Version
226
+ await lambda . waitFor ( 'functionUpdated' , {
227
+ FunctionName : this . lambdaFunctionResource . physicalName ,
228
+ } ) . promise ( ) ;
229
+
230
+ const publishVersionPromise = lambda . publishVersion ( {
231
+ FunctionName : this . lambdaFunctionResource . physicalName ,
232
+ } ) . promise ( ) ;
233
+
234
+ if ( this . lambdaFunctionResource . aliasesNames . length > 0 ) {
235
+ // we need to wait for the Version to finish publishing
236
+ const versionUpdate = await publishVersionPromise ;
237
+
238
+ for ( const alias of this . lambdaFunctionResource . aliasesNames ) {
239
+ operations . push ( lambda . updateAlias ( {
240
+ FunctionName : this . lambdaFunctionResource . physicalName ,
241
+ Name : alias ,
242
+ FunctionVersion : versionUpdate . Version ,
243
+ } ) . promise ( ) ) ;
244
+ }
245
+ } else {
246
+ operations . push ( publishVersionPromise ) ;
247
+ }
248
+ } else {
249
+ operations . push ( updateFunctionCodePromise ) ;
250
+ }
173
251
}
174
252
175
253
if ( resource . tags !== undefined ) {
@@ -184,7 +262,6 @@ class LambdaFunctionHotswapOperation implements HotswapOperation {
184
262
tagsToSet [ tagName ] = tagValue as string ;
185
263
} ) ;
186
264
187
-
188
265
if ( tagsToDelete . length > 0 ) {
189
266
operations . push ( lambda . untagResource ( {
190
267
Resource : this . lambdaFunctionResource . functionArn ,
0 commit comments