mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
make command execution over ssh way neater
This commit is contained in:
parent
267e8e8930
commit
b4f8cd894f
2 changed files with 57 additions and 26 deletions
|
|
@ -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');
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue