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
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 @@
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
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 @@
helpers/util.js
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 @@
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 @@
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
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 @@
models/request.js
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 @@
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
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 @@
Returns:
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 @@
Parameters:
subdomain
option
Object
+
+
+
+ Name | + + +Type | + + + + + +Description | +
---|---|---|
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 + + |
+
Parameters:
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 @@
(inner) Source:
@@ -507,7 +507,7 @@ (inner) Source:
@@ -611,7 +611,7 @@ (inner) Source:
@@ -825,6 +825,265 @@ Parameters:
+
+Returns:
+
+
+
+
+ -
+ Type:
+
+ -
+
+
Promise
+
+
+
+
+
+
+
+
+
+
+
+
(inner) Source:
@@ -825,6 +825,265 @@ Parameters:
+
+Returns:
+
+
+
+
+ -
+ Type:
+
+ -
+
+
Promise
+
+
+
+
+
+
+
+
+
+
+
+
Returns:
+ + + +-
+
- + Type: + +
-
+
+
Promise
+ + +
+
(inner) getLogs(options) → {Promise}
+ + + + + +gets the application logs
+-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- Source: +
- + helpers/cli.js, line 152 +
+
+
+
+
+
+
+
+
Parameters:
+ + +Name | + + +Type | + + + + + +Description | +||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
options |
+
+
+
+
+
+Object
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
|
+
Returns:
@@ -1799,7 +2058,7 @@Returns:
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 @@
Returns:
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
+ + + + + + + ++ models/deployment. + + Deployment +
+ +{Object} Deployment
Constructor
+ + +new Deployment()
+ + + + + +Deployment definition
+Properties:
+ + + +Name | + + +Type | + + + + + +Description | +
---|---|---|
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: +
- + + + + + + + +
+ + + + + + + \ 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 @@
models/deployment
Classes
-
-
- Deployment +
- Deployment
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:
+
+
+
+
+
+
+ Name
+
+
+ Type
+
+
+
+
+
+ Description
+
+
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
option.create
Boolean
+
+
+
+ create a deployment if not found
+ +gets the port for the requested subdomain
+express middleware to proxy to correct container
Returns:
-(inner) updateDeployments(options) → {Promise}
+(inner) remove(name) → {Promise}
+ + + + + +removes a specific container, will stop and cleanup all necessary files
+-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- Source: +
- + + + + + + + +
Parameters:
+ + +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:
+ + +Name | + + +Type | + + +Attributes | + + + + +Description | +
---|---|---|---|
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:
+ + +Name | + + +Type | + + +Attributes | + + + + +Description | +
---|---|---|---|
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
(inner)
Source:
@@ -822,7 +1532,7 @@ Returns:
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:
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:
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:
+
+
+
+
+
+
+
+ Name
+
+
+ Type
+
+
+ Attributes
+
+
+
+
+ Description
+
+
+
+
+
+
+
+
+ username
+
+
+
+
+
+String
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ a string that defines the user's accounts
+
+
+
+
+
+
+
+
+ password
+
+
+
+
+
+String
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ a password for the user
+
+
+
+
+
+
+
+
+ token
+
+
+
+
+
+String
+
+
+
+
+
+
+
+
+ <optional>
+
+
+
+
+
+
+
+
+ an access token
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - Source:
+ -
+ models/user.js, line 32
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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
@@ -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:
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
+
+
+
+
+
+ `);
+ }
+ 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
+
+
+
+
+
+ `);
+ });
+ 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
-
-
-
-
-
- `);
- });
- req.addListener('data', function(chunk) {
- proxy.write(chunk, 'binary');
- });
- req.addListener('end', function() {
- proxy.end();
- });
- })
- .catch(() => {
- res.status(404).end(`
-
-
-
-
-
- Error
-
-
-
-
-
- `);
- });
-});
+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": {
Properties:
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 @@
Parameters:
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
+ + + + + + + ++ models/user. + + User +
+ +{Object} User
Constructor
+ + +new User()
+ + + + + +User definition
+Properties:
+ + + +Name | + + +Type | + + +Attributes | + + + + +Description | +
---|---|---|---|
username |
+
+
+
+
+
+String
+
+
+
+ |
+
+
+ + + + + | + + + + +a string that defines the user's accounts |
+
password |
+
+
+
+
+
+String
+
+
+
+ |
+
+
+ + + + + | + + + + +a password for the user |
+
token |
+
+
+
+
+
+String
+
+
+
+ |
+
+
+
+
+ <optional> + + + + |
+
+
+
+
+ an access token |
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- Source: +
- + models/user.js, line 32 +
+
+
+
+
+
+
+
+
+ + + + + + + \ 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 @@
models/user
Classes
@@ -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:
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
+
+
+
+
+
+ `);
+ }
+ 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
+
+
+
+
+
+ `);
+ });
+ 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
-
-
-
-
-
- `);
- });
- req.addListener('data', function(chunk) {
- proxy.write(chunk, 'binary');
- });
- req.addListener('end', function() {
- proxy.end();
- });
- })
- .catch(() => {
- res.status(404).end(`
-
-
-
-
-
- Error
-
-
-
-
-
- `);
- });
-});
+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": {
(inner) loginSource:
@@ -676,7 +676,7 @@ (inner) logout
Source:
@@ -858,7 +858,7 @@ (inner) regi
Source:
@@ -1009,7 +1009,7 @@ Returns:
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
+
+
+
+
+
+ `);
+ }
+ 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
+
+
+
+
+
+ `);
+ });
+ 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
-
-
-
-
-
- `);
- });
- req.addListener('data', function(chunk) {
- proxy.write(chunk, 'binary');
- });
- req.addListener('end', function() {
- proxy.end();
- });
- })
- .catch(() => {
- res.status(404).end(`
-
-
-
-
-
- Error
-
-
-
-
-
- `);
- });
-});
+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": {
(inner) logout
Source:
@@ -858,7 +858,7 @@ (inner) regi
Source:
@@ -1009,7 +1009,7 @@ Returns:
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
+
+
+
+
+
+ `);
+ }
+ 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
+
+
+
+
+
+ `);
+ });
+ 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
-
-
-
-
-
- `);
- });
- req.addListener('data', function(chunk) {
- proxy.write(chunk, 'binary');
- });
- req.addListener('end', function() {
- proxy.end();
- });
- })
- .catch(() => {
- res.status(404).end(`
-
-
-
-
-
- Error
-
-
-
-
-
- `);
- });
-});
+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": {
Returns:
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(` + + + + + +