├── .gitignore ├── README.md ├── bin └── lambdr ├── package.json ├── src ├── cli.js ├── commands │ ├── function-create.js │ ├── function-deploy.js │ ├── function-run.js │ ├── new.js │ ├── stage-create.js │ ├── stage-list.js │ └── stage-remove.js └── lib │ ├── aws-helper.js │ ├── aws-recursive-resource-creator.js │ ├── config.js │ ├── fn.js │ ├── project.js │ ├── stage.js │ └── utils.js └── templates ├── function.js ├── lambdr.js └── project ├── .gitignore ├── config └── env.json └── functions └── .keep /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lambdr 2 | ====== 3 | Lambdr is automated deployment and flow management module to create powerful micro services with AWS Lambda functions. It's still in development and looking for contributors. 🤘 4 | 5 | ## Getting Started 6 | First we need to install cli globally. 7 | ``` 8 | npm install lambdr -g 9 | ``` 10 | 11 | ### Creating a project 12 | Assume that a lambdr project as a micro service. 13 | ``` 14 | lambdr new my-micro-service 15 | ``` 16 | 17 | This will create a folder named 'my-micro-service' and it includes the following files: 18 | 19 | ``` 20 | my-micro-service 21 | |-- config 22 | | |-- aws.json (AWS credentials) 23 | | |-- env.json (Environment variables for every stage) 24 | | |-- lambdr.json (Lambder configurations) 25 | |-- functions 26 | | |-- .... (You lambda functions will be here) 27 | |-- package.json 28 | |-- .gitignore 29 | ``` 30 | 31 | ### Creating a function 32 | Let's create a signup function. After you enter the command the cli will ask you the HTTP method for the function and the endpoint. 33 | 34 | We can use ```POST``` method and ```/users``` endpoint for signup function. 35 | 36 | ``` 37 | lambdr function:create signup 38 | ``` 39 | 40 | After the command finishes, a js file will be added into ```functions``` folder. 41 | 42 | ### Running a function locally 43 | It's really easy to run the function in your local machine by using this command: 44 | ``` 45 | lambdr function:run signup 46 | ``` 47 | 48 | Before running a function you can pass ```event``` parameters by changing ```testEvents``` function property in ```config/lambdr.json```. 49 | ```javascript 50 | { 51 | "accountId": "123456789", 52 | "name": "my-micro-service", 53 | "functions": { 54 | "signup": { 55 | "method": "POST", 56 | "endpoint": "/users", 57 | "testEvent": { 58 | "email": "test@example.com", 59 | "password": "incredible_password" 60 | } 61 | } 62 | } 63 | } 64 | ``` 65 | 66 | ### Create a stage 67 | To deploy your functions you need to create a stage. You can create multiple stages like (development, staging, production). 68 | 69 | Assume we want a ```development``` stage for now. 70 | 71 | ``` 72 | lambdr stage:create development 73 | ``` 74 | 75 | ### Deploy a function 76 | ``` 77 | lambdr function:deploy signup development 78 | ``` 79 | 80 | We deployed signup function into development stage. After this command you will see an endpoint to test this function. 81 | 82 | ### Using environment variables 83 | Lambdr deploys environment variables for it's own stage. You can set environment variables in ```config/env.json``` file. An example ```env.json```: 84 | ```javascript 85 | { 86 | "default": { 87 | "APP_NAME": "Example OAuth Micro Service", 88 | "TEST_STAGE": true 89 | }, 90 | "local": { 91 | "DYNAMO_TABLE": "users-local" 92 | }, 93 | "development": { 94 | "DYNAMO_TABLE": "users-dev" 95 | }, 96 | "production": { 97 | "TEST_STAGE": false 98 | } 99 | } 100 | ``` 101 | 102 | Notice that ```TEST_STAGE``` will be overridden in production stage. 103 | 104 | ```javascript 105 | exports.handler = function(event, context) { 106 | if (process.env.TEST_STAGE) { 107 | context.done(null, 'This stage is for testing'); 108 | } else { 109 | context.done(null, 'This stage is production'); 110 | } 111 | } 112 | ``` 113 | 114 | 115 | 116 | ## Other Commands 117 | ### List stages 118 | ``` 119 | lambdr stage:list 120 | ``` 121 | 122 | ### Remove a stage 123 | ``` 124 | lambdr stage:remove staging 125 | ``` 126 | -------------------------------------------------------------------------------- /bin/lambdr: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../src/cli'); 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambdr", 3 | "version": "0.0.5", 4 | "bin": { 5 | "lambdr": "./bin/lambdr" 6 | }, 7 | "dependencies": { 8 | "archiver": "^0.21.0", 9 | "aws-sdk": "^2.2.26", 10 | "commander": "^2.9.0", 11 | "fs-extra": "^0.26.3", 12 | "inquirer": "^0.11.1", 13 | "lodash": "^3.10.1", 14 | "ncp": "^2.0.0", 15 | "rimraf": "^2.5.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Project = require('./lib/project'); 4 | const program = require('commander'); 5 | 6 | global.project = new Project(process.cwd()); 7 | 8 | program 9 | .version('0.0.3') 10 | .description('Lambdr'); 11 | 12 | program 13 | .command('new ') 14 | .description('creates a new project') 15 | .action(require('./commands/new')); 16 | 17 | program 18 | .command('function:create ') 19 | .description('creates a new lambda function') 20 | .action(require('./commands/function-create')); 21 | 22 | program 23 | .command('function:deploy ') 24 | .description('deploys a function') 25 | .action(require('./commands/function-deploy')); 26 | 27 | program 28 | .command('function:run ') 29 | .description('executes a lambda function') 30 | .action(require('./commands/function-run')); 31 | 32 | program 33 | .command('stage:create') 34 | .description('creates a new stage') 35 | .action(require('./commands/stage-create')); 36 | 37 | program 38 | .command('stage:list') 39 | .description('lists all stages') 40 | .action(require('./commands/stage-list')); 41 | 42 | program 43 | .command('stage:remove ') 44 | .description('removes a stage') 45 | .action(require('./commands/stage-remove')); 46 | 47 | program.parse(process.argv); 48 | 49 | if (!process.argv.slice(2).length) { 50 | program.outputHelp(); 51 | } 52 | -------------------------------------------------------------------------------- /src/commands/function-create.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Project = require('../lib/project'); 4 | const Fn = require('../lib/fn'); 5 | const inquirer = require('inquirer'); 6 | 7 | module.exports = (name) => { 8 | const command = new Command(global.project, name); 9 | return command.start(); 10 | } 11 | 12 | class Command { 13 | constructor(project, name) { 14 | this.project = project; 15 | this.fn = new Fn(project, name); 16 | this.name = name; 17 | } 18 | 19 | start() { 20 | return this.project.correct() 21 | .then(() => Fn.find(this.project, this.name).catch(() => null)) 22 | .then(fn => { 23 | if (fn) throw 'The function already exists.'; 24 | }) 25 | .then(() => this.getConfig()) 26 | .then(() => this.fn.save()) 27 | .then(() => this.finish()) 28 | .catch(err => console.log(err.stack || err)); 29 | } 30 | 31 | getConfig() { 32 | return new Promise((resolve, reject) => { 33 | inquirer.prompt([ 34 | { 35 | type: 'input', 36 | name: 'endpoint', 37 | message: 'Enter the endpoint for this function :', 38 | default: `/${this.name}` 39 | }, 40 | { 41 | type: 'list', 42 | name: 'method', 43 | message: 'Select HTTP method for this function :', 44 | choices: [ 45 | 'GET', 46 | 'POST', 47 | 'PUT', 48 | 'PATCH', 49 | 'DELETE' 50 | ], 51 | default: 'GET' 52 | } 53 | ], answers => { 54 | this.fn.method = answers.method; 55 | this.fn.endpoint = answers.endpoint; 56 | resolve(); 57 | }); 58 | }); 59 | } 60 | 61 | finish() { 62 | console.log(`${this.name} function has been created successfully.`); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/commands/function-deploy.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Fn = require('../lib/fn'); 4 | const Stage = require('../lib/stage'); 5 | 6 | module.exports = (name, stage) => { 7 | const command = new Command(global.project, name, stage); 8 | return command.start(); 9 | } 10 | 11 | class Command { 12 | constructor(project, name, stage) { 13 | this.project = project; 14 | this.name = name; 15 | this.stage = stage; 16 | } 17 | 18 | start() { 19 | return this.project.correct() 20 | .then(() => Stage.find(this.project, this.stage)) 21 | .then(stage => this.stage = stage) 22 | .then(() => Fn.find(this.project, this.name)) 23 | .then(fn => this.fn = fn) 24 | .then(() => this.fn.deploy(this.stage)) 25 | .then(() => this.finish()) 26 | .catch(err => console.log(err.stack || err)); 27 | } 28 | 29 | finish() { 30 | const apiId = this.stage.config.restApiId; 31 | console.log('The function has been deployed to:'); 32 | console.log(`https://${apiId}.execute-api.us-east-1.amazonaws.com/lambdr${this.fn.config.endpoint}`); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/commands/function-run.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Fn = require('../lib/fn'); 4 | 5 | module.exports = (name) => { 6 | const command = new Command(global.project, name); 7 | return command.start(); 8 | } 9 | 10 | class Command { 11 | constructor(project, name) { 12 | this.project = project; 13 | this.name = name; 14 | } 15 | 16 | start() { 17 | return this.project.correct() 18 | .then(() => Fn.find(this.project, this.name)) 19 | .then(fn => fn.run()) 20 | .catch(err => console.log(err.stack || err)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/commands/new.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Project = require('../lib/project'); 4 | const inquirer = require('inquirer'); 5 | const path = require('path'); 6 | const fs = require('fs'); 7 | 8 | module.exports = (name) => { 9 | const command = new Command(name); 10 | return command.start(); 11 | } 12 | 13 | class Command { 14 | constructor(name) { 15 | this.name = name; 16 | this.path = path.join(process.cwd(), this.name); 17 | this.project = new Project(this.path, this.name); 18 | } 19 | 20 | start() { 21 | return this.checkDirectory() 22 | .then(() => this.getCredentials()) 23 | .then(() => this.project.save()) 24 | .then(this.finish) 25 | .catch(err => { 26 | console.log(err.stack || err); 27 | this.remove().then(() => { 28 | console.log('Operation aborted!'); 29 | }); 30 | }); 31 | } 32 | 33 | checkDirectory() { 34 | return new Promise((resolve, reject) => { 35 | if (fs.existsSync(this.path)) 36 | console.log(`There is already a folder named ${this.name}`); 37 | else resolve(); 38 | }) 39 | } 40 | 41 | getCredentials() { 42 | return new Promise((resolve, reject) => { 43 | inquirer.prompt([ 44 | { 45 | type: 'list', 46 | name: 'region', 47 | message: 'Select you region :', 48 | choices: [ 49 | 'us-east-1', 50 | 'us-east-2', 51 | 'eu-west-1', 52 | 'eu-central-1', 53 | 'ap-northeast-1' 54 | ] 55 | }, 56 | { 57 | type: 'input', 58 | name: 'key', 59 | message: 'You AWS KEY ID :' 60 | }, 61 | { 62 | type: 'input', 63 | name: 'secret', 64 | message: 'You AWS KEY SECRET :' 65 | }, 66 | ], answers => { 67 | this.region = answers.region; 68 | this.key = answers.key; 69 | this.secret = answers.secret; 70 | this.project._credentials = { 71 | accessKeyId: answers.key, 72 | secretAccessKey: answers.secret, 73 | region: answers.region 74 | } 75 | resolve(); 76 | }); 77 | }); 78 | } 79 | 80 | finish() { 81 | console.log('Project created!'); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/commands/stage-create.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Stage = require('../lib/stage'); 4 | 5 | module.exports = (name) => { 6 | const command = new Command(global.project, name); 7 | return command.start(); 8 | } 9 | 10 | class Command { 11 | constructor(project, name) { 12 | this.project = project; 13 | this.name = name; 14 | this.stage = new Stage(project, name); 15 | } 16 | 17 | start() { 18 | return this.project.correct() 19 | .then(() => Stage.find(this.project, this.name).catch(() => null)) 20 | .then(stage => { 21 | if (stage) throw 'The stage already exists.'; 22 | }) 23 | .then(() => this.stage.save()) 24 | .then(() => this.finish()) 25 | .catch(err => console.log(err.stack || err)); 26 | } 27 | 28 | finish() { 29 | console.log(`${this.name} stage has been created successfully.`); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/commands/stage-list.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Stage = require('../lib/stage'); 4 | 5 | module.exports = () => { 6 | const command = new Command(global.project); 7 | return command.start(); 8 | } 9 | 10 | class Command { 11 | constructor(project) { 12 | this.project = project; 13 | } 14 | 15 | start() { 16 | return this.project.correct() 17 | .then(() => this.list()) 18 | .catch(err => console.log(err.stack || err)); 19 | } 20 | 21 | list() { 22 | const stages = Stage.all(this.project); 23 | 24 | if (stages.length) { 25 | stages.forEach(stage => { 26 | console.log((`* ${stage.name}`)); 27 | }); 28 | } else { 29 | console.log('No stages added.'); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/commands/stage-remove.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Stage = require('../lib/stage'); 4 | 5 | module.exports = (name) => { 6 | const command = new Command(global.project, name); 7 | return command.start(); 8 | } 9 | 10 | class Command { 11 | constructor(project, name) { 12 | this.project = project; 13 | this.name = name; 14 | } 15 | 16 | start() { 17 | return this.project.correct() 18 | .then(() => Stage.find(this.project, this.name)) 19 | .then(stage => { 20 | return stage.remove(); 21 | }) 22 | .then(() => this.finish()) 23 | .catch(err => console.log(err.stack || err)); 24 | } 25 | 26 | finish() { 27 | console.log(`${this.name} stage has been removed successfully.`); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/aws-helper.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const fs = require('fs'); 3 | 4 | module.exports = { 5 | getUser(credentials) { 6 | return new Promise((resolve, reject) => { 7 | const iam = new AWS.IAM(credentials); 8 | 9 | iam.getUser({}, function(err, data) { 10 | if (err) reject(err); 11 | else resolve(data.User); 12 | }); 13 | }); 14 | }, 15 | 16 | verifyCredentials(credentials) { 17 | return new Promise((resolve, reject) => { 18 | const lambda = new AWS.Lambda(credentials); 19 | 20 | lambda.listFunctions((err, fns) => { 21 | if (err) return reject('Your AWS credentials are not valid!'); 22 | resolve(); 23 | }); 24 | }); 25 | }, 26 | 27 | createRolePolicy(credentials, projectName, stageName) { 28 | return new Promise((resolve, reject) => { 29 | const iam = new AWS.IAM(credentials); 30 | const policy = JSON.stringify(this.getPolicyDocument(credentials.region)); 31 | const variables = { 32 | PolicyDocument: policy, 33 | RoleName: `lambdr_${projectName}_${stageName}`, 34 | PolicyName: `lambdr_${projectName}_${stageName}_policy` 35 | } 36 | 37 | console.log('Adding role policy...'); 38 | 39 | iam.putRolePolicy(variables, (err, data) => { 40 | if (err) reject(err); 41 | else resolve(); 42 | }); 43 | }); 44 | }, 45 | 46 | removeRolePolicy(credentials, projectName, stageName) { 47 | return new Promise((resolve, reject) => { 48 | const iam = new AWS.IAM(credentials); 49 | const variables = { 50 | RoleName: `lambdr_${projectName}_${stageName}`, 51 | PolicyName: `lambdr_${projectName}_${stageName}_policy` 52 | } 53 | 54 | console.log('Removing role policy...'); 55 | 56 | iam.deleteRolePolicy(variables, (err, data) => { 57 | if (err) reject(err); 58 | else resolve(); 59 | }); 60 | }); 61 | }, 62 | 63 | createRole(credentials, projectName, stageName) { 64 | return new Promise((resolve, reject) => { 65 | const iam = new AWS.IAM(credentials); 66 | const policy = JSON.stringify(this.getAssumeRolePolicyDocument()) 67 | const variables = { 68 | AssumeRolePolicyDocument: policy, 69 | RoleName: `lambdr_${projectName}_${stageName}` 70 | } 71 | 72 | console.log('Creating role...'); 73 | 74 | iam.createRole(variables, (err, data) => { 75 | if (err) reject(err); 76 | else resolve(); 77 | }); 78 | }); 79 | }, 80 | 81 | removeRole(credentials, projectName, stageName) { 82 | return new Promise((resolve, reject) => { 83 | const iam = new AWS.IAM(credentials); 84 | const variables = { 85 | RoleName: `lambdr_${projectName}_${stageName}` 86 | } 87 | 88 | console.log('Removing role...'); 89 | 90 | iam.deleteRole(variables, (err, data) => { 91 | if (err) reject(err); 92 | else resolve(); 93 | }); 94 | }); 95 | }, 96 | 97 | createApi(credentials, projectName, stageName) { 98 | return new Promise((resolve, reject) => { 99 | const apigateway = new AWS.APIGateway(credentials); 100 | const params = { 101 | name: `${projectName}-${stageName}` 102 | }; 103 | 104 | console.log('Creating api...'); 105 | 106 | apigateway.createRestApi(params, (err, data) => { 107 | if (err) reject(err); 108 | else { 109 | resolve(data.id); 110 | } 111 | }); 112 | }); 113 | }, 114 | 115 | deployApi(credentials, restApiId) { 116 | return new Promise((resolve, reject) => { 117 | const apigateway = new AWS.APIGateway(credentials); 118 | const params = { 119 | restApiId: restApiId, 120 | stageName: 'lambdr' 121 | }; 122 | 123 | console.log('Deploying api...'); 124 | 125 | apigateway.createDeployment(params, err => { 126 | if (err) reject(err); 127 | else resolve() 128 | }); 129 | }); 130 | }, 131 | 132 | removeApi(credentials, id) { 133 | return new Promise((resolve, reject) => { 134 | const apigateway = new AWS.APIGateway(credentials); 135 | const params = { 136 | restApiId: id 137 | }; 138 | 139 | console.log('Removing api...'); 140 | 141 | apigateway.deleteRestApi(params, err => { 142 | if (err) reject(err); 143 | else resolve(); 144 | }); 145 | }); 146 | }, 147 | 148 | putMethod(credentials, restApiId, resourceId, method) { 149 | return new Promise((resolve, reject) => { 150 | const apigateway = new AWS.APIGateway(credentials); 151 | const params = { 152 | authorizationType: 'NONE', 153 | httpMethod: method, 154 | resourceId: resourceId, 155 | restApiId: restApiId, 156 | apiKeyRequired: false 157 | }; 158 | 159 | apigateway.putMethod(params, err => { 160 | if (err) reject(err); 161 | else resolve(); 162 | }); 163 | }); 164 | }, 165 | 166 | putMethodResponse(credentials, restApiId, resourceId, method) { 167 | return new Promise((resolve, reject) => { 168 | const apigateway = new AWS.APIGateway(credentials); 169 | const params = { 170 | httpMethod: method, 171 | resourceId: resourceId, 172 | restApiId: restApiId, 173 | statusCode: '200' 174 | }; 175 | 176 | apigateway.putMethodResponse(params, (err, data) => { 177 | if (err) reject(err); 178 | else resolve(); 179 | }); 180 | }); 181 | }, 182 | 183 | putIntegration(credentials, restApiId, resourceId, method, accountId, projectName, stageName, functionName) { 184 | return new Promise((resolve, reject) => { 185 | const apigateway = new AWS.APIGateway(credentials); 186 | const params = { 187 | httpMethod: method, 188 | integrationHttpMethod: 'POST', 189 | resourceId: resourceId, 190 | restApiId: restApiId, 191 | type: 'AWS', 192 | uri: `arn:aws:apigateway:${credentials.region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${credentials.region}:${accountId}:function:${projectName}-${functionName}-${stageName}/invocations`, 193 | credentials: null, 194 | requestParameters: {}, 195 | cacheKeyParameters: [], 196 | cacheNamespace: null 197 | }; 198 | 199 | apigateway.putIntegration(params, (err, data) => { 200 | if (err) reject(err); 201 | else resolve(); 202 | }); 203 | }); 204 | }, 205 | 206 | putIntegrationResponse(credentials, restApiId, resourceId, method) { 207 | return new Promise((resolve, reject) => { 208 | const apigateway = new AWS.APIGateway(credentials); 209 | const params = { 210 | httpMethod: method, 211 | resourceId: resourceId, 212 | restApiId: restApiId, 213 | statusCode: '200' 214 | }; 215 | 216 | apigateway.putIntegrationResponse(params, (err, data) => { 217 | if (err) reject(err); 218 | else resolve(); 219 | }); 220 | }); 221 | }, 222 | 223 | createFunction(credentials, name, accountId, projectName, stageName, zip) { 224 | return new Promise((resolve, reject) => { 225 | const params = { 226 | Code: { 227 | ZipFile: new Buffer(fs.readFileSync(zip)) 228 | }, 229 | FunctionName: `${projectName}-${name}-${stageName}`, 230 | Handler: `functions/${name}.handler`, 231 | Role: `arn:aws:iam::${accountId}:role/lambdr_${projectName}_${stageName}`, 232 | Runtime: 'nodejs' 233 | }; 234 | 235 | const lambda = new AWS.Lambda(credentials); 236 | 237 | lambda.createFunction(params, err => { 238 | if (err) reject(err); 239 | else resolve(); 240 | }); 241 | }).catch(() => this.updateFunctionCode( 242 | credentials, name, projectName, stageName, zip 243 | )); 244 | }, 245 | 246 | updateFunctionCode(credentials, name, projectName, stageName, zip) { 247 | return new Promise((resolve, reject) => { 248 | const params = { 249 | ZipFile: new Buffer(fs.readFileSync(zip)), 250 | Publish: true, 251 | FunctionName: `${projectName}-${name}-${stageName}`, 252 | }; 253 | 254 | const lambda = new AWS.Lambda(credentials); 255 | 256 | lambda.updateFunctionCode(params, (err, v) => { 257 | if (err) reject(err); 258 | else resolve(); 259 | }); 260 | }); 261 | }, 262 | 263 | addLambdaPermission(credentials, restApiId, method, path, accountId, projectName, stageName, name) { 264 | return new Promise((resolve, reject) => { 265 | const params = { 266 | Action: 'lambda:InvokeFunction', 267 | FunctionName: `${projectName}-${name}-${stageName}`, 268 | Principal: 'apigateway.amazonaws.com', 269 | SourceArn: `arn:aws:execute-api:${credentials.region}:${accountId}:${restApiId}/*/${method}${path}`, 270 | StatementId: `lambdr-sapid-${projectName}-${name}-${stageName}-${path}-${method}`.replace(/[\/{}]/g, '-') 271 | }; 272 | 273 | const lambda = new AWS.Lambda(credentials); 274 | lambda.addPermission(params, err => { 275 | if (err) reject(err); 276 | else resolve(); 277 | }); 278 | }); 279 | }, 280 | 281 | getAssumeRolePolicyDocument() { 282 | return { 283 | Version: "2012-10-17", 284 | Statement: [ 285 | { 286 | Sid: "", 287 | Effect: "Allow", 288 | Principal: { 289 | Service: "lambda.amazonaws.com" 290 | }, 291 | Action: "sts:AssumeRole" 292 | } 293 | ] 294 | } 295 | }, 296 | 297 | getPolicyDocument(region) { 298 | return { 299 | Statement: [ 300 | { 301 | Resource: `arn:aws:logs:${region}:*:*`, 302 | Action: [ 303 | "logs:CreateLogGroup", 304 | "logs:CreateLogStream", 305 | "logs:PutLogEvents" 306 | ], 307 | Effect: "Allow" 308 | } 309 | ], 310 | Version: "2012-10-17" 311 | } 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /src/lib/aws-recursive-resource-creator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const AWS = require('aws-sdk'); 4 | const _ = require('lodash'); 5 | 6 | class ResourceCreator { 7 | constructor(credentials, restApiId) { 8 | this.api = new AWS.APIGateway(credentials); 9 | this.restApiId = restApiId; 10 | } 11 | 12 | getOrCreate(path) { 13 | return this.loadResources().then(() => this._getOrCreate(path)); 14 | } 15 | 16 | _getOrCreate(path) { 17 | const pathArray = path.split('/'); 18 | const currentPath = pathArray.splice(pathArray.length - 1, 1)[0]; 19 | const parentPath = pathArray.length === 1 ? '/' : pathArray.join('/'); 20 | const parent = _.find(this.resources, i => i.path === parentPath); 21 | const resource = _.find(this.resources, i => i.path === path); 22 | 23 | if (resource) { 24 | return new Promise(resolve => resolve(resource.id)); 25 | } else if (parent) { 26 | return new Promise((resolve, reject) => { 27 | this.api.createResource({ 28 | parentId: parent.id, 29 | pathPart: currentPath, 30 | restApiId: this.restApiId 31 | }, (err, data) => { 32 | if (err) { 33 | reject(err); 34 | } else { 35 | this.resources.push(data); 36 | resolve(data.id); 37 | } 38 | }); 39 | }); 40 | } else { 41 | return this 42 | .getOrCreate(parentPath) 43 | .then(() => this.getOrCreate(path)); 44 | } 45 | } 46 | 47 | loadResources() { 48 | return new Promise((resolve, reject) => { 49 | this.api.getResources({restApiId: this.restApiId}, (err, data) => { 50 | if (err) { 51 | reject(err); 52 | } else { 53 | this.resources = data.items; 54 | resolve(); 55 | } 56 | }); 57 | }); 58 | } 59 | } 60 | 61 | module.exports = ResourceCreator 62 | -------------------------------------------------------------------------------- /src/lib/config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require('fs'); 4 | 5 | class Config { 6 | constructor(project, configPath) { 7 | this.project = project; 8 | this.configPath = configPath || this.project.configPath 9 | } 10 | 11 | create() { 12 | fs.writeFileSync(this.configPath, '{}'); 13 | } 14 | 15 | get all() { 16 | return require(this.configPath); 17 | } 18 | 19 | get(key) { 20 | return this.all[key]; 21 | } 22 | 23 | set(key, value) { 24 | const config = this.all; 25 | config[key] = value; 26 | fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2)); 27 | } 28 | } 29 | 30 | module.exports = Config; 31 | -------------------------------------------------------------------------------- /src/lib/fn.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Config = require('./config'); 4 | const Utils = require('./utils'); 5 | const Stage = require('./stage'); 6 | const fs = require('fs-extra'); 7 | const ResourceCreator = require('./aws-recursive-resource-creator'); 8 | const AWSHelper = require('./aws-helper'); 9 | const ncp = require('ncp'); 10 | const archiver = require('archiver'); 11 | const rmdir = require('rimraf'); 12 | 13 | class Fn { 14 | constructor(project, name) { 15 | this.project = project; 16 | this.name = name; 17 | } 18 | 19 | static all(project) { 20 | const functions = project.config.get('functions') || {}; 21 | return Object.keys(functions).map(i => new Fn(project, i)); 22 | } 23 | 24 | static find(project, name) { 25 | return new Promise((resolve, reject) => { 26 | const functions = project.config.get('functions'); 27 | 28 | if (functions && functions[name]) 29 | resolve(new Fn(project, name)); 30 | else reject('No function found!'); 31 | }); 32 | } 33 | 34 | get config() { 35 | return this.project.config.get('functions')[this.name]; 36 | } 37 | 38 | get path() { 39 | return this.project.path('functions', this.name + '.js') 40 | } 41 | 42 | copyTemplate() { 43 | return new Promise((resolve, reject) => { 44 | const source = Utils.modulePath('templates', 'function.js'); 45 | const target = this.project.path('functions', this.name + '.js'); 46 | 47 | fs.copy(source, target, err => { 48 | if (err) reject(err); 49 | else resolve(); 50 | }); 51 | }); 52 | } 53 | 54 | createEndpoint(stage) { 55 | console.log('Configuring the endpoints...'); 56 | 57 | const resourceCreator = new ResourceCreator( 58 | this.project.credentials, 59 | stage.config.restApiId 60 | ); 61 | 62 | return resourceCreator.getOrCreate(this.config.endpoint) 63 | .then((resourceId) => AWSHelper.putMethod( 64 | this.project.credentials, 65 | stage.config.restApiId, 66 | resourceId, 67 | this.config.method 68 | ) 69 | .then(() => resourceId).catch(() => resourceId) 70 | ) 71 | .then(resourceId => AWSHelper.putIntegration( 72 | this.project.credentials, 73 | stage.config.restApiId, 74 | resourceId, 75 | this.config.method, 76 | this.project.config.get('accountId'), 77 | this.project.name, 78 | stage.name, 79 | this.name 80 | ).then(() => resourceId).catch(() => resourceId) 81 | ) 82 | .then(resourceId => AWSHelper.putMethodResponse( 83 | this.project.credentials, 84 | stage.config.restApiId, 85 | resourceId, 86 | this.config.method 87 | ).then(() => resourceId).catch(() => resourceId) 88 | ) 89 | .then(resourceId => AWSHelper.putIntegrationResponse( 90 | this.project.credentials, 91 | stage.config.restApiId, 92 | resourceId, 93 | this.config.method 94 | ).then(() => resourceId).catch(() => resourceId) 95 | ) 96 | .then(resourceId => AWSHelper.addLambdaPermission( 97 | this.project.credentials, 98 | stage.config.restApiId, 99 | this.config.method, 100 | this.config.endpoint, 101 | this.project.config.get('accountId'), 102 | this.project.name, 103 | stage.name, 104 | this.name 105 | ).then(() => resourceId).catch(() => resourceId) 106 | ) 107 | .then(resourceId => AWSHelper.deployApi( 108 | this.project.credentials, 109 | stage.config.restApiId 110 | )); 111 | } 112 | 113 | removeTemp() { 114 | return new Promise((resolve, reject) => { 115 | rmdir(Utils.modulePath('.deploy'), err => { 116 | if (err) reject(err); 117 | else resolve(); 118 | }); 119 | }); 120 | } 121 | 122 | zip(stage) { 123 | return this.removeTemp() 124 | .then(() => new Promise(resolve => { 125 | fs.mkdirSync(Utils.modulePath('.deploy')); 126 | resolve(); 127 | })) 128 | .then(() => new Promise((resolve, reject) => { 129 | const source = this.project.path(); 130 | const target = Utils.modulePath('.deploy', 'codes'); 131 | 132 | ncp(source, target, err => { 133 | fs.copySync(Utils.modulePath('templates', 'lambdr.js'), `${target}/lambdr.js`); 134 | if (err) reject(err); 135 | else resolve(); 136 | }); 137 | })) 138 | .then(() => this.deleteSensitiveFiles()) 139 | .then(() => this.wrapFunction(stage)) 140 | .then(() => new Promise((resolve, reject) => { 141 | const archive = archiver('zip'); 142 | const out = fs.createWriteStream(Utils.modulePath('.deploy/code.zip')); 143 | out.on('close', resolve); 144 | archive.on('error', reject); 145 | archive.pipe(out); 146 | archive.directory(Utils.modulePath('.deploy', 'codes'), ''); 147 | archive.finalize(); 148 | })); 149 | } 150 | 151 | deleteSensitiveFiles() { 152 | return new Promise((resolve, reject) => { 153 | fs.removeSync(Utils.modulePath('.deploy', 'codes', 'config', 'aws.json')); 154 | resolve(); 155 | }); 156 | } 157 | 158 | wrapFunction(stage) { 159 | return new Promise(resolve => { 160 | const fnTarget = Utils.modulePath('.deploy', 'codes', 'functions', this.name + '.js'); 161 | const file = fs.readFileSync(this.path); 162 | 163 | let wrapped = `process.env.NODE_ENV="${stage.name}";\n`; 164 | wrapped += `require('../lambdr');\n${file}`; 165 | fs.writeFileSync(fnTarget, wrapped); 166 | resolve(); 167 | }); 168 | } 169 | 170 | syncFunction(stage) { 171 | console.log('Uploading function...'); 172 | 173 | return AWSHelper.createFunction( 174 | this.project.credentials, 175 | this.name, 176 | this.project.config.get('accountId'), 177 | this.project.name, 178 | stage.name, 179 | Utils.modulePath('.deploy', 'code.zip') 180 | ); 181 | } 182 | 183 | deploy(stage) { 184 | console.log('Preparing zip file...'); 185 | 186 | return this.zip(stage) 187 | .then(() => this.syncFunction(stage)) 188 | .then(() => this.createEndpoint(stage)) 189 | .then(() => this.removeTemp()); 190 | } 191 | 192 | save() { 193 | return this.copyTemplate() 194 | .then(() => { 195 | const functions = this.project.config.get('functions') || {}; 196 | functions[this.name] = functions[this.name] || {}; 197 | functions[this.name].method = this.method; 198 | functions[this.name].endpoint = this.endpoint; 199 | functions[this.name].testEvent = {}; 200 | this.project.config.set('functions', functions); 201 | }); 202 | } 203 | 204 | run() { 205 | return this.zip({ name: 'local' }) 206 | .then(() => new Promise((resolve, reject) => { 207 | const fnModule = require(`../../.deploy/codes/functions/${this.name}`); 208 | const context = { 209 | done: (err, res) => { 210 | if (err) reject(err); 211 | else resolve(res); 212 | } 213 | }; 214 | 215 | fnModule.handler(this.config.testEvent, context); 216 | })) 217 | .then(console.log) 218 | .catch(console.log) 219 | .then(() => this.removeTemp()); 220 | } 221 | } 222 | 223 | module.exports = Fn; 224 | -------------------------------------------------------------------------------- /src/lib/project.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Utils = require('./utils'); 4 | const Config = require('./config'); 5 | const ncp = require('ncp'); 6 | const path = require('path'); 7 | const AWS = require('aws-sdk'); 8 | const AWSHelper = require('./aws-helper'); 9 | const rmdir = require('rimraf'); 10 | const fs = require('fs'); 11 | 12 | class Project { 13 | constructor(path, name) { 14 | this._path = path; 15 | this._name = name; 16 | this.configPath = this.path('config', 'lambdr.json'); 17 | this.credentialsPath = this.path('config', 'aws.json'); 18 | this.envPath = this.path('config', 'env.json'); 19 | this.config = new Config(this); 20 | this.envVariables = new Config(this, this.envPath); 21 | } 22 | 23 | get credentials() { 24 | this._credentials = this._credentials || require(this.credentialsPath); 25 | return this._credentials; 26 | } 27 | 28 | get name() { 29 | this._name = this._name || this.config.get('name'); 30 | return this._name 31 | } 32 | 33 | exists() { 34 | return Utils.exists(this._path); 35 | } 36 | 37 | path() { 38 | const args = Array.prototype.slice.call(arguments); 39 | args.unshift(this._path); 40 | return path.join.apply(null, args); 41 | } 42 | 43 | correct() { 44 | const message1 = 'This is not a Lambdr project'; 45 | const message2 = 'AWS Credentials file does not exist'; 46 | const message3 = 'Environment Variables config file does not exist'; 47 | 48 | return Utils.existsOrThrow(this.configPath, message1) 49 | .then(() => Utils.existsOrThrow(this.configPath, message2)) 50 | .then(() => Utils.existsOrThrow(this.configPath, message3)) 51 | } 52 | 53 | copyTemplate() { 54 | return new Promise((resolve, reject) => { 55 | const source = Utils.modulePath('templates', 'project'); 56 | 57 | ncp(source, this._path, err => { 58 | if (err) reject(err); 59 | else resolve(); 60 | }); 61 | }); 62 | } 63 | 64 | set credentials(credentials) { 65 | this._credentials = credentials; 66 | const content = JSON.stringify(credentials, null, 2); 67 | fs.writeFileSync(this.credentialsPath, content); 68 | } 69 | 70 | generatePackageJSON() { 71 | const content = JSON.stringify({ 72 | name: this.name 73 | }, null, 2); 74 | 75 | fs.writeFileSync(this.path('package.json'), content); 76 | } 77 | 78 | save() { 79 | console.log('Verifying aws credentials...'); 80 | return AWSHelper.verifyCredentials(this.credentials) 81 | .then(() => this.copyTemplate()) 82 | .then(() => AWSHelper.getUser(this.credentials)) 83 | .then(user => { 84 | this.credentials = this._credentials; 85 | this.generatePackageJSON(); 86 | this.config.create(); 87 | this.config.set('accountId', user.UserId); 88 | this.config.set('name', this.name); 89 | }); 90 | } 91 | 92 | remove() { 93 | return new Promise((resolve, reject) => { 94 | rmdir(this._path, err => { 95 | if (err) reject(err); 96 | else resolve(); 97 | }); 98 | }); 99 | } 100 | } 101 | 102 | module.exports = Project; 103 | -------------------------------------------------------------------------------- /src/lib/stage.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Config = require('./config'); 4 | const AWSHelper = require('./aws-helper'); 5 | 6 | class Stage { 7 | constructor(project, name) { 8 | this.project = project; 9 | this.name = name; 10 | } 11 | 12 | static all(project) { 13 | const stages = project.config.get('stages') || {}; 14 | return Object.keys(stages).map(i => new Stage(project, i)); 15 | } 16 | 17 | static find(project, name) { 18 | return new Promise((resolve, reject) => { 19 | const stages = project.config.get('stages'); 20 | 21 | if (stages && stages[name]) 22 | resolve(new Stage(project, name)); 23 | else reject('No stages found!'); 24 | }); 25 | } 26 | 27 | get config() { 28 | return this.project.config.get('stages')[this.name]; 29 | } 30 | 31 | exists() { 32 | return new Promise((resolve, reject) => { 33 | const stages = Config.get('stages'); 34 | 35 | if (stages && stages[this.name]) resolve(true) 36 | else resolve(false); 37 | }); 38 | } 39 | 40 | save() { 41 | const credentials = this.project.credentials; 42 | const stage = this.name; 43 | const project = this.project.name; 44 | 45 | return AWSHelper.createRole(credentials, project, stage) 46 | .then(() => AWSHelper.createRolePolicy(credentials, project, stage)) 47 | .then(() => AWSHelper.createApi(credentials, project, stage)) 48 | .then(apiId => { 49 | const stages = this.project.config.get('stages') || {}; 50 | stages[this.name] = stages[this.name] || {}; 51 | stages[this.name].restApiId = apiId; 52 | this.project.config.set('stages', stages); 53 | this.project.envVariables.set(this.name, {}); 54 | }); 55 | } 56 | 57 | remove() { 58 | const credentials = this.project.credentials; 59 | const stage = this.name; 60 | const project = this.project.name; 61 | 62 | return AWSHelper.removeRolePolicy(credentials, project, stage) 63 | .then(() => AWSHelper.removeRole(credentials, project, stage)) 64 | .then(() => AWSHelper.removeApi(credentials, this.config.restApiId)) 65 | .then(() => { 66 | const stages = this.project.config.get('stages'); 67 | delete(stages[stage]); 68 | this.project.config.set('stages', stages); 69 | }); 70 | } 71 | } 72 | 73 | module.exports = Stage; 74 | -------------------------------------------------------------------------------- /src/lib/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | module.exports = { 7 | modulePath() { 8 | const args = Array.prototype.slice.call(arguments); 9 | args.unshift('../../'); 10 | args.unshift(__dirname); 11 | return path.join.apply(null, args); 12 | }, 13 | 14 | exists(path) { 15 | return new Promise(resolve => { 16 | fs.exists(path, exists => { 17 | resolve(exists); 18 | }); 19 | }); 20 | }, 21 | 22 | existsOrThrow(path, error) { 23 | return this.exists(path).then(result => { 24 | if (!result) throw new Error(error); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /templates/function.js: -------------------------------------------------------------------------------- 1 | exports.handler = function(event, context) { 2 | context.done(null, "It's working!"); 3 | } 4 | -------------------------------------------------------------------------------- /templates/lambdr.js: -------------------------------------------------------------------------------- 1 | var env = require('./config/env'); 2 | 3 | Object.keys(env.default).forEach(function(key) { 4 | process.env[key] = env.default[key]; 5 | }); 6 | 7 | Object.keys(env[process.env.NODE_ENV]).forEach(function(key) { 8 | process.env[key] = env[process.env.NODE_ENV][key]; 9 | }); 10 | -------------------------------------------------------------------------------- /templates/project/.gitignore: -------------------------------------------------------------------------------- 1 | /config/aws.json 2 | /node_modules 3 | -------------------------------------------------------------------------------- /templates/project/config/env.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": {}, 3 | "local": {} 4 | } 5 | -------------------------------------------------------------------------------- /templates/project/functions/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alidavut/lambdr/4d607186dee655e3e6c832e3aaf93e0b01558509/templates/project/functions/.keep --------------------------------------------------------------------------------