diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index f8f3aaa..b1817dc 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [12.x] + node-version: [16.x] steps: - uses: actions/checkout@v2 diff --git a/package.json b/package.json index e1b4b7e..00d240e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "serverless-layers", - "version": "2.8.3", + "version": "2.8.4", "description": "", "main": "lib/index.js", "bugs": { diff --git a/src/aws/LayersService.js b/src/aws/LayersService.js index 1584b94..2d23f21 100644 --- a/src/aws/LayersService.js +++ b/src/aws/LayersService.js @@ -1,14 +1,19 @@ const AbstractService = require('../AbstractService'); class LayersService extends AbstractService { - async publishVersion() { + + descriptionWithVersionKey(versionKey) { + return 'created by serverless-layers plugin (' + versionKey + ')' + } + + async publishVersion(versionKey) { const params = { Content: { S3Bucket: this.bucketName, S3Key: this.zipFileKeyName }, LayerName: this.layerName, - Description: 'created by serverless-layers plugin', + Description: this.descriptionWithVersionKey(versionKey), CompatibleRuntimes: this.plugin.settings.compatibleRuntimes, CompatibleArchitectures: this.plugin.settings.compatibleArchitectures @@ -22,6 +27,48 @@ class LayersService extends AbstractService { }); } + + async findVersionChecksumInList(versionKey, marker) { + const params = { + LayerName: this.layerName, + // TODO: Question: is layer name specific enough? Is there a way to deploy multiple runtime architectures per name? + // CompatibleRuntime: this.plugin.settings.compatibleRuntimes, + // CompatibleArchitecture: this.plugin.settings.compatibleArchitectures + }; + + if (marker) { + params.Marker = marker; + } + + const result = await this.awsRequest('Lambda:listLayerVersions', params, { checkError: true }); + + const description = this.descriptionWithVersionKey(versionKey); + + const matchingLayerVersion = result.LayerVersions.find((layer) => layer.Description === description); + if (matchingLayerVersion) { + return matchingLayerVersion.LayerVersionArn; + } else if (result.NextMarker) { + return this.findVersionChecksumInList(versionKey, result.NextMarker); + } else { + return null; + } + } + + async checkLayersForVersionKey(versionKey) { + this.plugin.log('Looking for version with "' + versionKey + '"'); + const layerVersionArn = await this.findVersionChecksumInList(versionKey); + + if (layerVersionArn) { + return layerVersionArn; + // TODO: double-check to confirm layer content is as expected + // const params = { arn: layerVersionArn } + // const matchingLayerWithContent = await this.awsRequest('Lambda:getLayerVersionByArn', params, { checkError: true }); + // if (matchingLayerWithContent) { + // matchingLayerWithContent.Content.Location // A link to the layer archive in Amazon S3 that is valid for 10 minutes. + // } + } + } + async cleanUpLayers(retainVersions = 0) { const params = { LayerName: this.layerName diff --git a/src/index.js b/src/index.js index c78787e..97bc9c0 100644 --- a/src/index.js +++ b/src/index.js @@ -309,8 +309,16 @@ class ServerlessLayers { // merge package default options this.mergePackageOptions(); - // It returns the layer arn if exists. - const existentLayerArn = await this.getLayerArn(); + let existentLayerArn = ''; + const versionKey = + (this.runtimes.getDependenciesChecksum()) + + (this.settings.customHash ? '.' + this.settings.customHash : ''); + + // If nothing has changed, confirm layer with same checksum + if (!verifyChanges) { + this.log('Checking if layer already exists...') + existentLayerArn = await this.layersService.checkLayersForVersionKey(versionKey); + } // It improves readability const skipInstallation = ( @@ -338,7 +346,7 @@ class ServerlessLayers { await this.zipService.package(); await this.bucketService.uploadZipFile(); - const version = await this.layersService.publishVersion(); + const version = await this.layersService.publishVersion(versionKey); await this.bucketService.putFile(this.dependencies.getDepsPath()); this.relateLayerWithFunctions(version.LayerVersionArn); @@ -563,7 +571,7 @@ class ServerlessLayers { let pattern = /arn:aws:lambda:([^:]+):([0-9]+):layer:([^:]+):([0-9]+)/g; let region = chalk.bold('$1'); let name = chalk.magenta('$3'); - let formated = chalk.white(`arn:aws:lambda:${region}:*********:${name}:$4`); + let formatted = chalk.white(`arn:aws:lambda:${region}:*********:${name}:$4`); let text = ""; switch (typeof arn) { @@ -580,7 +588,7 @@ class ServerlessLayers { text = String(arn); break; } - return text.replace(pattern, formated); + return text.replace(pattern, formatted); } } diff --git a/src/runtimes/index.js b/src/runtimes/index.js index 52e1dbd..452f509 100644 --- a/src/runtimes/index.js +++ b/src/runtimes/index.js @@ -72,6 +72,10 @@ class Runtimes { hasDependenciesChanges() { return this._runtime.hasDependenciesChanges(); } + + getDependenciesChecksum() { + return this._runtime.getDependenciesChecksum(); + } } module.exports = Runtimes; diff --git a/src/runtimes/nodejs.js b/src/runtimes/nodejs.js index abe5e97..5c5ade3 100644 --- a/src/runtimes/nodejs.js +++ b/src/runtimes/nodejs.js @@ -1,4 +1,5 @@ const path = require('path'); +const crypto = require('crypto'); class NodeJSRuntime { constructor(parent, runtime, runtimeDir) { @@ -9,7 +10,7 @@ class NodeJSRuntime { runtime, runtimeDir, libraryFolder: 'node_modules', - packageManager: 'npm', + packageManager: 'npm', packageManagerExtraArgs: '', dependenciesPath: 'package.json', compatibleRuntimes: [runtimeDir], @@ -123,6 +124,10 @@ class NodeJSRuntime { return isDifferent; } + + getDependenciesChecksum() { + return crypto.createHash('md5').update(JSON.stringify(this.localPackage.dependencies)).digest('hex'); + } } module.exports = NodeJSRuntime; diff --git a/src/runtimes/python.js b/src/runtimes/python.js index a345301..733ef1f 100644 --- a/src/runtimes/python.js +++ b/src/runtimes/python.js @@ -1,5 +1,6 @@ const fs = require('fs'); const path = require('path'); +const crypto = require('crypto'); class PythonRuntime { constructor(parent, runtime, runtimeDir) { @@ -10,7 +11,7 @@ class PythonRuntime { runtime, runtimeDir, libraryFolder: 'site-packages', - packageManager: 'pip', + packageManager: 'pip', packageManagerExtraArgs: '', dependenciesPath: 'requirements.txt', compatibleRuntimes: [runtime], @@ -78,6 +79,10 @@ class PythonRuntime { return isDifferent; } + + getDependenciesChecksum() { + return crypto.createHash('md5').update(JSON.stringify(this.localPackage)).digest('hex'); + } } module.exports = PythonRuntime; diff --git a/src/runtimes/ruby.js b/src/runtimes/ruby.js index 74d8be1..00a37ad 100644 --- a/src/runtimes/ruby.js +++ b/src/runtimes/ruby.js @@ -1,6 +1,6 @@ const fs = require('fs'); const path = require('path'); - +const crypto = require('crypto'); class RubyRuntime { constructor(parent, runtime, runtimeDir) { @@ -11,7 +11,7 @@ class RubyRuntime { runtime, runtimeDir, libraryFolder: 'gems', - packageManager: 'bundle', + packageManager: 'bundle', packageManagerExtraArgs: '', dependenciesPath: 'Gemfile', compatibleRuntimes: [runtime], @@ -86,6 +86,10 @@ class RubyRuntime { return isDifferent; } + + getDependenciesChecksum() { + return crypto.createHash('md5').update(JSON.stringify(this.localPackage)).digest('hex'); + } } module.exports = RubyRuntime;