Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(lambda): use of currentVersion fails deployment after upgrade #26777

Merged
merged 3 commits into from
Aug 17, 2023
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
136 changes: 92 additions & 44 deletions packages/aws-cdk-lib/aws-lambda/lib/function-hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,16 @@ export function calculateFunctionHash(fn: LambdaFunction, additional: string = '
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;
const { properties, template, logicalId } = resolveSingleResourceProperties(stack, functionResource);

let stringifiedConfig;
if (FeatureFlags.of(fn).isEnabled(LAMBDA_RECOGNIZE_VERSION_PROPS)) {
const updatedProps = sortProperties(filterUsefulKeys(properties));
const updatedProps = sortFunctionProperties(filterUsefulKeys(properties));
stringifiedConfig = JSON.stringify(updatedProps);
} else {
const sorted = sortProperties(properties);
config.Resources[logicalId].Properties = sorted;
stringifiedConfig = JSON.stringify(config);
const sorted = sortFunctionProperties(properties);
template.Resources[logicalId].Properties = sorted;
stringifiedConfig = JSON.stringify(template);
}

if (FeatureFlags.of(fn).isEnabled(LAMBDA_RECOGNIZE_LAYER_VERSION)) {
Expand Down Expand Up @@ -103,26 +93,6 @@ function filterUsefulKeys(properties: any) {
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) {
Expand All @@ -143,17 +113,95 @@ function calculateLayersHash(layers: ILayerVersion[]): string {
}
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;

const { properties } = resolveSingleResourceProperties(stack, layerResource);

// all properties require replacement, so they are all version locked.
layerConfig[layer.node.id] = properties;
layerConfig[layer.node.id] = sortLayerVersionProperties(properties);
}

return md5hash(JSON.stringify(layerConfig));
}

/**
* Sort properties in an object according to a sort order of known keys
*
* Any additional keys are added at the end, but also sorted.
*
* We only sort one level deep, because we rely on the fact that everything
* that needs to be sorted happens to be sorted by the codegen already, and
* we explicitly rely on some objects NOT being sorted.
*/
class PropertySort {
constructor(private readonly knownKeysOrder: string[]) {
}

public sortObject(properties: any): any {
const ret: any = {};

// Scratch-off set for keys we don't know about yet
const unusedKeys = new Set(Object.keys(properties));
for (const prop of this.knownKeysOrder) {
ret[prop] = properties[prop];
unusedKeys.delete(prop);
}

for (const prop of Array.from(unusedKeys).sort()) {
ret[prop] = properties[prop];
}

return ret;
}
}

/**
* Sort properties in a stable order, even as we switch to new codegen
*
* <=2.87.0, we used to generate properties in the order that they occurred in
* the CloudFormation spec. >= 2.88.0, we switched to a new spec source, which
* sorts the properties lexicographically. The order change changed the hash,
* even though the properties themselves have not changed.
*
* We now have a set of properties with the sort order <=2.87.0, and add any
* additional properties later on, but also sort them.
*
* We should be making sure that the orderings for all subobjects
* between 2.87.0 and 2.88.0 are the same, but fortunately all the subobjects
* were already in lexicographic order in <=2.87.0 so we only need to sort some
* top-level properties on the resource.
*
* We also can't deep-sort everything, because for backwards compatibility
* reasons we have a test that ensures that environment variables are not
* lexicographically sorted, but emitted in the order they are added in source
* code, so for now we rely on the codegen being lexicographically sorted.
*/
function sortFunctionProperties(properties: any) {
return new PropertySort([
// <= 2.87 explicitly fixed order
'Code', 'Handler', 'Role', 'Runtime',
// <= 2.87 implicitly fixed order
'Architectures', 'CodeSigningConfigArn', 'DeadLetterConfig', 'Description', 'Environment',
'EphemeralStorage', 'FileSystemConfigs', 'FunctionName', 'ImageConfig', 'KmsKeyArn', 'Layers',
'MemorySize', 'PackageType', 'ReservedConcurrentExecutions', 'RuntimeManagementConfig', 'SnapStart',
'Tags', 'Timeout', 'TracingConfig', 'VpcConfig',
]).sortObject(properties);
}

function sortLayerVersionProperties(properties: any) {
return new PropertySort([
// <=2.87.0 implicit sort order
'Content', 'CompatibleArchitectures', 'CompatibleRuntimes', 'Description',
'LayerName', 'LicenseInfo',
]).sortObject(properties);
}

function resolveSingleResourceProperties(stack: Stack, res: CfnResource): any {
const template = stack.resolve(res._toCloudFormation());
const resources = template.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];
return { properties: resources[logicalId].Properties, template, logicalId };
}
62 changes: 62 additions & 0 deletions packages/aws-cdk-lib/aws-lambda/test/function.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3246,6 +3246,68 @@ test('set SnapStart to desired value', () => {
});
});

test('test 2.87.0 version hash stability', () => {
// GIVEN
const app = new cdk.App({
context: {
'@aws-cdk/aws-lambda:recognizeLayerVersion': true,
},
});
const stack = new cdk.Stack(app, 'Stack');

// WHEN
const layer = new lambda.LayerVersion(stack, 'MyLayer', {
code: lambda.Code.fromAsset(path.join(__dirname, 'x.zip')),
compatibleRuntimes: [
lambda.Runtime.NODEJS_18_X,
],
});

const role = new iam.Role(stack, 'MyRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
iam.ManagedPolicy.fromAwsManagedPolicyName('AWSXRayDaemonWriteAccess'),
],
});

const lambdaFn = new lambda.Function(stack, 'MyLambda', {
runtime: lambda.Runtime.NODEJS_18_X,
memorySize: 128,
handler: 'index.handler',
timeout: cdk.Duration.seconds(30),
environment: {
VARIABLE_1: 'ONE',
},
code: lambda.Code.fromAsset(path.join(__dirname, 'x.zip')),
role,
currentVersionOptions: {
removalPolicy: cdk.RemovalPolicy.RETAIN,
},
layers: [
layer,
],
});

new lambda.Alias(stack, 'MyAlias', {
aliasName: 'current',
version: lambdaFn.currentVersion,
});

// THEN
// Precalculated version hash using 2.87.0 version
Template.fromStack(stack).hasResource('AWS::Lambda::Alias', {
Properties: {
FunctionVersion: {
'Fn::GetAtt': [
'MyLambdaCurrentVersionE7A382CCd55a48b26bd9a860d8842137f2243c37',
'Version',
],
},
},
});
});

function newTestLambda(scope: constructs.Construct) {
return new lambda.Function(scope, 'MyLambda', {
code: new lambda.InlineCode('foo'),
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk-lib/aws-lambda/test/x.zip
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
x