From 72d2aba201453d5b0c339d521d79b83d43d7ef03 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 30 Aug 2016 00:00:02 -0700 Subject: [PATCH 1/8] implement `-e` --- bin/now-deploy | 100 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 90 insertions(+), 10 deletions(-) diff --git a/bin/now-deploy b/bin/now-deploy index 9ef3867..398954e 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,79 @@ 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)`); + val = process.env[key]; + } + } 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, val]; + })); + + 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, From efdd6b8d29f3c209e4c472939c3bdc23ab455f8b Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 30 Aug 2016 00:00:15 -0700 Subject: [PATCH 2/8] index: pass along `env` to `/create` --- lib/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/index.js b/lib/index.js index 3c231ba..c46e200 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, From 08801432c69b432d14db5a1ca9ad62ef152eb335 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 30 Aug 2016 00:00:25 -0700 Subject: [PATCH 3/8] index: add `listSecrets` method --- lib/index.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/index.js b/lib/index.js index c46e200..01eed61 100644 --- a/lib/index.js +++ b/lib/index.js @@ -312,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)}` : ''; From 954be0b9394552c8163f094401caeb05eaf14dcc Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 30 Aug 2016 00:00:38 -0700 Subject: [PATCH 4/8] secrets: fix api calls --- lib/secrets.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) 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 From cedf4a40bf2670c8bc6ad08fd2293e7ea86d0872 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 30 Aug 2016 00:10:20 -0700 Subject: [PATCH 5/8] support for escaping --- bin/now-deploy | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bin/now-deploy b/bin/now-deploy index 398954e..b11dc4d 100755 --- a/bin/now-deploy +++ b/bin/now-deploy @@ -274,7 +274,8 @@ async function sync (token) { return process.exit(1); } else { console.log(`> Reading ${chalk.bold(`"${chalk.bold(key)}"`)} from your env (as no value was specified)`); - val = process.env[key]; + // escape value if it begins with @ + val = process.env[key].replace(/^\@/, '\\@'); } } else { val = val_; @@ -298,7 +299,11 @@ async function sync (token) { } } - return [key, val]; + return [ + key, + // add support for escaping the @ as \@ + val.replace(/^\\@/, '@') + ]; })); let env = {}; From b83dbfb6ae1df30c90953eba2341dcaaaabaa46e Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 30 Aug 2016 23:57:13 -0700 Subject: [PATCH 6/8] index: drastically improve error handling / messages for `create` --- lib/index.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/index.js b/lib/index.js index 01eed61..33b54a1 100644 --- a/lib/index.js +++ b/lib/index.js @@ -203,18 +203,24 @@ export default class Now extends EventEmitter { if (this._debug) console.timeEnd('> [debug] /now/create'); // no retry on 4xx - if (200 !== res.status && (400 <= res.status && 500 > res.status)) { - if (this._debug) { - console.log('> [debug] bailing on creating due to %s', res.status); - } - return bail(responseError(res)); + let body; + try { + body = await res.json(); + } catch (err) { + throw new Error('Unexpected response'); } - if (200 !== res.status) { - throw new Error('Deployment initialization failed'); + if (429 === res.status) { + return bail(responseError(res)); + } else if (res.status >= 400 && res.status < 500) { + const err = new Error(body.error.message); + err.userError = true; + return bail(err); + } else if (200 !== res.status) { + throw new Error(body.error.message); } - return res.json(); + return body; }); // we report about files whose sizes are too big From bf685a0dde87cea43328682312cc34694a840f72 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 30 Aug 2016 23:57:34 -0700 Subject: [PATCH 7/8] now-deploy: do not attempt to unescape non-string env values --- bin/now-deploy | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bin/now-deploy b/bin/now-deploy index b11dc4d..22d67ed 100755 --- a/bin/now-deploy +++ b/bin/now-deploy @@ -301,8 +301,10 @@ async function sync (token) { return [ key, - // add support for escaping the @ as \@ - val.replace(/^\\@/, '@') + typeof val === 'string' + // add support for escaping the @ as \@ + ? val.replace(/^\\@/, '@') + : val ]; })); From d450d4584292aa4787703bba7ae0b781109f2acd Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 30 Aug 2016 23:59:17 -0700 Subject: [PATCH 8/8] merge with master --- bin/now | 2 +- lib/index.js | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/now b/bin/now index 1de2cdf..855bce4 100755 --- a/bin/now +++ b/bin/now @@ -1,7 +1,7 @@ #!/usr/bin/env node import minimist from 'minimist'; import { resolve } from 'path'; -import { spawn } from 'cross-spawn-async'; +import { spawn } from 'cross-spawn'; import checkUpdate from '../lib/check-update'; const argv = minimist(process.argv.slice(2)); diff --git a/lib/index.js b/lib/index.js index 33b54a1..c7b579d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -96,7 +96,7 @@ export default class Now extends EventEmitter { if (!docker.some((cmd) => 'CMD' === cmd.name)) { const e = Error('No `CMD` found in `Dockerfile`. ' + - 'See: https://docs.docker.com/engine/reference/builder/#/run'); + 'See: https://docs.docker.com/engine/reference/builder/#/cmd'); e.userError = true; throw e; } diff --git a/package.json b/package.json index f56619a..df8853b 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "bytes": "2.4.0", "chalk": "1.1.3", "copy-paste": "1.3.0", - "cross-spawn-async": "2.2.4", + "cross-spawn": "4.0.0", "docker-file-parser": "0.1.0", "email-prompt": "0.1.8", "email-validator": "1.0.5",