diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c27fba..fd8bfaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ -# Unreleased +# 0.1.0 -- adds caching to static-server +- adds api and cli action to be able retrieve logs +- deals with cleaning up old images +- deletes image and container when application is being redeployed +- further consolidates deployment logic into the deployment model +- starts up containers from cold start +- shuts down containers when process is closing +- adds caching to static-server - abstract models into their own files and their own collections - fixes the middleware request logger - fixes CLI responses diff --git a/TODO.md b/TODO.md index fc98d28..ca07da7 100644 --- a/TODO.md +++ b/TODO.md @@ -1,8 +1,10 @@ - [x] be able to persist data - [x] Once there is a way to store metadata, have an in memory store of a proxy routing to deal with subrouting application. -- [ ] be able retrieve logs +- [x] be able retrieve logs - [ ] add timing metrics to cli calls (maybe add just overall function call tracing) - [ ] add lamba functionality - [ ] add web gui -- [ ] deal with shutting down and cleaning up old images -- [ ] store most recent tars and metadata somewhere to make sure when the service starts back up it will start those sub services also +- [x] deal with shutting down +- [x] deal with cleaning up old images +- [x] store most recent tars and metadata somewhere to make sure when the service starts back up it will start those sub services also +- [ ] have a pull command that retrieves the contents of a deployed instance diff --git a/bin/deploy-logs.js b/bin/deploy-logs.js new file mode 100644 index 0000000..ee0f1ac --- /dev/null +++ b/bin/deploy-logs.js @@ -0,0 +1,48 @@ +#!/usr/bin/env node + +const Async = require('async'); +const ora = require('ora'); + +const program = require('commander'); +program + .option('-u, --url [url]', 'The endpoint of the deploy.sh server', 'http://localhost:5000') + .parse(process.argv); + +const name = program.args[0]; +const { getLogs, getCredentials } = require('../lib/helpers/cli')(program.url); + +const spinner = ora(`Opening up url to deployment instance`).start(); + +Async.waterfall([ + function(callback) { + spinner.text = 'Getting deploy keys'; + + getCredentials() + .then((credentials) => callback(null, credentials)) + .catch((ex) => callback(ex, null)); + }, + function(credentials, callback) { + spinner.text = 'Calling log API'; + + const { token, username } = credentials; + + getLogs({ token, username, name }) + .then((response) => callback(null, response)) + .catch((error) => callback(error, null)); + } +], (ex, result) => { + if (ex) return spinner.fail(`API call failed 🙈 ${JSON.stringify({ + ex + }, null, 4)}`); + + spinner.stop(); + const { logs } = result; + + console.log( // eslint-disable-line + logs.map((l) => { + let log = l.split(' '); + log.unshift('-'); + return log.join(' '); + }).join('') + ); +}); diff --git a/bin/deploy.js b/bin/deploy.js index 5984aab..9563675 100755 --- a/bin/deploy.js +++ b/bin/deploy.js @@ -16,5 +16,6 @@ program .command('login', 'login to access deploy and deployment functionality') .command('logout', 'logout and invalidate token') .command('open [project]', 'open the deployment instance in the browser') + .command('logs [project]', 'shows the logs for the specificed project') .command('server', 'starts a server instance locally') .parse(process.argv); diff --git a/docs/code/classifier.js.html b/docs/code/classifier.js.html index c312105..22e30f2 100644 --- a/docs/code/classifier.js.html +++ b/docs/code/classifier.js.html @@ -24,7 +24,7 @@
@@ -107,7 +107,7 @@

classifier.js


diff --git a/docs/code/deploy.js.html b/docs/code/deploy.js.html index 263bb0d..8a4b54a 100644 --- a/docs/code/deploy.js.html +++ b/docs/code/deploy.js.html @@ -24,7 +24,7 @@
@@ -40,7 +40,6 @@

deploy.js

const Async = require('async');
-const Docker = require('dockerode');
 const tar = require('tar');
 const path = require('path');
 const fs = require('fs');
@@ -49,122 +48,73 @@ 

deploy.js

