159 lines
5.2 KiB
JavaScript
Executable File
159 lines
5.2 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
import assert from 'node:assert';
|
|
import async from 'async';
|
|
import { execSync } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import net from 'node:net';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { program } from 'commander';
|
|
import safe from '@cloudron/safetydance';
|
|
import { Client as SshClient } from 'ssh2';
|
|
import util from 'node: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 <version> is required');
|
|
if (!options.sshKey) exit('--ssh-key is required');
|
|
|
|
let ip;
|
|
if (net.isIP(options.cloudron)) {
|
|
ip = options.cloudron;
|
|
} else {
|
|
const 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';
|
|
|
|
const tarballScript = path.join(import.meta.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(import.meta.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 <domain/ip>', 'Cloudron domain or IP')
|
|
.option('--release <version>', 'Cloudron release version')
|
|
.option('--ssh-port <ssh port>', 'SSH port')
|
|
.option('--ssh-key <ssh key>', 'SSH Key file path')
|
|
.option('--ssh-user <ssh user>', 'SSH username');
|
|
|
|
program.parse();
|
|
|
|
hotfix(program.opts());
|