-
Notifications
You must be signed in to change notification settings - Fork 3.9k
/
function-hash.ts
154 lines (140 loc) · 5.89 KB
/
function-hash.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import * as crypto from 'crypto';
import { CfnResource, FeatureFlags, Stack } from '@aws-cdk/core';
import { LAMBDA_RECOGNIZE_LAYER_VERSION, LAMBDA_RECOGNIZE_VERSION_PROPS } from '@aws-cdk/cx-api';
import { Function as LambdaFunction } from './function';
import { ILayerVersion } from './layers';
export function calculateFunctionHash(fn: LambdaFunction) {
const stack = Stack.of(fn);
const functionResource = fn.node.defaultChild as CfnResource;
// render the cloudformation resource from this function
const config = stack.resolve((functionResource as any)._toCloudFormation());
// config is of the shape: { Resources: { LogicalId: { Type: 'Function', Properties: { ... } }}}
const resources = config.Resources;
const resourceKeys = Object.keys(resources);
if (resourceKeys.length !== 1) {
throw new Error(`Expected one rendered CloudFormation resource but found ${resourceKeys.length}`);
}
const logicalId = resourceKeys[0];
const properties = resources[logicalId].Properties;
let stringifiedConfig;
if (FeatureFlags.of(fn).isEnabled(LAMBDA_RECOGNIZE_VERSION_PROPS)) {
const updatedProps = sortProperties(filterUsefulKeys(properties));
stringifiedConfig = JSON.stringify(updatedProps);
} else {
const sorted = sortProperties(properties);
config.Resources[logicalId].Properties = sorted;
stringifiedConfig = JSON.stringify(config);
}
if (FeatureFlags.of(fn).isEnabled(LAMBDA_RECOGNIZE_LAYER_VERSION)) {
stringifiedConfig = stringifiedConfig + calculateLayersHash(fn._layers);
}
const hash = crypto.createHash('md5');
hash.update(stringifiedConfig);
return hash.digest('hex');
}
export function trimFromStart(s: string, maxLength: number) {
const desiredLength = Math.min(maxLength, s.length);
const newStart = s.length - desiredLength;
return s.substring(newStart);
}
/*
* The list of properties found in CfnFunction (or AWS::Lambda::Function).
* They are classified as "locked" to a Function Version or not.
* When a property is locked, any change to that property will not take effect on previously created Versions.
* Instead, a new Version must be generated for the change to take effect.
* Similarly, if a property that's not locked to a Version is modified, a new Version
* must not be generated.
*
* Adding a new property to this list - If the property is part of the UpdateFunctionConfiguration
* API or UpdateFunctionCode API, then it must be classified as true, otherwise false.
* See https://docs.aws.amazon.com/lambda/latest/dg/API_UpdateFunctionConfiguration.html and
* https://docs.aws.amazon.com/lambda/latest/dg/API_UpdateFunctionConfiguration.html
*/
export const VERSION_LOCKED: { [key: string]: boolean } = {
// locked to the version
Architectures: true,
Code: true,
DeadLetterConfig: true,
Description: true,
Environment: true,
EphemeralStorage: true,
FileSystemConfigs: true,
FunctionName: true,
Handler: true,
ImageConfig: true,
KmsKeyArn: true,
Layers: true,
MemorySize: true,
PackageType: true,
Role: true,
Runtime: true,
RuntimeManagementConfig: true,
SnapStart: true,
Timeout: true,
TracingConfig: true,
VpcConfig: true,
// not locked to the version
CodeSigningConfigArn: false,
ReservedConcurrentExecutions: false,
Tags: false,
};
function filterUsefulKeys(properties: any) {
const versionProps = { ...VERSION_LOCKED, ...LambdaFunction._VER_PROPS };
const unclassified = Object.entries(properties)
.filter(([k, v]) => v != null && !Object.keys(versionProps).includes(k))
.map(([k, _]) => k);
if (unclassified.length > 0) {
throw new Error(`The following properties are not recognized as version properties: [${unclassified}].`
+ ' See the README of the aws-lambda module to learn more about this and to fix it.');
}
const notLocked = Object.entries(versionProps).filter(([_, v]) => !v).map(([k, _]) => k);
notLocked.forEach(p => delete properties[p]);
const ret: { [key: string]: any } = {};
Object.entries(properties).filter(([k, _]) => versionProps[k]).forEach(([k, v]) => ret[k] = v);
return ret;
}
function sortProperties(properties: any) {
const ret: any = {};
// We take all required properties in the order that they were historically,
// to make sure the hash we calculate is stable.
// There cannot be more required properties added in the future,
// as that would be a backwards-incompatible change.
const requiredProperties = ['Code', 'Handler', 'Role', 'Runtime'];
for (const requiredProperty of requiredProperties) {
ret[requiredProperty] = properties[requiredProperty];
}
// then, add all of the non-required properties,
// in the original order
for (const property of Object.keys(properties)) {
if (requiredProperties.indexOf(property) === -1) {
ret[property] = properties[property];
}
}
return ret;
}
function calculateLayersHash(layers: ILayerVersion[]): string {
const layerConfig: {[key: string]: any } = {};
for (const layer of layers) {
const stack = Stack.of(layer);
const layerResource = layer.node.defaultChild as CfnResource;
// if there is no layer resource, then the layer was imported
// and we will include the layer arn and runtimes in the hash
if (layerResource === undefined) {
layerConfig[layer.layerVersionArn] = layer.compatibleRuntimes;
continue;
}
const config = stack.resolve((layerResource as any)._toCloudFormation());
const resources = config.Resources;
const resourceKeys = Object.keys(resources);
if (resourceKeys.length !== 1) {
throw new Error(`Expected one rendered CloudFormation resource but found ${resourceKeys.length}`);
}
const logicalId = resourceKeys[0];
const properties = resources[logicalId].Properties;
// all properties require replacement, so they are all version locked.
layerConfig[layer.node.id] = properties;
}
const hash = crypto.createHash('md5');
hash.update(JSON.stringify(layerConfig));
return hash.digest('hex');
}