@@ -8,26 +8,16 @@ export function calculateFunctionHash(fn: LambdaFunction, additional: string = '
8
8
const stack = Stack . of ( fn ) ;
9
9
10
10
const functionResource = fn . node . defaultChild as CfnResource ;
11
-
12
- // render the cloudformation resource from this function
13
- const config = stack . resolve ( ( functionResource as any ) . _toCloudFormation ( ) ) ;
14
- // config is of the shape: { Resources: { LogicalId: { Type: 'Function', Properties: { ... } }}}
15
- const resources = config . Resources ;
16
- const resourceKeys = Object . keys ( resources ) ;
17
- if ( resourceKeys . length !== 1 ) {
18
- throw new Error ( `Expected one rendered CloudFormation resource but found ${ resourceKeys . length } ` ) ;
19
- }
20
- const logicalId = resourceKeys [ 0 ] ;
21
- const properties = resources [ logicalId ] . Properties ;
11
+ const { properties, template, logicalId } = resolveSingleResourceProperties ( stack , functionResource ) ;
22
12
23
13
let stringifiedConfig ;
24
14
if ( FeatureFlags . of ( fn ) . isEnabled ( LAMBDA_RECOGNIZE_VERSION_PROPS ) ) {
25
- const updatedProps = sortProperties ( filterUsefulKeys ( properties ) ) ;
15
+ const updatedProps = sortFunctionProperties ( filterUsefulKeys ( properties ) ) ;
26
16
stringifiedConfig = JSON . stringify ( updatedProps ) ;
27
17
} else {
28
- const sorted = sortProperties ( properties ) ;
29
- config . Resources [ logicalId ] . Properties = sorted ;
30
- stringifiedConfig = JSON . stringify ( config ) ;
18
+ const sorted = sortFunctionProperties ( properties ) ;
19
+ template . Resources [ logicalId ] . Properties = sorted ;
20
+ stringifiedConfig = JSON . stringify ( template ) ;
31
21
}
32
22
33
23
if ( FeatureFlags . of ( fn ) . isEnabled ( LAMBDA_RECOGNIZE_LAYER_VERSION ) ) {
@@ -103,26 +93,6 @@ function filterUsefulKeys(properties: any) {
103
93
return ret ;
104
94
}
105
95
106
- function sortProperties ( properties : any ) {
107
- const ret : any = { } ;
108
- // We take all required properties in the order that they were historically,
109
- // to make sure the hash we calculate is stable.
110
- // There cannot be more required properties added in the future,
111
- // as that would be a backwards-incompatible change.
112
- const requiredProperties = [ 'Code' , 'Handler' , 'Role' , 'Runtime' ] ;
113
- for ( const requiredProperty of requiredProperties ) {
114
- ret [ requiredProperty ] = properties [ requiredProperty ] ;
115
- }
116
- // then, add all of the non-required properties,
117
- // in the original order
118
- for ( const property of Object . keys ( properties ) ) {
119
- if ( requiredProperties . indexOf ( property ) === - 1 ) {
120
- ret [ property ] = properties [ property ] ;
121
- }
122
- }
123
- return ret ;
124
- }
125
-
126
96
function calculateLayersHash ( layers : ILayerVersion [ ] ) : string {
127
97
const layerConfig : { [ key : string ] : any } = { } ;
128
98
for ( const layer of layers ) {
@@ -143,17 +113,95 @@ function calculateLayersHash(layers: ILayerVersion[]): string {
143
113
}
144
114
continue ;
145
115
}
146
- const config = stack . resolve ( ( layerResource as any ) . _toCloudFormation ( ) ) ;
147
- const resources = config . Resources ;
148
- const resourceKeys = Object . keys ( resources ) ;
149
- if ( resourceKeys . length !== 1 ) {
150
- throw new Error ( `Expected one rendered CloudFormation resource but found ${ resourceKeys . length } ` ) ;
151
- }
152
- const logicalId = resourceKeys [ 0 ] ;
153
- const properties = resources [ logicalId ] . Properties ;
116
+
117
+ const { properties } = resolveSingleResourceProperties ( stack , layerResource ) ;
118
+
154
119
// all properties require replacement, so they are all version locked.
155
- layerConfig [ layer . node . id ] = properties ;
120
+ layerConfig [ layer . node . id ] = sortLayerVersionProperties ( properties ) ;
156
121
}
157
122
158
123
return md5hash ( JSON . stringify ( layerConfig ) ) ;
159
124
}
125
+
126
+ /**
127
+ * Sort properties in an object according to a sort order of known keys
128
+ *
129
+ * Any additional keys are added at the end, but also sorted.
130
+ *
131
+ * We only sort one level deep, because we rely on the fact that everything
132
+ * that needs to be sorted happens to be sorted by the codegen already, and
133
+ * we explicitly rely on some objects NOT being sorted.
134
+ */
135
+ class PropertySort {
136
+ constructor ( private readonly knownKeysOrder : string [ ] ) {
137
+ }
138
+
139
+ public sortObject ( properties : any ) : any {
140
+ const ret : any = { } ;
141
+
142
+ // Scratch-off set for keys we don't know about yet
143
+ const unusedKeys = new Set ( Object . keys ( properties ) ) ;
144
+ for ( const prop of this . knownKeysOrder ) {
145
+ ret [ prop ] = properties [ prop ] ;
146
+ unusedKeys . delete ( prop ) ;
147
+ }
148
+
149
+ for ( const prop of Array . from ( unusedKeys ) . sort ( ) ) {
150
+ ret [ prop ] = properties [ prop ] ;
151
+ }
152
+
153
+ return ret ;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Sort properties in a stable order, even as we switch to new codegen
159
+ *
160
+ * <=2.87.0, we used to generate properties in the order that they occurred in
161
+ * the CloudFormation spec. >= 2.88.0, we switched to a new spec source, which
162
+ * sorts the properties lexicographically. The order change changed the hash,
163
+ * even though the properties themselves have not changed.
164
+ *
165
+ * We now have a set of properties with the sort order <=2.87.0, and add any
166
+ * additional properties later on, but also sort them.
167
+ *
168
+ * We should be making sure that the orderings for all subobjects
169
+ * between 2.87.0 and 2.88.0 are the same, but fortunately all the subobjects
170
+ * were already in lexicographic order in <=2.87.0 so we only need to sort some
171
+ * top-level properties on the resource.
172
+ *
173
+ * We also can't deep-sort everything, because for backwards compatibility
174
+ * reasons we have a test that ensures that environment variables are not
175
+ * lexicographically sorted, but emitted in the order they are added in source
176
+ * code, so for now we rely on the codegen being lexicographically sorted.
177
+ */
178
+ function sortFunctionProperties ( properties : any ) {
179
+ return new PropertySort ( [
180
+ // <= 2.87 explicitly fixed order
181
+ 'Code' , 'Handler' , 'Role' , 'Runtime' ,
182
+ // <= 2.87 implicitly fixed order
183
+ 'Architectures' , 'CodeSigningConfigArn' , 'DeadLetterConfig' , 'Description' , 'Environment' ,
184
+ 'EphemeralStorage' , 'FileSystemConfigs' , 'FunctionName' , 'ImageConfig' , 'KmsKeyArn' , 'Layers' ,
185
+ 'MemorySize' , 'PackageType' , 'ReservedConcurrentExecutions' , 'RuntimeManagementConfig' , 'SnapStart' ,
186
+ 'Tags' , 'Timeout' , 'TracingConfig' , 'VpcConfig' ,
187
+ ] ) . sortObject ( properties ) ;
188
+ }
189
+
190
+ function sortLayerVersionProperties ( properties : any ) {
191
+ return new PropertySort ( [
192
+ // <=2.87.0 implicit sort order
193
+ 'Content' , 'CompatibleArchitectures' , 'CompatibleRuntimes' , 'Description' ,
194
+ 'LayerName' , 'LicenseInfo' ,
195
+ ] ) . sortObject ( properties ) ;
196
+ }
197
+
198
+ function resolveSingleResourceProperties ( stack : Stack , res : CfnResource ) : any {
199
+ const template = stack . resolve ( res . _toCloudFormation ( ) ) ;
200
+ const resources = template . Resources ;
201
+ const resourceKeys = Object . keys ( resources ) ;
202
+ if ( resourceKeys . length !== 1 ) {
203
+ throw new Error ( `Expected one rendered CloudFormation resource but found ${ resourceKeys . length } ` ) ;
204
+ }
205
+ const logicalId = resourceKeys [ 0 ] ;
206
+ return { properties : resources [ logicalId ] . Properties , template, logicalId } ;
207
+ }
0 commit comments