make command execution over ssh way neater

This commit is contained in:
Siddharth Singh 2024-09-30 11:15:24 +05:30
parent 267e8e8930
commit b4f8cd894f
No known key found for this signature in database
2 changed files with 57 additions and 26 deletions

View file

@ -1,18 +1,16 @@
import { Command, connect } from '../utils/mod.ts'; import { Command, makeSshAction, privkeyToPubkey } from '../utils/mod.ts';
export const init = new Command('init', 'Initialise a brand-new Ditto remote') export const init = new Command('init', 'Initialise a brand-new Ditto remote')
.option('-e --email', { .option('-e --email', {
description: "The e-mail address to use for requesting Let's Encrypt certificates.", description: "The e-mail address to use for requesting Let's Encrypt certificates.",
}) })
.setAction(async (args) => { .setAction(makeSshAction(async ({ arg, tribes, domain }) => {
const arg = (name: string) => args[name] as string; const pubkey = await privkeyToPubkey(arg('identity-file'));
const domain = args._[0] as string; await tribes.loud('DOKKU_SET_ADMIN_PUBKEY', pubkey);
const tribes = await connect(domain, arg('identity-file')); await tribes.loud('DOKKU_INSTALL_PLUGIN', 'postgres');
await tribes.actionL('DOKKU_SET_ADMIN_PUBKEY', await Deno.readTextFile(arg('identity-file'))); await tribes.loud('DOKKU_INSTALL_PLUGIN', 'letsencrypt');
await tribes.actionL('DOKKU_INSTALL_PLUGIN', 'postgres'); await tribes.loud('DOKKU_CREATE_POSTGRES_SERVICE', 'dittodb');
await tribes.actionL('DOKKU_INSTALL_PLUGIN', 'letsencrypt'); await tribes.loud('DOKKU_SET_GLOBAL_DOMAIN', domain);
await tribes.actionL('DOKKU_CREATE_POSTGRES_SERVICE', 'dittodb'); await tribes.loud('DOKKU_LETSENCRYPT_SETUP_EMAIL', arg('email'));
await tribes.actionL('DOKKU_SET_GLOBAL_DOMAIN', domain); await tribes.loud('DOKKU_LETSENCRYPT_CRON');
await tribes.actionL('DOKKU_LETSENCRYPT_SETUP_EMAIL', arg('email')); }));
await tribes.actionL('DOKKU_LETSENCRYPT_CRON');
});

View file

@ -1,4 +1,5 @@
import { NodeSSH } from 'node-ssh'; import { NodeSSH } from 'node-ssh';
import { ParsedArgs, runLocally } from '../mod.ts';
const commands = { const commands = {
DOKKU_SET_ADMIN_PUBKEY(identity: string) { DOKKU_SET_ADMIN_PUBKEY(identity: string) {
@ -6,7 +7,7 @@ const commands = {
}, },
DOKKU_INSTALL_PLUGIN(plugin: string, opts: { source: 'dokku' | 'custom' } = { source: 'dokku' }) { DOKKU_INSTALL_PLUGIN(plugin: string, opts: { source: 'dokku' | 'custom' } = { source: 'dokku' }) {
if (opts.source === 'dokku') { if (opts.source === 'dokku') {
return `dokku plugin:install https://github.com/dokku/${plugin}.git`; return `dokku plugin:install https://github.com/dokku/dokku-${plugin}.git`;
} }
return `dokku plugin:install ${plugin}`; return `dokku plugin:install ${plugin}`;
}, },
@ -24,39 +25,71 @@ const commands = {
}, },
}; };
const tribesClient = (ssh: NodeSSH) => ({ class TribesClient {
ssh: NodeSSH;
constructor(ssh: NodeSSH) {
this.ssh = ssh;
}
/** /**
* Execute a command on the Tribes remote, storing the output for use. Throws an error with stderr if the exit code was nonzero. * Execute a command on the Tribes remote, storing the output for use. Throws an error with stderr if the exit code was nonzero.
* @param name The name of the command to execute. * @param name The name of the command to execute.
* @param args The args for the command. * @param args The args for the command.
* @returns The standard output of the command. * @returns The standard output of the command.
*/ */
async action<K extends keyof typeof commands>(name: K, ...args: Parameters<typeof commands[K]>) { async do<K extends keyof typeof commands>(name: K, ...args: Parameters<typeof commands[K]>) {
const cmd = commands[name]; const cmd = commands[name];
// @ts-ignore this is okay because typescript enforces the correct args at the callsite // @ts-ignore this is okay because typescript enforces the correct args at the callsite
const res = await ssh.execCommand(cmd(...args)); const res = await this.ssh.execCommand(cmd(...args));
if (res.code) throw new Error(`Error executing ${name}: ${res.stderr}`); if (res.code) throw new Error(`Error executing ${name}: ${res.stderr}`);
console.error(res);
return res.stdout; return res.stdout;
}, }
/** /**
* **L**oudly execute a command on the remote, printing the output after execution. * Execute a command on the remote, printing the output after execution.
* @param name The name of the command to execute. * @param name The name of the command to execute.
* @param args The args for the command. * @param args The args for the command.
* @returns The standard output of the command. * @returns The standard output of the command.
*/ */
async actionL<K extends keyof typeof commands>(name: K, ...args: Parameters<typeof commands[K]>) { async loud<K extends keyof typeof commands>(name: K, ...args: Parameters<typeof commands[K]>) {
const stdout = await this.action(name, ...args); const stdout = await this.do(name, ...args);
console.log(stdout); console.log(stdout);
}, }
}); }
interface HandlerOptions {
args: ParsedArgs;
arg: (name: string) => string;
tribes: TribesClient;
domain: string;
}
export const makeSshAction = (handler: (opts: HandlerOptions) => Promise<void> | void) => {
return async (args: ParsedArgs) => {
const arg = (name: string) => args[name] as string;
const domain = args._[0] as string;
const tribes = await connect(domain, arg('identity-file'));
handler({ args, arg, tribes, domain });
};
};
const ssh = new NodeSSH(); const ssh = new NodeSSH();
export const connect = async (host: string, identityFile: string) => { const connect = async (host: string, identityFile: string) => {
if (ssh.isConnected() && ssh.connection?.host === host) return tribesClient(ssh); if (ssh.isConnected() && ssh.connection?.host === host) return new TribesClient(ssh);
await ssh.connect({ await ssh.connect({
host, host,
username: 'root', username: 'root',
privateKeyPath: identityFile, privateKeyPath: identityFile,
}); });
return tribesClient(ssh); return new TribesClient(ssh);
};
export const privkeyToPubkey = async (path: string) => {
const result = await runLocally(`ssh-keygen -y -f ${path}`);
const decoder = new TextDecoder();
if (result.code) {
throw new Error(
`tribes-cli: error generating public key from private key ${path}:` + ` ${decoder.decode(result.stderr)}`,
);
}
return decoder.decode(result.stdout);
}; };