UNPKG

haibu-ishiki

Version:

Node deployment server - wrapper for haibu, http-proxy and carapace with permissions and automatic node versions

409 lines (337 loc) 13.2 kB
/* The MIT License Copyright (c) 2013 Hadrien Jouet https://github.com/grownseed Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ var crypto = require('crypto'), nodev = require('./nodev'), droneModel = require('../models/drone'), logModel = require('../models/log'); module.exports = function(app, haibu, path, fs, drone, proxy) { //user/app logs function logInfo(type, msg, meta) { if (msg.trim() != '' && meta && meta.app && meta.user) { var data = { type: type, msg: msg, user: meta.user, app: meta.app, ts: new Date() }; logModel.add(data, function(){}); } } //update drone state function switchState(started, pkg) { pkg.started = started; //add available port back into port range if (!started && pkg.env && pkg.env.PORT) ports.push(pkg.env.PORT); console.log((started ? 'Started' : 'Stopped') + ' application ' + pkg.user + '/' + pkg.name); logInfo('info', 'Application ' + (started ? 'started' : 'stopped'), {name: pkg.app, user: pkg.user}); droneModel.createOrUpdate(pkg, function(){}); } //update drone state and clear proxy routes function switchAndClear(pkg) { switchState(false, pkg); //unload proxy routes proxy.deleteBy(app.config.get('public-port'), {user: pkg.user, appid: pkg.name}); } //find drone by uid, update drone state and clear proxy routes function findSwitchAndClear(uid) { if (uid) { droneModel.getProcessed({uid: uid}, function(err, result) { if (!err && result && result.length == 1) { switchAndClear(result[0]); } }); } } //listen to drones haibu.on('drone:stdout', logInfo); haibu.on('drone:stderr', logInfo); haibu.on('drone:start', function(type, msg) { switchState(true, msg.pkg); }); haibu.on('drone:stop', function(type, msg) { switchAndClear(msg.pkg); }); haibu.on('drone:stop:error', function(result) { findSwitchAndClear(result.key); }); haibu.on('drone:stop:success', function(result) { findSwitchAndClear(result.key); }); //generate port range var ports = []; for (var i = app.config.get('port-range:min'); i <= app.config.get('port-range:max'); i++) ports.push(i); //returns available port within provided range function getPort() { if (port = ports.shift()) return port; else return false; } //starts a drone and sets up proxy routes for it function startDrone(pkg, userid, appid, callback) { var drone_port = getPort(); if (drone_port) { pkg.env = pkg.env || {}; pkg.env['PORT'] = drone_port; //ensure package user and name match internally pkg.user = userid; pkg.name = appid; drone.start(pkg, function(err, result) { if (err) callback(err); pkg.host = result.host; pkg.port = result.port; pkg.uid = result.uid; //async update package in db droneModel.createOrUpdate(pkg, function(){}); //load proxy routes proxy.load(app.config.get('public-port'), pkg); callback(null, {drone: result}); }); }else{ callback({message: 'No more ports available'}); } } //stops a drone function stopDrone(userid, appid, callback) { droneModel.getProcessed({user: userid, name: appid}, function(err, result) { if (err) return callback(err); if (result.length == 1) { if (!result[0].started) { callback({message: 'Drone is already stopped'}); }else{ //need to namespace apps to allow for two users with same app name drone.stop(appid, function(err, response) { if (err) return callback(err); result[0].started = false; callback(null, result[0]); }); } }else{ callback({message: 'No drone matching ' + userid + '/' + appid}); } }); } //find drones for given filter and sends response function sendDrones(filter, res) { droneModel.getProcessed(filter, function(err, result) { if (err) return haibu.sendResponse(res, 500, err); haibu.sendResponse(res, 200, {drones: result}); }); } //automatically start drones droneModel.getProcessed({started: true}, function(err, results) { results.forEach(function(result) { startDrone(result, result.user, result.name, function(err, drone) { if (err) console.log('Error starting ' + result.user + '/' + result.name, (err.message || err)); }) }); }); //drones API app.router.path('/drones', function() { //list all drones this.get(function() { sendDrones({}, this.res); }); //list all drones for particular user this.get('/:userid', function(userid) { sendDrones({user: userid}, this.res); }); //list all running drones this.get('/running', function() { sendDrones({started: true}, this.res); }); //show status of particular app/drone for given user this.get('/:userid/:appid', function(userid, appid) { sendDrones({user: userid, name: appid}, this.res); }); //start drone this.post('/:userid/:appid/start', function(userid, appid) { var self = this; droneModel.getProcessed({user: userid, name: appid}, function(err, result) { if (err) return haibu.sendResponse(self.res, 500, err); if (result.length == 1) { if (result[0].started) return haibu.sendResponse(self.res, 500, {message: 'Drone is already started'}); else startDrone(result[0], result[0].user, result[0].name, function(err, response) { if (err) return haibu.sendResponse(self.res, 500, err); result[0].started = true; haibu.sendResponse(self.res, 200, result[0]); }); }else{ haibu.sendResponse(self.res, 500, {message: 'No drone matching ' + userid + '/' + appid}); } }); }); //stop drone this.post('/:userid/:appid/stop', function(userid, appid) { var self = this; stopDrone(userid, appid, function(err, result) { if (err) return haibu.sendResponse(self.res, 500, err); haibu.sendResponse(self.res, 200, result); }); }); //restart drone this.post('/:userid/:appid/restart', function(userid, appid) { var self = this; stopDrone(userid, appid, function(err, result) { if (err) return haibu.sendResponse(self.res, 500, err); startDrone(result, userid, appid, function(err, result) { if (err) return haibu.sendResponse(self.res, 500, err); haibu.sendResponse(self.res, 200, result); }); }); }); //deploy app this.post('/:userid/:appid/deploy', {stream: true}, function(userid, appid) { var res = this.res, req = this.req; //clean up app drone.clean({user: userid, name: appid}, function() { //deploy drone.deployOnly(userid, appid, req, function(err, pkg) { if (err) { haibu.sendResponse(res, 500, err); }else{ var pkg_keys = Object.keys(pkg), errors = []; //validate app package if (pkg_keys.length == 0) { errors.push('package.json is empty'); }else{ //check for app name if (!pkg.name || (pkg.name && pkg.name.trim() == '')) errors.push('`name` is required'); //check for start script if (!pkg.scripts) { errors.push('`scripts` is required'); }else{ if (!pkg.scripts.start || (pkg.scripts.start && pkg.scripts.start.trim() == '')) errors.push('`scripts.start` is required'); } //check for engine if (!pkg.engines) { errors.push('`engines` is required'); }else{ if (!pkg.engines.node || (pkg.engines.node && pkg.engines.node.trim() == '')) errors.push('`engines.node` is required'); } //check for domains var domains = [], in_use = []; ['domain', 'domains', 'subdomain', 'subdomains'].forEach(function(key) { if (pkg[key]) { if (typeof pkg[key] == 'string') pkg[key] = pkg[key].split(' '); pkg[key].forEach(function(d) { //use proxy to check whether domain is already in use if (d && proxy.proxies[app.config.get('public-port')] && proxy.proxies[app.config.get('public-port')].routes[d]) in_use.push(d); else if (d) domains.push(d); }); } }); if (domains.length == 0) errors.push('at least one of `domain`, `domains`, `subdomain` or `subdomains` is required'); if (in_use.length > 0) errors.push('the following domains are already in use: ' + in_use.join(', ')); //check env isn't wrongly set if (pkg.env) { if (typeof pkg.env != 'object') errors.push('if specified, `env` has to be an object'); } } //return errors if there are any if (errors.length > 0) { var message = 'There are issues with your package.json file:\n' + errors.join('\n'); return haibu.sendResponse(res, 400, {message: message}); } var node_version = new nodev.Nodev({ install_dir: app.config.get('haibu:directories:node-installs'), tmp_dir: app.config.get('haibu:directories:tmp') }); //check for node version if (pkg.engines && pkg.engines.node) { node_version.checkInstall(pkg.engines.node, function(err, version_match) { if (err) return haibu.sendResponse(res, 500, {message: err.message}); startDrone(pkg, userid, appid, function(err, result) { if (err) return haibu.sendResponse(res, 500, err); haibu.sendResponse(res, 200, result); }); }); } } }); }); }); //return logs for given user/app this.get('/:userid/:appid/logs', function(userid, appid) { var req = this.req, res = this.res, filter = {}, options = {limit: 10, sort: {$natural: -1}}, stream = false; filter.user = userid; filter.app = appid; if (this.req.body) { if (this.req.body.type) filter.type = this.req.body.type; if (this.req.body.limit) options.limit = this.req.body.limit; if (this.req.body.stream) stream = true; } logModel.get(filter, options, function(err, result) { if (!stream) { if (err) return haibu.sendResponse(res, 500, err); haibu.sendResponse(res, 200, result); }else{ res.writeHead(200, {'Content-type': 'plain/text'}); result.on('data', function(item) { var output = '[' + item.ts + '] '; output += '[' + item.type + '] '; output += item.msg; res.write(output); }); //need to find a solution to close stream when req cancelled } }, stream); }); }); };