#!/usr/bin/env node 'use strict'; const program = require('commander'), assert = require('assert'), async = require('async'), execSync = require('child_process').execSync, fs = require('fs'), ipaddr = require('ipaddr.js'), os = require('os'), path = require('path'), safe = require('safetydance'), SshClient = require('ssh2').Client, util = require('util'); function exit(error) { if (error instanceof Error) console.log(error.message); else if (error) console.error(util.format.apply(null, Array.prototype.slice.call(arguments))); process.exit(error ? 1 : 0); } function findSshKey(key) { assert.strictEqual(typeof key, 'string'); let test = key; // remove .pub in case the user passed in the public key if (path.extname(test) === '.pub') test = test.slice(0, -4); if (fs.existsSync(test)) return test; test = path.join(os.homedir(), '.ssh', key); if (fs.existsSync(test)) return test; test = path.join(os.homedir(), '.ssh', 'id_ed25519_' + key); if (fs.existsSync(test)) return test; test = path.join(os.homedir(), '.ssh', 'id_rsa_' + key); if (fs.existsSync(test)) return test; test = path.join(os.homedir(), '.ssh', 'id_rsa_caas_' + key); if (fs.existsSync(test)) return test; return null; } function sshExec(host, port, sshKey, username, cmds, callback) { const passphrase = process.env.SSH_PASSPHRASE || undefined; console.log(`connecting ${username}@${host}:${port} with ${sshKey} (passphrase: ${passphrase || ''})`); const sshClient = new SshClient(); sshClient.connect({ host, port, username, privateKey: fs.readFileSync(sshKey), passphrase, // debug: (s) => { console.log(s); } // https://github.com/mscdex/ssh2/issues/989 }); sshClient.on('ready', function () { console.log('connected'); async.eachSeries(cmds, function (cmd, iteratorDone) { const command = cmd.cmd; console.log(`Executing: ${command}`); sshClient.exec(command, function(err, stream) { if (err) return callback(err); if (cmd.stdin) cmd.stdin.pipe(stream); stream.pipe(process.stdout); stream.on('close', function () { iteratorDone(); }).stderr.pipe(process.stderr); }); }, function seriesDone(error) { if (error) return callback(error); sshClient.end(); }); }); sshClient.on('end', function () { console.log('\ndisconnected'); callback(); }); sshClient.on('error', function (error) { console.log(`SSH client error: ${error.message}`); callback(error); }); sshClient.on('exit', function (exitCode) { callback(exitCode === 0 ? null : new Error('ssh exec returned + ' + exitCode)); }); } function hotfix(options) { assert.strictEqual(typeof options, 'object'); if (!options.cloudron) exit('--cloudron is required'); if (!options.release) exit('--release is required'); if (!options.sshKey) exit('--ssh-key is required'); let ip; if (ipaddr.isValid(options.cloudron)) { ip = options.cloudron; } else { let out = safe.child_process.execSync(`host -t A ${options.cloudron}`, { encoding: 'utf8' }); if (!out) exit(`Could not resolve ${options.cloudron}`); ip = out.trim().split(' ')[3]; } const sshKey = findSshKey(options.sshKey); if (!sshKey) exit('Unable to find SSH key'); const version = options.release; const sshPort = options.sshPort || 22; const sshUser = options.sshUser || 'root'; let tarballScript = path.join(__dirname, 'create-release-tarball'); if (!fs.existsSync(tarballScript)) exit('Could not find create-release-taball script. Run this command from the box repo checkout directory.'); const tarball = `${os.tmpdir()}/boxtarball-${version}.tar.gz`; try { execSync(`${tarballScript} --output ${tarball} --version ${version}`, { stdio: [ null, process.stdout, process.stderr ] }); } catch (e) { console.log('Unable to create version tarball.'); process.exit(1); } const cmds = [ { cmd: 'sudo rm -rf /tmp/box-src-hotfix' }, { cmd: 'sudo mkdir -p /tmp/box-src-hotfix' }, { cmd: 'sudo tar zxf - -C /tmp/box-src-hotfix', stdin: fs.createReadStream(tarball) }, { cmd: 'sudo dd of=/tmp/remote_hotfix.js', stdin: fs.createReadStream(path.resolve(__dirname, './remote_hotfix.js')) }, { cmd: 'sudo HOME=/home/yellowtent BOX_ENV=cloudron node /tmp/remote_hotfix.js' }, { cmd: 'sudo rm -rf /tmp/box-src-hotfix' } ]; sshExec(ip, sshPort, sshKey, sshUser, cmds, function (error) { if (error) exit(error); console.log('Done patching'); }); } // main commander setup program.description('Hotfix Cloudron with latest code') .version('1.0.0') .option('--cloudron ', 'Cloudron domain or IP') .option('--release ', 'Cloudron release version') .option('--ssh-port ', 'SSH port') .option('--ssh-key ', 'SSH Key file path') .option('--ssh-user ', 'SSH username'); program.parse(); hotfix(program.opts());