From 092d994ec541b5dbeea2ee87958de6f477afbdaf Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Thu, 11 Aug 2016 17:04:09 -0700 Subject: [PATCH] initial sketch --- bin/now | 6 +- bin/now-deploy | 1 + bin/now-domains | 2 +- bin/now-secrets | 175 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/secrets.js | 36 ++++++++++ 5 files changed, 216 insertions(+), 4 deletions(-) create mode 100755 bin/now-secrets create mode 100644 lib/secrets.js diff --git a/bin/now b/bin/now index f160437..1de2cdf 100755 --- a/bin/now +++ b/bin/now @@ -19,8 +19,8 @@ const exit = (code) => { }; const defaultCommand = 'deploy'; -const commands = new Set([defaultCommand, 'list', 'ls', 'rm', 'remove', 'alias', 'aliases', 'ln', 'domain', 'domains']); -const aliases = new Map([['ls', 'list'], ['rm', 'remove'], ['ln', 'alias'], ['aliases', 'alias'], ['domain', 'domains']]); +const commands = new Set([defaultCommand, 'list', 'ls', 'rm', 'remove', 'alias', 'aliases', 'ln', 'domain', 'domains', 'secret', 'secrets']); +const aliases = new Map([['ls', 'list'], ['rm', 'remove'], ['ln', 'alias'], ['aliases', 'alias'], ['domain', 'domains'], ['secret', 'secrets']]); let cmd = argv._[0]; let args = []; @@ -41,7 +41,7 @@ if (commands.has(cmd)) { let bin = resolve(__dirname, 'now-' + cmd); if (process.enclose) { - args.unshift("--entrypoint", bin); + args.unshift('--entrypoint', bin); bin = process.execPath; } diff --git a/bin/now-deploy b/bin/now-deploy index 6f7d8ec..1d6f277 100755 --- a/bin/now-deploy +++ b/bin/now-deploy @@ -41,6 +41,7 @@ const help = () => { rm | remove [id] remove a deployment ln | alias [id] [url] configures aliases for deployments domains [name] manages your domain names + secrets [name] manages your env secrets help [cmd] displays complete help for [cmd] ${chalk.dim('Options:')} diff --git a/bin/now-domains b/bin/now-domains index e989c94..b2491d9 100755 --- a/bin/now-domains +++ b/bin/now-domains @@ -21,7 +21,7 @@ const subcommand = argv._[0]; // options const help = () => { console.log(` - ${chalk.bold('𝚫 now domains')} + ${chalk.bold('𝚫 now domains')} ${chalk.dim('Options:')} diff --git a/bin/now-secrets b/bin/now-secrets new file mode 100755 index 0000000..a94a5da --- /dev/null +++ b/bin/now-secrets @@ -0,0 +1,175 @@ +#!/usr/bin/env node +import chalk from 'chalk'; +import minimist from 'minimist'; +import * as cfg from '../lib/cfg'; +import { error } from '../lib/error'; +import NowSecrets from '../lib/secrets'; +import ms from 'ms'; + +const argv = minimist(process.argv.slice(2), { + boolean: ['help', 'debug', 'base64'], + alias: { + help: 'h', + debug: 'd', + base64: 'b' + } +}); +const subcommand = argv._[0]; + +// options +const help = () => { + console.log(` + ${chalk.bold('𝚫 now secrets')} + + ${chalk.dim('Options:')} + + -h, --help output usage information + -b, --base64 treat value as base64-encoded + -d, --debug debug mode [off] + + ${chalk.dim('Examples:')} + + ${chalk.gray('–')} Lists all your secrets: + + ${chalk.cyan('$ now secrets ls')} + + ${chalk.gray('–')} Adds a new secret: + + ${chalk.cyan('$ now secrets add my-secret "my value"')} + + ${chalk.gray('–')} Once added, a secret's value can't be retrieved in plaintext anymore + ${chalk.gray('–')} If the secret's value is more than one word, wrap it in quotes + ${chalk.gray('–')} Actually, when in doubt, wrap your value in quotes + + ${chalk.gray('–')} Exposes a secret as an env variable: + + ${chalk.cyan(`$ now -e MY_SECRET=${chalk.bold('@my-secret')}`)} + + Notice the ${chalk.cyan.bold('`@`')} symbol which makes the value a secret reference. + + ${chalk.gray('–')} Renames a secret: + + ${chalk.cyan(`$ now secrets rename my-secret my-renamed-secret`)} + + ${chalk.gray('–')} Removes a secret: + + ${chalk.cyan(`$ now secrets rm my-secret`)} +`); +}; + +// options +const debug = argv.debug; +const apiUrl = argv.url || 'https://api.zeit.co'; + +const exit = (code) => { + // we give stdout some time to flush out + // because there's a node bug where + // stdout writes are asynchronous + // https://github.com/nodejs/node/issues/6456 + setTimeout(() => process.exit(code || 0), 100); +}; + +if (argv.help || !subcommand) { + help(); + exit(0); +} else { + const config = cfg.read(); + + Promise.resolve(config.token || login(apiUrl)) + .then(async (token) => { + try { + await run(token); + } catch (err) { + if (err.userError) { + error(err.message); + } else { + error(`Unknown error: ${err.stack}`); + } + exit(1); + } + }) + .catch((e) => { + error(`Authentication error – ${e.message}`); + exit(1); + }); +} + +async function run (token) { + const secrets = new NowSecrets(apiUrl, token, { debug }); + const args = argv._.slice(1); + const start = Date.now(); + + if ('ls' === subcommand || 'list' === subcommand) { + if (0 !== args.length) { + error(`Invalid number of arguments. Usage: ${chalk.cyan('`now secret ls`')}`); + return exit(1); + } + + const list = []; + + const elapsed = ms(new Date() - start); + console.log(`> ${list.length} secrets found ${chalk.gray(`[${elapsed}]`)}`); + return secrets.close(); + } + + if ('rm' === subcommand || 'remove' === subcommand) { + if (1 !== args.length) { + error(`Invalid number of arguments. Usage: ${chalk.cyan('`now secret rm `')}`); + return exit(1); + } + + const elapsed = ms(new Date() - start); + console.log(`${chalk.cyan('> Success!')} Secret ${chalk.bold(secret.name)} ${chalk.gray(`(${secret.uid})`)} removed ${chalk.gray(`[${elapsed}]`)}`); + return secrets.close(); + } + + if ('rename' === subcommand) { + if (2 !== args.length) { + error(`Invalid number of arguments. Usage: ${chalk.cyan('`now secret rename `')}`); + return exit(1); + } + + const secret = { + name: 'my-password', + uid: 'sec_iuh32u23bfigf2gu' + }; + + const elapsed = ms(new Date() - start); + console.log(`${chalk.cyan('> Success!')} Secret ${chalk.bold(secret.name)} ${chalk.gray(`(${secret.uid})`)} renamed to ${chalk.bold('my-secret-name')} ${chalk.gray(`[${elapsed}]`)}`); + return secrets.close(); + } + + if ('add' === subcommand || 'set' === subcommand) { + if (2 !== args.length) { + error(`Invalid number of arguments. Usage: ${chalk.cyan('`now secret add `')}`); + if (args.length > 2) { + const [, ...rest] = args; + console.log('> If your secret has spaces, make sure to wrap it in quotes. Example: \n' + + ` ${chalk.cyan('$ now secret add ${args[0]} "${escaped}"')} `); + } + return exit(1); + } + + const [name, value_] = args; + + let value; + if (argv.base64) { + value = { base64: value_ }; + } else { + value = value_; + } + + const secret = { + name: 'my-password', + uid: 'sec_iuh32u23bfigf2gu' + }; + + const elapsed = ms(new Date() - start); + console.log(`${chalk.cyan('> Success!')} Secret ${chalk.bold(secret.name)} ${chalk.gray(`(${secret.uid})`)} added ${chalk.gray(`[${elapsed}]`)}`); + return secrets.close(); + } + + error('Please specify a valid subcommand: ls | add | rename | rm'); + help(); + exit(1); +} diff --git a/lib/secrets.js b/lib/secrets.js new file mode 100644 index 0000000..b4b2820 --- /dev/null +++ b/lib/secrets.js @@ -0,0 +1,36 @@ +import Now from '../lib'; + +export default class Secrets extends Now { + + async ls () { + return this.retry(async (bail, attempt) => { + if (this._debug) console.time(`> [debug] #${attempt} GET /secrets`); + const res = await this._fetch('/secrets'); + if (this._debug) console.timeEnd(`> [debug] #${attempt} GET /secrets`); + const body = await res.json(); + return body.secrets; + }); + } + + async rm (nameOrId) { + return this.retry(async (bail, attempt) => { + if (this._debug) console.time(`> [debug] #${attempt} DELETE /secrets/${nameOrId}`); + const res = await this._fetch(`/secrets/${nameOrId}`, { method: 'DELETE' }); + if (this._debug) console.timeEnd(`> [debug] #${attempt} DELETE /secrets/${nameOrId}`); + + if (403 === res.status) { + return bail(new Error('Unauthorized')); + } + + if (res.status !== 200) { + const body = await res.json(); + throw new Error(body.error.message); + } + }); + } + + async add (name, value) { + + } + +}