All: Add classes for Project, Module and Function

This commit is contained in:
Austen Collins 2015-12-28 23:49:29 -08:00
parent 8a06c1ca2b
commit fc3786f086
25 changed files with 649 additions and 419 deletions

View File

@ -32,17 +32,17 @@ class Serverless {
this._awsAdminSecretKey = config.awsAdminSecretKey;
this._version = require('./../package.json').version;
this._projectRootPath = SUtils.getProjectPath(process.cwd());
this._project = false;
this._meta = false;
this.actions = {};
this.hooks = {};
this.commands = {};
this.classes = {
Project: require('./ServerlessProject'),
Module: require('./ServerlessModule'),
};
// If within private, add further queued data
// If project
if (this._projectRootPath) {
this._project = SUtils.getProject(this._projectRootPath);
// Load Admin ENV information
require('dotenv').config({
silent: true, // Don't display dotenv load failures for admin.env if we already have the required environment variables
@ -54,44 +54,61 @@ class Serverless {
this._awsAdminSecretKey = process.env.SERVERLESS_ADMIN_AWS_SECRET_ACCESS_KEY;
}
// Load Plugins: Defaults
// Load Plugins: Framework Defaults
let defaults = require('./Actions.json');
this._loadPlugins(__dirname, defaults.plugins);
// Load Plugins: Project
if (this._projectRootPath && this._project.plugins) {
this._loadPlugins(this._projectRootPath, this._project.plugins);
if (this._projectRootPath && SUtils.fileExistsSync(path.join(this._projectRootPath, 's-project.json'))) {
let projectJson = require(path.join(this._projectRootPath, 's-project.json'));
if (projectJson.plugins) this._loadPlugins(this._projectRootPath, projectJson.plugins);
}
let prj = new this.classes.Project(this, {});
console.log(prj.get());
}
/**
* Validate Project
* Ensures:
* - A Valid Serverless Project is found
* - Project's s-project.json has one valid region and stage
* Load Plugins
* - @param relDir string path to start from when rel paths are specified
* - @param pluginMetadata [{path:'path (re or loadable npm mod',config{}}]
*/
validateProject() {
_loadPlugins(relDir, pluginMetadata) {
let _this = this;
// Check for root path
if (!this._projectRootPath) {
return BbPromise.reject(new SError('Must be in a Serverless project', SError.errorCodes.NOT_IN_SERVERLESS_PROJECT));
for (let pluginMetadatum of pluginMetadata) {
// Find Plugin
let PluginClass;
if (pluginMetadatum.path.indexOf('.') == 0) {
// Load non-npm plugin from the private plugins folder
let pluginAbsPath = path.join(relDir, pluginMetadatum.path);
SUtils.sDebug('Attempting to load plugin from ' + pluginAbsPath);
PluginClass = require(pluginAbsPath);
PluginClass = PluginClass(SPlugin, __dirname);
} else {
// Load plugin from either custom or node_modules in plugins folder
if (SUtils.dirExistsSync(path.join(relDir, 'plugins', 'custom', pluginMetadatum.path))) {
PluginClass = require(path.join(relDir, 'plugins', 'custom', pluginMetadatum.path));
PluginClass = PluginClass(SPlugin, __dirname);
} else if (SUtils.dirExistsSync(path.join(relDir, 'plugins', 'node_modules', pluginMetadatum.path))) {
PluginClass = require(path.join(relDir, 'plugins', 'node_modules', pluginMetadatum.path));
PluginClass = PluginClass(SPlugin, __dirname);
}
}
// Load Plugin
if (!PluginClass) {
console.log('WARNING: This plugin was requested by this project but could not be found: ' + pluginMetadatum.path);
} else {
SUtils.sDebug(PluginClass.getName() + ' plugin loaded');
this.addPlugin(new PluginClass(_this));
}
}
//TODO: Validate "meta" folder exists w/ essential variable files, or auto-create here temporarily to help transition users.
//TODO: Check _project has at least one region and a stage
// Check for AWS API Keys
if (!this._awsAdminKeyId || !this._awsAdminSecretKey) {
return BbPromise.reject(new SError(
'Missing AWS API Keys',
SError.errorCodes.INVALID_PROJECT_SERVERLESS
));
}
return BbPromise.resolve();
}
/**
@ -173,16 +190,6 @@ class Serverless {
// Add Action
this.actions[config.handler] = function(evt) {
// Get Meta Object, Populate Variables
if (_this._projectRootPath) {
_this._meta = SUtils.getMeta(_this._projectRootPath);
//_this._project = SUtils.populateVariables(_this._project, _this._meta, {
// type: 'private'
//});
//console.log(_this._project);
}
// Add pre hooks, action, then post hooks to queued
let queue = _this.hooks[config.handler + 'Pre'];
@ -210,9 +217,7 @@ class Serverless {
}
/**
* Register Hook
* @param hook
* @param config
* Add Hook
*/
addHook(hook, config) {
@ -222,8 +227,6 @@ class Serverless {
/**
* Add Plugin
* @param ServerlessPlugin class object
* @returns {Promise}
*/
addPlugin(ServerlessPlugin) {
@ -232,74 +235,6 @@ class Serverless {
ServerlessPlugin.registerHooks(),
]);
}
/**
* Execute Pre/Post shell based hook
* @param fullScriptPath
* @returns {Promise.<int>} return code
* @private
* TODO Re-implement this
*/
//_executeShellHook(fullScriptPath) {
// SCli.log(`Executing shell hook ${fullScriptPath}...`);
//
// try {
// let rc = exec(fullScriptPath, {silent: false}).code;
// if (rc !== 0) {
// return BbPromise.reject(new SError(`ERROR executing shell hook ${fullScriptPath}. RC: ${rc}...`, SError.errorCodes.UNKNOWN));
// }
// } catch (e) {
// console.error(e);
// return BbPromise.reject(new SError(`ERROR executing shell hook ${fullScriptPath}. Threw error. RC: ${rc}...`, SError.errorCodes.UNKNOWN));
// }
//
// return BbPromise.resolve(rc);
//}
/**
* Load Plugins
* @param relDir string path to start from when rel paths are specified
* @param pluginMetadata [{path:'path (re or loadable npm mod',config{}}]
* @private
*/
_loadPlugins(relDir, pluginMetadata) {
let _this = this;
for (let pluginMetadatum of pluginMetadata) {
// Find Plugin
let PluginClass;
if (pluginMetadatum.path.indexOf('.') == 0) {
// Load non-npm plugin from the private plugins folder
let pluginAbsPath = path.join(relDir, pluginMetadatum.path);
SUtils.sDebug('Attempting to load plugin from ' + pluginAbsPath);
PluginClass = require(pluginAbsPath);
PluginClass = PluginClass(SPlugin, __dirname);
} else {
// Load plugin from either custom or node_modules in plugins folder
if (SUtils.dirExistsSync(path.join(relDir, 'plugins', 'custom', pluginMetadatum.path))) {
PluginClass = require(path.join(relDir, 'plugins', 'custom', pluginMetadatum.path));
PluginClass = PluginClass(SPlugin, __dirname);
} else if (SUtils.dirExistsSync(path.join(relDir, 'plugins', 'node_modules', pluginMetadatum.path))) {
PluginClass = require(path.join(relDir, 'plugins', 'node_modules', pluginMetadatum.path));
PluginClass = PluginClass(SPlugin, __dirname);
}
}
// Load Plugin
if (!PluginClass) {
console.log('WARNING: This plugin was requested by this project but could not be found: ' + pluginMetadatum.path);
} else {
SUtils.sDebug(PluginClass.getName() + ' plugin loaded');
this.addPlugin(new PluginClass(_this));
}
}
}
}
module.exports = Serverless;
module.exports = Serverless;

96
lib/ServerlessFunction.js Normal file
View File

@ -0,0 +1,96 @@
'use strict';
/**
* Serverless Function Class
* - options.path format is: "moduleFolder/functionFolder#functionName"
*/
const SError = require('./ServerlessError'),
SUtils = require('./utils/index'),
SCli = require('./utils/cli'),
awsMisc = require('./utils/aws/Misc'),
extend = require('util')._extend,
path = require('path'),
fs = require('fs'),
BbPromise = require('bluebird');
class ServerlessFunction {
/**
* Constructor
*/
constructor(Serverless, options) {
this.S = Serverless;
this.load(options.path);
}
/**
* Load
* - Load from source (i.e., file system);
*/
load(functionPath) {
let _this = this;
//TODO: Validate Path (ensure it has '/' and '#')
// Defaults
_this._populated = false;
_this.data = {};
_this.data.endpoints = [];
// If no project path exists, return
if (!functionPath) return;
let func = SUtils.readAndParseJsonSync(path.join(
_this.S._projectRootPath,
'back',
'modules',
functionPath.split('/')[0],
'functions',
functionPath.split('/')[1].split('#')[0],
's-function.json'));
func = func.functions[functionPath.split('#')[1]];
func.name = functionPath.split('#')[1]; // Add name, for consistency
_this = extend(_this.data, func);
}
/**
* Get
* - Return data
*/
get() {
return this.data;
}
/**
* Set
* - Update data
*/
set(data) {
// TODO: Validate data
this.data = data;
}
/**
* Populate
* - Fill in templates then variables
*/
populate(stage, region) {
this._populated = true;
// TODO: Implement via SUtils class which can be used in Project, Module & Function Classes
}
}
module.exports = ServerlessFunction;

130
lib/ServerlessModule.js Normal file
View File

@ -0,0 +1,130 @@
'use strict';
/**
* Serverless Module Class
* - options.path format is: "moduleFolder"
*/
const SError = require('./ServerlessError'),
SUtils = require('./utils/index'),
SCli = require('./utils/cli'),
awsMisc = require('./utils/aws/Misc'),
ServerlessFunction = require('./ServerlessFunction'),
extend = require('util')._extend,
path = require('path'),
fs = require('fs'),
BbPromise = require('bluebird');
class ServerlessModule {
/**
* Constructor
*/
constructor(Serverless, options) {
this.S = Serverless;
this.load(options.path);
}
/**
* Load
* - Load from source (i.e., file system);
*/
load(modulePath) {
let _this = this;
// TODO: Validate Module path. Should consist of Module 'serverless-users' name only.
// Defaults
_this._populated = false;
_this.data = {};
_this.data.name = 'serverless' + SUtils.generateShortId(6);
_this.data.version = '0.0.1';
_this.data.profile = 'aws-v' + require('../package.json').version;
_this.data.location = 'https://github.com/...';
_this.data.author = '';
_this.data.description = 'A Serverless Module';
_this.data.custom = {};
_this.data.functions = {};
_this.data.cloudFormation = {
resources: {},
lambdaIamPolicyDocumentStatements: []
};
// If no project path exists, return
if (!modulePath) return;
let module = SUtils.readAndParseJsonSync(path.join(
_this.S._projectRootPath,
'back',
'modules',
modulePath,
's-module.json'));
// Add Functions
module.functions = {};
let functionList = fs.readdirSync(path.join(
_this.S._projectRootPath,
'back',
'modules',
modulePath,
'functions'));
for (let i = 0; i < functionList.length; i++) {
let funcFile = SUtils.readAndParseJsonSync(path.join(
_this.S._projectRootPath,
'back',
'modules',
modulePath,
'functions',
functionList[i],
's-function.json'));
Object.keys(funcFile.functions).forEach(function(funcKey) {
let func = new ServerlessFunction(_this.S, { path: modulePath + '/' + functionList[i] + '#' + funcKey });
func = func.get();
module.functions[func.name] = func;
});
}
_this = extend(_this.data, module);
}
/**
* Get
* - Return data
*/
get() {
return this.data;
}
/**
* Set
* - Update data
*/
set(data) {
// TODO: Validate data
this.data = data;
}
/**
* Populate
* - Fill in templates then variables
*/
populate(stage, region) {
this._populated = true;
// TODO: Implement via SUtils class which can be used in Project, Module & Function Classes
}
}
module.exports = ServerlessModule;

171
lib/ServerlessProject.js Normal file
View File

@ -0,0 +1,171 @@
'use strict';
/**
* Serverless Project Class
*/
const SError = require('./ServerlessError'),
SUtils = require('./utils/index'),
SCli = require('./utils/cli'),
awsMisc = require('./utils/aws/Misc'),
ServerlessModule = require('./ServerlessModule'),
extend = require('util')._extend,
path = require('path'),
fs = require('fs'),
BbPromise = require('bluebird');
class ServerlessProject {
/**
* Constructor
*/
constructor(Serverless, options) {
this.S = Serverless;
this.load(this.S._projectRootPath);
}
/**
* Load
* - Load from source (i.e., file system);
*/
load(projectPath) {
let _this = this;
// Defaults
_this._populated = false;
_this.data = {};
_this.data.name = 'serverless' + SUtils.generateShortId(6);
_this.data.version = '0.0.1';
_this.data.profile = 'serverless-v' + require('../package.json').version;
_this.data.location = 'https://github.com/...';
_this.data.author = '';
_this.data.description = 'A Serverless Project';
_this.data.custom = {};
_this.data.modules = {};
_this.data.plugins = [];
_this.data.cloudFormation = {
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "The AWS CloudFormation template for this Serverless application's resources outside of Lambdas and Api Gateway",
"Resources": {
"IamRoleLambda": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"lambda.amazonaws.com"
]
},
"Action": [
"sts:AssumeRole"
]
}
]
},
"Path": "/"
}
},
"IamPolicyLambda": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyName": "${stage}-${projectName}-lambda",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:${region}:*:"
}
]
},
"Roles": [
{
"Ref": "IamRoleLambda"
}
],
"Groups": [
{
"Ref": "IamGroupLambda"
}
]
}
}
},
"Outputs": {
"IamRoleArnLambda": {
"Description": "ARN of the lambda IAM role",
"Value": {
"Fn::GetAtt": [
"IamRoleLambda",
"Arn"
]
}
}
}
};
// If no project path exists, return
if (!projectPath) return;
// Get Project JSON
let project = SUtils.readAndParseJsonSync(path.join(projectPath, 's-project.json'));
// Add Modules & Functions
project.modules = {};
let moduleList = fs.readdirSync(path.join(projectPath, 'back', 'modules'));
for (let i = 0; i < moduleList.length; i++) {
let module = new ServerlessModule(_this.S, { path: moduleList[i] });
module = module.get();
project.modules[module.name] = module;
}
_this = extend(_this.data, project);
}
/**
* Get
* - Return data
*/
get() {
return this.data;
}
/**
* Set
* - Update data
*/
set(data) {
// TODO: Validate data
this.data = data;
}
/**
* Populate
* - Fill in templates then variables
*/
populate(stage, region) {
this._populated = true;
// TODO: Implement via SUtils class which can be used in Project, Module & Function Classes
}
}
module.exports = ServerlessProject;

