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 logical version id for layers in same stack #8066

Merged
merged 44 commits into from Sep 24, 2020
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
3e6fa90
Include layer details in logical id digest
pwithams Aug 5, 2020
34b0877
Enable change by removing empty array
pwithams Aug 6, 2020
91e3077
Add version id change based on layer zip content and non-S3Key proper…
pwithams Aug 8, 2020
f927470
Fix version id creation logic to pass existing tests
pwithams Aug 8, 2020
f93b1aa
Add tests for new lambda version layer logic
pwithams Aug 8, 2020
b4ee79a
Merge branch 'master' into fix-logical-version-id-for-layers
pwithams Aug 8, 2020
3a01dc9
Remove some lodash uses
pwithams Aug 9, 2020
b51b81e
Add test to check for reservedConcurrency version id change
pwithams Aug 9, 2020
6d412bc
Refactor first test to use version id comparisions rather than explic…
pwithams Aug 10, 2020
b5cb580
Update tests to use comparisons rather than explicit checks
pwithams Aug 11, 2020
3080dd3
Use path.join to create test file paths
pwithams Aug 11, 2020
14de011
Refactor compile logic and update tests
pwithams Aug 18, 2020
001f440
Add test to check for version update on key change
pwithams Aug 22, 2020
8678062
Merge branch 'master' into fix-logical-version-id-for-layers
pwithams Aug 22, 2020
619050c
Fix windows tests
pwithams Aug 22, 2020
781aa98
Use correct layer reference syntax
pwithams Aug 24, 2020
a6ccbac
Improve artifact path detection logic
pwithams Aug 26, 2020
3ad5651
Change promise map to all and update reservedConcurrency test
pwithams Aug 26, 2020
bccd773
Clean up function fixture use in reservedConcurrency test
pwithams Aug 26, 2020
997c0f0
Remove duplicate layerConfiguration definitions
pwithams Aug 26, 2020
8df8aac
Remove extra function layer fixture logic into single fixture
pwithams Aug 27, 2020
a6ec21d
Update main logic and test fixture to handle different layer name and…
pwithams Aug 27, 2020
8b21863
Make layer artifact hashing deterministic
pwithams Aug 27, 2020
18e6b1f
Merge branch 'master' into fix-logical-version-id-for-layers
pwithams Aug 27, 2020
231cf14
Add more reliable serverless layer key resolution logic
pwithams Aug 29, 2020
4941a15
Merge branch 'master' into fix-logical-version-id-for-layers and reso…
pwithams Aug 29, 2020
62cd779
Merge branch 'master' into fix-logical-version-id-for-layers
pwithams Aug 29, 2020
625aea8
Fix fallback layer artifact name resolution logic
pwithams Aug 29, 2020
d7704ec
Simplfy naming of hidden serverless layer name object
pwithams Sep 13, 2020
b1571c3
Merge branch 'master' into fix-logical-version-id-for-layers
pwithams Sep 13, 2020
9ee00b4
Move layer artifact name resolution into separate method
pwithams Sep 16, 2020
38c867e
Update tests to use async/await and new fixture engine
pwithams Sep 16, 2020
ff09d04
Merge branch 'master' into fix-logical-version-id-for-layers
pwithams Sep 16, 2020
6e0be19
Use fs-extra for async/await functionality
pwithams Sep 16, 2020
193c516
Add test case for function with arn layer reference
pwithams Sep 16, 2020
7ad4818
Merge branch 'master' into fix-logical-version-id-for-layers
pwithams Sep 19, 2020
9e08cf5
Improve naming and refactor tests
pwithams Sep 19, 2020
c8be293
Fix equality check used in refactored existing test
pwithams Sep 19, 2020
eee300e
Move object clone logic out of function
pwithams Sep 22, 2020
76bf822
Merge branch 'master' into fix-logical-version-id-for-layers
pwithams Sep 22, 2020
a9baf17
Merge master and fix conflicts
pwithams Sep 22, 2020
6967213
Fix naming issue and increase native interface usage
pwithams Sep 23, 2020
b3cea34
Removed refactored logic
pwithams Sep 23, 2020
7c3ac73
Merge branch 'master' into fix-logical-version-id-for-layers
pwithams Sep 23, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
226 changes: 143 additions & 83 deletions lib/plugins/aws/package/compile/functions/index.js
Expand Up @@ -368,6 +368,7 @@ class AwsCompileFunctions {
: this.serverless.service.provider.versionFunctions;

let versionCompilationPromise;

if (shouldVersionFunction || functionObject.provisionedConcurrency) {
// Create hashes for the artifact and the logical id of the version resource
// The one for the version resource must include the function configuration
Expand All @@ -377,105 +378,130 @@ class AwsCompileFunctions {
const versionHash = crypto.createHash('sha256');
fileHash.setEncoding('base64');
versionHash.setEncoding('base64');
const layerConfigurations = _.cloneDeep(
extractLayerConfigurationsFromFunction(functionResource.Properties, cfTemplate)
);

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

readStream
.on('data', chunk => {
fileHash.write(chunk);
versionHash.write(chunk);
})
.on('close', () => {
cb();
})
.on('error', error => {
cb(error);
// Include function and layer configuration in version id hash (without the Code part)
versionCompilationPromise = addFileContentsToHashes(artifactFilePath, [
fileHash,
versionHash,
medikoo marked this conversation as resolved.
Show resolved Hide resolved
])
.then(async () => {
// Include all referenced layer code in the version id hash
const layerArtifactPaths = [];
layerConfigurations.forEach(layer => {
const layerArtifactPath = this.provider.resolveLayerArtifactName(layer.name);
layerArtifactPaths.push(layerArtifactPath);
});
}).then(() => {
// Include function configuration in version id hash (without the Code part)
const properties = _.omit(
_.get(functionResource, 'Properties', {}),
'Code',
// Properties applied to function globally (not specific to version or alias)
'ReservedConcurrentExecutions',
'Tags'
);
_.forOwn(properties, value => {
if (_.isObject(value)) {
versionHash.write(JSON.stringify(value));
} else {
versionHash.write(value != null ? String(value) : '');

for (const layerArtifactPath of layerArtifactPaths.sort()) {
await addFileContentsToHashes(layerArtifactPath, [versionHash]);
}
});

// Finalize hashes
fileHash.end();
versionHash.end();
return layerArtifactPaths
.sort()
.reduce(
(previousHashCalculation, artifactName) =>
previousHashCalculation.then(() =>
addFileContentsToHashes(artifactName, [versionHash])
),
BbPromise.resolve()
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're on v2 we can safely rely on async/await and native promises, so now we can construct it way more simply as:

for (const layerArtifactPath of layerArtifactPaths) {
  await addFileContentsToHashes(layerArtifactPath, [versionHash])
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we refactor that one, to form as proposed in above comment?

Copy link
Contributor Author

@pwithams pwithams Sep 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had refactored but I'd forgotten to remove the old logic... it should be fixed now.

})
.then(() => {
// Include function and layer configuration details in the version id hash
for (const layerConfig of layerConfigurations) {
delete layerConfig.properties.Content.S3Key;
}

const fileDigest = fileHash.read();
const versionDigest = versionHash.read();
// sort the layer conifigurations for hash consistency
const sortedLayerConfigurations = {};
const byKey = ([key1], [key2]) => key1.localeCompare(key2);
for (const { name, properties: layerProperties } of layerConfigurations) {
sortedLayerConfigurations[name] = _.fromPairs(
Object.entries(layerProperties).sort(byKey)
);
}

const versionResource = this.cfLambdaVersionTemplate();
const functionProperties = _.cloneDeep(functionResource.Properties);
delete functionProperties.Code;
// Properties applied to function globally (not specific to version or alias)
delete functionProperties.ReservedConcurrentExecutions;
delete functionProperties.Tags;
functionProperties.layerConfigurations = sortedLayerConfigurations;

versionResource.Properties.CodeSha256 = fileDigest;
versionResource.Properties.FunctionName = { Ref: functionLogicalId };
if (functionObject.description) {
versionResource.Properties.Description = functionObject.description;
}
const sortedFunctionProperties = _.fromPairs(
Object.entries(functionProperties).sort(byKey)
);
versionHash.write(JSON.stringify(sortedFunctionProperties));

// 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
);
functionObject.versionLogicalId = versionLogicalId;
const newVersionObject = {
[versionLogicalId]: versionResource,
};
// Finalize hashes
fileHash.end();
versionHash.end();

Object.assign(cfTemplate.Resources, newVersionObject);
const fileDigest = fileHash.read();
const versionDigest = versionHash.read();

// Add function versions to Outputs section
const functionVersionOutputLogicalId = this.provider.naming.getLambdaVersionOutputLogicalId(
functionName
);
const newVersionOutput = this.cfOutputLatestVersionTemplate();
const versionResource = this.cfLambdaVersionTemplate();

newVersionOutput.Value = { Ref: versionLogicalId };
versionResource.Properties.CodeSha256 = fileDigest;
versionResource.Properties.FunctionName = { Ref: functionLogicalId };
if (functionObject.description) {
versionResource.Properties.Description = functionObject.description;
}

Object.assign(cfTemplate.Outputs, {
[functionVersionOutputLogicalId]: newVersionOutput,
});
// 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
);
functionObject.versionLogicalId = versionLogicalId;
const newVersionObject = {
[versionLogicalId]: versionResource,
};

if (!functionObject.provisionedConcurrency) return;
if (!shouldVersionFunction) delete versionResource.DeletionPolicy;
Object.assign(cfTemplate.Resources, newVersionObject);

const provisionedConcurrency = Number(functionObject.provisionedConcurrency);
const aliasLogicalId = this.provider.naming.getLambdaProvisionedConcurrencyAliasLogicalId(
functionName
);
const aliasName = this.provider.naming.getLambdaProvisionedConcurrencyAliasName();

functionObject.targetAlias = { name: aliasName, logicalId: aliasLogicalId };

const aliasResource = {
Type: 'AWS::Lambda::Alias',
Properties: {
FunctionName: { Ref: functionLogicalId },
FunctionVersion: { 'Fn::GetAtt': [versionLogicalId, 'Version'] },
Name: aliasName,
ProvisionedConcurrencyConfig: {
ProvisionedConcurrentExecutions: provisionedConcurrency,
// Add function versions to Outputs section
const functionVersionOutputLogicalId = this.provider.naming.getLambdaVersionOutputLogicalId(
functionName
);
const newVersionOutput = this.cfOutputLatestVersionTemplate();

newVersionOutput.Value = { Ref: versionLogicalId };

Object.assign(cfTemplate.Outputs, {
[functionVersionOutputLogicalId]: newVersionOutput,
});

if (!functionObject.provisionedConcurrency) return;
if (!shouldVersionFunction) delete versionResource.DeletionPolicy;

const provisionedConcurrency = Number(functionObject.provisionedConcurrency);
const aliasLogicalId = this.provider.naming.getLambdaProvisionedConcurrencyAliasLogicalId(
functionName
);
const aliasName = this.provider.naming.getLambdaProvisionedConcurrencyAliasName();

functionObject.targetAlias = { name: aliasName, logicalId: aliasLogicalId };

const aliasResource = {
Type: 'AWS::Lambda::Alias',
Properties: {
FunctionName: { Ref: functionLogicalId },
FunctionVersion: { 'Fn::GetAtt': [versionLogicalId, 'Version'] },
Name: aliasName,
ProvisionedConcurrencyConfig: {
ProvisionedConcurrentExecutions: provisionedConcurrency,
},
},
},
DependsOn: functionLogicalId,
};
DependsOn: functionLogicalId,
};

cfTemplate.Resources[aliasLogicalId] = aliasResource;
});
cfTemplate.Resources[aliasLogicalId] = aliasResource;
});
} else {
versionCompilationPromise = BbPromise.resolve();
}
Expand Down Expand Up @@ -629,4 +655,38 @@ class AwsCompileFunctions {
}
}

function addFileContentsToHashes(filePath, hashes) {
return new Promise((resolve, reject) => {
const readStream = fs.createReadStream(filePath);
readStream
.on('data', chunk => {
hashes.forEach(hash => {
hash.write(chunk);
});
})
.on('close', () => {
resolve();
})
.on('error', error => {
reject(new Error(`Could not add file content to hash: ${error}`));
});
});
}

function extractLayerConfigurationsFromFunction(functionProperties, cfTemplate) {
const layerConfigurations = [];
if (!functionProperties.Layers) return layerConfigurations;
functionProperties.Layers.forEach(potentialLocalLayerObject => {
if (potentialLocalLayerObject.Ref) {
const configuration = cfTemplate.Resources[potentialLocalLayerObject.Ref];
layerConfigurations.push({
name: configuration._serverlessLayerName,
ref: potentialLocalLayerObject.Ref,
properties: configuration.Properties,
});
}
});
return layerConfigurations;
}

module.exports = AwsCompileFunctions;