├── .npmignore ├── .releaserc.json ├── limit.png ├── screenshot.png ├── renovate.json ├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── package.json ├── README.md ├── CHANGELOG.md └── index.js /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.png 3 | 4 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@prescott/semantic-release-config" 3 | } 4 | -------------------------------------------------------------------------------- /limit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooyoul/serverless-latest-layer-version/HEAD/limit.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooyoul/serverless-latest-layer-version/HEAD/screenshot.png -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@vingle", 4 | "@vingle:semantic-commit" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: workflow 2 | on: [push] 3 | jobs: 4 | job: 5 | runs-on: ubuntu-latest 6 | container: node:lts 7 | timeout-minutes: 15 8 | steps: 9 | - uses: actions/checkout@v1 10 | - name: Prepare 11 | run: npm ci 12 | - name: Publish 13 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' 14 | run: | 15 | npx semantic-release 16 | env: 17 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 18 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dst 2 | dst.zip 3 | 4 | .env 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (http://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directories 35 | node_modules 36 | jspm_packages 37 | 38 | # Optional npm cache directory 39 | .npm 40 | 41 | # Optional REPL history 42 | .node_repl_history 43 | 44 | .serverless 45 | 46 | # IDE Configurations 47 | .vscode 48 | .idea 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | Copyright © 2019 MooYeol Prescott Lee, http://debug.so 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the “Software”), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-latest-layer-version", 3 | "version": "2.1.2", 4 | "description": "A serverless plugin that replaces 'latest' version tag to actual lambda layer version", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js" 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/mooyoul/serverless-latest-layer-version.git" 12 | }, 13 | "keywords": [ 14 | "serverless", 15 | "serverless-plugin", 16 | "aws", 17 | "lambda" 18 | ], 19 | "author": "MooYeol Prescott Lee ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/mooyoul/serverless-latest-layer-version/issues" 23 | }, 24 | "homepage": "https://github.com/mooyoul/serverless-latest-layer-version#readme", 25 | "dependencies": { 26 | "traverse": "^0.6.6" 27 | }, 28 | "devDependencies": { 29 | "@prescott/commitlint-preset": "1.0.9", 30 | "@prescott/semantic-release-config": "1.0.17", 31 | "husky": "4.3.8", 32 | "semantic-release": "17.4.7" 33 | }, 34 | "config": { 35 | "commitizen": { 36 | "path": "cz-conventional-changelog" 37 | } 38 | }, 39 | "husky": { 40 | "hooks": { 41 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 42 | } 43 | }, 44 | "commitlint": { 45 | "extends": [ 46 | "@prescott/commitlint-preset" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serverless-latest-layer-version 2 | 3 | [![Build Status](https://github.com/mooyoul/serverless-latest-layer-version/workflows/workflow/badge.svg)](https://github.com/mooyoul/serverless-latest-layer-version/actions) 4 | [![Semantic Release enabled](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 5 | [![Renovate enabled](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovatebot.com/) 6 | [![MIT license](http://img.shields.io/badge/license-MIT-blue.svg)](http://mooyoul.mit-license.org/) 7 | 8 | This is the Serverless plugin for AWS Lambda Layers which enables missing "latest" version tag 9 | 10 | ![demo](/screenshot.png) 11 | 12 | ## Why? 13 | 14 | ![limits](/limit.png) 15 | 16 | Unlike invoking Lambda function. Lambda Layer does not support `$LATEST` version tag. 17 | 18 | 19 | ## Install 20 | 21 | First, install package as development dependency. 22 | 23 | ```bash 24 | $ npm i serverless-latest-layer-version --save-dev 25 | ``` 26 | 27 | Then, add the plugin to serverless.yml 28 | 29 | ```yaml 30 | # serverless.yml 31 | 32 | plugins: 33 | - serverless-latest-layer-version 34 | ``` 35 | 36 | ## Setup 37 | 38 | Just change layer version to `latest`. 39 | The plugin automatically replaces `latest` version tag to actual latest version number. 40 | 41 | For example, if Previously specified layer arn is `arn:aws:lambda:us-east-1:800406105498:layer:nsolid-node-10:6`. 42 | replace that as `arn:aws:lambda:us-east-1:800406105498:layer:nsolid-node-10:latest`. That's it! 43 | 44 | ## Changelog 45 | 46 | See [CHANGELOG](/CHANGELOG.md). 47 | 48 | ## License 49 | [MIT](LICENSE) 50 | 51 | See full license on [mooyoul.mit-license.org](http://mooyoul.mit-license.org/) 52 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.1.2](https://github.com/mooyoul/serverless-latest-layer-version/compare/v2.1.1...v2.1.2) (2022-11-08) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * avoid logging the same resolved layer multiple times ([#33](https://github.com/mooyoul/serverless-latest-layer-version/issues/33)) ([33f289f](https://github.com/mooyoul/serverless-latest-layer-version/commit/33f289f1a7902cc8420c63d2f88df9060cfa2476)) 7 | 8 | ## [2.1.1](https://github.com/mooyoul/serverless-latest-layer-version/compare/v2.1.0...v2.1.1) (2020-09-20) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * use aws credential from serverless internal ([3a79fe0](https://github.com/mooyoul/serverless-latest-layer-version/commit/3a79fe033f44394bb0f58dd5ff2ed4991dcb76a4)), closes [#7](https://github.com/mooyoul/serverless-latest-layer-version/issues/7) 14 | 15 | # [2.1.0](https://github.com/mooyoul/serverless-latest-layer-version/compare/v2.0.0...v2.1.0) (2019-12-31) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * fix invalid case-insensitive layer resolution ([1310f6b](https://github.com/mooyoul/serverless-latest-layer-version/commit/1310f6bb0d06924d7233d62a53a26e3caf85c8b5)), closes [#5](https://github.com/mooyoul/serverless-latest-layer-version/issues/5) 21 | 22 | 23 | ### Features 24 | 25 | * add invidual function deployment support ([2191273](https://github.com/mooyoul/serverless-latest-layer-version/commit/2191273f1fbe81df4d5009fb7bf0e28988157f91)), closes [#3](https://github.com/mooyoul/serverless-latest-layer-version/issues/3) 26 | 27 | # [2.0.0](https://github.com/mooyoul/serverless-latest-layer-version/compare/v1.0.3...v2.0.0) (2019-12-16) 28 | 29 | 30 | ### Bug Fixes 31 | 32 | * resolve pseudo parameters in layer arn ([abcc6f9](https://github.com/mooyoul/serverless-latest-layer-version/commit/abcc6f94390d7c7d92335a7c035bed47b69e9180)), closes [#2](https://github.com/mooyoul/serverless-latest-layer-version/issues/2) [#3](https://github.com/mooyoul/serverless-latest-layer-version/issues/3) 33 | 34 | 35 | ### BREAKING CHANGES 36 | 37 | * these changes may break deployments 38 | 39 | # 1.0.3 40 | 41 | - Fixed a bug that cause plugin failure when there's no any `Resources` field exists on the `serverless.yml` (#1, Thanks to [@falaa](https://github.com/falaa)) 42 | 43 | # 1.0.2 44 | 45 | - Initial release 46 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * This Serverless plugin replaces 'latest' pseudo version tag to actual latest version 5 | */ 6 | 7 | const traverse = require('traverse'); 8 | const util = require('util'); 9 | 10 | class ServerlessPlugin { 11 | constructor(serverless, options) { 12 | this.serverless = serverless; 13 | this.provider = serverless.getProvider("aws"); 14 | this.options = options; 15 | this.resolvedLayers = new Set(); 16 | 17 | this.hooks = { 18 | 'after:aws:package:finalize:mergeCustomProviderResources': this.updateCFNLayerVersion.bind(this), 19 | 'before:deploy:function:deploy': this.updateSLSLayerVersion.bind(this), 20 | }; 21 | } 22 | 23 | updateSLSLayerVersion() { 24 | // Find All Lambda Layer associations from compiled serverless configuration 25 | return this.update(this.listSLSLayerAssociations()); 26 | } 27 | 28 | updateCFNLayerVersion() { 29 | // Find All Lambda Layer associations from compiled CFN template 30 | return this.update(this.listCFNLayerAssociations()); 31 | } 32 | 33 | async update(layerAssociations) { 34 | // Collect target Layer ARNs 35 | const collectedLayerARNs = this.collectLayerARNs(layerAssociations); 36 | 37 | // Resolve actual Layer ARNs 38 | const resolvedLayerARNs = await this.fetchLatestVersions(collectedLayerARNs); 39 | 40 | // Recursively replace layer ARNs 41 | this.replaceLayerVersions(layerAssociations, resolvedLayerARNs); 42 | } 43 | 44 | listCFNLayerAssociations() { 45 | // Lookup compiled CFN template to support individual deployments 46 | const compiledTemplate = this.serverless.service.provider.compiledCloudFormationTemplate; 47 | 48 | const resources = compiledTemplate.Resources; 49 | 50 | return Object.keys(resources).reduce((collection, key) => { 51 | const resource = resources[key]; 52 | 53 | if (resource.Type === 'AWS::Lambda::Function') { 54 | const layers = resource.Properties && resource.Properties.Layers; 55 | 56 | if (Array.isArray(layers) && layers.length > 0) { 57 | collection.push({ name: key, layers }); 58 | } 59 | } 60 | 61 | return collection; 62 | }, []); 63 | } 64 | 65 | listSLSLayerAssociations() { 66 | const { functions } = this.serverless.service; 67 | 68 | return Object.keys(functions).reduce((collection, name) => { 69 | const fn = functions[name]; 70 | const layers = fn.layers; 71 | 72 | if (Array.isArray(layers) && layers.length > 0) { 73 | collection.push({ name, layers }); 74 | } 75 | 76 | return collection; 77 | }, []); 78 | } 79 | 80 | async lookupLatestLayerVersionArn(layerArn) { 81 | const layer = this.extractLayerArn(layerArn); 82 | 83 | if (!layer) { 84 | return null; 85 | } 86 | 87 | const versions = []; 88 | 89 | let marker; 90 | do { 91 | const result = await this.provider.request("Lambda", "listLayerVersions", { 92 | LayerName: layer.layerName, 93 | Marker: marker, 94 | }); 95 | 96 | versions.push(...result.LayerVersions); 97 | marker = result.NextMarker; 98 | } while (marker); 99 | 100 | const sortedVersions = versions.sort((a, b) => { 101 | if (a.Version > b.Version) { 102 | return -1; 103 | } else if (a.Version < b.Version) { 104 | return 1 105 | } else { 106 | return 0; 107 | } 108 | }); 109 | 110 | return sortedVersions.length > 0 ? 111 | sortedVersions[0].LayerVersionArn : 112 | null; 113 | } 114 | 115 | extractLayerArn(arn) { 116 | const SEPARATOR = "__SEPARATOR__"; 117 | 118 | // arn:aws:lambda:REGION:ACCOUNT_ID:layer:LAYER_NAME:LAYER_VERSION 119 | const tokens = arn.replace(/([^:]):([^:])/g, (match, prev, next) => `${prev}${SEPARATOR}${next}`).split(SEPARATOR); 120 | 121 | if (tokens.length !== 8) { 122 | return null; 123 | } 124 | 125 | let region = tokens[3]; 126 | if (/AWS::Region/i.test(region)) { 127 | region = this.serverless.service.provider.region; 128 | } 129 | 130 | const accountId = tokens[4]; 131 | const layerName = tokens[6]; 132 | 133 | return { 134 | region, 135 | layerName: /AWS::AccountId/i.test(accountId) ? 136 | layerName : 137 | `arn:aws:lambda:${region}:${accountId}:layer:${layerName}`, 138 | }; 139 | } 140 | 141 | collectLayerARNs(layerAssociations) { 142 | const set = new Set(); 143 | 144 | for (const layerAssociation of layerAssociations) { 145 | traverse(layerAssociation.layers).forEach(function (node) { 146 | const matched = this.isLeaf 147 | && typeof node === "string" 148 | && /^arn:/i.test(node) 149 | && /latest$/i.test(node); 150 | 151 | if (matched) { 152 | set.add(node); 153 | } 154 | }); 155 | } 156 | 157 | return set; 158 | } 159 | 160 | async fetchLatestVersions(layerARNs) { 161 | const dict = new Map(); 162 | 163 | for (const layerARN of layerARNs) { 164 | const version = await this.lookupLatestLayerVersionArn(layerARN); 165 | dict.set(layerARN, version); 166 | } 167 | 168 | return dict; 169 | } 170 | 171 | replaceLayerVersions(layerAssociations, arnVersionMap) { 172 | const self = this; 173 | 174 | for (const layerAssociation of layerAssociations) { 175 | traverse(layerAssociation.layers).forEach(function (node) { 176 | const matched = this.isLeaf 177 | && typeof node === "string" 178 | && /^arn:/i.test(node) 179 | && /latest$/i.test(node); 180 | 181 | if (matched) { 182 | const resolvedLayerArn = arnVersionMap.get(node); 183 | if (resolvedLayerArn) { 184 | this.update(resolvedLayerArn); 185 | // Avoid logging the same resolved layer multiple times 186 | if (self.resolvedLayers.has(resolvedLayerArn)) { 187 | return; 188 | } 189 | self.resolvedLayers.add(resolvedLayerArn); 190 | self.log("Resolved %s to %s", node, resolvedLayerArn); 191 | } else { 192 | self.log("Detected unknown Layer ARN %s. Please create a new issue to github.com/mooyoul/serverless-latest-layer-version", node); 193 | } 194 | } 195 | }); 196 | } 197 | } 198 | 199 | log(...args) { 200 | const TAG = '[serverless-latest-layer-version]'; 201 | 202 | if (typeof args[0] === 'string') { 203 | args[0] = `${TAG} ${args[0]}`; 204 | } else { 205 | args.unshift(TAG); 206 | } 207 | 208 | this.serverless.cli.log(util.format(...args)); 209 | } 210 | } 211 | 212 | module.exports = ServerlessPlugin; 213 | --------------------------------------------------------------------------------