Skip to content

Commit

Permalink
Merge pull request #4510 from serverless/fix-version-logical-id
Browse files Browse the repository at this point in the history
Fix lambda version generation when only function config changes
  • Loading branch information
HyperBrain committed Dec 3, 2017
2 parents 8a02d2a + b1d4925 commit 4863835
Show file tree
Hide file tree
Showing 2 changed files with 532 additions and 376 deletions.
146 changes: 95 additions & 51 deletions lib/plugins/aws/package/compile/functions/index.js
@@ -1,5 +1,6 @@
'use strict';

const BbPromise = require('bluebird');
const crypto = require('crypto');
const fs = require('fs');
const _ = require('lodash');
Expand All @@ -15,16 +16,14 @@ class AwsCompileFunctions {

this.provider = this.serverless.getProvider('aws');

this.compileFunctions = this.compileFunctions.bind(this);
this.compileFunction = this.compileFunction.bind(this);

if (this.serverless.service.provider.versionFunctions === undefined ||
this.serverless.service.provider.versionFunctions === null) {
this.serverless.service.provider.versionFunctions = true;
}

this.hooks = {
'package:compileFunctions': this.compileFunctions,
'package:compileFunctions': () => BbPromise.bind(this)
.then(this.compileFunctions),
};
}

Expand Down Expand Up @@ -103,8 +102,7 @@ class AwsCompileFunctions {
' For example: handler.hello.',
' Please check the docs for more info',
].join('');
throw new this.serverless.classes
.Error(errorMessage);
return BbPromise.reject(new this.serverless.classes.Error(errorMessage));
}

const Handler = functionObject.handler;
Expand Down Expand Up @@ -165,7 +163,7 @@ class AwsCompileFunctions {
' race condition when using SQS queue arns and updating the IAM role.',
' Please check the docs for more info.',
].join('');
throw new this.serverless.classes.Error(errorMessage);
return BbPromise.reject(new this.serverless.classes.Error(errorMessage));
}

// update the PolicyDocument statements (if default policy is used)
Expand All @@ -174,7 +172,7 @@ class AwsCompileFunctions {
}
} else {
const errorMessage = 'onError config must be a SNS topic arn or SQS queue arn';
throw new this.serverless.classes.Error(errorMessage);
return BbPromise.reject(new this.serverless.classes.Error(errorMessage));
}
} else if (this.isArnRefOrImportValue(arn)) {
newFunction.Properties.DeadLetterConfig = {
Expand All @@ -185,7 +183,7 @@ class AwsCompileFunctions {
'onError config must be provided as an arn string,',
' Ref or Fn::ImportValue',
].join('');
throw new this.serverless.classes.Error(errorMessage);
return BbPromise.reject(new this.serverless.classes.Error(errorMessage));
}
}

Expand Down Expand Up @@ -226,11 +224,11 @@ class AwsCompileFunctions {
}
} else {
const errorMessage = 'awsKmsKeyArn config must be a KMS key arn';
throw new this.serverless.classes.Error(errorMessage);
return BbPromise.reject(new this.serverless.classes.Error(errorMessage));
}
} else {
const errorMessage = 'awsKmsKeyArn config must be provided as a string';
throw new this.serverless.classes.Error(errorMessage);
return BbPromise.reject(new this.serverless.classes.Error(errorMessage));
}
}

Expand All @@ -242,13 +240,21 @@ class AwsCompileFunctions {
functionObject.environment
);

Object.keys(newFunction.Properties.Environment.Variables).forEach((key) => {
// taken from the bash man pages
if (!key.match(/^[A-Za-z_][a-zA-Z0-9_]*$/)) {
const errorMessage = 'Invalid characters in environment variable';
throw new this.serverless.classes.Error(errorMessage);
let invalidEnvVar = null;
_.forEach(
_.keys(newFunction.Properties.Environment.Variables),
key => { // eslint-disable-line consistent-return
// taken from the bash man pages
if (!key.match(/^[A-Za-z_][a-zA-Z0-9_]*$/)) {
invalidEnvVar = `Invalid characters in environment variable ${key}`;
return false; // break loop with lodash
}
}
});
);

if (invalidEnvVar) {
return BbPromise.reject(new this.serverless.classes.Error(invalidEnvVar));
}
}

