Add hotfix command

This commit is contained in:
Johannes Zellner
2023-03-29 22:52:51 +02:00
parent 7af44e1fdd
commit ba74eb6fa2
4 changed files with 259 additions and 6 deletions

76
package-lock.json generated
View File

@@ -56,13 +56,15 @@
"xml2js": "^0.4.23"
},
"devDependencies": {
"commander": "^10.0.0",
"eslint": "^8.36.0",
"expect.js": "*",
"hock": "^1.4.1",
"js2xmlparser": "^5.0.0",
"mocha": "^10.2.0",
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^13.3.0"
"nock": "^13.3.0",
"ssh2": "^1.11.0"
}
},
"node_modules/@balena/dockerignore": {
@@ -803,6 +805,16 @@
"version": "1.0.1",
"license": "BSD-3-Clause"
},
"node_modules/buildcheck": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.3.tgz",
"integrity": "sha512-pziaA+p/wdVImfcbsZLNF32EiWyujlQLwolMqUQE8xpKNOH7KmZQaY8sXN7DGOEzPAElo9QTaeNRfGnf3iOJbA==",
"dev": true,
"optional": true,
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"license": "MIT",
@@ -1004,6 +1016,15 @@
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.0.tgz",
"integrity": "sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==",
"dev": true,
"engines": {
"node": ">=14"
}
},
"node_modules/component-emitter": {
"version": "1.3.0",
"license": "MIT"
@@ -1214,6 +1235,21 @@
"version": "1.0.2",
"license": "MIT"
},
"node_modules/cpu-features": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.4.tgz",
"integrity": "sha512-fKiZ/zp1mUwQbnzb9IghXtHtDoTMtNeb8oYGx6kX2SYfhnG0HNdBEBIzB9b5KlXu5DQPhfy3mInbBxFcgwAr3A==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"dependencies": {
"buildcheck": "0.0.3",
"nan": "^2.15.0"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/cron": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/cron/-/cron-2.3.0.tgz",
@@ -3923,6 +3959,13 @@
"version": "2.1.2",
"license": "ISC"
},
"node_modules/nan": {
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz",
"integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==",
"dev": true,
"optional": true
},
"node_modules/nanoid": {
"version": "3.3.3",
"dev": true,
@@ -4961,16 +5004,27 @@
}
},
"node_modules/ssh2": {
"version": "0.5.4",
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.11.0.tgz",
"integrity": "sha512-nfg0wZWGSsfUe/IBJkXVll3PEZ//YH2guww+mP88gTpuSU4FtZN7zu9JoeTGOyCNx2dTDtT9fOpWwlzyj4uOOw==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"ssh2-streams": "~0.1.15"
"asn1": "^0.2.4",
"bcrypt-pbkdf": "^1.0.2"
},
"engines": {
"node": ">=0.10.0"
"node": ">=10.16.0"
},
"optionalDependencies": {
"cpu-features": "~0.0.4",
"nan": "^2.16.0"
}
},
"node_modules/ssh2-streams": {
"version": "0.1.20",
"resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.1.20.tgz",
"integrity": "sha512-uqI2NfwMXF0PgY1IWivWWlfr4Ws6wsFF5Eug/bmpyyVn/k7T2VoNfJT6ynhM0JW1NpeIZuYHOENUCLx6NFK6Jw==",
"dependencies": {
"asn1": "~0.2.0",
"semver": "^5.1.0",
@@ -4982,7 +5036,8 @@
},
"node_modules/ssh2-streams/node_modules/semver": {
"version": "5.7.1",
"license": "ISC",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"bin": {
"semver": "bin/semver"
}
@@ -5291,6 +5346,17 @@
"version": "2.0.0",
"license": "MIT"
},
"node_modules/tunnel-ssh/node_modules/ssh2": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.5.4.tgz",
"integrity": "sha512-ZnC+u9CRgg0BKkYrpygna2723zhRxOtsEcrjsSCwJCZvj95fbE5qdCEndqQtbzA3IgftlmAgNafyy20kh9tbqw==",
"dependencies": {
"ssh2-streams": "~0.1.15"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tv4": {
"version": "1.3.0",
"license": [

View File

@@ -10,6 +10,9 @@
"type": "git",
"url": "https://git.cloudron.io/cloudron/box.git"
},
"bin": {
"hotfix": "./scripts/hotfix"
},
"dependencies": {
"@google-cloud/dns": "^3.0.2",
"@google-cloud/storage": "^6.9.4",
@@ -59,13 +62,15 @@
"xml2js": "^0.4.23"
},
"devDependencies": {
"commander": "^10.0.0",
"eslint": "^8.36.0",
"expect.js": "*",
"hock": "^1.4.1",
"js2xmlparser": "^5.0.0",
"mocha": "^10.2.0",
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^13.3.0"
"nock": "^13.3.0",
"ssh2": "^1.11.0"
},
"scripts": {
"test": "./run-tests"

160
scripts/hotfix Executable file
View File

@@ -0,0 +1,160 @@
#!/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 <version> 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 <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());

22
scripts/remote_hotfix.js Normal file
View File

@@ -0,0 +1,22 @@
'use strict';
if (process.env.BOX_ENV !== 'cloudron') {
console.error('!! This is only meant to be run with cloudron hotfix');
process.exit(1);
}
const spawn = require('child_process').spawn,
path = require('path');
const NEW_BOX_SOURCE_DIR = '/tmp/box-src-hotfix';
console.log('=> Running installer.sh');
const installer = spawn(path.join(NEW_BOX_SOURCE_DIR, 'scripts/installer.sh'), []);
installer.stdout.pipe(process.stdout);
installer.stderr.pipe(process.stderr);
installer.on('exit', function (code) {
console.log('Finished with code', code);
process.exit(code);
});