diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..98a761d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = tab +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[{package.json,*.yml}] +indent_style = space +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c9106a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +.nyc_output diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9f65308 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: node_js +node_js: + - '5' + - '4' + - '0.12' + - '0.10' +after_script: + - npm run coveralls diff --git a/index.js b/index.js new file mode 100644 index 0000000..4afdf00 --- /dev/null +++ b/index.js @@ -0,0 +1,47 @@ +'use strict'; +var path = require('path'); +var AWS = require('aws-sdk'); +var Promise = require('pinkie-promise'); +var utils = require('./lib/utils'); + +module.exports = function (name, alias, opts) { + if (typeof name !== 'string') { + return Promise.reject(new Error('Provide a AWS Lambda function name.')); + } + + if (typeof alias !== 'string') { + return Promise.reject(new Error('Provide an alias name.')); + } + + opts = opts || {}; + + // Load the credentials + AWS.config.region = opts.awsRegion || 'us-west-1'; + + if (opts.awsProfile) { + // Set the `credentials` property if a profile is provided + var objectCredentials = { + profile: opts.awsProfile + }; + + if (opts.awsFilename) { + objectCredentials.filename = path.resolve(process.cwd(), opts.awsFilename); + } + + AWS.config.credentials = new AWS.SharedIniFileCredentials(objectCredentials); + } + + // Create a lambda object + var lambda = new AWS.Lambda(); + + var options = { + FunctionName: name, + Name: alias + }; + + if (opts.version) { + options.FunctionVersion = opts.version; + } + + return utils.updateOrCreate(lambda, options); +}; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..0a20f9c --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,50 @@ +'use strict'; +var pify = require('pify'); +var objectAssign = require('object-assign'); +var Promise = require('pinkie-promise'); + +function findLatestVersion(lambda, opts) { + if (opts.FunctionVersion) { + // Return the function version if it was provided + return Promise.resolve(opts.FunctionVersion); + } + + return pify(lambda.listVersionsByFunction.bind(lambda), Promise)({FunctionName: opts.FunctionName}) + .then(function (data) { + if (!data.Versions || data.Versions.length === 0) { + throw new Error('No versions found.'); + } + + // Sort all the versions + data.Versions.sort(function (a, b) { + var aVersion = a.Version === '$LATEST' ? 0 : parseInt(a.Version, 10); + var bVersion = b.Version === '$LATEST' ? 0 : parseInt(b.Version, 10); + + return bVersion - aVersion; + }); + + return data.Versions[0].Version; + }); +} + +function updateOrCreate(lambda, opts) { + var options = objectAssign({}, opts); + + return findLatestVersion(lambda, opts) + .then(function (version) { + options.FunctionVersion = version; + + // Try to update the version first + return pify(lambda.updateAlias.bind(lambda), Promise)(options); + }) + .catch(function (err) { + if (err.code === 'ResourceNotFoundException') { + // If the alias does not exist yet, create it + return pify(lambda.createAlias.bind(lambda), Promise)(options); + } + + throw err; + }); +} + +exports.updateOrCreate = updateOrCreate; diff --git a/license b/license new file mode 100644 index 0000000..78b0855 --- /dev/null +++ b/license @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Sam Verschueren (github.com/SamVerschueren) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/package.json b/package.json new file mode 100644 index 0000000..c7cc7c5 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "lambda-update-alias", + "version": "0.0.0", + "description": "Update or create a AWS lambda alias", + "license": "MIT", + "repository": "SamVerschueren/lambda-update-alias", + "author": { + "name": "Sam Verschueren", + "email": "sam.verschueren@gmail.com", + "url": "github.com/SamVerschueren" + }, + "engines": { + "node": ">=0.10.0" + }, + "scripts": { + "test": "xo && nyc ava", + "coveralls": "nyc report --reporter=text-lcov | coveralls" + }, + "files": [ + "index.js", + "lib" + ], + "keywords": [ + "aws", + "lambda", + "alias", + "update", + "create" + ], + "dependencies": { + "aws-sdk": "^2.3.0", + "object-assign": "^4.0.1", + "pify": "^2.3.0", + "pinkie-promise": "^2.0.0" + }, + "devDependencies": { + "ava": "*", + "coveralls": "^2.11.9", + "nyc": "^6.1.1", + "sinon": "^1.17.3", + "xo": "*" + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..6c06d7a --- /dev/null +++ b/readme.md @@ -0,0 +1,118 @@ +# lambda-update-alias + +[![Build Status](https://travis-ci.org/SamVerschueren/lambda-update-alias.svg?branch=master)](https://travis-ci.org/SamVerschueren/lambda-update-alias) +[![Coverage Status](https://coveralls.io/repos/github/SamVerschueren/lambda-update-alias/badge.svg?branch=master)](https://coveralls.io/github/SamVerschueren/lambda-update-alias?branch=master) + +> Update or create a AWS lambda alias + + +## Install + +``` +$ npm install --save lambda-update-alias +``` + + +## Usage + +```js +const updateAlias = require('lambda-update-alias'); + +updateAlias('myLambdaFunction', 'v1'}).then(result => { + console.log(result); + /* + { + AliasArn: 'arn:aws:lambda:us-west-1:123456789012:function:myLambdaFunction:v1', + Name: 'v1', + FunctionVersion: '3', + Description: 'My lambda function description' + } + */ +}); +``` + + +## API + +### updateAlias(name, alias, [options]) + +Returns a promise for the result object. + +#### name + +Type: `string` + +Name of the lambda function. + +#### alias + +Type: `string` + +Name of the alias that should be attached to the lambda function. + +#### options + +##### version + +Type: `string`
+Default: *`latest`* + +Name of the version where the alias should be attached to. If not provided, the alias will be attached to the version +with the highest number. `$LATEST` is treated as version `0`. + +##### awsProfile + +Type: `string` + +[AWS Profile](http://docs.aws.amazon.com/AWSJavaScriptSDK/guide/node-configuring.html). The user related to the profile should have +admin access to API Gateway and should be able to invoke `lambda:AddPermission`. + +Can be overridden globally with the `AWS_PROFILE` environment variable. + +##### awsFilename + +Type: `string` + +[Filename](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SharedIniFileCredentials.html#constructor-property) to use when loading credentials. + +##### awsRegion + +Type: `string`
+Default: `us-west-1` + +AWS region. + + +## User Policy + +The profile creating or updating the alias should be able to list the versions of the function and create and update the aliases. + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "Stmt1454507191000", + "Effect": "Allow", + "Action": [ + "lambda:CreateAlias", + "lambda:ListVersionsByFunction", + "lambda:UpdateAlias" + ], + "Resource": [ + "*" + ] + } + ] +} +``` + + +## Related + +- [lambda-update-alias-cli](https://github.com/SamVerschueren/lambda-update-alias-cli) - CLI for this module + + +## License + +MIT © [Sam Verschueren](https://github.com/SamVerschueren) diff --git a/test/fixtures/aws.js b/test/fixtures/aws.js new file mode 100644 index 0000000..93209f2 --- /dev/null +++ b/test/fixtures/aws.js @@ -0,0 +1,13 @@ +'use strict'; +var AWS = require('aws-sdk'); + +var lambda = new AWS.Lambda(); + +AWS.Lambda = function () { + return lambda; +}; + +module.exports = { + lambda: lambda, + config: AWS.config +}; diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..2600ea6 --- /dev/null +++ b/test/test.js @@ -0,0 +1,55 @@ +import path from 'path'; +import test from 'ava'; +import sinon from 'sinon'; +import aws from './fixtures/aws'; +import utils from '../lib/utils'; +import m from '../'; + +test.before(() => { + sinon.stub(utils, 'updateOrCreate'); +}); + +test('throw error if no lambda function name is provided', t => { + t.throws(m(), 'Provide a AWS Lambda function name.'); +}); + +test('throw error if no alias name is provided', t => { + t.throws(m('foo'), 'Provide an alias name.'); +}); + +test('specify the aws profile', async t => { + await m('foo', 'v1', {awsProfile: 'foo-profile'}); + t.is(aws.config.credentials.profile, 'foo-profile'); +}); + +test('specify the aws filename', async t => { + await m('foo', 'v1', {awsProfile: 'foo-profile', awsFilename: './credentials'}); + t.is(aws.config.credentials.filename, path.resolve(process.cwd(), 'credentials')); +}); + +test.serial('use `us-west-1` as default region', async t => { + await m('foo', 'v1'); + t.is(aws.config.region, 'us-west-1'); +}); + +test.serial('provide region property', async t => { + await m('foo', 'v1', {awsRegion: 'eu-west-1'}); + t.is(aws.config.region, 'eu-west-1'); +}); + +test.serial('update or create the alias', async t => { + await m('foo', 'v1'); + t.same(utils.updateOrCreate.lastCall.args[1], { + FunctionName: 'foo', + Name: 'v1' + }); +}); + +test.serial('update or create the alias on a specific version', async t => { + await m('foo', 'v1', {version: '1'}); + t.same(utils.updateOrCreate.lastCall.args[1], { + FunctionName: 'foo', + FunctionVersion: '1', + Name: 'v1' + }); +}); diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 0000000..f892779 --- /dev/null +++ b/test/utils.js @@ -0,0 +1,78 @@ +import test from 'ava'; +import sinon from 'sinon'; +import {updateOrCreate} from '../lib/utils'; + +const resourceNotFoundException = new Error('Resource not found'); +resourceNotFoundException.code = 'ResourceNotFoundException'; + +const fooVersions = { + Versions: [ + {Version: '$LATEST'}, + {Version: '1'}, + {Version: '2'} + ] +}; + +const barVersions = { + Versions: [ + {Version: '$LATEST'} + ] +}; + +test.beforeEach(t => { + const listVersionsStub = sinon.stub(); + listVersionsStub.withArgs({FunctionName: 'foo'}).yields(undefined, fooVersions); + listVersionsStub.withArgs({FunctionName: 'bar'}).yields(undefined, barVersions); + listVersionsStub.withArgs({FunctionName: 'baz'}).yields(undefined, {Versions: []}); + listVersionsStub.withArgs({FunctionName: 'bax'}).yields(undefined, {}); + + const updateAliasStub = sinon.stub(); + updateAliasStub.withArgs({FunctionName: 'foo', FunctionVersion: '2'}).yields(undefined, {foo: 'bar'}); + updateAliasStub.yields(resourceNotFoundException); + + const createAliasStub = sinon.stub(); + createAliasStub.yields(undefined, {foo: 'bar'}); + + const lambda = {}; + lambda.listVersionsByFunction = listVersionsStub; + lambda.updateAlias = updateAliasStub; + lambda.createAlias = createAliasStub; + + t.context.lambda = lambda; +}); + +test('`listVersionsByFunction` should be called if no `version` is provided', async t => { + const lambda = t.context.lambda; + await updateOrCreate(lambda, {FunctionName: 'foo'}); + t.same(lambda.listVersionsByFunction.args[0][0], {FunctionName: 'foo'}); +}); + +test('`listVersionsByFunction` should not be called if `version` is provided', async t => { + const lambda = t.context.lambda; + await updateOrCreate(lambda, {FunctionName: 'foo', FunctionVersion: '1'}); + t.true(lambda.listVersionsByFunction.callCount === 0); +}); + +test('error if no versions could be found', t => { + const lambda = t.context.lambda; + t.throws(updateOrCreate(lambda, {FunctionName: 'baz'}), 'No versions found.'); + t.throws(updateOrCreate(lambda, {FunctionName: 'bax'}), 'No versions found.'); +}); + +test('`updateAlias` should be called', async t => { + const lambda = t.context.lambda; + await updateOrCreate(lambda, {FunctionName: 'foo'}); + t.same(lambda.updateAlias.args[0][0], { + FunctionName: 'foo', + FunctionVersion: '2' + }); +}); + +test('`createAlias` if alias does not exist', async t => { + const lambda = t.context.lambda; + await updateOrCreate(lambda, {FunctionName: 'bar'}); + t.same(lambda.createAlias.args[0][0], { + FunctionName: 'bar', + FunctionVersion: '$LATEST' + }); +});