From a3fe6eda84ee6d244931f6f73d2c5a1543f36f84 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 30 Aug 2016 11:48:20 -0700 Subject: [PATCH] Add -e (#126) * implement `-e` * index: pass along `env` to `/create` * index: add `listSecrets` method * secrets: fix api calls * support for escaping --- bin/now-deploy | 105 ++++++++++++++++++++++++++++++++++++++++++++----- lib/index.js | 12 ++++++ lib/secrets.js | 14 ++----- 3 files changed, 111 insertions(+), 20 deletions(-) diff --git a/bin/now-deploy b/bin/now-deploy index 9ef3867..b11dc4d 100755 --- a/bin/now-deploy +++ b/bin/now-deploy @@ -20,6 +20,7 @@ const argv = minimist(process.argv.slice(2), { string: ['config', 'token'], boolean: ['help', 'version', 'debug', 'force', 'login', 'no-clipboard', 'forward-npm', 'docker', 'npm'], alias: { + env: 'e', help: 'h', config: 'c', debug: 'd', @@ -49,16 +50,17 @@ const help = () => { ${chalk.dim('Options:')} - -h, --help output usage information - -v, --version output the version number - -c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline('FILE')} config file - -d, --debug debug mode [off] - -f, --force force a new deployment even if nothing has changed - -t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline('TOKEN')} login token - -L, --login configure login - -p, --public deployment is public (\`/_src\` is exposed) [on for oss, off for premium] - -C, --no-clipboard do not attempt to copy URL to clipboard - -N, --forward-npm Forward login information to install private NPM modules + -h, --help output usage information + -v, --version output the version number + -c ${chalk.underline('FILE')}, --config=${chalk.underline('FILE')} config file + -d, --debug debug mode [off] + -f, --force force a new deployment even if nothing has changed + -t ${chalk.underline('TOKEN')}, --token=${chalk.underline('TOKEN')} login token + -L, --login configure login + -p, --public deployment is public (\${clalk.dim('`/_src`')} is exposed) [on for oss, off for premium] + -e, --env include an env var (e.g.: ${chalk.dim('`-e KEY=value`')}). Can appear many times. + -C, --no-clipboard do not attempt to copy URL to clipboard + -N, --forward-npm forward login information to install private NPM modules ${chalk.dim('Examples:')} @@ -78,6 +80,14 @@ const help = () => { ${chalk.cyan('$ now alias deploymentId custom-domain.com')} + ${chalk.gray('–')} Stores a secret + + ${chalk.cyan('$ now secret add mysql-password 123456')} + + ${chalk.gray('–')} Deploys with ENV vars (using the ${chalk.dim('`mysql-password`')} secret stored above) + + ${chalk.cyan('$ now -e NODE_ENV=production -e MYSQL_PASSWORD=@mysql-password')} + ${chalk.gray('–')} Displays comprehensive help for the subcommand ${chalk.dim('`list`')} ${chalk.cyan('$ now help list')} @@ -229,9 +239,84 @@ async function sync (token) { const now = new Now(apiUrl, token, { debug }); + const envs = [].concat(argv.env); + + let secrets; + const findSecret = async (uidOrName) => { + if (!secrets) secrets = await now.listSecrets(); + return secrets.filter((secret) => { + return secret.name === uidOrName || secret.uid === uidOrName; + }); + }; + + const env_ = await Promise.all(envs.map(async (kv) => { + const [key, val_, ...rest] = kv.split('='); + let val; + + if (rest.length) { + error(`Invalid env ${chalk.bold(`"${kv}"`)}. It cannot contain more than one ${chalk.dim(`=`)} symbol`); + return process.exit(1); + } + + if (/[^A-z0-9_]/i.test(key)) { + error(`Invalid ${chalk.dim('-e')} key ${chalk.bold(`"${chalk.bold(key)}"`)}. Only letters, digits and underscores are allowed.`); + return process.exit(1); + } + + if ('' === key || null == key) { + error(`Invalid env option ${chalk.bold(`"${kv}"`)}`); + return process.exit(1); + } + + if (val_ == null) { + if (!(key in process.env)) { + error(`No value specified for env ${chalk.bold(`"${chalk.bold(key)}"`)} and it was not found in your env.`); + return process.exit(1); + } else { + console.log(`> Reading ${chalk.bold(`"${chalk.bold(key)}"`)} from your env (as no value was specified)`); + // escape value if it begins with @ + val = process.env[key].replace(/^\@/, '\\@'); + } + } else { + val = val_; + } + + if ('@' === val[0]) { + const uidOrName = val.substr(1); + const secrets = await findSecret(uidOrName); + if (secrets.length === 0) { + if ('' === uidOrName) { + error(`Empty reference provided for env key ${chalk.bold(`"${chalk.bold(key)}"`)}`); + } else { + error(`No secret found by uid or name ${chalk.bold(`"${uidOrName}"`)}`); + } + return process.exit(1); + } else if (secrets.length > 1) { + error(`Ambiguous secret ${chalk.bold(`"${uidOrName}"`)} (matches ${chalk.bold(secrets.length)} secrets)`); + return process.exit(1); + } else { + val = { uid: secrets[0].uid }; + } + } + + return [ + key, + // add support for escaping the @ as \@ + val.replace(/^\\@/, '@') + ]; + })); + + let env = {}; + env_ + .filter(v => !!v) + .forEach(([key, val]) => { + if (key in env) console.log(`> ${chalk.yellow('NOTE:')} Overriding duplicate env key ${chalk.bold(`"${key}"`)}`); + env[key] = val + }); try { await now.create(path, { + env, deploymentType, forceNew, forceSync, diff --git a/lib/index.js b/lib/index.js index 6514e8c..b85a120 100644 --- a/lib/index.js +++ b/lib/index.js @@ -37,6 +37,7 @@ export default class Now extends EventEmitter { async create (path, { wantsPublic, quiet = false, + env = {}, forceNew = false, forceSync = false, forwardNpm = false, @@ -177,6 +178,7 @@ export default class Now extends EventEmitter { const res = await this._fetch('/now/create', { method: 'POST', body: { + env, public: wantsPublic, forceNew, forceSync, @@ -310,6 +312,16 @@ export default class Now extends EventEmitter { uploadChunk(); } + async listSecrets () { + return this.retry(async (bail, attempt) => { + if (this._debug) console.time(`> [debug] #${attempt} GET /secrets`); + const res = await this._fetch('/now/secrets'); + if (this._debug) console.timeEnd(`> [debug] #${attempt} GET /secrets`); + const body = await res.json(); + return body.secrets; + }); + } + async list (app) { const query = app ? `?app=${encodeURIComponent(app)}` : ''; diff --git a/lib/secrets.js b/lib/secrets.js index a196dfb..c7080dd 100644 --- a/lib/secrets.js +++ b/lib/secrets.js @@ -3,19 +3,13 @@ import Now from '../lib'; export default class Secrets extends Now { 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; - }); + return this.listSecrets(); } 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' }); + const res = await this._fetch(`/now/secrets/${nameOrId}`, { method: 'DELETE' }); if (this._debug) console.timeEnd(`> [debug] #${attempt} DELETE /secrets/${nameOrId}`); if (403 === res.status) { @@ -41,7 +35,7 @@ export default class Secrets extends Now { add (name, value) { return this.retry(async (bail, attempt) => { if (this._debug) console.time(`> [debug] #${attempt} POST /secrets`); - const res = await this._fetch('/secrets', { + const res = await this._fetch('/now/secrets', { method: 'POST', body: { name, @@ -73,7 +67,7 @@ export default class Secrets extends Now { rename (nameOrId, newName) { return this.retry(async (bail, attempt) => { if (this._debug) console.time(`> [debug] #${attempt} PATCH /secrets/${nameOrId}`); - const res = await this._fetch(`/secrets/${nameOrId}`, { + const res = await this._fetch(`/now/secrets/${nameOrId}`, { method: 'PATCH', body: { name: newName