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

Cumulus 748 #409

Merged
merged 15 commits into from
Jul 5, 2018
2 changes: 1 addition & 1 deletion .eslint-ratchet-high-water-mark
Original file line number Diff line number Diff line change
@@ -1 +1 @@
670
467
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- update the StreamSpecification DynamoDB tables to have StreamViewType: "NEW_AND_OLD_IMAGES"
- delete granule files in s3
- **CUMULUS-398** - Fix not able to filter executions bu workflow
- **CUMULUS-748** - Fix invalid lambda .zip files being validated/uploaded to AWS

## [v1.6.0] - 2018-06-06

Expand Down Expand Up @@ -353,4 +354,3 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
[v1.1.0]: https://github.com/cumulus-nasa/cumulus/compare/v1.0.1...v1.1.0
[v1.0.1]: https://github.com/cumulus-nasa/cumulus/compare/v1.0.0...v1.0.1
[v1.0.0]: https://github.com/cumulus-nasa/cumulus/compare/pre-v1-release...v1.0.0

39 changes: 34 additions & 5 deletions packages/deployment/lib/lambda.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const path = require('path');
const utils = require('kes').utils;
const { Lambda } = require('kes');

const yauzl = require('yauzl');

/**
* A sub-class of the Kes Lambda class that changes
* how kes handles Lambda function compression and
Expand All @@ -27,6 +29,28 @@ class UpdatedLambda extends Lambda {
super(config);
this.config = config;
}

/**
* Makes use of yauzl.open's verification
* to validate the file referenced in lambda.local
* is a valid zip archive
*
* @param {Object} lambda - the lambda object
*
* @returns {Promise} promise resolution indicates success, will reject if
* zipfile cannot be opened
**/
validateZipFile(lambda) {
return new Promise((resolve, reject) => {
yauzl.open(lambda.local, (err, _zipfile) => {
if (err) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be:

if (err) return reject(err);
return resolve();

If you don't return the rejection, it will continue on in the function and call resolve() as well. Speaking from experience, that's an annoying bug to track down.

Copy link
Contributor

Choose a reason for hiding this comment

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

On second thought, if you add const util = require('util'); to the top of this file, this entire function can be rewritten as:

return (util.promisify(yauzl.open))(lambda.local);

Copy link
Member Author

Choose a reason for hiding this comment

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

@yjpa7145 Nice, I like that far better. TIL. Thanks!

reject(err);
}
resolve();
});
});
}