const classifer = require('./classifier'); const { getPort } = require('./helpers/util'); +const { get, build, remove } = require('./models/deployment'); /** * handles the deployment of an application tar * @module lib/deploy - * @param {String} subdomain - the unique identifier of the application that is to be run - * @param {String} bundlePath - the directory of which the tar of the application is located + * @param {Object} option + * @param {String} option.name - the name of the the deployment + * @param {String} option.bundlePath - the directory of which the tar of the application is located + * @param {String} option.token - token associated with the user that want to deploy the application + * @param {String} option.username - username of the user the token is associated too */ -module.exports = function deploy(subdomain, bundlePath) { - const outputDir = path.resolve(__dirname, '..', 'tmp', subdomain); - mkdirp.sync(outputDir); - +module.exports = function deploy({ name, bundlePath, token, username }) { return new Promise(function(resolve, reject) { - tar.x({ - file: bundlePath, - cwd: outputDir - }).then(() => { - const docker = new Docker({ - socketPath: '/var/run/docker.sock' - }); - - Async.waterfall([ - (callback) => { - let found = false; - docker.listContainers({ - all: 1 - }, (err, containers) => { - if(err) return callback(err, null); - containers.forEach((container) => { - if(container.Image == subdomain) { - found = true; - const old = docker.getContainer(container.Id); - old.stop(() => { - // ignore any errors with it being stopped already - old.remove(() => { - callback(); - }); - }); + get({ username, token, name, create: true }) + .then((deployment) => { + const outputDir = path.resolve(__dirname, '..', 'tmp', deployment.subdomain); + mkdirp.sync(outputDir); + + tar.x({ + file: bundlePath, + cwd: outputDir + }).then(() => { + Async.waterfall([ + (callback) => { + remove({ + token, + username, + name: deployment.subdomain + }) + .then(() => callback()) + .catch((ex) => callback(ex)); + }, + (callback) => { + const config = classifer(outputDir); + if (config.type === 'unknown') { + callback('deployment not supported', null); + } + if (config.type === 'static') { + fs.writeFileSync(path.resolve(outputDir, 'index.js'), fs.readFileSync(path.resolve(__dirname, 'helpers', 'static-server.js'))); + } + callback(null, config.build); + }, + (config, callback) => { + fs.writeFile(path.resolve(outputDir, 'Dockerfile'), config, callback); + }, + (callback) => { + getPort(callback); + }, + (port, callback) => { + build({ + name, + token, + username, + subdomain: deployment.subdomain, + port, + directory: outputDir + }) + .then((deployment) => callback(null, deployment)) + .catch((ex) => callback(ex)); } + ], (err, result) => { + if (err) return reject(err); + resolve(result); }); - if(!found) return callback(); - }); - }, - (callback) => { - const config = classifer(outputDir); - if (config.type === 'unknown') { - callback('deployment not supported', null); - } - if (config.type === 'static') { - fs.writeFileSync(path.resolve(outputDir, 'index.js'), fs.readFileSync(path.resolve(__dirname, 'helpers', 'static-server.js'))); - } - callback(null, config.build); - }, - (config, callback) => { - fs.writeFile(path.resolve(outputDir, 'Dockerfile'), config, callback); - }, - (callback) => { - docker.buildImage({ - context: outputDir, - src: fs.readdirSync(outputDir) - }, { - t: subdomain - }, (err, stream) => { - if(err) return callback(err); - docker.modem.followProgress(stream, onFinished, onProgress); - - function onFinished(err) { - if(err) return callback(err); - callback(); - } - // TODO: be able to stream the output of this to a socket to give real time updates - // onProgress(ev) - function onProgress() { - // console.log(ev); - } - }); - }, - (callback) => { - getPort(callback); - }, - (port, callback) => { - // TODO: before create a container, check for existing one to remove - docker.createContainer({ - Image: subdomain, - name: subdomain, - env: [ - 'PORT=3000' - ], - ExposedPorts: { - '3000/tcp': {} - }, - PortBindings: { - '3000/tcp': [{ - 'HostPort': `${port}` - }] - }, - Privileged: true - }, (err, container) => { - if (err) return callback(err); - container.start((err) => { - if (err) return callback(err); - return callback(null, { - port, - id: container.id, - subdomain, - directory: outputDir - }); - }); - }); - } - ], (err, result) => { - if (err) return reject(err); - resolve(result); + }) + .catch((ex) => reject(ex)); }); - }).catch((error) => { - reject(error); - }); }); };
@@ -179,7 +129,7 @@

deploy.js


- Generated by JSDoc 3.5.4 on Fri Aug 11 2017 00:05:36 GMT-0700 (PDT) using the Minami theme. + Generated by JSDoc 3.5.4 on Sun Aug 13 2017 16:16:41 GMT-0700 (PDT) using the Minami theme.
diff --git a/docs/code/helpers_cli.js.html b/docs/code/helpers_cli.js.html index e437ece..416c7d0 100644 --- a/docs/code/helpers_cli.js.html +++ b/docs/code/helpers_cli.js.html @@ -24,7 +24,7 @@
@@ -182,7 +182,35 @@

helpers/cli.js

'x-deploy-username': username } }, function optionalCallback(err, httpResponse, body) { - if (err) return reject('error trying to logout') + if (err) return reject('error trying to logout'); + const response = JSON.parse(body); + if(response.error) return reject(response.error); + return resolve(response); + }); + }); + }; + + /** + * gets the application logs + * @method getLogs + * @param {Object} options + * @param {String} options.token - token to make authenticated calls + * @param {String} options.username - username linked to the token + * @param {String} options.name - name of the deployment + * @return {Promise} + */ + methods.getLogs = function getLogs({ token, username, name }) { + const request = require('request'); + + return new Promise(function(resolve, reject) { + request.get({ + url: `${url}/api/deployments/${name}/logs`, + headers: { + 'x-deploy-token': token, + 'x-deploy-username': username + } + }, function optionalCallback(err, httpResponse, body) { + if (err) return reject(err); const response = JSON.parse(body); if(response.error) return reject(response.error); return resolve(response); @@ -314,7 +342,7 @@

helpers/cli.js


- Generated by JSDoc 3.5.4 on Fri Aug 11 2017 00:05:36 GMT-0700 (PDT) using the Minami theme. + Generated by JSDoc 3.5.4 on Sun Aug 13 2017 16:16:41 GMT-0700 (PDT) using the Minami theme.
diff --git a/docs/code/helpers_util.js.html b/docs/code/helpers_util.js.html index 11e464a..a460a0c 100644 --- a/docs/code/helpers_util.js.html +++ b/docs/code/helpers_util.js.html @@ -24,7 +24,7 @@
@@ -118,7 +118,7 @@

helpers/util.js


- Generated by JSDoc 3.5.4 on Fri Aug 11 2017 00:05:36 GMT-0700 (PDT) using the Minami theme. + Generated by JSDoc 3.5.4 on Sun Aug 13 2017 16:16:41 GMT-0700 (PDT) using the Minami theme.
diff --git a/docs/code/index.html b/docs/code/index.html index c2183d7..4933a45 100644 --- a/docs/code/index.html +++ b/docs/code/index.html @@ -24,7 +24,7 @@
@@ -72,7 +72,7 @@
- Generated by JSDoc 3.5.4 on Fri Aug 11 2017 00:05:36 GMT-0700 (PDT) using the Minami theme. + Generated by JSDoc 3.5.4 on Sun Aug 13 2017 16:16:41 GMT-0700 (PDT) using the Minami theme.
diff --git a/docs/code/models_deployment.js.html b/docs/code/models_deployment.js.html index b4ae631..6d61395 100644 --- a/docs/code/models_deployment.js.html +++ b/docs/code/models_deployment.js.html @@ -24,7 +24,7 @@
@@ -43,7 +43,15 @@

models/deployment.js

* @module models/deployment */ +const Async = require('async'); const mongoose = require('mongoose'); +const http = require('http'); +const fs = require('fs'); +const Docker = require('dockerode'); +const docker = new Docker({ + socketPath: '/var/run/docker.sock' +}); +const stream = require('stream'); const Schema = mongoose.Schema; @@ -65,6 +73,8 @@

models/deployment.js

} }); +const Deployment = mongoose.model('Deployment', DeploymentSchema); + /** * Deployment definition * @class {Object} Deployment @@ -75,11 +85,11 @@

models/deployment.js

* @property {String} directory - the directory of tared application * @property {String} username - the username who owns the deployment */ -const Deployment = mongoose.model('Deployment', DeploymentSchema); +module.exports.Deployment = Deployment; /** - * [updateDeployments description] - * @method updateDeployments + * updates a deployment + * @method update * @param {Object} options * @param {String} options.name - the name of the deployment * @param {String} options.token - the token for the user who owns the deployment @@ -87,114 +97,431 @@

models/deployment.js

* @param {Deployment} options.deployment - a deployment model that contains the updates that will be applied * @return {Promise} */ -module.exports.updateDeployments = function updateDeployments({ name, token, username, deployment }) { +var update; +module.exports.update = update = function update({ name, token, username, deployment }) { return new Promise(function(resolve, reject) { User.get({ username, token }) - .then((user) => { - if(!user) return reject('not authenticated'); - Deployment.findOne({ - username, - name - }, (err, found) => { - if(found) { - found.id = deployment.id || found.id; - found.name = deployment.name || found.name; - found.port = deployment.port || found.port; - found.subdomain = deployment.subdomain || found.subdomain; - found.directory = deployment.directory || found.directory; - found.save((err) => { - if(err) return reject(err); - resolve(found); - }); - } else { - const deployment = { - name, - subdomain: `${name}-${hash(6)}`, - username - }; - Deployment.create(deployment, (err, deployment) => { - if(err) return reject(err); - return resolve(deployment); - }); - } - }); - }) - .catch((ex) => reject(ex)); + .then((user) => { + if(!user) return reject('not authenticated'); + Deployment.findOne({ + username, + name + }, (err, found) => { + if(found) { + found.id = deployment.id || found.id; + found.name = deployment.name || found.name; + found.port = deployment.port || found.port; + found.subdomain = deployment.subdomain || found.subdomain; + found.directory = deployment.directory || found.directory; + found.save((err) => { + if(err) return reject(err); + resolve(found); + }); + } else { + const deployment = { + name, + subdomain: `${name}-${hash(6)}`, + username + }; + Deployment.create(deployment, (err, deployment) => { + if(err) return reject(err); + return resolve(deployment); + }); + } + }); + }) + .catch((ex) => reject(ex)); }); }; /** - * gets the port for the requested subdomain - * @method getProxy + * express middleware to proxy to correct container + * @method proxy * @param {String} subdomain - the subdomain for the application being requested * @return {Promise} */ -module.exports.getProxy = function getProxy({ subdomain }) { - return new Promise(function(resolve, reject) { - Deployment.findOne({ - "subdomain":{ - "$eq": subdomain - } - }, (err, deployment) => { - if(err || !deployment) return reject(err); - return resolve(deployment.port); +module.exports.proxy = function proxy(req, res) { + const { url, method, headers } = req; + const { host } = headers; + + // If this is not an upload request, it is a proxy request + const subdomain = host.split('.')[0]; + + Deployment.findOne({ + "subdomain":{ + "$eq": subdomain + } + }, (err, deployment) => { + if(err || !deployment) { + return res.status(404).end(` + <!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + <style media="screen"> + html, body { + height: 100%; + width: 100%; + overflow: hidden; + } + .message { + text-align: center; + top: 50%; + width: 100%; + position: absolute; + } + h3 { + display: inline-block; + border-right: 1px solid #a2a2a2; + padding-right: 10px; + } + </style> + <title>Error</title> + </head> + <body> + <div class="message"> + <h3>404</h3> <span> Sorry this page could not be found 🙈 </span> + </div> + </body> + </html> + `); + } + const { port } = deployment; + const proxy = http.request({ + method, + path: url, + headers, + port, + host: 'localhost' + }); + proxy.addListener('response', function (proxy_response) { + proxy_response.addListener('data', function(chunk) { + res.write(chunk, 'binary'); + }); + proxy_response.addListener('end', function() { + res.end(); + }); + res.writeHead(proxy_response.statusCode, proxy_response.headers); + }); + proxy.on('error', function() { + res.status(502).end(` + <!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + <style media="screen"> + html, body { + height: 100%; + width: 100%; + overflow: hidden; + } + .message { + text-align: center; + top: 50%; + width: 100%; + position: absolute; + } + h3 { + display: inline-block; + border-right: 1px solid #a2a2a2; + padding-right: 10px; + } + </style> + <title>Error</title> + </head> + <body> + <div class="message"> + <h3>502</h3> <span> Sorry this page could not be loaded 🙈 </span> + </div> + </body> + </html> + `); + }); + req.addListener('data', function(chunk) { + proxy.write(chunk, 'binary'); + }); + req.addListener('end', function() { + proxy.end(); }); }); }; /** * gets a specific deployment if name is passed or all deployments for the specified user - * @method getDeployments + * @method get * @param {Object} options * @param {String=} options.name - the name of the deployment * @param {String} options.token - the token for the user who owns the deployment * @param {String} options.username - the username associated with this deployment + * @param {Boolean} option.create - create a deployment if not found * @return {Promise} */ -module.exports.getDeployments = function getDeployments({ name, token, username }) { +module.exports.get = function get({ name, token, username, create }) { + // TODO: query the container to get the status return new Promise(function(resolve, reject) { User.get({ username, token }) - .then((user) => { - if(!user) return reject('not authenticated'); + .then((user) => { + if(!user) return reject('not authenticated'); - if(!name) { - Deployment.find({ - username - }, (err, deployments) => { - if(err) return reject(err); + if(!name) { + Deployment.find({ + username + }, (err, deployments) => { + if(err) return reject(err); - resolve(deployments); + resolve(deployments); + }); + } else { + Deployment.findOne({ + username, + name + }, (err, deployment) => { + if(deployment) { + return resolve(deployment); + } + if(create) { + // add a new deployment name + // TODO: figure out how to avoid collisions + const deployment = { + name, + subdomain: `${name}-${hash(6)}`, + username + }; + Deployment.create(deployment, (err) => { + if(err) return reject('issue updating deployment'); + resolve(deployment); + }); + } + }); + } + }) + .catch((ex) => reject(ex)); + }); +}; + +module.exports.build = function build({ name, subdomain, username, token, port, directory }) { + return new Promise(function(resolve, reject) { + docker.buildImage({ + context: directory, + src: fs.readdirSync(directory) + }, { + t: subdomain + }, (err, stream) => { + if(err) return reject(err); + docker.modem.followProgress(stream, onFinished, onProgress); + + function onFinished(err) { + if(err) return reject(err); + docker.createContainer({ + Image: subdomain, + name: subdomain, + env: [ + 'PORT=3000' + ], + ExposedPorts: { + '3000/tcp': {} + }, + PortBindings: { + '3000/tcp': [{ + 'HostPort': `${port}` + }] + }, + Privileged: true + }, (err, container) => { + if (err) return reject(err); + container.start((err) => { + if (err) return reject(err); + const id = container.id; + + update({ + name, + username, + token, + deployment: { + id, + port, + directory + } + }) + .then((deployment) => resolve(deployment)) + .catch((ex) => reject(ex)); }); - } else { - Deployment.findOne({ - username, - name - }, (err, deployment) => { - if(deployment) { - return resolve(deployment); - } else { - // add a new deployment name - // TODO: figure out how to avoid collisions - const deployment = { - name, - subdomain: `${name}-${hash(6)}`, - username - }; - Deployment.create(deployment, (err) => { - if(err) return reject('issue updating deployment'); - resolve(deployment); - }); + }); + } + // TODO: be able to stream the output of this to a socket to give real time updates + function onProgress() { + // console.log(ev); + } + }); + }); +}; + +/** + * starts a container or all containers + * @method start + * @param {String=} name - to start a specific container a name property is needed + * @param {String} options.token - the token for the user who owns the deployment + * @param {String} options.username - the username associated with this deployment + * @return {Promise} + */ +module.exports.start = function start({ name, username, token }) { + return new Promise(function(resolve, reject) { + let opts = {}; + // TODO: check username and token to make sure the request is authenticated + if(username && token) { + opts.username = username; + } + + Deployment.find(opts, (err, deployments) => { + Async.each(deployments, (deployment, callback) => { + if(name && deployment.name == name) { + const container = docker.getContainer(deployment.id); + container.start(callback); + } + + if(!name){ + const container = docker.getContainer(deployment.id); + container.start(callback); + } + }, (err) => { + if(err) return reject(err); + return resolve(deployments); + }); + }); + }); +}; + +/** + * stops a container or all containers + * @method stop + * @param {String=} name - to stop a specific container a name property is needed + * @param {String=} options.token - the token for the user who owns the deployment + * @param {String=} options.username - the username associated with this deployment + * @return {Promise} + */ +module.exports.stop = function stop({ name, token, username }) { + return new Promise(function(resolve, reject) { + let opts = {}; + // TODO: check username and token to make sure the request is authenticated + if(username && token) { + opts.username = username; + } + + Deployment.find(opts, (err, deployments) => { + Async.each(deployments, (deployment, callback) => { + if(name && deployment.name == name) { + const container = docker.getContainer(deployment.id); + container.stop({ force: true }, callback); + } + + if(!name){ + const container = docker.getContainer(deployment.id); + container.stop({ force: true }, callback); + } + }, (err) => { + if(err) return reject(err); + return resolve(deployments); + }); + }); + }); +}; + +module.exports.logs = function logs({ name, token, username }) { + return new Promise(function(resolve, reject) { + User.get({ + username, + token + }) + .then((user) => { + Deployment.findOne({ + name, + username: user.username + }, (err, deployment) => { + if(err) return reject(err); + + const logs = []; + const container = docker.getContainer(deployment.id); + + const logStream = new stream.PassThrough(); + logStream.on('data', function(chunk){ + logs.push(chunk.toString('utf8')); + }); + + container.logs({ + follow: true, + stdout: true, + stderr: true + }, (err, stream) => { + if(err) { + return reject(err); } + container.modem.demuxStream(stream, logStream, logStream); + stream.on('end', function(){ + return resolve(logs); + }); + + setTimeout(function() { + stream.destroy(); + }, 2000); }); - } - }) - .catch((ex) => reject(ex)); + }); + }) + .catch(() => reject('token not valid')); + }); +}; + +/** + * removes a specific container, will stop and cleanup all necessary files + * @method remove + * @param {String} name - the name of the container + * @param {String} options.token - the token for the user who owns the deployment + * @param {String} options.username - the username associated with this deployment + * @return {Promise} + */ +module.exports.remove = function remove({ name, token, username }) { + return new Promise(function(resolve, reject) { + User.get({ + username, + token + }) + .then((user) => { + Deployment.findOne({ + subdomain: name, + username: user.username + }, (err, deployment) => { + if(err) return reject(err); + + const container = docker.getContainer(deployment.id); + + Async.waterfall([ + function(callback) { + container.stop(() => callback()); + }, + function(callback) { + container.inspect((err, info) => { + if(err) return callback(); + + docker.getImage(info.Image).remove({ + force: true + }, () => callback()); + }); + }, + function(callback) { + container.remove(() => callback()); + } + ], (err) => { + if(err) return reject(err); + return resolve(); + }); + }); + }) + .catch(() => reject('user does not have access to this deployment')); }); }; @@ -209,7 +536,7 @@

models/deployment.js


- Generated by JSDoc 3.5.4 on Fri Aug 11 2017 00:05:36 GMT-0700 (PDT) using the Minami theme. + Generated by JSDoc 3.5.4 on Sun Aug 13 2017 16:16:41 GMT-0700 (PDT) using the Minami theme.
diff --git a/docs/code/models_request.js.html b/docs/code/models_request.js.html index f9865ff..40591cf 100644 --- a/docs/code/models_request.js.html +++ b/docs/code/models_request.js.html @@ -24,7 +24,7 @@
@@ -116,7 +116,7 @@

models/request.js


- Generated by JSDoc 3.5.4 on Fri Aug 11 2017 00:05:36 GMT-0700 (PDT) using the Minami theme. + Generated by JSDoc 3.5.4 on Sun Aug 13 2017 16:16:41 GMT-0700 (PDT) using the Minami theme.
diff --git a/docs/code/models_user.js.html b/docs/code/models_user.js.html index 02ce0cf..b384946 100644 --- a/docs/code/models_user.js.html +++ b/docs/code/models_user.js.html @@ -24,7 +24,7 @@
@@ -61,6 +61,8 @@

models/user.js

} }); +const User = mongoose.model('User', UserSchema); + /** * User definition * @class {Object} User @@ -68,7 +70,7 @@

models/user.js

* @property {String} password - a password for the user * @property {String=} token - an access token */ -const User = mongoose.model('User', UserSchema); +module.exports.User = User; /** * middleware to verify the username and token are valid, will then set the user to req.user @@ -204,7 +206,7 @@

models/user.js


- Generated by JSDoc 3.5.4 on Fri Aug 11 2017 00:05:36 GMT-0700 (PDT) using the Minami theme. + Generated by JSDoc 3.5.4 on Sun Aug 13 2017 16:16:41 GMT-0700 (PDT) using the Minami theme.
diff --git a/docs/code/module-lib_classifier.html b/docs/code/module-lib_classifier.html index b843a59..e99a988 100644 --- a/docs/code/module-lib_classifier.html +++ b/docs/code/module-lib_classifier.html @@ -24,7 +24,7 @@
@@ -237,7 +237,7 @@
Returns:

- Generated by JSDoc 3.5.4 on Fri Aug 11 2017 00:05:36 GMT-0700 (PDT) using the Minami theme. + Generated by JSDoc 3.5.4 on Sun Aug 13 2017 16:16:41 GMT-0700 (PDT) using the Minami theme.
diff --git a/docs/code/module-lib_deploy.html b/docs/code/module-lib_deploy.html index 99e702d..d5825e6 100644 --- a/docs/code/module-lib_deploy.html +++ b/docs/code/module-lib_deploy.html @@ -24,7 +24,7 @@
@@ -137,7 +137,50 @@
Parameters:
- subdomain + option + + + + + +Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -186,6 +229,66 @@
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
name @@ -154,7 +197,7 @@
Parameters:
-

the unique identifier of the application that is to be run

+

the name of the the deployment

token + + +String + + + + +

token associated with the user that want to deploy the application

+ +
username + + +String + + + + +

username of the user the token is associated too

+ +
+ + + + + + @@ -238,7 +341,7 @@
Parameters:

- Generated by JSDoc 3.5.4 on Fri Aug 11 2017 00:05:36 GMT-0700 (PDT) using the Minami theme. + Generated by JSDoc 3.5.4 on Sun Aug 13 2017 16:16:41 GMT-0700 (PDT) using the Minami theme.
diff --git a/docs/code/module-lib_helpers_cli.html b/docs/code/module-lib_helpers_cli.html index 3631575..9d1097f 100644 --- a/docs/code/module-lib_helpers_cli.html +++ b/docs/code/module-lib_helpers_cli.html @@ -24,7 +24,7 @@
@@ -118,7 +118,7 @@

(inner) Source:
@@ -507,7 +507,7 @@

(inner) Source:
@@ -611,7 +611,7 @@

(inner) Source:
@@ -825,6 +825,265 @@
Parameters:
+
+
Returns:
+ + + +
+
+ Type: +
+
+ +Promise + + +
+
+ + + +
+ + + +

+ + +
+ + + +

(inner) getLogs(options) → {Promise}

+ + + + + +
+

gets the application logs

+
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
options + + +Object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
token + + +String + + + + +

token to make authenticated calls

+ +
username + + +String + + + + +

username linked to the token

+ +
name + + +String + + + + +

name of the deployment

+ +
+ + +
+ + + + + + + + + + + + + +
Returns:
@@ -1799,7 +2058,7 @@
Returns:

- Generated by JSDoc 3.5.4 on Fri Aug 11 2017 00:05:36 GMT-0700 (PDT) using the Minami theme. + Generated by JSDoc 3.5.4 on Sun Aug 13 2017 16:16:41 GMT-0700 (PDT) using the Minami theme.
diff --git a/docs/code/module-lib_helpers_util.html b/docs/code/module-lib_helpers_util.html index 32127e8..ca0539d 100644 --- a/docs/code/module-lib_helpers_util.html +++ b/docs/code/module-lib_helpers_util.html @@ -24,7 +24,7 @@
@@ -599,7 +599,7 @@
Returns:

- Generated by JSDoc 3.5.4 on Fri Aug 11 2017 00:05:36 GMT-0700 (PDT) using the Minami theme. + Generated by JSDoc 3.5.4 on Sun Aug 13 2017 16:16:41 GMT-0700 (PDT) using the Minami theme.
diff --git a/docs/code/module-models_deployment.Deployment.html b/docs/code/module-models_deployment.Deployment.html new file mode 100644 index 0000000..88b7992 --- /dev/null +++ b/docs/code/module-models_deployment.Deployment.html @@ -0,0 +1,359 @@ + + + + + + Deployment - Documentation + + + + + + + + + + + + + + + + + +
+ +

Deployment

+ + + + + + + +
+ +
+ +

+ models/deployment. + + Deployment +

+ +

{Object} Deployment

+ + +
+ +
+
+ + +
+ + +

Constructor

+ + +

new Deployment()

+ + + + + +
+

Deployment definition

+
+ + + + + +
Properties:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
id + + +String + + + +

the container id

name + + +String + + + +

the name of the deployment

port + + +Number + + + +

the port that the container has exposed

subdomain + + +String + + + +

the subdomain of the application

directory + + +String + + + +

the directory of tared application

username + + +String + + + +

the username who owns the deployment

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ +
+ +
+ Generated by JSDoc 3.5.4 on Sun Aug 13 2017 16:16:41 GMT-0700 (PDT) using the Minami theme. +
+ + + + + \ No newline at end of file diff --git a/docs/code/module-models_deployment.html b/docs/code/module-models_deployment.html index 838626a..1f4ca46 100644 --- a/docs/code/module-models_deployment.html +++ b/docs/code/module-models_deployment.html @@ -24,7 +24,7 @@
@@ -62,7 +62,7 @@

models/deployment

Classes

-
Deployment
+
Deployment
@@ -82,7 +82,7 @@

Methods

-

(inner) getDeployments(options) → {Promise}

+

(inner) get(options) → {Promise}

@@ -125,7 +125,7 @@

(inner) Source:
@@ -323,6 +323,32 @@
Parameters:
+ + + + option.create + + + + + +Boolean + + + + + + + + + + +

create a deployment if not found

+ + + + + @@ -369,14 +395,14 @@
Returns:
-

(inner) getProxy(subdomain) → {Promise}

+

(inner) proxy(subdomain) → {Promise}

-

gets the port for the requested subdomain

+

express middleware to proxy to correct container

@@ -412,7 +438,7 @@

(inner) getP
Source:
@@ -525,14 +551,698 @@

Returns:
-

(inner) updateDeployments(options) → {Promise}

+

(inner) remove(name) → {Promise}

+ + + + + +
+

removes a specific container, will stop and cleanup all necessary files

+
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
name + + +String + + + + +

the name of the container

+ +
options.token + + +String + + + + +

the token for the user who owns the deployment

+ +
options.username + + +String + + + + +

the username associated with this deployment

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Promise + + +
+
+ + + +
+ + + +

+ + +
+ + + +

(inner) start(nameopt) → {Promise}

+ + + + + +
+

starts a container or all containers

+
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
name + + +String + + + + + + <optional>
+ + + + + +
+

to start a specific container a name property is needed

+ +
options.token + + +String + + + + + + + + + + +

the token for the user who owns the deployment

+ +
options.username + + +String + + + + + + + + + + +

the username associated with this deployment

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Promise + + +
+
+ + + +
+ + + +
+ + +
+ + + +

(inner) stop(nameopt) → {Promise}

+ + + + + +
+

stops a container or all containers

+
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
name + + +String + + + + + + <optional>
+ + + + + +
+

to stop a specific container a name property is needed

+ +
options.token + + +String + + + + + + <optional>
+ + + + + +
+

the token for the user who owns the deployment

+ +
options.username + + +String + + + + + + <optional>
+ + + + + +
+

the username associated with this deployment

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Promise + + +
+
+ + + +
+ + + +
+ + +
+ + + +

(inner) update(options) → {Promise}

-

[updateDeployments description]

+

updates a deployment

@@ -568,7 +1278,7 @@

(inner) Source:
@@ -822,7 +1532,7 @@
Returns:

- Generated by JSDoc 3.5.4 on Fri Aug 11 2017 00:05:36 GMT-0700 (PDT) using the Minami theme. + Generated by JSDoc 3.5.4 on Sun Aug 13 2017 16:16:41 GMT-0700 (PDT) using the Minami theme.
diff --git a/docs/code/module-models_request-Request.html b/docs/code/module-models_request-Request.html index 98a1f06..491f657 100644 --- a/docs/code/module-models_request-Request.html +++ b/docs/code/module-models_request-Request.html @@ -24,7 +24,7 @@
@@ -350,7 +350,7 @@
Properties:

- Generated by JSDoc 3.5.4 on Fri Aug 11 2017 00:05:36 GMT-0700 (PDT) using the Minami theme. + Generated by JSDoc 3.5.4 on Sun Aug 13 2017 16:16:41 GMT-0700 (PDT) using the Minami theme.
diff --git a/docs/code/module-models_request.html b/docs/code/module-models_request.html index 1db1e13..56ad1e1 100644 --- a/docs/code/module-models_request.html +++ b/docs/code/module-models_request.html @@ -24,7 +24,7 @@
@@ -281,7 +281,7 @@
Parameters:

- Generated by JSDoc 3.5.4 on Fri Aug 11 2017 00:05:36 GMT-0700 (PDT) using the Minami theme. + Generated by JSDoc 3.5.4 on Sun Aug 13 2017 16:16:41 GMT-0700 (PDT) using the Minami theme.
diff --git a/docs/code/module-models_user.User.html b/docs/code/module-models_user.User.html new file mode 100644 index 0000000..699a4e8 --- /dev/null +++ b/docs/code/module-models_user.User.html @@ -0,0 +1,306 @@ + + + + + + User - Documentation + + + + + + + + + + + + + + + + + +
+ +

User

+ + + + + + + +
+ +
+ +

+ models/user. + + User +

+ +

{Object} User

+ + +
+ +
+
+ + +
+ + +

Constructor

+ + +

new User()

+ + + + + +
+

User definition

+
+ + + + + +
Properties:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
username + + +String + + + + + + + +

a string that defines the user's accounts

password + + +String + + + + + + + +

a password for the user

token + + +String + + + + + + <optional>
+ + + +

an access token

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ +
+ +
+ Generated by JSDoc 3.5.4 on Sun Aug 13 2017 16:16:41 GMT-0700 (PDT) using the Minami theme. +
+ + + + + \ No newline at end of file diff --git a/docs/code/module-models_user.html b/docs/code/module-models_user.html index a744f23..abcf7f0 100644 --- a/docs/code/module-models_user.html +++ b/docs/code/module-models_user.html @@ -24,7 +24,7 @@
@@ -62,7 +62,7 @@

models/user

Classes

-
User
+
User
@@ -125,7 +125,7 @@

(inner)
Source:
@@ -312,7 +312,7 @@

(inner) getSource:
@@ -494,7 +494,7 @@

(inner) loginSource:
@@ -676,7 +676,7 @@

(inner) logout
Source:
@@ -858,7 +858,7 @@

(inner) regi
Source:
@@ -1009,7 +1009,7 @@

Returns:

- Generated by JSDoc 3.5.4 on Fri Aug 11 2017 00:05:36 GMT-0700 (PDT) using the Minami theme. + Generated by JSDoc 3.5.4 on Sun Aug 13 2017 16:16:41 GMT-0700 (PDT) using the Minami theme.
diff --git a/index.js b/index.js new file mode 100644 index 0000000..015d2bb --- /dev/null +++ b/index.js @@ -0,0 +1,58 @@ +const mongoose = require('mongoose'); + +mongoose.connect('mongodb://localhost/deploy-sh'); +mongoose.Promise = global.Promise; + +process.on('unhandledRejection', (err, p) => { + console.log('An unhandledRejection occurred'); // eslint-disable-line + console.log(`Rejected Promise: ${p}`); // eslint-disable-line + console.log(`Rejection: ${err}`); // eslint-disable-line +}); + +const { start, stop } = require('./lib/models/deployment'); + +console.log('starting up deployments'); // eslint-disable-line +start({}) + .then((deployments) => { + console.log( // eslint-disable-line + ` + started ${deployments.length} deployment(s) successfully + ` + ); + require('./lib/server'); + }) + .catch((ex) => { + console.log( // eslint-disable-line + ` + error on startup: + ${ex} + ` + ); + require('./lib/server'); + }); + +function shutdown() { + console.log('shutting down deployments'); // eslint-disable-line + stop({}) + .then((deployments) => { + console.log( // eslint-disable-line + ` + shutdown ${deployments.length} deployment(s) successfully + ` + ); + process.exit(); + }) + .catch((ex) => { + + console.log( // eslint-disable-line + ` + error on shutdown: + ${ex} + ` + ); + process.exit(); + }); +} + +process.on('SIGTERM', shutdown); +process.on('SIGINT', shutdown); diff --git a/lib/deploy.js b/lib/deploy.js index 567c5cd..c9ff5de 100644 --- a/lib/deploy.js +++ b/lib/deploy.js @@ -1,5 +1,4 @@ const Async = require('async'); -const Docker = require('dockerode'); const tar = require('tar'); const path = require('path'); const fs = require('fs'); @@ -8,121 +7,72 @@ const mkdirp = require('mkdirp'); const classifer = require('./classifier'); const { getPort } = require('./helpers/util'); +const { get, build, remove } = require('./models/deployment'); /** * handles the deployment of an application tar * @module lib/deploy - * @param {String} subdomain - the unique identifier of the application that is to be run - * @param {String} bundlePath - the directory of which the tar of the application is located + * @param {Object} option + * @param {String} option.name - the name of the the deployment + * @param {String} option.bundlePath - the directory of which the tar of the application is located + * @param {String} option.token - token associated with the user that want to deploy the application + * @param {String} option.username - username of the user the token is associated too */ -module.exports = function deploy(subdomain, bundlePath) { - const outputDir = path.resolve(__dirname, '..', 'tmp', subdomain); - mkdirp.sync(outputDir); - +module.exports = function deploy({ name, bundlePath, token, username }) { return new Promise(function(resolve, reject) { - tar.x({ - file: bundlePath, - cwd: outputDir - }).then(() => { - const docker = new Docker({ - socketPath: '/var/run/docker.sock' - }); + get({ username, token, name, create: true }) + .then((deployment) => { + const outputDir = path.resolve(__dirname, '..', 'tmp', deployment.subdomain); + mkdirp.sync(outputDir); - Async.waterfall([ - (callback) => { - let found = false; - docker.listContainers({ - all: 1 - }, (err, containers) => { - if(err) return callback(err, null); - containers.forEach((container) => { - if(container.Image == subdomain) { - found = true; - const old = docker.getContainer(container.Id); - old.stop(() => { - // ignore any errors with it being stopped already - old.remove(() => { - callback(); - }); - }); + tar.x({ + file: bundlePath, + cwd: outputDir + }).then(() => { + Async.waterfall([ + (callback) => { + remove({ + token, + username, + name: deployment.subdomain + }) + .then(() => callback()) + .catch((ex) => callback(ex)); + }, + (callback) => { + const config = classifer(outputDir); + if (config.type === 'unknown') { + callback('deployment not supported', null); + } + if (config.type === 'static') { + fs.writeFileSync(path.resolve(outputDir, 'index.js'), fs.readFileSync(path.resolve(__dirname, 'helpers', 'static-server.js'))); + } + callback(null, config.build); + }, + (config, callback) => { + fs.writeFile(path.resolve(outputDir, 'Dockerfile'), config, callback); + }, + (callback) => { + getPort(callback); + }, + (port, callback) => { + build({ + name, + token, + username, + subdomain: deployment.subdomain, + port, + directory: outputDir + }) + .then((deployment) => callback(null, deployment)) + .catch((ex) => callback(ex)); } + ], (err, result) => { + if (err) return reject(err); + resolve(result); }); - if(!found) return callback(); - }); - }, - (callback) => { - const config = classifer(outputDir); - if (config.type === 'unknown') { - callback('deployment not supported', null); - } - if (config.type === 'static') { - fs.writeFileSync(path.resolve(outputDir, 'index.js'), fs.readFileSync(path.resolve(__dirname, 'helpers', 'static-server.js'))); - } - callback(null, config.build); - }, - (config, callback) => { - fs.writeFile(path.resolve(outputDir, 'Dockerfile'), config, callback); - }, - (callback) => { - docker.buildImage({ - context: outputDir, - src: fs.readdirSync(outputDir) - }, { - t: subdomain - }, (err, stream) => { - if(err) return callback(err); - docker.modem.followProgress(stream, onFinished, onProgress); - - function onFinished(err) { - if(err) return callback(err); - callback(); - } - // TODO: be able to stream the output of this to a socket to give real time updates - // onProgress(ev) - function onProgress() { - // console.log(ev); - } - }); - }, - (callback) => { - getPort(callback); - }, - (port, callback) => { - // TODO: before create a container, check for existing one to remove - docker.createContainer({ - Image: subdomain, - name: subdomain, - env: [ - 'PORT=3000' - ], - ExposedPorts: { - '3000/tcp': {} - }, - PortBindings: { - '3000/tcp': [{ - 'HostPort': `${port}` - }] - }, - Privileged: true - }, (err, container) => { - if (err) return callback(err); - container.start((err) => { - if (err) return callback(err); - return callback(null, { - port, - id: container.id, - subdomain, - directory: outputDir - }); - }); - }); - } - ], (err, result) => { - if (err) return reject(err); - resolve(result); + }) + .catch((ex) => reject(ex)); }); - }).catch((error) => { - reject(error); - }); }); }; diff --git a/lib/helpers/cli.js b/lib/helpers/cli.js index 9158e7e..e2e9faa 100644 --- a/lib/helpers/cli.js +++ b/lib/helpers/cli.js @@ -149,6 +149,34 @@ module.exports = function cli(url) { }); }; + /** + * gets the application logs + * @method getLogs + * @param {Object} options + * @param {String} options.token - token to make authenticated calls + * @param {String} options.username - username linked to the token + * @param {String} options.name - name of the deployment + * @return {Promise} + */ + methods.getLogs = function getLogs({ token, username, name }) { + const request = require('request'); + + return new Promise(function(resolve, reject) { + request.get({ + url: `${url}/api/deployments/${name}/logs`, + headers: { + 'x-deploy-token': token, + 'x-deploy-username': username + } + }, function optionalCallback(err, httpResponse, body) { + if (err) return reject(err); + const response = JSON.parse(body); + if(response.error) return reject(response.error); + return resolve(response); + }); + }); + }; + /** * gets the user's deployed applications * @method getDeployments diff --git a/lib/models/deployment.js b/lib/models/deployment.js index 43b3532..f8918f3 100644 --- a/lib/models/deployment.js +++ b/lib/models/deployment.js @@ -2,7 +2,15 @@ * @module models/deployment */ +const Async = require('async'); const mongoose = require('mongoose'); +const http = require('http'); +const fs = require('fs'); +const Docker = require('dockerode'); +const docker = new Docker({ + socketPath: '/var/run/docker.sock' +}); +const stream = require('stream'); const Schema = mongoose.Schema; @@ -24,6 +32,8 @@ const DeploymentSchema = new Schema({ } }); +const Deployment = mongoose.model('Deployment', DeploymentSchema); + /** * Deployment definition * @class {Object} Deployment @@ -34,11 +44,11 @@ const DeploymentSchema = new Schema({ * @property {String} directory - the directory of tared application * @property {String} username - the username who owns the deployment */ -const Deployment = mongoose.model('Deployment', DeploymentSchema); +module.exports.Deployment = Deployment; /** - * [updateDeployments description] - * @method updateDeployments + * updates a deployment + * @method update * @param {Object} options * @param {String} options.name - the name of the deployment * @param {String} options.token - the token for the user who owns the deployment @@ -46,113 +56,430 @@ const Deployment = mongoose.model('Deployment', DeploymentSchema); * @param {Deployment} options.deployment - a deployment model that contains the updates that will be applied * @return {Promise} */ -module.exports.updateDeployments = function updateDeployments({ name, token, username, deployment }) { +var update; +module.exports.update = update = function update({ name, token, username, deployment }) { return new Promise(function(resolve, reject) { User.get({ username, token }) - .then((user) => { - if(!user) return reject('not authenticated'); - Deployment.findOne({ - username, - name - }, (err, found) => { - if(found) { - found.id = deployment.id || found.id; - found.name = deployment.name || found.name; - found.port = deployment.port || found.port; - found.subdomain = deployment.subdomain || found.subdomain; - found.directory = deployment.directory || found.directory; - found.save((err) => { - if(err) return reject(err); - resolve(found); - }); - } else { - const deployment = { - name, - subdomain: `${name}-${hash(6)}`, - username - }; - Deployment.create(deployment, (err, deployment) => { - if(err) return reject(err); - return resolve(deployment); - }); - } - }); - }) - .catch((ex) => reject(ex)); + .then((user) => { + if(!user) return reject('not authenticated'); + Deployment.findOne({ + username, + name + }, (err, found) => { + if(found) { + found.id = deployment.id || found.id; + found.name = deployment.name || found.name; + found.port = deployment.port || found.port; + found.subdomain = deployment.subdomain || found.subdomain; + found.directory = deployment.directory || found.directory; + found.save((err) => { + if(err) return reject(err); + resolve(found); + }); + } else { + const deployment = { + name, + subdomain: `${name}-${hash(6)}`, + username + }; + Deployment.create(deployment, (err, deployment) => { + if(err) return reject(err); + return resolve(deployment); + }); + } + }); + }) + .catch((ex) => reject(ex)); }); }; /** - * gets the port for the requested subdomain - * @method getProxy + * express middleware to proxy to correct container + * @method proxy * @param {String} subdomain - the subdomain for the application being requested * @return {Promise} */ -module.exports.getProxy = function getProxy({ subdomain }) { - return new Promise(function(resolve, reject) { - Deployment.findOne({ - "subdomain":{ - "$eq": subdomain - } - }, (err, deployment) => { - if(err || !deployment) return reject(err); - return resolve(deployment.port); +module.exports.proxy = function proxy(req, res) { + const { url, method, headers } = req; + const { host } = headers; + + // If this is not an upload request, it is a proxy request + const subdomain = host.split('.')[0]; + + Deployment.findOne({ + "subdomain":{ + "$eq": subdomain + } + }, (err, deployment) => { + if(err || !deployment) { + return res.status(404).end(` + + + + + + Error + + +
+

404

Sorry this page could not be found 🙈 +
+ + + `); + } + const { port } = deployment; + const proxy = http.request({ + method, + path: url, + headers, + port, + host: 'localhost' + }); + proxy.addListener('response', function (proxy_response) { + proxy_response.addListener('data', function(chunk) { + res.write(chunk, 'binary'); + }); + proxy_response.addListener('end', function() { + res.end(); + }); + res.writeHead(proxy_response.statusCode, proxy_response.headers); + }); + proxy.on('error', function() { + res.status(502).end(` + + + + + + Error + + +
+

502

Sorry this page could not be loaded 🙈 +
+ + + `); + }); + req.addListener('data', function(chunk) { + proxy.write(chunk, 'binary'); + }); + req.addListener('end', function() { + proxy.end(); }); }); }; /** * gets a specific deployment if name is passed or all deployments for the specified user - * @method getDeployments + * @method get * @param {Object} options * @param {String=} options.name - the name of the deployment * @param {String} options.token - the token for the user who owns the deployment * @param {String} options.username - the username associated with this deployment + * @param {Boolean} option.create - create a deployment if not found * @return {Promise} */ -module.exports.getDeployments = function getDeployments({ name, token, username }) { +module.exports.get = function get({ name, token, username, create }) { + // TODO: query the container to get the status return new Promise(function(resolve, reject) { User.get({ username, token }) - .then((user) => { - if(!user) return reject('not authenticated'); + .then((user) => { + if(!user) return reject('not authenticated'); - if(!name) { - Deployment.find({ - username - }, (err, deployments) => { - if(err) return reject(err); + if(!name) { + Deployment.find({ + username + }, (err, deployments) => { + if(err) return reject(err); - resolve(deployments); + resolve(deployments); + }); + } else { + Deployment.findOne({ + username, + name + }, (err, deployment) => { + if(deployment) { + return resolve(deployment); + } + if(create) { + // add a new deployment name + // TODO: figure out how to avoid collisions + const deployment = { + name, + subdomain: `${name}-${hash(6)}`, + username + }; + Deployment.create(deployment, (err) => { + if(err) return reject('issue updating deployment'); + resolve(deployment); + }); + } + }); + } + }) + .catch((ex) => reject(ex)); + }); +}; + +module.exports.build = function build({ name, subdomain, username, token, port, directory }) { + return new Promise(function(resolve, reject) { + docker.buildImage({ + context: directory, + src: fs.readdirSync(directory) + }, { + t: subdomain + }, (err, stream) => { + if(err) return reject(err); + docker.modem.followProgress(stream, onFinished, onProgress); + + function onFinished(err) { + if(err) return reject(err); + docker.createContainer({ + Image: subdomain, + name: subdomain, + env: [ + 'PORT=3000' + ], + ExposedPorts: { + '3000/tcp': {} + }, + PortBindings: { + '3000/tcp': [{ + 'HostPort': `${port}` + }] + }, + Privileged: true + }, (err, container) => { + if (err) return reject(err); + container.start((err) => { + if (err) return reject(err); + const id = container.id; + + update({ + name, + username, + token, + deployment: { + id, + port, + directory + } + }) + .then((deployment) => resolve(deployment)) + .catch((ex) => reject(ex)); }); - } else { - Deployment.findOne({ - username, - name - }, (err, deployment) => { - if(deployment) { - return resolve(deployment); - } else { - // add a new deployment name - // TODO: figure out how to avoid collisions - const deployment = { - name, - subdomain: `${name}-${hash(6)}`, - username - }; - Deployment.create(deployment, (err) => { - if(err) return reject('issue updating deployment'); - resolve(deployment); - }); + }); + } + // TODO: be able to stream the output of this to a socket to give real time updates + function onProgress() { + // console.log(ev); + } + }); + }); +}; + +/** + * starts a container or all containers + * @method start + * @param {String=} name - to start a specific container a name property is needed + * @param {String} options.token - the token for the user who owns the deployment + * @param {String} options.username - the username associated with this deployment + * @return {Promise} + */ +module.exports.start = function start({ name, username, token }) { + return new Promise(function(resolve, reject) { + let opts = {}; + // TODO: check username and token to make sure the request is authenticated + if(username && token) { + opts.username = username; + } + + Deployment.find(opts, (err, deployments) => { + Async.each(deployments, (deployment, callback) => { + if(name && deployment.name == name) { + const container = docker.getContainer(deployment.id); + container.start(callback); + } + + if(!name){ + const container = docker.getContainer(deployment.id); + container.start(callback); + } + }, (err) => { + if(err) return reject(err); + return resolve(deployments); + }); + }); + }); +}; + +/** + * stops a container or all containers + * @method stop + * @param {String=} name - to stop a specific container a name property is needed + * @param {String=} options.token - the token for the user who owns the deployment + * @param {String=} options.username - the username associated with this deployment + * @return {Promise} + */ +module.exports.stop = function stop({ name, token, username }) { + return new Promise(function(resolve, reject) { + let opts = {}; + // TODO: check username and token to make sure the request is authenticated + if(username && token) { + opts.username = username; + } + + Deployment.find(opts, (err, deployments) => { + Async.each(deployments, (deployment, callback) => { + if(name && deployment.name == name) { + const container = docker.getContainer(deployment.id); + container.stop({ force: true }, callback); + } + + if(!name){ + const container = docker.getContainer(deployment.id); + container.stop({ force: true }, callback); + } + }, (err) => { + if(err) return reject(err); + return resolve(deployments); + }); + }); + }); +}; + +module.exports.logs = function logs({ name, token, username }) { + return new Promise(function(resolve, reject) { + User.get({ + username, + token + }) + .then((user) => { + Deployment.findOne({ + name, + username: user.username + }, (err, deployment) => { + if(err) return reject(err); + + const logs = []; + const container = docker.getContainer(deployment.id); + + const logStream = new stream.PassThrough(); + logStream.on('data', function(chunk){ + logs.push(chunk.toString('utf8')); + }); + + container.logs({ + follow: true, + stdout: true, + stderr: true + }, (err, stream) => { + if(err) { + return reject(err); } + container.modem.demuxStream(stream, logStream, logStream); + stream.on('end', function(){ + return resolve(logs); + }); + + setTimeout(function() { + stream.destroy(); + }, 2000); }); - } - }) - .catch((ex) => reject(ex)); + }); + }) + .catch(() => reject('token not valid')); + }); +}; + +/** + * removes a specific container, will stop and cleanup all necessary files + * @method remove + * @param {String} name - the name of the container + * @param {String} options.token - the token for the user who owns the deployment + * @param {String} options.username - the username associated with this deployment + * @return {Promise} + */ +module.exports.remove = function remove({ name, token, username }) { + return new Promise(function(resolve, reject) { + User.get({ + username, + token + }) + .then((user) => { + Deployment.findOne({ + subdomain: name, + username: user.username + }, (err, deployment) => { + if(err) return reject(err); + + const container = docker.getContainer(deployment.id); + + Async.waterfall([ + function(callback) { + container.stop(() => callback()); + }, + function(callback) { + container.inspect((err, info) => { + if(err) return callback(); + + docker.getImage(info.Image).remove({ + force: true + }, () => callback()); + }); + }, + function(callback) { + container.remove(() => callback()); + } + ], (err) => { + if(err) return reject(err); + return resolve(); + }); + }); + }) + .catch(() => reject('user does not have access to this deployment')); }); }; diff --git a/lib/models/user.js b/lib/models/user.js index d42edfa..4cf7a81 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -20,6 +20,8 @@ const UserSchema = new Schema({ } }); +const User = mongoose.model('User', UserSchema); + /** * User definition * @class {Object} User @@ -27,7 +29,7 @@ const UserSchema = new Schema({ * @property {String} password - a password for the user * @property {String=} token - an access token */ -const User = mongoose.model('User', UserSchema); +module.exports.User = User; /** * middleware to verify the username and token are valid, will then set the user to req.user diff --git a/lib/server.js b/lib/server.js index bc60747..425bc33 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1,17 +1,12 @@ -const mongoose = require('mongoose'); const express = require('express'); -const http = require('http'); const formidable = require('formidable'); const bodyParser = require('body-parser'); -mongoose.connect('mongodb://localhost/deploy-sh'); -mongoose.Promise = global.Promise; - const app = express(); const deploy = require('./deploy'); const { authenticate, register, logout, login } = require('./models/user'); -const { getProxy, getDeployments, updateDeployments } = require('./models/deployment'); +const { logs, get, proxy } = require('./models/deployment'); const { log } = require('./models/request'); const port = process.env.PORT || 5000; @@ -51,26 +46,11 @@ app.post('/upload', authenticate, (req, res) => { const { name } = fields; const { bundle } = files; - getDeployments({ - username, + deploy({ + name, + bundlePath: bundle.path, token, - name - }) - .then((deployment) => { - return deploy(deployment['subdomain'], bundle.path); - }) - .then((output) => { - const { port, directory, id } = output; - return updateDeployments({ - name, - username, - token, - deployment: { - id, - port, - directory - } - }); + username }) .then((deployment) => { res.status(200).send(deployment); @@ -82,10 +62,23 @@ app.post('/upload', authenticate, (req, res) => { }); +app.get('/api/deployments/:name/logs', authenticate, (req, res) => { + const { name } = req.params; + const { token, username } = req.user; + + logs({ + name, + username, + token + }) + .then((logs) => res.status(200).send({ logs })) + .catch((error) => res.status(500).send(error)); +}); + app.get('/api/deployments', authenticate, (req, res) => { const { token, username } = req.user; - getDeployments({ + get({ username, token }) @@ -97,7 +90,7 @@ app.get('/api/deployments/:name', authenticate, (req, res) => { const { name } = req.params; const { token, username } = req.user; - getDeployments({ + get({ username, token, name @@ -121,107 +114,8 @@ app.get('/api/user', authenticate, (req, res) => { res.status(200).send({ user }); }); -app.get('*', log, (req, res) => { - const { url, method, headers } = req; - const { host } = headers; - - // If this is not an upload request, it is a proxy request - const subdomain = host.split('.')[0]; - getProxy({ subdomain }) - .then((port) => { - var proxy = http.request({ - method, - path: url, - headers, - port, - host: 'localhost' - }); - proxy.addListener('response', function (proxy_response) { - proxy_response.addListener('data', function(chunk) { - res.write(chunk, 'binary'); - }); - proxy_response.addListener('end', function() { - res.end(); - }); - res.writeHead(proxy_response.statusCode, proxy_response.headers); - }); - proxy.on('error', function() { - res.status(502).end(` - - - - - - Error - - -
-

502

Sorry this page could not be loaded 🙈 -
- - - `); - }); - req.addListener('data', function(chunk) { - proxy.write(chunk, 'binary'); - }); - req.addListener('end', function() { - proxy.end(); - }); - }) - .catch(() => { - res.status(404).end(` - - - - - - Error - - -
-

404

Sorry this page could not be found 🙈 -
- - - `); - }); -}); +app.get('*', log, proxy); app.listen(port, () => { - console.log('⛅️ deploy.sh is running on port 5000'); // eslint-disable-line + console.log(`⛅️ deploy.sh is running on port ${port}`); // eslint-disable-line }); diff --git a/package.json b/package.json index ab39eaa..733ac62 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "test": "tap test/index.js test/lib", "coverage": "tap test/index.js test/lib --coverage --coverage-report=lcov", "lint": "eslint .", - "start": "node lib/server.js", + "start": "node index.js", "generate-docs": "jsdoc -c jsdoc.json" }, "repository": {