if ('role' in functionObject) {
Expand Down Expand Up @@ -287,49 +293,87 @@ class AwsCompileFunctions {

const newVersion = this.cfLambdaVersionTemplate();

const content = fs.readFileSync(artifactFilePath);
const hash = crypto.createHash('sha256');
hash.setEncoding('base64');
hash.write(content);
hash.end();
// Create hashes for the artifact and the logical id of the version resource
// The one for the version resource must include the function configuration
// to make sure that a new version is created on configuration changes and
// not only on source changes.
const fileHash = crypto.createHash('sha256');
const versionHash = crypto.createHash('sha256');
fileHash.setEncoding('base64');
versionHash.setEncoding('base64');

// Read the file in chunks and add them to the hash (saves memory and performance)
return BbPromise.fromCallback(cb => {
const readStream = fs.createReadStream(artifactFilePath);

readStream.on('data', chunk => {
fileHash.write(chunk);
versionHash.write(chunk);
})
.on('end', () => {
cb();
})
.on('error', error => {
cb(error);
});
})
.then(() => {
// Include function configuration in version id hash (without the Code part)
const properties = _.omit(_.get(newFunction, 'Properties', {}), 'Code');
_.forOwn(properties, value => {
const hashedValue = _.isObject(value) ? JSON.stringify(value) : _.toString(value);
versionHash.write(hashedValue);
});

newVersion.Properties.CodeSha256 = hash.read();
newVersion.Properties.FunctionName = { Ref: functionLogicalId };
if (functionObject.description) {
newVersion.Properties.Description = functionObject.description;
}
// Finalize hashes
fileHash.end();
versionHash.end();

// use the SHA in the logical resource ID of the version because
// AWS::Lambda::Version resource will not support updates
const versionLogicalId = this.provider.naming.getLambdaVersionLogicalId(
functionName, newVersion.Properties.CodeSha256);
const newVersionObject = {
[versionLogicalId]: newVersion,
};
const fileDigest = fileHash.read();
const versionDigest = versionHash.read();

if (this.serverless.service.provider.versionFunctions) {
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
newVersionObject);
}
newVersion.Properties.CodeSha256 = fileDigest;
newVersion.Properties.FunctionName = { Ref: functionLogicalId };
if (functionObject.description) {
newVersion.Properties.Description = functionObject.description;
}

// Add function versions to Outputs section
const functionVersionOutputLogicalId = this.provider.naming
.getLambdaVersionOutputLogicalId(functionName);
const newVersionOutput = this.cfOutputLatestVersionTemplate();
// use the version SHA in the logical resource ID of the version because
// AWS::Lambda::Version resource will not support updates
const versionLogicalId = this.provider.naming.getLambdaVersionLogicalId(
functionName, versionDigest);
const newVersionObject = {
[versionLogicalId]: newVersion,
};

if (this.serverless.service.provider.versionFunctions) {
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
newVersionObject);
}

newVersionOutput.Value = { Ref: versionLogicalId };
// Add function versions to Outputs section
const functionVersionOutputLogicalId = this.provider.naming
.getLambdaVersionOutputLogicalId(functionName);
const newVersionOutput = this.cfOutputLatestVersionTemplate();

if (this.serverless.service.provider.versionFunctions) {
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Outputs, {
[functionVersionOutputLogicalId]: newVersionOutput,
});
}
newVersionOutput.Value = { Ref: versionLogicalId };

if (this.serverless.service.provider.versionFunctions) {
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Outputs, {
[functionVersionOutputLogicalId]: newVersionOutput,
});
}

return BbPromise.resolve();
});
}

compileFunctions() {
this.serverless.service
.getAllFunctions()
.forEach((functionName) => this.compileFunction(functionName));
const allFunctions = this.serverless.service.getAllFunctions();
return BbPromise.each(
allFunctions,
functionName => this.compileFunction(functionName)
);
}

// helper functions
Expand Down

0 comments on commit 4863835

Please sign in to comment.