diff --git a/tribes-cli/remote/init.ts b/tribes-cli/remote/init.ts index 2201c023..441c024c 100644 --- a/tribes-cli/remote/init.ts +++ b/tribes-cli/remote/init.ts @@ -1,3 +1,18 @@ -import { Command } from '../utils/mod.ts'; +import { Command, connect } 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', { + 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'); + }); diff --git a/tribes-cli/utils/ssh/mod.ts b/tribes-cli/utils/ssh/mod.ts new file mode 100644 index 00000000..2a3cb1a0 --- /dev/null +++ b/tribes-cli/utils/ssh/mod.ts @@ -0,0 +1,62 @@ +import { NodeSSH } from 'node-ssh'; + +const commands = { + DOKKU_SET_ADMIN_PUBKEY(identity: string) { + return `bash -c "echo \\"${identity}\\" | dokku ssh-keys:add admin"`; + }, + 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 ${plugin}`; + }, + DOKKU_CREATE_POSTGRES_SERVICE(name: string) { + return `dokku postgres:create ${name}`; + }, + DOKKU_SET_GLOBAL_DOMAIN(domain: string) { + return `dokku domains:set-global "${domain}"`; + }, + DOKKU_LETSENCRYPT_SETUP_EMAIL(email: string) { + return `dokku letsencrypt:set --global email "${email}"`; + }, + DOKKU_LETSENCRYPT_CRON() { + return 'dokku letsencrypt:cron-job --add'; + }, +}; + +const tribesClient = (ssh: NodeSSH) => ({ + /** + * 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) { + 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)); + if (res.code) throw new Error(`Error executing ${name}: ${res.stderr}`); + return res.stdout; + }, + /** + * **L**oudly 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); + console.log(stdout); + }, +}); + +const ssh = new NodeSSH(); +export const connect = async (host: string, identityFile: string) => { + if (ssh.isConnected() && ssh.connection?.host === host) return tribesClient(ssh); + await ssh.connect({ + host, + username: 'root', + privateKeyPath: identityFile, + }); + return tribesClient(ssh); +};