diff --git a/tribes-cli/remote/init.ts b/tribes-cli/remote/init.ts index 441c024c..bdb7aed7 100644 --- a/tribes-cli/remote/init.ts +++ b/tribes-cli/remote/init.ts @@ -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') .option('-e --email', { description: "The e-mail address to use for requesting Let's Encrypt certificates.", }) - .setAction(async (args) => { - const arg = (name: string) => args[name] as string; - const domain = args._[0] as string; - const tribes = await connect(domain, arg('identity-file')); - await tribes.actionL('DOKKU_SET_ADMIN_PUBKEY', await Deno.readTextFile(arg('identity-file'))); - await tribes.actionL('DOKKU_INSTALL_PLUGIN', 'postgres'); - await tribes.actionL('DOKKU_INSTALL_PLUGIN', 'letsencrypt'); - await tribes.actionL('DOKKU_CREATE_POSTGRES_SERVICE', 'dittodb'); - await tribes.actionL('DOKKU_SET_GLOBAL_DOMAIN', domain); - await tribes.actionL('DOKKU_LETSENCRYPT_SETUP_EMAIL', arg('email')); - await tribes.actionL('DOKKU_LETSENCRYPT_CRON'); - }); + .setAction(makeSshAction(async ({ arg, tribes, domain }) => { + const pubkey = await privkeyToPubkey(arg('identity-file')); + await tribes.loud('DOKKU_SET_ADMIN_PUBKEY', pubkey); + await tribes.loud('DOKKU_INSTALL_PLUGIN', 'postgres'); + await tribes.loud('DOKKU_INSTALL_PLUGIN', 'letsencrypt'); + await tribes.loud('DOKKU_CREATE_POSTGRES_SERVICE', 'dittodb'); + await tribes.loud('DOKKU_SET_GLOBAL_DOMAIN', domain); + await tribes.loud('DOKKU_LETSENCRYPT_SETUP_EMAIL', arg('email')); + await tribes.loud('DOKKU_LETSENCRYPT_CRON'); + })); diff --git a/tribes-cli/utils/ssh/mod.ts b/tribes-cli/utils/ssh/mod.ts index 2a3cb1a0..9f7f9667 100644 --- a/tribes-cli/utils/ssh/mod.ts +++ b/tribes-cli/utils/ssh/mod.ts @@ -1,4 +1,5 @@ import { NodeSSH } from 'node-ssh'; +import { ParsedArgs, runLocally } from '../mod.ts'; const commands = { DOKKU_SET_ADMIN_PUBKEY(identity: string) { @@ -6,7 +7,7 @@ const commands = { }, DOKKU_INSTALL_PLUGIN(plugin: string, opts: { source: 'dokku' | 'custom' } = { 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}`; }, @@ -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. * @param name The name of the command to execute. * @param args The args for the command. * @returns The standard output of the command. */ - async action(name: K, ...args: Parameters) { + async do(name: K, ...args: Parameters) { const cmd = commands[name]; // @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}`); + console.error(res); 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 args The args for the command. * @returns The standard output of the command. */ - async actionL(name: K, ...args: Parameters) { - const stdout = await this.action(name, ...args); + async loud(name: K, ...args: Parameters) { + const stdout = await this.do(name, ...args); console.log(stdout); - }, -}); + } +} + +interface HandlerOptions { + args: ParsedArgs; + arg: (name: string) => string; + tribes: TribesClient; + domain: string; +} + +export const makeSshAction = (handler: (opts: HandlerOptions) => Promise | 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(); -export const connect = async (host: string, identityFile: string) => { - if (ssh.isConnected() && ssh.connection?.host === host) return tribesClient(ssh); +const connect = async (host: string, identityFile: string) => { + if (ssh.isConnected() && ssh.connection?.host === host) return new TribesClient(ssh); await ssh.connect({ host, username: 'root', 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); };