diff --git a/app/skybreak/deploy.js b/app/skybreak/deploy.js index 07045201ca..2e792e4433 100644 --- a/app/skybreak/deploy.js +++ b/app/skybreak/deploy.js @@ -1,16 +1,192 @@ +// URL parsing and validation +// RPC to server (endpoint, arguments) +// see if RPC requires password +// prompt for password +// send RPC with or without password as required + var crypto = require('crypto'); var tty = require('tty'); var request = require('request'); +var qs = require('querystring'); +var path = require('path'); +var files = require('../lib/files.js'); +var _ = require('../lib/third/underscore.js'); +// +// configuration +// -exports.HOSTNAME = 'deploy.skybreakplatform.com'; +var DEPLOY_HOSTNAME = 'deploy.skybreakplatform.com'; +// available RPCs are: deploy (with set-password), delete, logs, +// mongo_cred. each RPC might require a password, which we +// interactively prompt for here. + +var sky_rpc = function (rpc_name, method, site, query_params, callback) { + var url = "http://" + DEPLOY_HOSTNAME + '/' + rpc_name + '/' + site; + + if (!_.isEmpty(query_params)) + url += '?' + qs.stringify(query_params); + + var r = request({method: method, url: url}, function (error, response, body) { + if (error || ((response.statusCode !== 200) + && (response.statusCode !== 201))) + // pass some non-falsy error back to callback + callback(error || response.statusCode, body); + else + callback(null, body); + }); + + return r; +} + +var deploy_app = function (url, app_dir, opt_set_password) { + var parsed_url = parse_url(url); + + // a bit contorted here to make sure we ask for the password before + // launching the slow bundle process. + + with_password(parsed_url.hostname, function (password) { + if (opt_set_password) + get_new_password(function (set_password) { + bundle_and_deploy(parsed_url.hostname, app_dir, password, set_password); + }); + else + bundle_and_deploy(parsed_url.hostname, app_dir, password); + }); +}; + +var bundle_and_deploy = function (site, app_dir, password, set_password) { + var build_dir = path.join(app_dir, '.skybreak/local/build_tar'); + var bundle_path = path.join(build_dir, 'bundle'); + var bundle_opts = { skip_dev_bundle: true }; + + process.stdout.write('Deploying to ' + site + '. Bundling ... '); + require('../lib/bundler.js').bundle(app_dir, bundle_path, bundle_opts); + + process.stdout.write('uploading ... '); + + var opts = {}; + if (password) opts.password = password; + if (set_password) opts.set_password = set_password; + + var spawn = require('child_process').spawn; + var tar = spawn('tar', ['czf', '-', 'bundle'], {cwd: build_dir}); + + var rpc = sky_rpc('deploy', 'POST', site, opts, function (err, body) { + // XXX this is gross. maybe some way to automate? + process.stdin.destroy(); // clean up after maybe_password + + if (err) { + process.stderr.write("\nError deploying application: " + body + "\n"); + process.exit(1); + } + + process.stdout.write('done.\n'); + process.stdout.write('Now serving at ' + site + '\n'); + + files.rm_recursive(build_dir); + + if (!site.match('skybreakplatform.com')) { + var dns = require('dns'); + dns.resolve(site, 'CNAME', function (err, cnames) { + if (err || cnames[0] !== 'origin.skybreakplatform.com') { + dns.resolve(site, 'A', function (err, addresses) { + if (err || addresses[0] !== '107.22.210.133') { + process.stdout.write('-------------\n'); + process.stdout.write("You've deployed to a custom domain.\n"); + process.stdout.write("Please be sure to CNAME your hostname to origin.skybreakplatform.com,\n"); + process.stdout.write("or set an A record to 107.22.210.133.\n"); + process.stdout.write('-------------\n'); + } + }); + } + }); + } + }); + + tar.stdout.pipe(rpc); +}; + +var delete_app = function (url) { + var parsed_url = parse_url(url); + + with_password(parsed_url.hostname, function (password) { + var opts = {}; + if (password) opts.password = password; + + sky_rpc('deploy', 'DELETE', parsed_url.hostname, opts, function (err, body) { + process.stdin.destroy(); // clean up after with_password + + if (err) { + process.stderr.write("Error deleting application: " + body + "\n"); + process.exit(1); + } + + process.stdout.write("Deleted.\n"); + }); + }); +}; + +// either print the mongo credential (just_credential is true) or open +// a mongo shell. +var mongo = function (url, just_credential) { + var parsed_url = parse_url(url); + + with_password(parsed_url.hostname, function (password) { + var opts = {}; + if (password) opts.password = password; + + sky_rpc('mongo', 'GET', parsed_url.hostname, opts, function (err, body) { + if (err) { + process.stderr.write(body + "\n"); + process.exit(1); + } + + if (just_credential) { + // just print the URL + process.stdout.write(body + "\n"); + + // only do this if we're printing the URL. Don't do it if + // we're running the mongo shell, since that will close off + // stdin for the shell. + process.stdin.destroy(); // clean up after with_password + + } else { + // pause stdin so we don't try to read it while mongo is + // running. + process.stdin.pause(); + run_mongo_shell(body); + } + }); + }); +}; + +var logs = function (url) { + var parsed_url = parse_url(url); + + with_password(parsed_url.hostname, function (password) { + var opts = {}; + if (password) opts.password = password; + + sky_rpc('logs', 'GET', parsed_url.hostname, opts, function (err, body) { + process.stdin.destroy(); // clean up after with_password + + if (err) { + process.stderr.write(body + '\n'); + process.exit(1); + } + + process.stdout.write(body); + }); + }); +}; // accepts www.host.com, defaults domain to skybreakplatform, defaults -// protocol to http. +// protocol to http. on bad URL, prints error and exits the process. // // XXX shared w/ proxy.js -exports.parse_url = function (url) { +var parse_url = function (url) { if (!url.match(':\/\/')) url = 'http://' + url; @@ -21,25 +197,41 @@ exports.parse_url = function (url) { if (parsed.hostname && !parsed.hostname.match(/\./)) parsed.hostname += '.skybreakplatform.com'; - return parsed; -}; - -exports.validate_url = function (url) { - if (!url.hostname) { + if (!parsed.hostname) { process.stdout.write( "Please specify a domain to connect to, such as www.example.com or\n" + "http://www.example.com/\n"); process.exit(1); } - if (url.pathname != '/' || url.hash || url.query) { + if (parsed.pathname != '/' || parsed.hash || parsed.query) { process.stdout.write( "Sorry, Skybreak does not yet support specific path URLs, such as\n" + "http://www.example.com/blog . Please specify the root of a domain.\n"); process.exit(1); } -} + return parsed; +}; + +var run_mongo_shell = function (url) { + var mongo_path = path.join(files.get_dev_bundle(), 'mongodb/bin/mongo'); + var mongo_url = require('url').parse(url); + var auth = mongo_url.auth && mongo_url.auth.split(':'); + var spawn = require('child_process').spawn; + + var args = []; + if (auth) args.push('-u', auth[0]); + if (auth) args.push('-p', auth[1]); + args.push(mongo_url.hostname + ':' + mongo_url.port + mongo_url.pathname); + + var proc = spawn(mongo_path, + args, + { customFds: [0, 1, 2] }); + proc.on('exit', function () { + process.stdin.destroy(); // clean up after maybe_password + }); +}; // hash the password so we never send plaintext over the wire. Doesn't // actually make us more secure, but it means we won't leak a user's @@ -96,41 +288,35 @@ var read_password = function (callback) { }; - // Check if a particular endpoint requires a password. If so, prompt for // it. // -// takes an endpoint name and callback function(password). This is -// always called exactly once. If no password is needed, password will -// be undefined. -exports.maybe_password = function (endpoint, callback) { - var check_url = "http://" + exports.HOSTNAME + "/has_password/" + endpoint; +// takes an site name and callback function(password). This is always +// called exactly once. Calls callback with the entered password, or +// undefined if no password is required. +var with_password = function (site, callback) { + var check_url = "http://" + DEPLOY_HOSTNAME + "/has_password/" + site; request(check_url, function (error, response, body) { if (error || response.statusCode !== 200) { - // XXX more fine grained error handling callback(); - return; - } - // XXX in theory we should JSON parse the result, and use that. But - // we happen to know we'll only ever get 'true' or 'false' if we got - // a 200, so don't bother. - - if (body === "false") { + } else if (body === "false") { + // XXX in theory we should JSON parse the result, and use + // that. But we happen to know we'll only ever get 'true' or + // 'false' if we got a 200, so don't bother. callback(); - return; - } - process.stdout.write("Password: "); - read_password(callback); + } else { + process.stdout.write("Password: "); + read_password(callback); + } }); }; - // Prompts for a new password, asking you twice so you don't typo // it. Keeps prompting you until you have two that match. -exports.get_new_password = function (callback) { +var get_new_password = function (callback) { process.stdout.write("New Password: "); read_password(function (p1) { process.stdout.write("Confirm Password: "); @@ -140,7 +326,12 @@ exports.get_new_password = function (callback) { return; } process.stdout.write("Passwords do not match! Try again.\n"); - exports.get_new_password(callback); + get_new_password(callback); }); }); }; + +exports.deploy_app = deploy_app; +exports.delete_app = delete_app; +exports.mongo = mongo; +exports.logs = logs; diff --git a/app/skybreak/skybreak.js b/app/skybreak/skybreak.js index 21c21483d3..9a3148dc5d 100644 --- a/app/skybreak/skybreak.js +++ b/app/skybreak/skybreak.js @@ -1,6 +1,7 @@ var files = require('../lib/files.js'); var path = require('path'); var _ = require('../lib/third/underscore.js'); +var deploy = require('./deploy'); var usage = function() { process.stdout.write( @@ -354,25 +355,6 @@ Commands.push({ } }); -var run_mongo_shell = function (url) { - var mongo_path = path.join(files.get_dev_bundle(), 'mongodb/bin/mongo'); - var mongo_url = require('url').parse(url); - var auth = mongo_url.auth && mongo_url.auth.split(':'); - var spawn = require('child_process').spawn; - - var args = []; - if (auth) args.push('-u', auth[0]); - if (auth) args.push('-p', auth[1]); - args.push(mongo_url.hostname + ':' + mongo_url.port + mongo_url.pathname); - - var proc = spawn(mongo_path, - args, - { customFds: [0, 1, 2] }); - proc.on('exit', function () { - process.stdin.destroy(); // clean up after maybe_password - }); -}; - Commands.push({ name: "mongo", help: "Connect to the Mongo database for the specified site", @@ -428,55 +410,7 @@ Commands.push({ } else if (new_argv._.length === 2) { // remote mode - var deploy = require('./deploy'); - var url = deploy.parse_url(new_argv._[1]); - deploy.validate_url(url); - - deploy.maybe_password(url.hostname, function (password) { - - var options = { - host: deploy.HOSTNAME, - port: 80, - path: '/mongo/' + url.hostname, - }; - if (password) { - options.path += '?password=' + password; - } - var data = ''; - - var req = require('http').get(options, function(res) { - res.setEncoding('utf8'); - res.on('data', function (chunk) { data += chunk; }); - res.on('end', function () { - if (res.statusCode == 200) { - if (new_argv.url) { - console.log(data); - - // only do this if we're printing the URL. Don't do it - // if we're running the mongo shell, since that will - // close off stdin for the shell. - process.stdin.destroy(); // clean up after maybe_password - } else { - // pause stdin so we don't try to read it while mongo is - // running. - process.stdin.pause(); - run_mongo_shell(data); - } - - } else { - process.stderr.write(data); - process.stderr.write("\n"); - process.exit(1); - } - }); - }); - - req.on('error', function(e) { - console.log(e); - console.log("Error connecting to Skybreak: " + e.message); - process.exit(1); - }); - }); + deploy.mongo(new_argv._[1], new_argv.url); } else { // usage @@ -494,6 +428,9 @@ Commands.push({ .boolean('password') .alias('password', 'P') .describe('password', 'set a password for the deployment') + .boolean('delete') + .alias('delete', 'D') + .describe('delete', "permanently delete this project and its data from Skybreak") .usage( "Usage: skybreak deploy \n" + "\n" + @@ -508,95 +445,12 @@ Commands.push({ process.exit(1); } - var deploy = require('./deploy'); - - var url = deploy.parse_url(new_argv._[1]); - deploy.validate_url(url); - - var app_dir = path.resolve(require_project("bundle")); - var build_dir = path.join(app_dir, '.skybreak/local/build_tar'); - var bundle_path = path.join(build_dir, 'bundle'); - var bundle_opts = { skip_dev_bundle: true }; - - var do_deploy = function (password, set_password) { - process.stdout.write('Deploying to ' + url.hostname + '. Bundling ... '); - - require('../lib/bundler.js').bundle(app_dir, bundle_path, bundle_opts); - - process.stdout.write('uploading ... '); - - var spawn = require('child_process').spawn; - - var tar = spawn('tar', ['czf', '-', 'bundle'], {cwd: build_dir}); - - var deploy_req_opts = { - method: 'POST', - host: deploy.HOSTNAME, - path: '/deploy/' + url.hostname - }; - var password_opts = {}; - if (password) password_opts.password = password; - if (set_password) password_opts.set_password = set_password; - if (password || set_password) - deploy_req_opts.path += "?" + require('querystring').stringify(password_opts); - - var http = require('http'); - var deploy_data = ''; - var deploy_req = http.request(deploy_req_opts, function (deploy_res) { - deploy_res.setEncoding('utf8'); - deploy_res.on('data', function (chunk) { deploy_data += chunk; }); - deploy_res.on('end', function () { - if (deploy_res.statusCode !== 200) { - console.log("failed!"); - console.log(deploy_data); - process.exit(1); - } - - process.stdout.write('done.\n'); - process.stdout.write('Now serving at ' + url.hostname + '\n'); - - files.rm_recursive(build_dir); - - if (!url.hostname.match('skybreakplatform.com')) { - var dns = require('dns'); - dns.resolve(url.hostname, 'CNAME', function (err, cnames) { - if (err || cnames[0] !== 'origin.skybreakplatform.com') { - dns.resolve(url.hostname, 'A', function (err, addresses) { - if (err || addresses[0] !== '107.22.210.133') { - process.stdout.write('-------------\n'); - process.stdout.write("You've deployed to a custom domain.\n"); - process.stdout.write("Please be sure to CNAME your hostname to origin.skybreakplatform.com,\n"); - process.stdout.write("or set an A record to 107.22.210.133.\n"); - process.stdout.write('-------------\n'); - } - }); - } - }); - } - }); - }); - - tar.stdout.on('data', function (data) { - deploy_req.write(data); - }); - - tar.on('exit', function (code) { - deploy_req.end(); - }); - - // XXX this is gross. maybe some way to automate? - process.stdin.destroy(); // clean up after maybe_password - }; - - deploy.maybe_password(url.hostname, function (password) { - if (new_argv.password) { - deploy.get_new_password(function (set_password) { - do_deploy(password, set_password); - }); - } else { - do_deploy(password); - } - }); + if (new_argv.delete) { + deploy.delete_app(new_argv._[1]); + } else { + var app_dir = path.resolve(require_project("bundle")); + deploy.deploy_app(new_argv._[1], app_dir, new_argv.password); + } } }); @@ -612,36 +466,7 @@ Commands.push({ process.exit(1); } - var deploy = require('./deploy'); - var url = deploy.parse_url(argv._[0]); - deploy.validate_url(url); - - deploy.maybe_password(url.hostname, function (password) { - var http = require('http'); - var options = { - host: deploy.HOSTNAME, - port: 80, - path: '/logs/' + url.hostname, - }; - if (password) { - options.path += '?password=' + password; - } - - var req = http.get(options, function(res) { - res.setEncoding('utf8'); - res.on('data', function (chunk) { - process.stdout.write(chunk); - }); - }); - - req.on('error', function(e) { - console.log("Error connecting to Skybreak: " + e.message); - process.exit(1); - }); - - // XXX this is gross. maybe some way to automate? - process.stdin.destroy(); // clean up after maybe_password - }); + deploy.logs(argv._[0]); } });