View File

@ -142,7 +142,7 @@ module.exports = function(SPlugin, serverlessPath) {
return _this.S3.sPutLambdaZip(
evt.region.regionBucket,
_this.S._project.name,
_this.S.data.project.get('name'),
evt.stage,
evt.function.name,
fs.createReadStream(evt.function.pathCompressed))
@ -168,7 +168,7 @@ module.exports = function(SPlugin, serverlessPath) {
lambdaVersion;
var params = {
FunctionName: _this.Lambda.sGetLambdaName(_this.S._project, evt.function),
FunctionName: _this.Lambda.sGetLambdaName(_this.S.data.project.get('name'), evt.function.name),
Qualifier: '$LATEST'
};
@ -193,11 +193,11 @@ module.exports = function(SPlugin, serverlessPath) {
S3Bucket: evt.function.s3Bucket,
S3Key: evt.function.s3Key
},
FunctionName: _this.Lambda.sGetLambdaName(_this.S._project, evt.function), /* required */
FunctionName: _this.Lambda.sGetLambdaName(_this.S.data.project.get('name'), evt.function.name), /* required */
Handler: evt.function.handler, /* required */
Role: evt.region.iamRoleArnLambda, /* required */
Runtime: evt.function.module.runtime, /* required */
Description: 'Serverless Lambda function for project: ' + _this.S._project.name,
Description: 'Serverless Lambda function for project: ' + _this.S.data.project.get('name'),
MemorySize: evt.function.memorySize,
Publish: true, // Required by Serverless Framework & recommended by AWS
Timeout: evt.function.timeout

View File

@ -57,7 +57,7 @@ module.exports = function(SPlugin, serverlessPath) {
async.eachLimit(evt.function.events, 5, function (event, cb) {
let params = {
FunctionName: _this.Lambda.sGetLambdaName(_this.S._project, evt.function),
FunctionName: _this.Lambda.sGetLambdaName(_this.S.data.project.get('name'), evt.function.name),
EventSourceArn: event.eventSourceArn,
StartingPosition: event.startingPosition,
BatchSize: event.batchSize,

View File

@ -198,7 +198,7 @@ module.exports = function(SPlugin, serverlessPath) {
let _this = this;
let params = {
FunctionName: _this.Lambda.sGetLambdaName(_this.S._project, evt.endpoint.function), /* required */
FunctionName: _this.Lambda.sGetLambdaName(_this.S.data.project.get('name'), evt.endpoint.function.name), /* required */
Qualifier: evt.stage
};

View File

@ -101,9 +101,8 @@ usage: serverless env get`,
}
}
return _this.S.validateProject()
return _this._prompt()
.bind(_this)
.then(_this._prompt)
.then(_this._validateAndPrepare)
.then(_this._getEnvVar)
.then(function() {

View File

@ -95,9 +95,8 @@ Usage: serverless env list`,
}
}
return _this.S.validateProject()
return _this._prompt()
.bind(_this)
.then(_this._prompt)
.then(_this._validateAndPrepare)
.then(function(evt) {
return evt;

View File

@ -105,9 +105,8 @@ usage: serverless env set`,
}
}
return _this.S.validateProject()
return _this._prompt()
.bind(_this)
.then(_this._prompt)
.then(_this._validateAndPrepare)
.then(_this._setEnvVar)
.then(function() {

View File

@ -99,9 +99,8 @@ usage: serverless env unset`,
}
}
return _this.S.validateProject()
return _this._prompt()
.bind(_this)
.then(_this._prompt)
.then(_this._validateAndPrepare)
.then(_this._unsetEnvVar)
.then(function() {

View File

@ -98,9 +98,8 @@ usage: serverless function create <function>`,
if (_this.S.cli.options.nonInteractive) _this.S._interactive = false;
}
return _this.S.validateProject()
return _this._promptModuleFunction()
.bind(_this)
.then(_this._promptModuleFunction)
.then(_this._validateAndPrepare)
.then(_this._createFunctionSkeleton)
.then(function() {

View File

@ -19,6 +19,7 @@
*/
module.exports = function(SPlugin, serverlessPath) {
const path = require('path'),
SError = require(path.join(serverlessPath, 'ServerlessError')),
SUtils = require(path.join(serverlessPath, 'utils/index')),
@ -96,7 +97,7 @@ module.exports = function(SPlugin, serverlessPath) {
if (_this.S.cli) {
// Options
evt = JSON.parse(JSON.stringify(this.S.cli.options)); // Important: Clone objects, don't refer to them
evt = JSON.parse(JSON.stringify(_this.S.cli.options)); // Important: Clone objects, don't refer to them
// Option - Non-interactive
if (_this.S.cli.options.nonInteractive) _this.S._interactive = false

View File

@ -99,9 +99,8 @@ usage: serverless module create`,
}
}
return _this.S.validateProject()
return _this._promptModuleFunction()
.bind(_this)
.then(_this._promptModuleFunction)
.then(_this._validateAndPrepare)
.then(_this._createModuleSkeleton)
.then(function() {

View File

@ -78,9 +78,8 @@ usage: serverless module install <github-url>`,
_this.evt.url = this.S.cli.params[0];
}
return this.S.validateProject()
return _this._downloadModule()
.bind(_this)
.then(_this._downloadModule)
.then(_this._validateAndPrepare)
.then(_this._installModule)
.then(_this._updateCfTemplate)
@ -222,6 +221,7 @@ usage: serverless module install <github-url>`,
*/
_updateCfTemplate() {
let _this = this,
projectCfPath = path.join(_this.S._projectRootPath, 'cloudformation'),
cfExtensionPoints = SUtils.readAndParseJsonSync(path.join(_this.evt.pathTempModule, 's-module.json')).cloudFormation;

View File

@ -108,7 +108,7 @@ module.exports = function(SPlugin, serverlessPath) {
// If CLI, parse arguments
if (_this.S.cli) {
_this.evt = JSON.parse(JSON.stringify(this.S.cli.options)); // Important: Clone objects, don't refer to them
_this.evt = JSON.parse(JSON.stringify(_this.S.cli.options)); // Important: Clone objects, don't refer to them
if (_this.S.cli.options.nonInteractive) _this.S._interactive = false;
}
@ -410,7 +410,6 @@ module.exports = function(SPlugin, serverlessPath) {
*/
_createProjectBucket() {
SCli.log('Creating a project region bucket on S3: ' + this.evt.projectBucket + '...');
return this.S3.sCreateBucket(this.evt.projectBucket);
}
@ -423,18 +422,6 @@ module.exports = function(SPlugin, serverlessPath) {
let _this = this;
//TODO: Move to RegionCreate
//if (cfStackData) {
// for (let i = 0; i < cfStackData.Outputs.length; i++) {
// if (cfStackData.Outputs[i].OutputKey === 'IamRoleArnLambda') {
// _this.evt.iamRoleLambdaArn = cfStackData.Outputs[i].OutputValue;
// }
// }
//
// // Save StackName to Evt
// _this.evt.stageCfStack = cfStackData.StackName;
//}
// Create s-project.json
let prjJson = SUtils.readAndParseJsonSync(path.join(_this._templatesDir, 's-project.json'));
prjJson.name = _this.evt.name;

View File

@ -95,14 +95,12 @@ usage: serverless region create`,
if (_this.S.cli.options.nonInteractive) _this.S._interactive = false;
}
return _this.S.validateProject()
return _this._prompt()
.bind(_this)
.then(_this._prompt)
.then(_this._validateAndPrepare)
.then(_this._initAWS)
.then(_this._putEnvFile)
.then(_this._putCfFile)
.then(_this._createCfStack)
.then(_this._resourcesDeploy)
.then(function() {
SCli.log('Successfully created region ' + _this.evt.region + ' within stage ' + _this.evt.stage);
return _this.evt;
@ -167,6 +165,9 @@ usage: serverless region create`,
this.S._meta.private.stages[this.evt.stage].regions[this.evt.region] = {
variables: {}
};
// Save Meta before deploying resources
SUtils.saveMeta(this.S._projectRootPath, this.S._meta);
}
/**
@ -212,7 +213,16 @@ SERVERLESS_PROJECT_NAME=${this.S._project.name}`;
*/
_resourcesDeploy() {
// TODO: Call Resources Deploy subaction
let _this = this;
let newEvent = {
stage: 'development',
region: _this.evt.region,
_subaction: true
};
return _this.S.actions.resourcesDeploy(newEvent);
}
}

View File

@ -9,8 +9,6 @@
* region (String) the name of the region you want to deploy resources to. Must exist in provided stage.
*/
//TODO: Refactor how resources work... Check the Road Map for more information
module.exports = function(SPlugin, serverlessPath) {
const path = require('path'),
@ -89,9 +87,8 @@ usage: serverless resources deploy`,
if (_this.S.cli.options.nonInteractive) _this.S._interactive = false;
}
return this.S.validateProject()
return _this._prompt()
.bind(_this)
.then(_this._prompt)
.then(_this._validateAndPrepare)
.then(_this._updateResources)
.then(() => {
@ -131,7 +128,7 @@ usage: serverless resources deploy`,
* Validate & Prepare
*/
_validateAndPrepare(){
_validateAndPrepare() {
let _this = this;
@ -156,7 +153,7 @@ usage: serverless resources deploy`,
}
// Validate region: make sure region exists in stage
if (!_this.S._meta.project.stages[_this.evt.stage].regions[_this.evt.region]) {
if (!_this.S._meta.private.stages[_this.evt.stage].regions[_this.evt.region]) {
return BbPromise.reject(new SError('Region "' + _this.evt.region + '" does not exist in stage "' + _this.evt.stage + '"'));
}
@ -189,58 +186,46 @@ usage: serverless resources deploy`,
};
_this.CF = require('../utils/aws/CloudFormation')(awsConfig);
return _this.CF.sUpdateResourcesStack(
// Create or update CF Stack
return _this.CF.sCreateOrUpdateResourcesStack(
_this.S,
_this.evt.stage,
_this.evt.region.region)
_this.evt.region.region,
'update')
.then(cfData => {
// Monitor CF Update
return _this.CF.sMonitorCf(cfData, 'update');
})
.catch(function(e) {
if (e.message.indexOf('does not exist') !== -1) {
// If Resources stack does not exit, create it.
return _this.CF.sPutCfFile(
_this.S._projectRootPath,
_this.evt.region.regionBucket,
_this.S._project.name,
return _this.CF.sCreateOrUpdateResourcesStack(
_this.S,
_this.evt.stage,
_this.evt.region,
'resources')
.then(function(cfTemplateUrl) {
_this.evt.region.region,
'create')
.then(cfData => {
let projResoucesCfPath = path.join(_this.S._projectRootPath, 'cloudformation', 'resources-cf.json'),
cfTemplate = SUtils.readAndParseJsonSync(projResoucesCfPath);
// Monitor CF Create
return _this.CF.sMonitorCf(cfData, 'create')
.then(cfStackData => {
// Create CF resources stack
return _this.CF.sCreateResourcesStack(
_this.S._projectRootPath,
_this.S._project.name,
_this.evt.stage,
_this.S._project.domain,
'',
cfTemplateUrl
)
.then(cfData => {
return _this.CF.sMonitorCf(cfData, 'create')
.then(cfStackData => {
_this._spinner.stop(true);
_this._spinner.stop(true);
if (cfStackData) {
for (let i = 0; i < cfStackData.Outputs.length; i++) {
if (cfStackData.Outputs[i].OutputKey === 'IamRoleArnLambda') {
_this.evt.region.iamRoleArnLambda = cfStackData.Outputs[i].OutputValue;
}
}
}
});
if (cfStackData) {
for (let i = 0; i < cfStackData.Outputs.length; i++) {
if (cfStackData.Outputs[i].OutputKey === 'IamRoleArnLambda') {
_this.evt.region.iamRoleArnLambda = cfStackData.Outputs[i].OutputValue;
}
}
}
});
});
} else {
if( e.message.indexOf('No updates are to be performed.') !== -1) {
if (e.message.indexOf('No updates are to be performed.') !== -1) {
return BbPromise.resolve({});
} else {
return BbPromise.reject(new SError(e));

View File

@ -94,9 +94,8 @@ usage: serverless stage create`,
if (_this.S.cli.options.nonInteractive) _this.S._interactive = false;
}
return _this.S.validateProject()
return _this._prompt()
.bind(_this)
.then(_this._prompt)
.then(_this._validateAndPrepare)
.then(_this._createRegion)
.then(function() {

View File

@ -10,8 +10,8 @@ let BbPromise = require('bluebird'),
os = require('os'),
async = require('async'),
AWS = require('aws-sdk'),
SUtils = require('../../utils'),
SError = require('../../ServerlessError'),
SUtils = require('../../utils'),
SError = require('../../ServerlessError'),
fs = require('fs');
// Promisify fs module. This adds "Async" to the end of every method
@ -100,8 +100,15 @@ module.exports = function(config) {
});
};
/**
* Get Lambda PHysical ID From Logical
* @param logicalIds
* @param lambdaResourceSummaries
* @returns {Array}
*/
CloudFormation.sGetLambdaPhysicalsFromLogicals = function(logicalIds, lambdaResourceSummaries) {
let lambdaPhysicalIds = [];
for (let lid of logicalIds) {
let foundLambda = lambdaResourceSummaries.find(element=> {
@ -122,15 +129,20 @@ module.exports = function(config) {
* Put CF File On S3
*/
CloudFormation.sPutCfFile = function(projRootPath, bucketName, projName, projStage, projRegion) {
CloudFormation.sPutCfFile = function(Serverless, stage, region) {
let S3 = require('./S3')(config);
// Get Resources
let cfTemplate = SUtils.getResources(Serverless._project);
console.log("here", cfTemplate);
let d = new Date(),
cfPath = path.join(projRootPath, 'meta', 'private', 'resources', 's-resources-cf.json'),
key = ['Serverless', projName, projStage, projRegion, 'resources/' + 's-resources-cf'].join('/') + '@' + d.getTime() + '.json',
cfPath = path.join(Serverless._projectRootPath, 'meta', 'private', 'resources', 's-resources-cf.json'),
key = ['Serverless', Serverless._project.name, stage, region, 'resources/' + 's-resources-cf'].join('/') + '@' + d.getTime() + '.json',
params = {
Bucket: bucketName,
Bucket: Serverless._meta.private.variables.projectBucket,
Key: key,
ACL: 'private',
ContentType: 'application/json',
@ -138,89 +150,56 @@ module.exports = function(config) {
};
return S3.putObjectPromised(params)
.then(function() {
.then(function() {
// TemplateURL is an https:// URL. You force us to lookup endpt vs bucket/key attrs!?!? wtf not cool
let s3 = new AWS.S3();
return 'https://' + s3.endpoint.hostname + `/${bucketName}/${key}`;
});
// TemplateURL is an https:// URL. You force us to lookup endpt vs bucket/key attrs!?!? wtf not cool
let s3 = new AWS.S3();
return 'https://' + s3.endpoint.hostname + `/${Serverless._meta.private.variables.projectBucket}/${key}`;
});
};
/**
* Create Resources Stack
* Create Or Update Resources Stack
*/
CloudFormation.sCreateResourcesStack = function(
projRootPath,
projName,
projStage,
projDomain,
projNotificationEmail,
templateUrl) {
CloudFormation.sCreateOrUpdateResourcesStack = function(Serverless, stage, region, type) {
let _this = this;
let stackName = CloudFormation.sGetResourcesStackName(projStage, projName);
let params = {
StackName: stackName,
Capabilities: [
'CAPABILITY_IAM',
],
TemplateURL: templateUrl,
OnFailure: 'ROLLBACK',
Parameters: [],
Tags: [{
Key: 'STAGE',
Value: projStage,
}]
};
let _this = this;
let stackName = CloudFormation.sGetResourcesStackName(stage, Serverless._project.name);
// Create CloudFormation Stack
return CloudFormation.createStackPromised(params);
};
// Put CF file on S3
return CloudFormation.sPutCfFile(
Serverless,
stage,
region)
.then(function(templateUrl) {
/**
* Update Resources Stack
*/
// CF Params
let params = {
StackName: stackName,
Capabilities: [
'CAPABILITY_IAM',
],
UsePreviousTemplate: false,
Parameters: [],
Tags: [{
Key: 'STAGE',
Value: stage,
}],
TemplateURL: templateUrl
};
CloudFormation.sUpdateResourcesStack = function(Serverless, stage, region) {
// Create or Update
if (type == 'create') {
let _this = this,
projRootPath = Serverless._projectRootPath,
bucketName = SUtils.getRegionConfig(Serverless._projectJson, stage, region).regionBucket,
projName = Serverless._projectJson.name;
return CloudFormation.createStackPromised(params);
let stackName = CloudFormation.sGetResourcesStackName(stage, projName);
} else if (type == 'update') {
let params = {
StackName: stackName,
Capabilities: [
'CAPABILITY_IAM',
],
UsePreviousTemplate: false,
Parameters: [
{
ParameterKey: 'ProjectName',
ParameterValue: projName,
UsePreviousValue: false,
},
{
ParameterKey: 'Stage',
ParameterValue: stage,
UsePreviousValue: false,
},
{
ParameterKey: 'DataModelStage',
ParameterValue: stage,
UsePreviousValue: false,
},
],
};
return CloudFormation.sPutCfFile(projRootPath, bucketName, projName, stage, 'resources')
.then(function(templateUrl) {
params.TemplateURL = templateUrl;
return CloudFormation.updateStackPromised(params);
});
params.OnFailure = 'ROLLBACK';
return CloudFormation.updateStackPromised(params);
}
});
};
/**
@ -251,36 +230,36 @@ module.exports = function(config) {
stackData = null;
async.whilst(
function() {
return stackStatus !== stackStatusComplete;
},
function() {
return stackStatus !== stackStatusComplete;
},
function(callback) {
setTimeout(function() {
let params = {
StackName: cfData.StackId,
};
CloudFormation.describeStacksPromised(params)
.then(function(data) {
stackData = data;
stackStatus = stackData.Stacks[0].StackStatus;
function(callback) {
setTimeout(function() {
let params = {
StackName: cfData.StackId,
};
CloudFormation.describeStacksPromised(params)
.then(function(data) {
stackData = data;
stackStatus = stackData.Stacks[0].StackStatus;
SUtils.sDebug('CF stack status: ', stackStatus);
SUtils.sDebug('CF stack status: ', stackStatus);
if (!stackStatus || validStatuses.indexOf(stackStatus) === -1) {
let prefix = createOrUpdate.slice(0,-1);
return reject(new SError(
`Something went wrong while ${prefix}ing your cloudformation`));
} else {
return callback();
}
});
}, checkFreq);
},
if (!stackStatus || validStatuses.indexOf(stackStatus) === -1) {
let prefix = createOrUpdate.slice(0,-1);
return reject(new SError(
`Something went wrong while ${prefix}ing your cloudformation`));
} else {
return callback();
}
});
}, checkFreq);
},
function() {
return resolve(stackData.Stacks[0]);
}
function() {
return resolve(stackData.Stacks[0]);
}
);
});

View File

@ -26,8 +26,8 @@ module.exports = function(config) {
* Get Lambda Name
*/
Lambda.sGetLambdaName = function(projectJson, functionJson) {
return projectJson.name + '-' + functionJson.name;
Lambda.sGetLambdaName = function(projectName, functionName) {
return projectName + '-' + functionName;
};
/**

View File

@ -30,7 +30,8 @@ module.exports.supportedRuntimes = {
};
/**
* Get Root Path
* Get Project Path
* - Returns path string
*/
exports.getProjectPath = function(startDir) {
@ -64,56 +65,10 @@ exports.getProjectPath = function(startDir) {
return projRootPath;
};
/**
* GetProject
* - Create a project javascript object from al JSON files
*/
exports.getProject = function(projectRootPath) {
let _this = this,
project = {};
// Get Project JSON
project = _this.readAndParseJsonSync(path.join(projectRootPath, 's-project.json'));
// Add Modules & Functions
project.modules = {};
let moduleList = fs.readdirSync(path.join(projectRootPath, 'back', 'modules'));
for (let i = 0; i < moduleList.length; i++) {
try {
let module = _this.readAndParseJsonSync(path.join(projectRootPath, 'back', 'modules', moduleList[i], 's-module.json'));
project.modules[module.name] = module;
project.modules[module.name].pathModule = path.join('back', 'modules', moduleList[i], 's-module.json');
project.modules[module.name].functions = {};
// Get Functions
let moduleFolders = fs.readdirSync(path.join(projectRootPath, 'back', 'modules', moduleList[i]));
for (let j = 0; j < moduleFolders.length; j++) {
// Check functionPath exists
if (_this.fileExistsSync(path.join(projectRootPath, 'back', 'modules', moduleList[i], moduleFolders[j], 's-function.json'))) {
let funcs = _this.readAndParseJsonSync(path.join(projectRootPath, 'back', 'modules', moduleList[i], moduleFolders[j], 's-function.json'));
for (let k = 0; k < Object.keys(funcs.functions).length; k++) {
project.modules[module.name].functions[Object.keys(funcs.functions)[k]] = funcs.functions[Object.keys(funcs.functions)[k]];
project.modules[module.name].functions[Object.keys(funcs.functions)[k]].name = Object.keys(funcs.functions)[k];
project.modules[module.name].functions[Object.keys(funcs.functions)[k]].pathFunction = path.join('back', 'modules', moduleList[i], moduleFolders[j], 's-function.json');
}
}
}
} catch (e) {
console.log(e);
}
}
return project;
};
/**
* Get Meta
* - Get Project Meta Information
* - Load Project Meta Information from file system
* - TODO: Validate against Serverless JSON "Meta" Schema
*/
exports.getMeta = function(projectRootPath) {
@ -179,11 +134,15 @@ exports.getMeta = function(projectRootPath) {
if (_this.dirExistsSync(path.join(projectRootPath, 'meta', 'public'))) _getVariables('public');
if (_this.dirExistsSync(path.join(projectRootPath, 'meta', 'private'))) _getVariables('private');
// TODO: Validate Meta from JSON "META" Schema
return projectMeta;
};
/**
* Save Meta
* - Saves Meta data to file system
* - TODO: Validate Meta from JSON "META" Schema before saving
*/
exports.saveMeta = function(projectRootPath, projectMeta) {
@ -220,71 +179,56 @@ exports.saveMeta = function(projectRootPath, projectMeta) {
};
/**
* Populate Variables
* - variable names are provided with the ${someVar} syntax. Ex. {projectName: '${someVar}'}
* - variables can be of any type, but since they're referenced in JSON, variable names should always be in string quotes:
* - Ex. {projectName: '${someVar}'} - someVar can be an obj, or array...etc
* - you can provide more than one variable in a string. Ex. {projectName: '${firstName}-${lastName}-project'}
* - we get the value of the variable based on stage/region provided. If both are provided, we'll get the region specific value,
* - if only stage is provided, we'll get the stage specific value, if none is provided, we'll get the private or public specific value (depending on the type property of the option obj)
* - example options: {type: 'private', stage: 'development', region: 'us-east-1'}
**/
* GetResources
* - Dynamically Create CloudFormation resources from s-project.json and s-module.json
* - Returns Aggregated CloudFormation Template
*/
exports.populateVariables = function(project, meta, options) {
exports.getResources = function(projectJson) {
options.type = options.type || 'private'; // private is the default type
let cfTemplate = JSON.parse(JSON.stringify(projectJson.cloudFormation));
// Validate providing region without stage is invalid!
if (!options.stage) options.region = null;
// Loop through modules and aggregate resources
for (let i = 0; i < Object.keys(projectJson.modules).length; i++) {
// Validate Stage exists
if (typeof options.stage != 'undefined' && !meta.private.stages[options.stage]) {
throw Error('Stage doesnt exist!');
}
let moduleJson = projectJson.modules[Object.keys(projectJson.modules)[i]];
// Validate Region exists
if (typeof options.region != 'undefined' && !meta.private.stages[options.stage].regions[options.region]) {
throw Error('Region doesnt exist in the provided stage!');
}
// If no cloudFormation in module, skip...
if (!moduleJson.cloudFormation) continue;
// Traverse the whole project and replace variables
traverse(project).forEach(function(projectVal) {
// Merge Lambda Policy Statements
if (moduleJson.cloudFormation.lambdaIamPolicyDocumentStatements &&
moduleJson.cloudFormation.lambdaIamPolicyDocumentStatements.length > 0) {
SCli.log('Merging in Lambda IAM Policy statements from s-module');
// check if the current string is a variable
if (typeof(projectVal) === 'string' && projectVal.match(/\${([^{}]*)}/g) != null) {
let newVal = projectVal;
// get all ${variable}
projectVal.match(/\${([^{}]*)}/g).forEach(function(variableSyntax) {
let variableName = variableSyntax.replace('${', '').replace('}', '');
let value;
if (options.stage && options.region) {
value = meta[options.type].stages[options.stage].regions[options.region].variables[variableName];
} else if (options.stage) {
value = meta[options.type].stages[options.stage].variables[variableName];
} else if (!options.stage && !options.region) {
value = meta[options.type].variables[variableName];
}
if (typeof value === 'undefined') {
throw Error('Variable Doesnt exist!');
} else if (typeof value === 'string') {
newVal = newVal.replace(variableSyntax, value);
} else {
newVal = value;
}
moduleJson.cloudFormation.lambdaIamPolicyDocumentStatements.forEach(function(policyStmt) {
cfTemplate.Resources.IamPolicyLambda.Properties.PolicyDocument.Statement.push(policyStmt);
});
this.update(newVal);
}
});
return project;
// Merge resources
if (moduleJson.cloudFormation.resources) {
let cfResourceKeys = Object.keys(moduleJson.cloudFormation.resources);
if (cfResourceKeys.length > 0) {
SCli.log('Merging in CF Resources from s-module');
}
cfResourceKeys.forEach(function (resourceKey) {
if (cfTemplate.Resources[resourceKey]) {
SCli.log(
chalk.bgYellow.white(' WARN ') +
chalk.magenta(` Resource key ${resourceKey} already defined in ${file}. Overwriting...`)
);
}
cfTemplate.Resources[resourceKey] = moduleJson.cloudFormation.resources[resourceKey];
});
}
}
return cfTemplate;
};
/**
@ -713,6 +657,12 @@ exports.writeFile = function(filePath, contents) {
});
};
/**
* Generate Short ID
* @param maxLen
* @returns {string}
*/
exports.generateShortId = function(maxLen) {
return shortid.generate().replace(/\W+/g, '').substring(0, maxLen).replace(/[_-]/g, '');
};
@ -734,13 +684,13 @@ exports.generateProjectBucketName = function(region, projectDomain) {
* Given list of project stage objects, extract given region
*/
exports.getRegionConfig = function(projectMeta, stage, regionName) {
exports.getRegionConfig = function(projectMeta, stage, region) {
if (!projectMeta.project.stages[stage].regions[region]) {
throw new SError(`Could not find region ${regionName}`, SError.errorCodes.UNKNOWN);
if (!projectMeta.private.stages[stage].regions[region]) {
throw new SError(`Could not find region ${region}`, SError.errorCodes.UNKNOWN);
}
return projectMeta.project.stages[stage].regions[region];
return projectMeta.private.stages[stage].regions[region];
};
exports.dirExistsSync = function(path) {

View File

@ -13,16 +13,6 @@
"variables": {}
},
"public": {
"stages": {
"development": {
"regions": {
"us-east-1": {
"variables": {}
}
},
"variables": {}
}
},
"variables": {}
}
}

View File

@ -131,7 +131,9 @@
}
]
}
}
},
"templates": {}
}
}
}

View File

@ -49,6 +49,7 @@
"dotenv": "^1.2.0",
"download": "^4.2.0",
"expand-home-dir": "0.0.2",
"immutable": "^3.7.6",
"insert-module-globals": "^6.5.2",
"keypress": "^0.2.1",
"minimist": "^1.2.0",