├── Procfile ├── config ├── .gitignore └── sample.json ├── .gitignore ├── README.md ├── test ├── mocha.opts ├── support │ ├── init.js │ ├── clear-db.js │ └── faker │ │ ├── user.js │ │ ├── deployment.js │ │ ├── deployment-ready.js │ │ └── index.js ├── mocks │ ├── lib │ │ ├── deployment-handler │ │ │ └── fetch-status.js │ │ ├── db-handler.js │ │ └── deis-api.js │ └── index.js ├── models.js ├── models.deployment.js ├── db-api.js └── deployment-handler.js ├── lib ├── db-api │ ├── index.js │ ├── user.js │ └── deployment.js ├── deployment-handler │ ├── env.js │ ├── fetch-status.js │ └── index.js ├── models │ ├── user.js │ ├── index.js │ ├── feed.js │ └── deployment.js ├── ssl │ └── index.js ├── config │ ├── index.js │ └── env.js ├── boot │ └── index.js ├── db-handler │ ├── admin-client.js │ └── index.js ├── deployment-api │ └── index.js └── deis-api │ └── index.js ├── index.js ├── Makefile ├── History.md ├── LICENSE └── package.json /Procfile: -------------------------------------------------------------------------------- 1 | web: node index.js -------------------------------------------------------------------------------- /config/.gitignore: -------------------------------------------------------------------------------- 1 | *json 2 | !sample.json -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | *.log 4 | .tmp/ 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # manager 2 | DemocracyOS deployments manager 3 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require test/support/init 2 | --reporter list 3 | --ui bdd 4 | --check-leaks -------------------------------------------------------------------------------- /test/support/init.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Extend module's NODE_PATH 3 | * HACK: temporary solution 4 | */ 5 | 6 | require('node-path')(module); 7 | -------------------------------------------------------------------------------- /test/support/clear-db.js: -------------------------------------------------------------------------------- 1 | var config = require('lib/config'); 2 | var clearDB = require('mocha-mongoose')(config('mongoUrl'), { noClear: true }); 3 | 4 | module.exports = clearDB; -------------------------------------------------------------------------------- /test/mocks/lib/deployment-handler/fetch-status.js: -------------------------------------------------------------------------------- 1 | exports.untilIsUp = function(deployment, fn) { 2 | fn(null); 3 | } 4 | 5 | exports.isUp = function(deployment, fn) { 6 | fn(null, true); 7 | } 8 | -------------------------------------------------------------------------------- /lib/db-api/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Expose models's database api 3 | */ 4 | 5 | [ 6 | 'deployment', 7 | 'user' 8 | ].forEach(function(model){ 9 | exports[model] = require('./'+model); 10 | }); 11 | -------------------------------------------------------------------------------- /lib/deployment-handler/env.js: -------------------------------------------------------------------------------- 1 | var env = require('lib/config')('deploymentEnv'); 2 | var merge = require('mout/object/merge'); 3 | 4 | module.exports = function(_env){ 5 | return merge(env, _env); 6 | } 7 | -------------------------------------------------------------------------------- /test/mocks/lib/db-handler.js: -------------------------------------------------------------------------------- 1 | exports.create = function (name, fn) { 2 | fn(null, 'mongodb://fakeuser:fakepassword@fakemongohost.dev:27017/fakedb'); 3 | } 4 | 5 | exports.drop = function (name, fn) { 6 | fn(null); 7 | } 8 | -------------------------------------------------------------------------------- /lib/db-api/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var User = require('lib/models').User; 6 | 7 | exports.exists = function(id, fn){ 8 | User.findById(id).exec(function(err, user) { 9 | if (err) return fn(err); 10 | if (!user) return fn(null, false); 11 | fn(null, true); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /lib/models/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var mongoose = require('mongoose'); 6 | var Schema = mongoose.Schema; 7 | 8 | /** 9 | * Define `User` Schema Read-Only 10 | */ 11 | 12 | var UserSchema = new Schema({ 13 | email: { type: String, lowercase: true, trim: true } 14 | }); 15 | 16 | module.exports = function initialize(conn) { 17 | return conn.model('User', UserSchema); 18 | } 19 | -------------------------------------------------------------------------------- /test/support/faker/user.js: -------------------------------------------------------------------------------- 1 | var User = require('lib/models').User; 2 | var crypto = require('crypto'); 3 | 4 | exports.create = function create(identifier) { 5 | return new User({ 6 | email: identifier + '@faker-domain.dev' 7 | }); 8 | } 9 | 10 | exports.save = function save(instance, fn) { 11 | instance.save(fn); 12 | } 13 | 14 | exports.destroy = function destroy(instance, fn) { 15 | instance.remove(fn); 16 | } -------------------------------------------------------------------------------- /test/models.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var models = require('lib/models'); 3 | 4 | describe('Models', function(){ 5 | it('should export Deployment', function(){ 6 | expect(models.Deployment).to.exist; 7 | }); 8 | 9 | it('should export Feed', function(){ 10 | expect(models.Feed).to.exist; 11 | }); 12 | 13 | it('should export User', function(){ 14 | expect(models.User).to.exist; 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var log = require('debug')('manager:root'); 6 | var app = module.exports = require('lib/boot'); 7 | var http = require('http'); 8 | var config = require('lib/config'); 9 | 10 | /** 11 | * Launch the server 12 | */ 13 | 14 | var port = config('port'); 15 | 16 | if (module === require.main) { 17 | http.createServer(app).listen(port, function() { 18 | log('Application started on port %d', port); 19 | }); 20 | } -------------------------------------------------------------------------------- /test/support/faker/deployment.js: -------------------------------------------------------------------------------- 1 | var Deployment = require('lib/models').Deployment; 2 | 3 | exports.create = function create(identifier, options) { 4 | return new Deployment({ 5 | name: identifier, 6 | title: identifier + ' fake democracy.', 7 | owner: options.owner._id, 8 | status: 'creating' 9 | }); 10 | } 11 | 12 | exports.save = function save(instance, fn) { 13 | instance.save(fn); 14 | } 15 | 16 | exports.destroy = function destroy(instance, fn) { 17 | instance.remove(fn); 18 | } -------------------------------------------------------------------------------- /test/mocks/lib/deis-api.js: -------------------------------------------------------------------------------- 1 | exports.create = function (name, fn) { 2 | var app = { 3 | created: '2014-01-01T00:00:00UTC', 4 | id: name, 5 | owner: 'test', 6 | structure: {}, 7 | updated: '2014-01-01T00:00:00UTC', 8 | url: name+'.democracyos.dev', 9 | uuid: 'de1bf5b5-4a72-4f94-a10c-d2a3741cdf75' 10 | }; 11 | 12 | fn(null, app); 13 | } 14 | 15 | exports.deploy = function (name, envs, fn) { 16 | fn(null); 17 | } 18 | 19 | exports.destroy = function (name, fn) { 20 | fn(null); 21 | } -------------------------------------------------------------------------------- /lib/models/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var db = require('democracyos-db'); 6 | var config = require('lib/config'); 7 | 8 | /** 9 | * Connect to mongo 10 | */ 11 | 12 | var dataDb = db.getDefaultConnection(); 13 | 14 | // Register user separately since we need to expose it 15 | exports.Deployment = require('./deployment')(dataDb); 16 | exports.Feed = require('./feed')(dataDb); 17 | exports.User = require('./user')(dataDb); 18 | 19 | // Perform primary connection 20 | db.connect(config('mongoUrl')); -------------------------------------------------------------------------------- /test/support/faker/deployment-ready.js: -------------------------------------------------------------------------------- 1 | var Deployment = require('lib/models').Deployment; 2 | 3 | exports.create = function create(identifier, options) { 4 | return new Deployment({ 5 | name: identifier, 6 | deisId: '123-123-123-123', 7 | url: 'identifier.democracyos.dev', 8 | title: identifier + ' fake democracy.', 9 | owner: options.owner._id, 10 | mongoUrl: 'mongodb://fakemongo:27017/fake-db', 11 | status: 'ready' 12 | }); 13 | } 14 | 15 | exports.save = function save(instance, fn) { 16 | instance.save(fn); 17 | } 18 | 19 | exports.destroy = function destroy(instance, fn) { 20 | instance.remove(fn); 21 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # DemocracyOS Makefile 3 | # 4 | 5 | ifndef DEBUG 6 | DEBUG="manager*" 7 | endif 8 | 9 | ifndef NODE_ENV 10 | NODE_ENV="development" 11 | endif 12 | 13 | run: packages 14 | @echo "Starting application..." 15 | @NODE_PATH=. DEBUG=$(DEBUG) node index.js 16 | 17 | packages: 18 | @echo "Installing dependencies..." 19 | @npm install 20 | 21 | clean: 22 | @echo "Removing dependencies, components and built assets." 23 | @rm -rf node_modules 24 | @echo "Done.\n" 25 | 26 | test: packages run-test 27 | 28 | run-test: 29 | @NODE_ENV="test" NODE_PATH=. DEBUG=$(DEBUG) ./node_modules/mocha/bin/mocha 30 | 31 | .PHONY: clean test 32 | -------------------------------------------------------------------------------- /test/mocks/index.js: -------------------------------------------------------------------------------- 1 | var mockery = require('mockery'); 2 | 3 | var mocked = 0; 4 | 5 | module.exports = { 6 | enable: function(name, mock){ 7 | if (!name) return; 8 | if (!mock) mock = './' + name; 9 | return function(){ 10 | if (++mocked === 1) { 11 | mockery.enable({ warnOnUnregistered: false, useCleanCache: true }); 12 | } 13 | mockery.registerMock(name, require(mock)); 14 | } 15 | }, 16 | 17 | disable: function(name){ 18 | return function(){ 19 | if (!name) return; 20 | mockery.deregisterMock(name); 21 | if (--mocked === 0) mockery.disable(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/models/feed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var mongoose = require('mongoose'); 6 | var Schema = mongoose.Schema; 7 | 8 | /** 9 | * Define `Feed` Schema 10 | */ 11 | 12 | var types = [ 13 | 'law-published' 14 | ]; 15 | 16 | var FeedSchema = new Schema({ 17 | type: { type: String, enum: types, required: true } 18 | , deploymentId: { type: Schema.ObjectId, ref: 'Deployment' } 19 | , data: { type: Schema.Types.Mixed, required: true } 20 | , url: { type: String, required: true } 21 | }); 22 | 23 | FeedSchema.index({ law: 1 }); 24 | FeedSchema.index({ type: 1 }); 25 | FeedSchema.index({ deploymentId: 1 }); 26 | 27 | module.exports = function initialize(conn) { 28 | return conn.model('Feed', FeedSchema); 29 | }; 30 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.1.0 / 2015-03-19 3 | ================== 4 | 5 | * Add DEPLOYMENT_LINK_PROTOCOL env var 6 | * refactor env.js file structure 7 | * Add setting cpu and mem limits to new deployments 8 | * Add setting EXTERNAL_SETTINGS_URL 9 | * Fix deis secure configuration 10 | * Fix Deis Api connection error 11 | * Add validation of Deployment status 12 | * Fix validateToken declaration 13 | * adapt config names to standard 14 | * fix usage of env variable name 15 | * Rename 'DEPLOYMENT_DB_SERVER' to 'DEPLOYMENT_MONGO_URL' 16 | * remove PROTOCOL env var and fix env.js format 17 | * Add force https-redirect for Heroku 18 | * Pin deis-api version to DemocracyOS fork 19 | * Create and Drop databases using node mongodb 20 | 21 | 0.0.1 / 2015-03-16 22 | ================== 23 | 24 | * Initial release 25 | -------------------------------------------------------------------------------- /lib/ssl/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Force load with https on parameter environments (defaults to production) 3 | * https://devcenter.heroku.com/articles/http-routing#heroku-headers 4 | * 5 | * Orignal function from https://github.com/paulomcnally/node-heroku-ssl-redirect 6 | * 7 | * WARNING: only use in a heroku (or other reverse-proxy) environments 8 | */ 9 | var _ = require('underscore'); 10 | var log = require('debug')('ssl'); 11 | 12 | //TODO: replace this with `ssl` module from `DemocracyOS/app` 13 | 14 | module.exports = function herokuSslRedirect (environments) { 15 | environments = environments || ['production']; 16 | 17 | return function(req, res, next) { 18 | if (_.contains(environments, process.env.NODE_ENV)) { 19 | if (req.headers['x-forwarded-proto'] != 'https') { 20 | 21 | var redirectionUrl = 'https://' + req.host + req.originalUrl; 22 | log('Forcing redirection to [%s]', redirectionUrl); 23 | res.redirect(redirectionUrl); 24 | } 25 | else { 26 | next(); 27 | } 28 | } 29 | else { 30 | next(); 31 | } 32 | }; 33 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 The DemocracyOS Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /lib/config/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Extend module's NODE_PATH 3 | * HACK: temporary solution 4 | */ 5 | 6 | require('node-path')(module); 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var log = require('debug')('manager:config'); 13 | var environment = process.env.NODE_ENV || 'development'; 14 | var resolve = require('path').resolve; 15 | var merge = require('merge-util'); 16 | var has = Object.prototype.hasOwnProperty; 17 | var envConf = require('./env'); 18 | 19 | var filepath = resolve(__dirname, '..', '..', 'config', environment + '.json'); 20 | 21 | var localConf = {}; 22 | 23 | try { 24 | log('Load local configuration from %s', filepath); 25 | localConf = require(filepath); 26 | } catch (e) { 27 | log('Unalbe to read configuration from file %s: %s', filepath, e.message); 28 | } 29 | 30 | log('Merge environment set configuration variables'); 31 | 32 | var conf = merge(localConf, envConf, { discardEmpty: false }); 33 | conf.env = environment; 34 | 35 | log('Loaded config object for env %s'); 36 | 37 | module.exports = config; 38 | 39 | function config(key) { 40 | if (has.call(conf, key)) return conf[key]; 41 | log('Invalid config key [%s]', key); 42 | return undefined; 43 | } 44 | 45 | for (var key in conf) config[key] = conf[key]; -------------------------------------------------------------------------------- /lib/db-api/deployment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var Deployment = require('lib/models').Deployment; 6 | var log = require('debug')('manager:db-api:deployment'); 7 | 8 | exports.get = function get(name, fn) { 9 | log('Looking for deployment "%s"', name); 10 | 11 | if (!Deployment.nameIsValid(name)) { 12 | return fn('Invalid deployment name.'); 13 | } 14 | 15 | Deployment 16 | .findOne({ name: name }) 17 | .exec(function (err, deployment) { 18 | if (err) { 19 | log('Found error %o', err); 20 | return fn(err); 21 | } 22 | fn(null, deployment); 23 | }); 24 | 25 | return this; 26 | }; 27 | 28 | exports.create = function create(data, fn) { 29 | log('Creating new deployment.'); 30 | 31 | var deployment = new Deployment(data); 32 | deployment.save(onsave); 33 | 34 | function onsave(err) { 35 | if (err) return log('Found error: %s', err), fn(err); 36 | log('Saved deployment with id %s', deployment.id); 37 | fn(null, deployment); 38 | } 39 | }; 40 | 41 | exports.remove = function remove(deployment, fn) { 42 | deployment.remove(fn); 43 | }; 44 | 45 | exports.exists = function exists(query, fn) { 46 | Deployment.find(query).limit(1).exec(function(err, deployments){ 47 | if (err) return fn(err); 48 | fn(null, !!deployments.length); 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "manager", 3 | "version": "0.1.0", 4 | "description": "An manager for DemocracyOS deployments.", 5 | "homepage": "http://www.democracyos.org/", 6 | "keywords": [ 7 | "democracy", 8 | "democracyos", 9 | "liquid democracy", 10 | "partido de la red", 11 | "democracia en red", 12 | "net democracy", 13 | "net party", 14 | "online democracy", 15 | "web democracy", 16 | "change the tool" 17 | ], 18 | "author": "Democracia en Red ", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/DemocracyOS/manager" 22 | }, 23 | "dependencies": { 24 | "debug": "2.1.1", 25 | "deis-api": "git://github.com/DemocracyOS/deis-api", 26 | "democracyos-db": "0.0.1", 27 | "express": "3.20.0", 28 | "git-wrapper": "0.1.1", 29 | "merge-util": "0.3.1", 30 | "mjlescano-batch": "0.5.3", 31 | "mkdirp": "0.5.0", 32 | "mongodb": "1.4.35", 33 | "mongodb-uri": "0.9.7", 34 | "mongoose": "3.8.24", 35 | "mongoose-unique-validator": "0.4.1", 36 | "mout": "0.11.0", 37 | "node-path": "0.0.3", 38 | "rimraf": "2.3.1", 39 | "superagent": "0.21.0", 40 | "underscore": "^1.7.0", 41 | "url-join": "0.0.1", 42 | "urlencode": "0.2.0" 43 | }, 44 | "devDependencies": { 45 | "chai": "^2.1.2", 46 | "mocha": "^2.2.1", 47 | "mocha-mongoose": "^1.0.4", 48 | "mockery": "^1.4.0" 49 | }, 50 | "engines": { 51 | "node": "0.10.x", 52 | "npm": "2.5.x" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/deployment-handler/fetch-status.js: -------------------------------------------------------------------------------- 1 | var log = require('debug')('manager:deployment-handler:fetch-status'); 2 | var request = require('superagent'); 3 | var urljoin = require('url-join'); 4 | 5 | exports.untilIsUp = function(deployment, fn) { 6 | var retries = 20; 7 | var timeout = 5000; 8 | 9 | function get(err, isUp) { 10 | if (err) return log('Error requesting for GET: %s/api', deployment.url), fn(err); 11 | 12 | if (isUp) { 13 | log('Deployment "%s" is ready to access on %s', deployment.name, deployment.url); 14 | fn(null); 15 | } else { 16 | if (--retries !== 0) { 17 | log('Deployment %s is still not available, retrying in ' + (timeout / 1000) + ' seconds.', deployment.url); 18 | return setTimeout(function(){ 19 | exports.isUp(deployment, get); 20 | }, timeout); 21 | } else { 22 | log('Deployment %s is still not available after %s retries, giving up', deployment.url, retries); 23 | fn(new Error('The deployment "' + deployment.name + '" is not up after retrying ' + retries + ' times.')) 24 | } 25 | } 26 | } 27 | 28 | setTimeout(function(){ 29 | exports.isUp(deployment, get); 30 | }, timeout); 31 | } 32 | 33 | exports.isUp = function(deployment, fn) { 34 | request 35 | .get(urljoin(deployment.url, 'api')) 36 | .accept('application/json') 37 | .end(function (err, res) { 38 | log('Deployment responded with %s', res.status); 39 | 40 | if (res.status == 200 || res.status == 403) { 41 | return fn(null, true); 42 | } 43 | 44 | fn(null, false); 45 | }); 46 | } -------------------------------------------------------------------------------- /test/support/faker/index.js: -------------------------------------------------------------------------------- 1 | module.exports = Faker; 2 | 3 | var allowedModels = [ 4 | 'user', 5 | 'deployment', 6 | 'deployment-ready' 7 | ]; 8 | 9 | function Faker(scope) { 10 | if (!(this instanceof Faker)) return new Faker(scope); 11 | var self = this; 12 | 13 | this.models = {}; 14 | this.scope = (scope && scope + '-') || ''; 15 | 16 | allowedModels.forEach(function(model){ 17 | self.models[model] = {}; 18 | }) 19 | } 20 | 21 | Faker.prototype.create = function create(model, identifier, options){ 22 | var self = this; 23 | var Model = require('./' + model); 24 | 25 | if (this.models[model][identifier]) { 26 | throw new Error('Trying to fake two models with same identifier.'); 27 | } 28 | 29 | var instance = Model.create(this.scope + identifier, options); 30 | this.models[model][identifier] = instance; 31 | 32 | return function(fn){ 33 | Model.save(instance, function(err){ 34 | if (err) throw err; 35 | if (fn) fn(null); 36 | }); 37 | } 38 | } 39 | 40 | Faker.prototype.destroy = function destroy(model, identifier){ 41 | var self = this; 42 | var Model = require('./' + model); 43 | var instance = this.models[model][identifier]; 44 | 45 | if (!instance) throw new Error('Trying to destroy unexistent faked model.'); 46 | 47 | return function(fn){ 48 | Model.destroy(instance, function(err){ 49 | if (err) throw err; 50 | this.models[model][identifier] = undefined; 51 | if (fn) fn(null); 52 | }); 53 | } 54 | } 55 | 56 | Faker.prototype.get = function get(model, identifier){ 57 | return this.models[model][identifier]; 58 | } 59 | -------------------------------------------------------------------------------- /lib/boot/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var package = require('package.json'); 6 | var express = require('express'); 7 | var app = module.exports = express(); 8 | var config = require('lib/config'); 9 | var sslRedirect = require('lib/ssl'); 10 | 11 | /** 12 | * Link models with 13 | * mongoDB database 14 | */ 15 | 16 | require('lib/models'); 17 | 18 | var cors = function (req, res, next) { 19 | res.header('Access-Control-Allow-Origin', '*'); 20 | res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'); 21 | res.header('Access-Control-Allow-Headers', 'X-Requested-With, X-Access-Token, X-Revision, Content-Type'); 22 | 23 | next(); 24 | }; 25 | 26 | var version = function (req, res) { 27 | res.json({ 28 | app: 'manager', 29 | env: process.env.NODE_ENV, 30 | port: config.port, 31 | version: package.version, 32 | apiUrl: '/' 33 | }); 34 | }; 35 | 36 | function validateToken(req, res, next) { 37 | var token = req.query.access_token || (req.body && req.body.access_token); 38 | 39 | if (!token) return res.send(401, { msg: 'access_token missing.' }); 40 | 41 | if (token !== config('accessToken')) { 42 | return res.send(401, { msg: 'access_token is wrong.' }); 43 | } 44 | 45 | next(); 46 | } 47 | 48 | app.use(sslRedirect()); 49 | app.use(express.bodyParser()); 50 | app.use(express.cookieParser()); 51 | app.use(cors); 52 | app.use(express.methodOverride()); 53 | app.use(app.router); 54 | 55 | /* 56 | * Local signin routes 57 | */ 58 | 59 | app.get('/', version); 60 | 61 | app.use('/deployments', validateToken); 62 | app.use('/deployments', require('lib/deployment-api')); 63 | -------------------------------------------------------------------------------- /lib/db-handler/admin-client.js: -------------------------------------------------------------------------------- 1 | var mongodbUri = require('mongodb-uri'); 2 | var mongodb = require('mongodb'); 3 | 4 | module.exports = AdminClient; 5 | 6 | function AdminClient(url) { 7 | if (!(this instanceof AdminClient)) return new AdminClient(url); 8 | 9 | this.url = url; 10 | this.uri = mongodbUri.parse(url); 11 | 12 | this.client = null; 13 | this.pending = []; 14 | this.connecting = false; 15 | 16 | this.connect = this.connect.bind(this); 17 | } 18 | 19 | AdminClient.prototype.connect = function connect(fn) { 20 | var self = this; 21 | 22 | if (this.client) return fn && fn(null, this.client); 23 | if (fn) this.pending.push(fn); 24 | if (this.connecting) return; 25 | 26 | this.connecting = true; 27 | 28 | 29 | open(this.uri, function(err, client){ 30 | if (err) throw err; 31 | 32 | self.client = client; 33 | 34 | if (self.pending.length) { 35 | self.pending.forEach(self.connect); 36 | self.pending.length = 0; 37 | } 38 | }); 39 | } 40 | 41 | function createServer(host) { 42 | return new mongodb.Server(host.host, host.port); 43 | } 44 | 45 | function createServers(hosts) { 46 | if (hosts.length === 1) { 47 | return createServer(hosts[0]); 48 | } else if (hosts.length > 1) { 49 | return new mongodb.ReplSetServers(hosts.map(createServer)); 50 | } else { 51 | throw new Error('Must provide a valid mongodb server uri string.'); 52 | } 53 | } 54 | 55 | function open(uri, fn) { 56 | var mongoClient = new mongodb.MongoClient(createServers(uri.hosts)); 57 | 58 | mongoClient.open(function(err, newClient){ 59 | if (err) return fn(err); 60 | 61 | var adminDb = newClient.db('admin'); 62 | 63 | adminDb.authenticate(uri.username, uri.password, function(err){ 64 | if (err) return fn(err); 65 | fn(null, newClient); 66 | }); 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /test/models.deployment.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var clearDb = require('./support/clear-db'); 3 | var Faker = require('test/support/faker'); 4 | var Deployment = require('lib/models').Deployment; 5 | 6 | var faker = new Faker('models-deployment'); 7 | 8 | describe('Models Deployment', function(){ 9 | after(clearDb); 10 | 11 | describe('.nameIsValid()', function(){ 12 | [ 13 | 'name', 14 | 'name-with-slash', 15 | 'a', 16 | 'abc' 17 | ].forEach(function(name){ 18 | it('"'+name+'" should be valid', function(){ 19 | var isValid = Deployment.nameIsValid(name); 20 | expect(isValid).to.be.true; 21 | }); 22 | }); 23 | 24 | [ 25 | '-asd', 26 | 'asd-', 27 | 'coneñe', 28 | 'name!', 29 | 'asdqwertvxasdqwertvxasdqwertvxasdqwertvxasdqwertvxasdqwertvxasdqwertvxasdqwertvxq', 30 | ].forEach(function(name){ 31 | it('"'+name+'" should be invalid', function(){ 32 | var isValid = Deployment.nameIsValid(name); 33 | expect(isValid).to.be.false; 34 | }); 35 | }); 36 | }); 37 | 38 | describe('#setStatus()', function(){ 39 | before(faker.create('user', 'setStatus')); 40 | 41 | before(function(done){ 42 | var user = faker.get('user', 'setStatus'); 43 | faker.create('deployment', 'setStatus', { 44 | owner: user 45 | })(done); 46 | }); 47 | 48 | it('should change status from "creating" to "ready"', function(done){ 49 | var self = this; 50 | var deployment = faker.get('deployment', 'setStatus'); 51 | 52 | deployment.setStatus('ready', function(err){ 53 | if (err) throw err; 54 | if (deployment.status !== 'ready') { 55 | return done(new Error('Deployment status didnt change.')); 56 | } 57 | done(null); 58 | }); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /lib/db-handler/index.js: -------------------------------------------------------------------------------- 1 | var log = require('debug')('manager:db-handler') 2 | var config = require('lib/config'); 3 | var crypto = require('crypto'); 4 | var mongodbUri = require('mongodb-uri'); 5 | var urlencode = require('urlencode'); 6 | var AdminClient = require('./admin-client'); 7 | 8 | var adminClient = new AdminClient(config('deploymentMongoUrl')); 9 | 10 | exports.create = function (name, fn) { 11 | var database = [name, date(), crypto.randomBytes(2).toString('hex')].join('-'); 12 | var username = name + '-' + crypto.randomBytes(12).toString('hex'); 13 | var password = crypto.randomBytes(48).toString('base64'); 14 | 15 | var uri = mongodbUri.parse(config('deploymentMongoUrl')); 16 | 17 | uri.database = database; 18 | uri.username = username; 19 | uri.password = password; 20 | 21 | uri = urlencode.decode(mongodbUri.format(uri)); 22 | 23 | adminClient.connect(function(err, client){ 24 | if (err) return fn(err); 25 | 26 | var db = client.db(database); 27 | 28 | db.addUser(username, password, { 29 | roles: [{ role: 'readWrite', db: database }] 30 | }, function(err) { 31 | if (err) return fn(err); 32 | log('User "%s" created for database "%s".', username, database); 33 | fn(null, uri); 34 | }); 35 | }); 36 | } 37 | 38 | function date() { 39 | var d = new Date; 40 | return [ 41 | d.getFullYear(), 42 | (d.getMonth() + 1), 43 | d.getDate(), 44 | d.getHours(), 45 | d.getMinutes() 46 | ].join('-'); 47 | } 48 | 49 | exports.drop = function (mongoUrl, fn){ 50 | var uri = mongodbUri.parse(mongoUrl); 51 | 52 | adminClient.connect(function(err, client){ 53 | if (err) return fn(err); 54 | 55 | var db = client.db(uri.database); 56 | 57 | db.removeUser(uri.username, function(err) { 58 | if (err) return fn(err); 59 | 60 | log('User "%s" of database "%s" removed.', uri.username, uri.database); 61 | 62 | db.dropDatabase(function(err) { 63 | if (err) return fn(err); 64 | 65 | log('Database "%s" dropped.', uri.database); 66 | 67 | fn(null); 68 | }); 69 | }); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /config/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "accessToken": "random token (http://randomkeygen.com)", 3 | "mongoUrl": "mongodb://localhost/DemocracyOS-dev", 4 | "port": 8989, 5 | "deis": { 6 | "controller": "deis-controller-host", 7 | "username": "deis-user", 8 | "password": "deis-password", 9 | "secure": false 10 | }, 11 | "deploymentImage": "democracyos/app-deis-compatible-image", 12 | "deploymentInternalRegistry": "internal-registry-ip:5000", 13 | "deploymentReservedNames": [ 14 | "deis", 15 | "hub", 16 | "manager", 17 | "dashboard", 18 | "admin", 19 | "status", 20 | "notifier" 21 | ], 22 | "deploymentLinksProtocol": "http", 23 | "deploymentMongoUrl": "mongodb://admin-user:admin-user-password@examplemongohost:27018", 24 | "deploymentEnv": { 25 | "COMMENTS_PER_PAGE": "25", 26 | "LEARN_MORE_URL": "", 27 | "LOCALE": "en", 28 | "LOGO": "http://democracyos-demo.s3.amazonaws.com/logo.png", 29 | "FAVICON": "http://democracyos-demo.s3.amazonaws.com/favicon.ico", 30 | "PORT": "80", 31 | "PUBLIC_PORT": "80", 32 | "NODE_ENV": "production", 33 | "PROTOCOL": "http", 34 | "SPAM_LIMIT": "5", 35 | "RSS_ENABLED": "false", 36 | "NODE_PATH": ".", 37 | "DEBUG": "democracyos*", 38 | "NOTIFICATIONS_TOKEN": "notifier-access-token", 39 | "NOTIFICATIONS_URL": "http://notifier-host:80/api/events", 40 | "CORS_DOMAINS": "", 41 | "MONGO_USERS_URL": "", 42 | "EXTERNAL_SIGNIN_URL": "", 43 | "EXTERNAL_SIGNUP_URL": "", 44 | "EXTERNAL_SETTINGS_URL": "", 45 | "JWT_SECRET": "deploymentJWTSecret", 46 | "CLIENT_CONF": "", 47 | "BASIC_USERNAME": "", 48 | "BASIC_PASSWORD": "", 49 | "FAQ": "", 50 | "TERMS_OF_SERVICE": "", 51 | "PRIVACY_POLICY": "", 52 | "GLOSSARY": "", 53 | "HTTPS_PORT": "443", 54 | "HTTPS_REDIRECT_MODE": "normal", 55 | "GOOGLE_ANALYTICS_TRACKING_ID": "" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/models/deployment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var mongoose = require('mongoose'); 6 | var Schema = mongoose.Schema; 7 | var ObjectId = Schema.ObjectId; 8 | var mongooseUniqueValidator = require('mongoose-unique-validator'); 9 | var config = require('lib/config'); 10 | 11 | /** 12 | * Deployment Statuses 13 | */ 14 | 15 | var statuses = [ 16 | 'creating', 17 | 'destroying', 18 | 'error', 19 | 'ready', 20 | ]; 21 | 22 | // String, between 1~80 chars, alphanumeric or `-`, not starting nor finishing with `-`. 23 | var nameRegex = /^([a-z0-9]{1,2}|[a-z0-9][a-z0-9\-]{1,78}[a-z0-9])$/i; 24 | 25 | /* 26 | * Deployment Schema 27 | */ 28 | 29 | var DeploymentSchema = new Schema({ 30 | name: { 31 | type: String, 32 | index: true, 33 | required: true, 34 | unique: true, 35 | trim: true, 36 | lowercase: true, 37 | match: nameRegex 38 | }, 39 | deisId: { type: String }, 40 | url: { type: String }, 41 | title: { type: String, required: true, trim: true }, 42 | summary: { type: String, trim: true }, 43 | imageUrl: { type: String }, 44 | mongoUrl: { type: String }, 45 | owner: { type: ObjectId, required: true, unique: true, ref: 'User' }, 46 | status: { type: String, required: true, enum: statuses }, 47 | createdAt: { type: Date, default: Date.now }, 48 | deletedAt: { type: Date } 49 | }); 50 | 51 | DeploymentSchema.index({ owner: -1 }); 52 | DeploymentSchema.index({ name: -1 }); 53 | 54 | DeploymentSchema.plugin(mongooseUniqueValidator); 55 | 56 | DeploymentSchema.path('name').validate(function(val){ 57 | return !/^deis/i.test(val); 58 | }, 'Name already taken.'); 59 | 60 | DeploymentSchema.path('name').validate(function(val){ 61 | return !~config('deploymentReservedNames').indexOf(val); 62 | }, 'Name already taken.'); 63 | 64 | DeploymentSchema.pre('remove', function(next) { 65 | mongoose.model('Feed').remove({ deploymentId: this._id }).exec(); 66 | next(); 67 | }); 68 | 69 | DeploymentSchema.statics.nameIsValid = function (name) { 70 | return name && nameRegex.test(name); 71 | } 72 | 73 | DeploymentSchema.methods.setStatus = function (status, fn){ 74 | this.status = status; 75 | this.save(fn); 76 | return this; 77 | } 78 | 79 | DeploymentSchema.methods.hasDeisDeployment = function (){ 80 | return !!this.deisId; 81 | } 82 | 83 | DeploymentSchema.methods.setDeisDeployment = function (app){ 84 | this.deisId = app.uuid; 85 | this.url = app.url; 86 | return this; 87 | } 88 | 89 | DeploymentSchema.methods.unsetDeisDeployment = function (){ 90 | this.deisId = undefined; 91 | this.url = undefined; 92 | return this; 93 | } 94 | 95 | DeploymentSchema.methods.canAssignDeisDeployment = function (){ 96 | if (this.hasDeisDeployment()) return false; 97 | return true; 98 | } 99 | 100 | module.exports = function initialize(conn) { 101 | return conn.model('Deployment', DeploymentSchema); 102 | } 103 | -------------------------------------------------------------------------------- /lib/deployment-api/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var express = require('express'); 6 | var log = require('debug')('manager:deployment-api'); 7 | var api = require('lib/db-api'); 8 | var Batch = require('mjlescano-batch'); 9 | var pick = require('mout/object/pick'); 10 | var mongoose = require('mongoose'); 11 | var deploymentHandler = require('lib/deployment-handler'); 12 | 13 | /** 14 | * Exports Application 15 | */ 16 | 17 | var app = module.exports = express(); 18 | 19 | /** 20 | * Define routes for Deployment API routes 21 | */ 22 | 23 | app.post('/', function(req, res) { 24 | var batch = new Batch; 25 | var deployment = null; 26 | var data = req.body.deployment; 27 | 28 | batch.concurrency(1); 29 | 30 | // Validate Data Structure 31 | batch.push(function(done){ 32 | if (!data) { 33 | return done({ code: 400, err: { msg:'Must define a deployment object.' }}); 34 | } 35 | 36 | data = pick(data, [ 37 | 'name', 38 | 'title', 39 | 'summary', 40 | 'imageUrl', 41 | 'owner', 42 | ]); 43 | 44 | log('Request landed for creationd of: %o', data); 45 | 46 | done(null); 47 | }); 48 | 49 | // Validate Owner Existance 50 | batch.push(function(done){ 51 | if (!mongoose.Types.ObjectId.isValid(data.owner)) { 52 | return done({ code: 400, err: { msg: 'Owner id invalid.' }}); 53 | } 54 | 55 | api.user.exists(data.owner, function(err, exists){ 56 | if (err) return done({ code: 500, err: err }); 57 | if (!exists) return done({ code: 400, err: { msg: 'Owner not found.' }}); 58 | done(null); 59 | }); 60 | }); 61 | 62 | // Verify the owner doesn't have any instance 63 | batch.push(function(done){ 64 | api.deployment.exists({ owner: data.owner }, function (err, exists) { 65 | if (err) return done({ code: 500, err: err }); 66 | if (exists) return done({ code: 400, err: { msg: 'Owner already has a deployment.' }}); 67 | done(null); 68 | }); 69 | }); 70 | 71 | // Create Deployment 72 | batch.push(function(done){ 73 | deploymentHandler.create(data, function(err, _deployment) { 74 | if (err) return done({ code: 500, err: err }); 75 | deployment = _deployment; 76 | done(null); 77 | }); 78 | }); 79 | 80 | batch.end(function(err){ 81 | if (err) { 82 | log('Found error: %o', err); 83 | return res.json(err.code, err); 84 | } 85 | log('Deployment created "%s"', deployment.name); 86 | res.json(200, { deployment: deployment.toJSON() }); 87 | }); 88 | }); 89 | 90 | app.delete('/:name', function(req, res) { 91 | log('DELETE /deployments/%s', req.params.name); 92 | 93 | var name = req.params.name; 94 | 95 | api.deployment.get(name, function (err, deployment) { 96 | if (err) return res.json(500, err); 97 | if (!deployment) return res.json(500, new Error('Deployment not found.')); 98 | 99 | deploymentHandler.destroy(deployment, function(err){ 100 | if (err) return res.json(500, err); 101 | log('Initiated Deployment destroy for "%s".', name); 102 | res.json(200); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /test/db-api.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var clearDb = require('./support/clear-db'); 3 | var Deployment = require('lib/models').Deployment; 4 | var api = require('lib/db-api'); 5 | var Faker = require('test/support/faker'); 6 | 7 | describe('Db Api User', function(){ 8 | var faker = new Faker('db-api-user'); 9 | 10 | describe('#exists()', function(){ 11 | before(faker.create('user', 'exists')); 12 | 13 | it('should return "true" when user exists', function(done){ 14 | var user = faker.get('user', 'exists'); 15 | api.user.exists(user._id, function(err, exists){ 16 | if (err) throw err; 17 | if (!exists) return done(new Error('User not found')); 18 | done(null); 19 | }); 20 | }); 21 | 22 | it('should return "false" when user dont exists', function(done){ 23 | api.user.exists(mongoose.Types.ObjectId(), function(err, exists){ 24 | if (err) throw err; 25 | if (exists) return done(new Error('User found (?)')); 26 | done(null); 27 | }); 28 | }); 29 | }); 30 | }); 31 | 32 | describe('Db Api Deployment', function(){ 33 | var faker = new Faker('db-api-deployment'); 34 | 35 | before(faker.create('user', 'index')); 36 | 37 | before(function(done){ 38 | var user = faker.get('user', 'index'); 39 | faker.create('deployment', 'index', { 40 | owner: user 41 | })(done); 42 | }); 43 | 44 | after(clearDb); 45 | 46 | describe('#get()', function(){ 47 | it('should find existent deployment', function(done){ 48 | var deployment = faker.get('deployment', 'index'); 49 | api.deployment.get(deployment.name, done) 50 | }); 51 | }); 52 | 53 | describe('#exists()', function(){ 54 | it('should return "true" when deployment exists', function(done){ 55 | var deployment = faker.get('deployment', 'index'); 56 | api.deployment.exists({ name: deployment.name }, function(err, exists){ 57 | if (err) throw err; 58 | if (!exists) return done(new Error('Deployment not found')); 59 | done(null); 60 | }); 61 | }); 62 | 63 | it('should return "false" when deployment dont exists', function(done){ 64 | api.deployment.exists({ name: 'non-existent-deployment' }, function(err, exists){ 65 | if (err) throw err; 66 | if (exists) return done(new Error('Deployment found (?)')); 67 | done(null); 68 | }); 69 | }); 70 | }); 71 | 72 | describe('#create()', function(){ 73 | before(faker.create('user', 'create')); 74 | 75 | it('should create deployment', function(done){ 76 | var user = faker.get('user', 'create'); 77 | 78 | api.deployment.create({ 79 | name: 'test-democracy-on-create', 80 | title: 'Test Democracy on Create', 81 | owner: user._id, 82 | status: 'creating' 83 | }, done); 84 | }); 85 | }); 86 | 87 | describe('#remove()', function(){ 88 | it('should delete deployment', function(done){ 89 | var deployment = faker.get('deployment', 'index'); 90 | api.deployment.remove(deployment, function(err){ 91 | if (err) throw err; 92 | Deployment.find({ _id: deployment._id }).limit(1).exec(function(err, deployments){ 93 | if (err) throw err; 94 | if (deployments.length > 0) return done(new Error('Deployment not deleted')); 95 | done(null); 96 | }); 97 | }); 98 | }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /lib/deis-api/index.js: -------------------------------------------------------------------------------- 1 | var config = require('lib/config'); 2 | var log = require('debug')('manager:deis-api'); 3 | var DeisAPI = require('deis-api'); 4 | var Batch = require('mjlescano-batch'); 5 | 6 | var deisConfig = { 7 | controller: config('deis').controller, 8 | username: config('deis').username, 9 | password: config('deis').password, 10 | }; 11 | 12 | if (config('deis').secure) deisConfig.secure = true; 13 | 14 | var deis = new DeisAPI(deisConfig); 15 | 16 | exports.create = function (name, fn) { 17 | login(function(err){ 18 | if (err) return fn(err); 19 | create(name, function(err, app){ 20 | if (err) return fn(err); 21 | fn(null, app); 22 | }); 23 | }); 24 | } 25 | 26 | exports.deploy = function(name, envs, fn) { 27 | var batch = new Batch(); 28 | 29 | batch.concurrency(1); 30 | 31 | batch.push(login); 32 | 33 | batch.push(function(done){ configurate(name, envs, done); }); 34 | 35 | batch.push(function(done){ build(name, done); }); 36 | 37 | batch.end(fn); 38 | } 39 | 40 | exports.destroy = function (name, fn) { 41 | var batch = new Batch(); 42 | batch.concurrency(1); 43 | 44 | batch.push(login); 45 | batch.push(function(done){ destroy(name, done); }); 46 | batch.end(fn); 47 | } 48 | 49 | function login (done){ 50 | log('Logging in to Deis.'); 51 | deis.login(function(err){ 52 | if (!err || err.toString() === 'Error: Already logged in') { 53 | log('Logged in.', err); 54 | return done(null); 55 | } 56 | done(err.toString()); 57 | }); 58 | } 59 | 60 | function create (name, done) { 61 | deis.apps.create(name, function(err, app) { 62 | if (err) { 63 | log('Error creating "%s": %s', name, err); 64 | destroyIfExists(name, function(_err){ 65 | if (_err) log('Found error when trying to destroy "%s" Deis instance: %s', name, _err); 66 | done(err); 67 | }); 68 | return; 69 | } 70 | 71 | log('Deis app created: %j', app); 72 | 73 | done(null, app); 74 | }); 75 | } 76 | 77 | function destroyIfExists (name, done) { 78 | deis.apps.info(name, function(err){ 79 | if (err && err.toString() === 'Error: Not found') return done(null); 80 | if (err) return done(err); 81 | destroy(name, done); 82 | }); 83 | } 84 | 85 | function destroy (name, done) { 86 | deis.apps.destroy(name, function(err) { 87 | if (err) { 88 | log('Couldn\'t destroy app with name %s: %s', name, err); 89 | return done(err); 90 | } 91 | 92 | log('Deis instance destroyed: %s.', name); 93 | 94 | done(null); 95 | }); 96 | } 97 | 98 | function configurate (name, envs, done) { 99 | var limits = { 100 | memory: { cmd: '512M' }, 101 | cpu: { cmd: 256 } 102 | }; 103 | 104 | deis.config.set(name, envs, limits, function(err) { 105 | if (err) { 106 | log('Error configurating "%s": %s', name, err); 107 | return done(err); 108 | } 109 | 110 | log('Deis app configured "%s".', name); 111 | 112 | done(null); 113 | }); 114 | } 115 | 116 | function build(name, done) { 117 | var registry = config('deploymentInternalRegistry') || ''; 118 | var image = config('deploymentImage'); 119 | 120 | if (image && image[0] === '/') image.slice(0, 1); 121 | 122 | if (registry && registry[registry.length - 1] !== '/') registry += '/'; 123 | 124 | var dock = registry + image; 125 | 126 | log('Building Deis app "%s" with docker image "%s".', name, dock); 127 | 128 | deis.builds.create(name, dock, function(err, app) { 129 | if (err) { 130 | log('Error building "%s" using image "%s".', name, dock, err); 131 | return done(err); 132 | } 133 | 134 | log('Deis app "%s" built successfully.', name, app); 135 | 136 | done(null); 137 | }); 138 | } 139 | -------------------------------------------------------------------------------- /lib/config/env.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var env = process.env; 6 | 7 | /** 8 | * Expose env settings 9 | */ 10 | 11 | module.exports = { 12 | accessToken: env.ACCESS_TOKEN, // 'access_token' param needed on any call to Manager. 13 | mongoUrl: env.MONGO_URL, // DB connection string for Manager 14 | port: env.PORT, 15 | 16 | // Deis API auth. Deis server where the Deployments will be instantiated. 17 | deis: { 18 | controller: env.DEIS_CONTROLLER, 19 | username: env.DEIS_USER, 20 | password: env.DEIS_PASSWORD, 21 | secure: env.DEIS_SECURE == 'true' ? true : undefined 22 | }, 23 | 24 | // The docker image of DemocracyOS/app that will be used for creating Deployments. 25 | deploymentImage: env.DEPLOYMENT_IMAGE, 26 | 27 | // (optional) internal Deis Docker registry for quick deployments 28 | deploymentInternalRegistry: env.DEPLOYMENT_INTERNAL_REGISTRY, 29 | 30 | /** 31 | * MongoDB connection string for cluster where deployment DBs will be created. 32 | * Must provide a user with 'root' role. Accepts single server or replicaSet string. 33 | */ 34 | deploymentMongoUrl: env.DEPLOYMENT_MONGO_URL, 35 | 36 | // Reserved names that can't be used for deployment subdomains 37 | deploymentReservedNames: env.DEPLOYMENT_RESERVED_NAMES ? env.DEPLOYMENT_RESERVED_NAMES.split(',') : [], 38 | 39 | deploymentLinkProtocol: env.DEPLOYMENT_LINK_PROTOCOL ? env.DEPLOYMENT_LINK_PROTOCOL : 'http', 40 | 41 | // ENV vars that will be setted by default on new Deployments. 42 | deploymentEnv: { 43 | BASIC_USERNAME: env.DEPLOYMENT_ENV_BASIC_USERNAME, 44 | BASIC_PASSWORD: env.DEPLOYMENT_ENV_BASIC_PASSWORD, 45 | CLIENT_CONF: env.DEPLOYMENT_ENV_CLIENT_CONF, 46 | CLIENT_DEBUG: env.DEPLOYMENT_ENV_CLIENT_DEBUG, 47 | COMMENTS_PER_PAGE: env.DEPLOYMENT_ENV_COMMENTS_PER_PAGE, 48 | CORS_DOMAINS: env.DEPLOYMENT_ENV_CORS_DOMAINS, 49 | DEBUG: env.DEPLOYMENT_ENV_DEBUG, 50 | EXTERNAL_SIGNIN_URL: env.DEPLOYMENT_ENV_EXTERNAL_SIGNIN_URL, 51 | EXTERNAL_SIGNUP_URL: env.DEPLOYMENT_ENV_EXTERNAL_SIGNUP_URL, 52 | EXTERNAL_SETTINGS_URL: env.DEPLOYMENT_ENV_EXTERNAL_SETTINGS_URL, 53 | FAVICON: env.DEPLOYMENT_ENV_FAVICON, 54 | FAQ: env.DEPLOYMENT_ENV_FAQ, 55 | GLOSSARY: env.DEPLOYMENT_ENV_GLOSSARY, 56 | GOOGLE_ANALYTICS_TRACKING_ID: env.DEPLOYMENT_ENV_GOOGLE_ANALYTICS_TRACKING_ID, 57 | HOME_LINK: env.DEPLOYMENT_ENV_HOME_LINK, 58 | HTTPS_PORT: env.DEPLOYMENT_ENV_HTTPS_PORT, 59 | HTTPS_REDIRECT_MODE: env.DEPLOYMENT_ENV_HTTPS_REDIRECT_MODE, 60 | JWT_SECRET: env.DEPLOYMENT_ENV_JWT_SECRET, 61 | LEARN_MORE_URL: env.DEPLOYMENT_ENV_LEARN_MORE_URL, 62 | LOCALE: env.DEPLOYMENT_ENV_LOCALE, 63 | LOGO: env.DEPLOYMENT_ENV_LOGO, 64 | LOGO_MOBILE: env.DEPLOYMENT_ENV_LOGO_MOBILE, 65 | MONGO_USERS_URL: env.DEPLOYMENT_ENV_MONGO_USERS_URL, 66 | NODE_ENV: env.DEPLOYMENT_ENV_NODE_ENV, 67 | NODE_PATH: env.DEPLOYMENT_ENV_NODE_PATH, 68 | NOTIFICATIONS_TOKEN: env.DEPLOYMENT_ENV_NOTIFICATIONS_TOKEN, 69 | NOTIFICATIONS_URL: env.DEPLOYMENT_ENV_NOTIFICATIONS_URL, 70 | PORT: env.DEPLOYMENT_ENV_PORT, 71 | PROTOCOL: env.DEPLOYMENT_ENV_PROTOCOL, 72 | PUBLIC_PORT: env.DEPLOYMENT_ENV_PUBLIC_PORT, 73 | PRIVACY_POLICY: env.DEPLOYMENT_ENV_PRIVACY_POLICY, 74 | RSS_ENABLED: env.DEPLOYMENT_ENV_RSS_ENABLED, 75 | SPAM_LIMIT: env.DEPLOYMENT_ENV_SPAM_LIMIT, 76 | TERMS_OF_SERVICE: env.DEPLOYMENT_ENV_TERMS_OF_SERVICE 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /test/deployment-handler.js: -------------------------------------------------------------------------------- 1 | var clearDb = require('test/support/clear-db'); 2 | var Faker = require('test/support/faker'); 3 | var mocks = require('test/mocks'); 4 | var api = require('lib/db-api'); 5 | 6 | var faker = new Faker('deployment-handler'); 7 | 8 | describe('Deployment Handler', function(){ 9 | 10 | var deploymentHandler = null; 11 | 12 | [ 13 | 'lib/db-handler', 14 | 'lib/deis-api', 15 | 'lib/deployment-handler/fetch-status' 16 | ].forEach(function(name){ 17 | before(mocks.enable(name)); 18 | after(mocks.disable(name)); 19 | }); 20 | 21 | before(function(){ 22 | deploymentHandler = require('lib/deployment-handler'); 23 | }); 24 | 25 | after(clearDb); 26 | 27 | describe('.create()', function(){ 28 | var deployment = null; 29 | 30 | before(faker.create('user', 'create')); 31 | 32 | it('should create a Deployment with status "creating"', function(done){ 33 | var user = faker.get('user', 'create'); 34 | var data = { 35 | name: 'deployment-handler-create', 36 | title: 'DeploymentHandler Create()', 37 | owner: user._id.toString() 38 | }; 39 | 40 | deploymentHandler.create(data, function(err, _deployment) { 41 | if (err) return done(err); 42 | deployment = _deployment; 43 | 44 | if (deployment.status !== 'creating') { 45 | return done(new Error('Deployment status is not "creating"')); 46 | } 47 | 48 | done(null); 49 | }); 50 | }); 51 | 52 | it('should set the Deployment with status "ready" after some time', function(done){ 53 | function check(){ 54 | if (deployment.status === 'creating') { 55 | setTimeout(check, 10); 56 | } else if (deployment.status === 'ready') { 57 | done(null); 58 | } else { 59 | done(new Error('Deployment status is not "ready"')); 60 | } 61 | } 62 | 63 | check(); 64 | }); 65 | }); 66 | 67 | describe('.destroy()', function(){ 68 | var deployment = null; 69 | 70 | before(faker.create('user', 'destroy')); 71 | 72 | before(function(done){ 73 | var user = faker.get('user', 'destroy'); 74 | faker.create('deployment-ready', 'destroy', { 75 | owner: user 76 | })(done); 77 | }); 78 | 79 | it('should set the Deployment with status "destroying"', function(done){ 80 | var deployment = faker.get('deployment-ready', 'destroy'); 81 | deploymentHandler.destroy(deployment, function(err){ 82 | if (err) return done(err); 83 | if (deployment.status !== 'destroying') { 84 | return done(new Error('Deployment status is not "destroying"')); 85 | } 86 | done(null); 87 | }); 88 | }); 89 | 90 | it('should delete the Deployment from the database after some time', function(done){ 91 | var deployment = faker.get('deployment-ready', 'destroy'); 92 | 93 | function check(){ 94 | api.deployment.exists({ name: deployment.name }, function(err, exists){ 95 | if (err) throw err; 96 | 97 | if (!exists) { 98 | deployment = undefined; 99 | return done(null); 100 | } 101 | 102 | if (deployment.status === 'destroying') { 103 | setTimeout(check, 10); 104 | } else { 105 | console.log('======================='); 106 | console.log(deployment); 107 | console.log('======================='); 108 | done(new Error('Deployment status is not "destroying"')); 109 | } 110 | }); 111 | } 112 | 113 | setTimeout(check, 10); 114 | }); 115 | }); 116 | 117 | describe('.validateData()', function(){ 118 | before(faker.create('user', 'validateData')); 119 | 120 | before(function(done){ 121 | var user = faker.get('user', 'validateData'); 122 | faker.create('deployment', 'validateData', { 123 | owner: user 124 | })(done); 125 | }); 126 | 127 | it('should fail if the owner already has a deployment', function(done){ 128 | var user = faker.get('user', 'validateData'); 129 | 130 | var data = { 131 | name: 'test-deployment-handler-validatedata', 132 | title: 'Test DeploymentHandler validateData()', 133 | owner: user._id.toString() 134 | }; 135 | 136 | deploymentHandler.validateData(data, function(err) { 137 | if (err && err.message === 'Owner already has a deployment.') { 138 | return done(null); 139 | } 140 | return done(new Error('Validation shouldnt pass.')); 141 | }); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /lib/deployment-handler/index.js: -------------------------------------------------------------------------------- 1 | var log = require('debug')('manager:deployment-handler'); 2 | var mongoose = require('mongoose'); 3 | var api = require('lib/db-api'); 4 | var Batch = require('mjlescano-batch'); 5 | var dbHandler = require('lib/db-handler'); 6 | var deisApi = require('lib/deis-api'); 7 | var env = require('./env'); 8 | var fetchStatus = require('lib/deployment-handler/fetch-status'); 9 | 10 | exports.create = function(data, fn) { 11 | var batch = new Batch; 12 | var deployment = null; 13 | 14 | data.status = 'creating'; 15 | 16 | log('Creating Deployment "%s"', data.name); 17 | 18 | batch.concurrency(1); 19 | 20 | // Validate Data. 21 | batch.push(function(done){ 22 | exports.validateData(data, done); 23 | }); 24 | 25 | // Create Model. 26 | batch.push(function(done){ 27 | exports.createModel(data, function(err, _deployment) { 28 | if (err) return fn(err); 29 | deployment = _deployment; 30 | done(null); 31 | }); 32 | }); 33 | 34 | // Done. 35 | batch.end(function(err){ 36 | if (err) { 37 | log('Error creating Deployment "%s"', data.name); 38 | return fn(err); 39 | } 40 | 41 | log('Deployment created "%s"', deployment.name); 42 | 43 | setTimeout(function(){ 44 | exports.createDBandDeis(deployment); 45 | }, 0); 46 | 47 | fn(null, deployment); 48 | }); 49 | } 50 | 51 | exports.destroy = function(deployment, fn) { 52 | var batch = new Batch; 53 | var name = deployment.name; 54 | 55 | batch.concurrency(1); 56 | 57 | log('Destroying Deployment "%s"', name); 58 | 59 | // Verify 60 | batch.push(function(done){ 61 | if (deployment.status !== 'ready') { 62 | return done(new Error('Only deployments with status "ready" can be destroyed: "'+name+'" has "'+deployment.status+'".')); 63 | } 64 | done(null); 65 | }); 66 | 67 | // Set 'destroying' status 68 | batch.push(function(done){ 69 | deployment.setStatus('destroying', function(err){ 70 | if (err) return done(err); 71 | log('Setted "destroying" status on "%s"', name); 72 | done(null); 73 | }); 74 | }); 75 | 76 | batch.end(function(err){ 77 | if (err) { 78 | log('Error destroying "%s"', name); 79 | return fn(err); 80 | } 81 | 82 | setTimeout(function(){ 83 | exports.destroyDBandDeis(deployment); 84 | }, 0); 85 | 86 | fn(null); 87 | }); 88 | } 89 | 90 | exports.validateData = function(data, fn) { 91 | var batch = new Batch; 92 | batch.concurrency(1); 93 | 94 | log('Validating data for "%s"', data.name); 95 | 96 | // Validate Owner Existance 97 | batch.push(function(done){ 98 | if (!mongoose.Types.ObjectId.isValid(data.owner)) { 99 | return done(new Error('Owner id invalid.')); 100 | } 101 | 102 | api.user.exists(data.owner, function(err, exists){ 103 | if (err) return done(err); 104 | if (!exists) return done(new Error('Owner not found.')); 105 | done(null); 106 | }); 107 | }); 108 | 109 | // Verify the owner doesn't have any instance 110 | batch.push(function(done){ 111 | api.deployment.exists({ owner: data.owner }, function (err, exists) { 112 | if (err) return done(err); 113 | if (exists) return done(new Error('Owner already has a deployment.')); 114 | done(null); 115 | }); 116 | }); 117 | 118 | batch.end(function(err){ 119 | if (err) return log('Found error: %o', err), fn(err); 120 | log('Data is valid of "%s"', data.name); 121 | fn(null); 122 | }); 123 | } 124 | 125 | exports.createModel = function(data, fn) { 126 | log('Creating Deployment model "%s"', data.name); 127 | api.deployment.create(data, function(err, deployment) { 128 | if (err) { 129 | log('Found error creating model "%s": %o', deployment.name, err); 130 | return fn(err); 131 | } 132 | log('Created Deployment model "%s"', deployment.name); 133 | fn(null, deployment); 134 | }); 135 | } 136 | 137 | exports.destroyModel = function(deployment, fn) { 138 | log('Deleting Deployment model "%s"', deployment.name); 139 | api.deployment.remove(deployment, function(err){ 140 | if (err) { 141 | log('Found error deleting model "%s": %o', deployment.name, err); 142 | return fn(err); 143 | } 144 | fn(null); 145 | }); 146 | } 147 | 148 | exports.createDBandDeis = function(deployment) { 149 | var batch = new Batch; 150 | 151 | batch.concurrency(1); 152 | 153 | log('Called createDBandDeis with "%s"', deployment.name); 154 | 155 | // Create DB 156 | batch.push(function(done){ 157 | if (deployment.mongoUrl) return done(new Error('The deployment already has a DB attached.')); 158 | exports.createDB(deployment, done); 159 | }, function(done){ 160 | if (!deployment.mongoUrl) return done(new Error('The deployment doesn\'t have a DB attached.')); 161 | exports.destroyDB(deployment, done); 162 | }); 163 | 164 | // Create Deis App 165 | batch.push(function(done){ 166 | if (!deployment.canAssignDeisDeployment()) { 167 | return done(new Error('The deployment already has a Deis instance attached.')); 168 | } 169 | exports.createDeis(deployment, done); 170 | }); 171 | 172 | // Verify Deis App is Up 173 | batch.push(function (done) { 174 | fetchStatus.untilIsUp(deployment, done); 175 | }); 176 | 177 | // Save Model 178 | batch.end(function(err){ 179 | if (err) { 180 | log('There was an error when creating Deployment "%s".', err); 181 | log('Destroying deployment model "%o".', deployment.toJSON()); 182 | 183 | exports.destroyModel(deployment, function(err) { 184 | if (err) throw err; 185 | log('Deployment model destroyed "%s".', deployment.name); 186 | }); 187 | } else { 188 | deployment.setStatus('ready', function(err){ 189 | if (err) throw err; 190 | log('Setted "ready" status on "%s"', deployment.name); 191 | }); 192 | } 193 | }); 194 | } 195 | 196 | exports.destroyDBandDeis = function(deployment) { 197 | var batch = new Batch; 198 | 199 | batch.concurrency(2); 200 | 201 | log('Called createDBandDeis with "%s"', deployment.name); 202 | 203 | // Destroy DB 204 | batch.push(function(done){ 205 | if (!deployment.mongoUrl) return done(new Error('The deployment doesn\'t have a DB attached.')); 206 | exports.destroyDB(deployment, done); 207 | }); 208 | 209 | // Destroy Deis App 210 | batch.push(function(done){ 211 | if (!deployment.hasDeisDeployment()) { 212 | return done(new Error('The deployment doesn\'t have a Deis instance attached.')); 213 | } 214 | exports.destroyDeis(deployment, done); 215 | }); 216 | 217 | // Destroy Model 218 | batch.end(function(err){ 219 | if (err) { 220 | log('There was an error when destroying deployment "%s".', err); 221 | 222 | deployment.setStatus('error', function(err){ 223 | if (err) throw err; 224 | log('Setted "error" status on "%s"', deployment.name); 225 | }); 226 | } else { 227 | exports.destroyModel(deployment, function(err) { 228 | if (err) throw err; 229 | log('Deployment destroyed "%s".', deployment.name); 230 | }); 231 | } 232 | }); 233 | } 234 | 235 | exports.createDB = function(deployment, fn) { 236 | log('Creating DB for "%s".', deployment.name); 237 | 238 | dbHandler.create(deployment.name, function(err, uri){ 239 | if (err) return fn(err); 240 | log('DB created for "%s".', deployment.name); 241 | deployment.mongoUrl = uri; 242 | fn(null); 243 | }); 244 | } 245 | 246 | exports.destroyDB = function(deployment, fn) { 247 | log('Dropping DB of "%s".', deployment.name); 248 | 249 | dbHandler.drop(deployment.mongoUrl, function(err){ 250 | if (err) return fn(err); 251 | log('DB dropped of "%s".', deployment.name); 252 | deployment.mongoUrl = undefined; 253 | fn(null); 254 | }); 255 | } 256 | 257 | exports.createDeis = function(deployment, fn) { 258 | deisApi.create(deployment.name, function(err, app){ 259 | if (err) return fn(err); 260 | 261 | deployment.setDeisDeployment(app); 262 | 263 | deployment.populate({ 264 | path: 'owner', 265 | select: 'email' 266 | }, function(err){ 267 | if (err) { 268 | log('Error when searching for owner.email on deployment "%s".', deployment.name, err); 269 | return fn(err); 270 | } 271 | 272 | var environment = env({ 273 | HOST: deployment.url, 274 | MONGO_URL: deployment.mongoUrl, 275 | STAFF: deployment.owner.email, 276 | ORGANIZATION_NAME: deployment.title, 277 | // FIXME: make config() work here, for some reason it doesn't 278 | ORGANIZATION_URL: process.env.DEPLOYMENT_LINK_PROTOCOL + '://' + deployment.url, 279 | DEPLOYMENT_ID: deployment._id 280 | }); 281 | 282 | deisApi.deploy(deployment.name, environment, function(err){ 283 | if (err) return fn(err); 284 | fn(null); 285 | }); 286 | }); 287 | }); 288 | } 289 | 290 | exports.destroyDeis = function(deployment, fn) { 291 | log('Destroying Deis app "%s".', deployment.name); 292 | deisApi.destroy(deployment.name, function(err){ 293 | if (err) return fn(err); 294 | deployment.unsetDeisDeployment(); 295 | log('Deis app destoyed "%s".', deployment.name); 296 | fn(null); 297 | }); 298 | } 299 | --------------------------------------------------------------------------------