/**
* Copies the source code of a given lambda function, zips it, calculates
* the hash of the source code and updates the lambda object with
Expand All @@ -35,23 +59,28 @@ class UpdatedLambda extends Lambda {
* @param {Object} lambda - the lambda object
* @returns {Promise} returns the updated lambda object
*/
zipLambda(lambda) {
async zipLambda(lambda) {
let msg = `Zipping ${lambda.local}`;
// skip if the file with the same hash is zipped
// and is a valid zip file
if (fs.existsSync(lambda.local)) {
return Promise.resolve(lambda);
try {
await this.validateZipFile(lambda);
return Promise.resolve(lambda);
}
catch (e) {
console.log(`${lambda.local} appears to be an invalid zip file, and will be re-built`);
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we actually retry it if it's invalid?

Copy link
Member Author

Choose a reason for hiding this comment

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

@laurenfrederick yes, the original code was skipping regeneration on L69, if the validator throws an error it logs and continues instead.

Copy link
Contributor

Choose a reason for hiding this comment

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

💯

}
}
const fileList = [lambda.source];

if (lambda.useMessageAdapter) {
const kesFolder = path.join(this.config.kesFolder, 'build', 'adapter');
fileList.push(kesFolder);
msg += ' and injecting message adapter';
}

console.log(`${msg} for ${lambda.name}`);

return utils.zip(lambda.local, fileList).then(() => lambda);
return utils.zip(lambda.local, fileList).then(() => lambda).catch((e) => console.log(`Error zipping ${e}`));
Copy link
Contributor

Choose a reason for hiding this comment

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

As discussed in chat, please put the .then() and .catch() statements on their own lines, and re-throw the exception in the catch block after logging it.

}

/**
Expand Down
Binary file not shown.
55 changes: 45 additions & 10 deletions packages/deployment/test/test_lambda.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ test.beforeEach(async (t) => {
bucket: 'testbucket',
stack: 'teststack'
};

t.context.fixturedir = 'test/fixtures';
t.context.lambda = {
handler: 'index.handler',
name: 'lambda-example',
Expand Down Expand Up @@ -62,7 +62,7 @@ test.serial('zipLambda: works for lambda not using message adapter', async (t) =
t.context.lambda.useMessageAdapter = false;
const lambdaLocalOrigin = t.context.lambda.local;
const lambdaRemoteOrigin = t.context.lambda.remote;
const l = new Lambda(t.context.config)
const l = new Lambda(t.context.config);
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this be given a more descriptive variable name?

Copy link
Member Author

@Jkovarik Jkovarik Jul 2, 2018

Choose a reason for hiding this comment

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

Hmm, I don't think it's possible to have a less descriptive variable name. How about testLambda.

await l.zipLambda(l.buildS3Path(t.context.lambda));
t.truthy(fs.statSync(t.context.lambda.local));
t.is(t.context.lambda.local, lambdaLocalOrigin);
Expand All @@ -73,7 +73,7 @@ test.serial('zipLambda: works for lambda using message adapter', async (t) => {
t.context.lambda.useMessageAdapter = true;
const lambdaLocalOrigin = t.context.lambda.local;
const lambdaRemoteOrigin = t.context.lambda.remote;
const l = new Lambda(t.context.config)
const l = new Lambda(t.context.config);
await l.zipLambda(l.buildS3Path(t.context.lambda));
t.truthy(fs.statSync(t.context.lambda.local));
t.is(
Expand All @@ -87,7 +87,42 @@ test.serial('zipLambda: works for lambda using message adapter', async (t) => {
});

test.serial(
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason that these tests can't be run in parallel?

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm. Probably not, the setup function tempdir should be unique, it doesn't appear anything here should collide, assuming I'm not missing anything in the underlying dependencies.... why was it implemented that way initially 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

Running them in parallel results in several failures. I can dig into that further and put up another PR if appropriate, or address it here if it feels like it should be sorted in this PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sounds like it's a defensive thing due to localstack constraints as much as anything - see #416

`zipLambda: for lambda using message adapter, no new file is generated
`zipLambda: given an invalid zip file generated from a previous run,
Copy link
Contributor

Choose a reason for hiding this comment

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

If string interpolation is not being performed, use single quotes instead of backticks.

Copy link
Member Author

@Jkovarik Jkovarik Jul 2, 2018

Choose a reason for hiding this comment

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

There's no interpolation, however it's a multi-line string due to line length. Do we have a project-standard (aside eslint) when it comes to that?

Copy link
Contributor

Choose a reason for hiding this comment

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

For the case of long, single-line strings, the project convention is to use single quotes. If eslint complains about the length of the line, add // eslint-disable-line max-len to the end of the line.

a new valid lambda file is generated`,
async (t) => {
t.context.lambda.useMessageAdapter = true;
const lambdaLocalOrigin = t.context.lambda.local;
const lambdaRemoteOrigin = t.context.lambda.remote;

// put an empty lambda zip file there as the result of the previous run
const existingLambdaLocal = path.join(
path.dirname(t.context.lambda.local),
`${Lambda.messageAdapterZipFileHash}-${path.basename(t.context.lambda.local)}`
);

fs.writeFileSync(existingLambdaLocal, 'hello');
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of fs.writeFileSync, look at using the fs-extra module and await fs.writeFile.

Copy link
Contributor

Choose a reason for hiding this comment

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

@yjpa7145 out of curiosity, what is the benefit of doing an await fs.writeFile over fs.writeFileSync when on the next line the result of writeFile is being referenced?

Copy link
Contributor

Choose a reason for hiding this comment

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

oh, I think I see. If the next line is also async, then everything makes sense.

Copy link
Contributor

Choose a reason for hiding this comment

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

@ifestus If you use the Sync call, the entire process stops until that writeFile operation finishes. That means that any other background operations (any asynchronous code running in the process) will hang until this write operation finishes. If the async version is used then those other asynchronous operations can continue while this write is being performed.

t.is(fs.statSync(existingLambdaLocal).size, 5);
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment as above, consider using the Promise-returning version from fs-extra.


const l = new Lambda(t.context.config);
await l.zipLambda(l.buildS3Path(t.context.lambda));
t.truthy(fs.statSync(t.context.lambda.local));
t.true(fs.statSync(t.context.lambda.local).size > 5);
t.is(t.context.lambda.local, existingLambdaLocal);

t.is(
path.basename(t.context.lambda.local),
`${Lambda.messageAdapterZipFileHash}-${path.basename(lambdaLocalOrigin)}`
);
t.is(
path.basename(t.context.lambda.remote),
`${Lambda.messageAdapterZipFileHash}-${path.basename(lambdaRemoteOrigin)}`
);
}
);


test.serial(
`zipLambda: for lambda using message adapter, no new file is generated
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment about using single quotes instead of backticks.

if the task and message adapter are not updated`,
async (t) => {
t.context.lambda.useMessageAdapter = true;
Expand All @@ -103,20 +138,20 @@ test.serial(
`${Lambda.messageAdapterZipFileHash}-${path.basename(t.context.lambda.remote)}`
);

fs.writeFileSync(existingLambdaLocal, 'hello');
t.is(fs.statSync(existingLambdaLocal).size, 5);
fs.copySync(`${t.context.fixturedir}/zipfile-fixture.zip`, existingLambdaLocal);
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment about using fs-extra and not Sync version.

t.is(fs.statSync(existingLambdaLocal).size, 180);
Copy link
Contributor

@ifestus ifestus Jul 2, 2018

Choose a reason for hiding this comment

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

Since the 180 is the size of zipfile-fixture.zip, would it make sense to do a

let origSize = fs.statSync(`${t.context.fixturedir}`/zipfile-fixture.zip).size;

(or something)? While it does add another line, it would make the test a bit more resilient to changes to the zip file.

Copy link
Member Author

Choose a reason for hiding this comment

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

@ifestus that's .... probably preferable for a fixture like this, as the contents are largely irrelevant, and it eliminates the magic-numbery feeling. 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

👍


const l = new Lambda(t.context.config)
const l = new Lambda(t.context.config);
Copy link
Contributor

Choose a reason for hiding this comment

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

A more descriptive variable name would be helpful.

await l.zipLambda(l.buildS3Path(t.context.lambda));
t.truthy(fs.statSync(t.context.lambda.local));
t.is(fs.statSync(t.context.lambda.local).size, 5);
t.is(fs.statSync(t.context.lambda.local).size, 180);
t.is(t.context.lambda.local, existingLambdaLocal);
t.is(t.context.lambda.remote, existingLambdaRemote);
}
);

test.serial(
`zipLambda: for lambda using message adapter, a new file is created
`zipLambda: for lambda using message adapter, a new file is created
if the message adapter is updated`,
async (t) => {
t.context.lambda.useMessageAdapter = true;
Expand All @@ -136,7 +171,7 @@ test.serial(
const adapterHashOrigin = Lambda.messageAdapterZipFileHash;
Lambda.messageAdapterZipFileHash = `${adapterHashOrigin}123`;

const l = new Lambda(t.context.config)
const l = new Lambda(t.context.config);
await l.zipLambda(l.buildS3Path(t.context.lambda));
t.truthy(fs.statSync(t.context.lambda.local));
t.true(fs.statSync(t.context.lambda.local).size > 5);
Expand Down