From 5a49cf01020f6387f6a50644cf7573e1ba103b39 Mon Sep 17 00:00:00 2001 From: Leo Lamprecht Date: Wed, 30 Aug 2017 13:03:38 +0200 Subject: [PATCH] Added `now scale` and `now alias` sub commands --- package-lock.json | 64 +++ package.json | 5 + src/providers/sh/commands/bin/alias.js | 533 +++++++++++++++++++++++++ src/providers/sh/commands/bin/scale.js | 392 ++++++++++++++++++ src/providers/sh/index.js | 8 +- 5 files changed, 1001 insertions(+), 1 deletion(-) create mode 100644 src/providers/sh/commands/bin/alias.js create mode 100644 src/providers/sh/commands/bin/scale.js diff --git a/package-lock.json b/package-lock.json index d1f5a58..e442e6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2347,6 +2347,12 @@ "tapable": "0.2.8" } }, + "epipebomb": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/epipebomb/-/epipebomb-1.0.0.tgz", + "integrity": "sha1-V9He2h1ryBYisRinX+a1p9NPbYg=", + "dev": true + }, "errno": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz", @@ -5070,6 +5076,12 @@ "integrity": "sha1-aYhLoUSsM/5plzemCG3v+t0PicU=", "dev": true }, + "lodash.range": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash.range/-/lodash.range-3.2.0.tgz", + "integrity": "sha1-9GHliPZmg/fq3q3lE+OKaaVloV0=", + "dev": true + }, "log-symbols": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", @@ -6214,6 +6226,12 @@ "integrity": "sha1-WdrcaDNF7GuI+IuU7Urn4do5S/4=", "dev": true }, + "printf": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/printf/-/printf-0.2.5.tgz", + "integrity": "sha1-xDjKLKM+OSdnHbSracDlL5NqTw8=", + "dev": true + }, "private": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/private/-/private-0.1.7.tgz", @@ -6262,6 +6280,12 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, + "psl": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.20.tgz", + "integrity": "sha512-JWUi+8DYZnEn9vfV0ppHFLBP0Lk7wxzpobILpBEMDV4nFket4YK+6Rn1Zn6DHmD9PqqsV96AM6l4R/2oirzkgw==", + "dev": true + }, "public-encrypt": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz", @@ -6804,6 +6828,46 @@ "integrity": "sha1-XKsQ6FGqccZnt3th/hux2QpguqQ=", "dev": true }, + "single-line-log": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-1.1.2.tgz", + "integrity": "sha1-wvg/Jzo+GhbtsJlWYdoO1e8DM2Q=", + "dev": true, + "requires": { + "string-width": "1.0.2" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + } + } + }, "slackup": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/slackup/-/slackup-2.2.1.tgz", diff --git a/package.json b/package.json index 20bb8d9..c5c211c 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "download": "6.2.5", "email-prompt": "0.3.1", "email-validator": "1.1.1", + "epipebomb": "1.0.0", "eslint": "4.4.1", "eslint-plugin-flowtype": "2.35.0", "flow-babel-webpack-plugin": "1.1.0", @@ -115,6 +116,7 @@ "inquirer": "3.2.2", "is-url": "1.2.2", "lint-staged": "4.0.3", + "lodash.range": "3.2.0", "mkdirp-promise": "5.0.1", "mri": "1.1.0", "ms": "2.0.0", @@ -125,9 +127,12 @@ "pkg": "4.2.3", "pre-commit": "1.2.2", "prettier": "1.5.3", + "printf": "0.2.5", "progress": "2.0.0", + "psl": "1.1.20", "resumer": "0.0.0", "shebang-loader": "0.0.1", + "single-line-log": "1.1.2", "slackup": "2.2.1", "socket.io-client": "2.0.3", "split-array": "1.0.1", diff --git a/src/providers/sh/commands/bin/alias.js b/src/providers/sh/commands/bin/alias.js new file mode 100644 index 0000000..90bec07 --- /dev/null +++ b/src/providers/sh/commands/bin/alias.js @@ -0,0 +1,533 @@ +#!/usr/bin/env node + +// Packages +const chalk = require('chalk') +const minimist = require('minimist') +const table = require('text-table') +const ms = require('ms') +const printf = require('printf') +require('epipebomb')() +const supportsColor = require('supports-color') + +// Ours +const strlen = require('../lib/strlen') +const NowAlias = require('../lib/alias') +const NowDomains = require('../lib/domains') +const login = require('../lib/login') +const cfg = require('../lib/cfg') +const { handleError, error } = require('../lib/error') +const toHost = require('../lib/to-host') +const { reAlias } = require('../lib/re-alias') +const exit = require('../lib/utils/exit') +const info = require('../lib/utils/output/info') +const logo = require('../lib/utils/output/logo') +const promptBool = require('../lib/utils/input/prompt-bool') + +const grayWidth = 10 +const underlineWidth = 11 + +// Options +const help = () => { + console.log(` + ${chalk.bold(`${logo} now alias`)} + + ${chalk.dim('Options:')} + + -h, --help Output usage information + -c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline( + 'FILE' + )} Config file + -r ${chalk.bold.underline('RULES_FILE')}, --rules=${chalk.bold.underline( + 'RULES_FILE' + )} Rules file + -d, --debug Debug mode [off] + -t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline( + 'TOKEN' + )} Login token + + ${chalk.dim('Examples:')} + + ${chalk.gray('–')} Lists all your aliases: + + ${chalk.cyan('$ now alias ls')} + + ${chalk.gray('–')} Adds a new alias to ${chalk.underline('my-api.now.sh')}: + + ${chalk.cyan( + `$ now alias set ${chalk.underline( + 'api-ownv3nc9f8.now.sh' + )} ${chalk.underline('my-api.now.sh')}` + )} + + The ${chalk.dim('`.now.sh`')} suffix can be ommited: + + ${chalk.cyan('$ now alias set api-ownv3nc9f8 my-api')} + + The deployment id can be used as the source: + + ${chalk.cyan('$ now alias set deploymentId my-alias')} + + Custom domains work as alias targets: + + ${chalk.cyan( + `$ now alias set ${chalk.underline( + 'api-ownv3nc9f8.now.sh' + )} ${chalk.underline('my-api.com')}` + )} + + ${chalk.dim('–')} The subcommand ${chalk.dim( + '`set`' + )} is the default and can be skipped. + ${chalk.dim('–')} ${chalk.dim( + '`http(s)://`' + )} in the URLs is unneeded / ignored. + + ${chalk.gray('–')} Add and modify path based aliases for ${chalk.underline( + 'zeit.ninja' + )}: + + ${chalk.cyan( + `$ now alias ${chalk.underline('zeit.ninja')} -r ${chalk.underline( + 'rules.json' + )}` + )} + + Export effective routing rules: + + ${chalk.cyan( + `$ now alias ls aliasId --json > ${chalk.underline('rules.json')}` + )} + + ${chalk.cyan(`$ now alias ls zeit.ninja`)} + + ${chalk.gray('–')} Removing an alias: + + ${chalk.cyan('$ now alias rm aliasId')} + + To get the list of alias ids, use ${chalk.dim('`now alias ls`')}. + + ${chalk.dim('Alias:')} ln +`) +} + +// Options +const debug = false +const apiUrl = 'https://api.zeit.co' + +let argv +let subcommand + +const main = async ctx => { + argv = minimist(ctx.argv.slice(2), { + string: ['config', 'token', 'rules'], + boolean: ['help', 'debug'], + alias: { + help: 'h', + config: 'c', + rules: 'r', + debug: 'd', + token: 't' + } + }) + + argv._ = argv._.slice(1) + subcommand = argv._[0] + + if (argv.help) { + help() + process.exit(0) + } + + const config = await cfg.read({ token: argv.token }) + + let token + try { + token = config.token || (await login(apiUrl)) + } catch (err) { + error(`Authentication error – ${err.message}`) + exit(1) + } + + try { + await run({ token, config }) + } catch (err) { + if (err.userError) { + error(err.message) + } else { + error(`Unknown error: ${err}\n${err.stack}`) + } + exit(1) + } +} + +module.exports = async ctx => { + try { + await main(ctx) + } catch (err) { + handleError(err) + process.exit(1) + } +} + +async function run({ token, config: { currentTeam, user } }) { + const alias = new NowAlias({ apiUrl, token, debug, currentTeam }) + const domains = new NowDomains({ apiUrl, token, debug, currentTeam }) + const args = argv._.slice(1) + + switch (subcommand) { + case 'ls': + case 'list': { + if (args.length === 1) { + const list = await alias.listAliases() + const item = list.find( + e => e.uid === argv._[1] || e.alias === argv._[1] + ) + if (!item || !item.rules) { + error(`Could not match path alias for: ${argv._[1]}`) + return exit(1) + } + + if (argv.json) { + console.log(JSON.stringify({ rules: item.rules }, null, 2)) + } else { + const header = [ + ['', 'pathname', 'method', 'dest'].map(s => chalk.dim(s)) + ] + const text = + list.length === 0 + ? null + : table( + header.concat( + item.rules.map(rule => { + return [ + '', + rule.pathname ? rule.pathname : '', + rule.method ? rule.method : '*', + rule.dest + ] + }) + ), + { + align: ['l', 'l', 'l', 'l'], + hsep: ' '.repeat(2), + stringLength: strlen + } + ) + + console.log(text) + } + break + } else if (args.length !== 0) { + error( + `Invalid number of arguments. Usage: ${chalk.cyan('`now alias ls`')}` + ) + return exit(1) + } + + const start_ = new Date() + const aliases = await alias.ls() + aliases.sort((a, b) => new Date(b.created) - new Date(a.created)) + const current = new Date() + const sourceUrlLength = + aliases.reduce((acc, i) => { + return Math.max(acc, (i.deployment && i.deployment.url.length) || 0) + }, 0) + 9 + const aliasLength = + aliases.reduce((acc, i) => { + return Math.max(acc, (i.alias && i.alias.length) || 0) + }, 0) + 8 + const elapsed_ = ms(new Date() - start_) + console.log( + `> ${aliases.length} alias${aliases.length === 1 + ? '' + : 'es'} found ${chalk.gray(`[${elapsed_}]`)} under ${chalk.bold( + (currentTeam && currentTeam.slug) || user.username || user.email + )}` + ) + console.log() + + if (supportsColor) { + const urlSpecHeader = `%-${sourceUrlLength + 1}s` + const aliasSpecHeader = `%-${aliasLength + 1}s` + console.log( + printf( + ` ${chalk.gray(urlSpecHeader + ' ' + aliasSpecHeader + ' %5s')}`, + 'source', + 'url', + 'age' + ) + ) + } else { + const urlSpecHeader = `%-${sourceUrlLength}s` + const aliasSpecHeader = `%-${aliasLength}s` + console.log( + printf( + ` ${urlSpecHeader} ${aliasSpecHeader} %5s`, + 'source', + 'url', + 'age' + ) + ) + } + + let text = '' + aliases.forEach(_alias => { + let urlSpec = sourceUrlLength + let aliasSpec = aliasLength + let ageSpec = 5 + const _url = chalk.underline(_alias.alias) + let _sourceUrl + if (supportsColor) { + aliasSpec += underlineWidth + ageSpec += grayWidth + } + if (_alias.deployment) { + _sourceUrl = chalk.underline(_alias.deployment.url) + if (supportsColor) { + urlSpec += grayWidth + } + } else if (_alias.rules) { + _sourceUrl = chalk.gray( + `[${_alias.rules.length} custom rule${_alias.rules.length > 1 + ? 's' + : ''}]` + ) + if (supportsColor) { + urlSpec += underlineWidth + } + } else { + _sourceUrl = chalk.gray('') + } + + const time = chalk.gray(ms(current - new Date(_alias.created))) + text += printf( + ` %-${urlSpec}s %-${aliasSpec}s %${ageSpec}s\n`, + _sourceUrl, + _url, + time + ) + }) + + console.log(text) + break + } + case 'rm': + case 'remove': { + const _target = String(args[0]) + if (!_target) { + const err = new Error('No alias id specified') + err.userError = true + throw err + } + + if (args.length !== 1) { + error( + `Invalid number of arguments. Usage: ${chalk.cyan( + '`now alias rm `' + )}` + ) + return exit(1) + } + + const _aliases = await alias.ls() + const _alias = findAlias(_target, _aliases) + + if (!_alias) { + const err = new Error( + `Alias not found by "${_target}" under ${chalk.bold( + (currentTeam && currentTeam.slug) || user.username || user.email + )}. Run ${chalk.dim('`now alias ls`')} to see your aliases.` + ) + err.userError = true + throw err + } + + try { + const confirmation = await confirmDeploymentRemoval(alias, _alias) + if (!confirmation) { + info('Aborted') + return process.exit(0) + } + + const start = new Date() + await alias.rm(_alias) + const elapsed = ms(new Date() - start) + console.log( + `${chalk.cyan('> Success!')} Alias ${chalk.bold( + _alias.uid + )} removed [${elapsed}]` + ) + } catch (err) { + error(err) + exit(1) + } + + break + } + case 'add': + case 'set': { + if (argv.rules) { + await updatePathAlias(alias, argv._[0], argv.rules, domains) + break + } + if (args.length !== 2) { + error( + `Invalid number of arguments. Usage: ${chalk.cyan( + '`now alias set `' + )}` + ) + return exit(1) + } + await alias.set( + String(args[0]), + String(args[1]), + domains, + currentTeam, + user + ) + break + } + default: { + if (argv._.length === 0) { + await reAlias( + token, + null, + null, + help, + exit, + apiUrl, + debug, + alias, + currentTeam, + user + ) + break + } + + if (argv.rules) { + await updatePathAlias(alias, argv._[0], argv.rules, domains) + break + } + + if (argv._.length === 1) { + await reAlias( + token, + null, + String(argv._[0]), + help, + exit, + apiUrl, + debug, + alias, + currentTeam, + user + ) + break + } else if (argv._.length === 2) { + await alias.set( + String(argv._[0]), + String(argv._[1]), + domains, + currentTeam, + user + ) + } else if (argv._.length >= 3) { + error('Invalid number of arguments') + help() + exit(1) + } else { + error('Please specify a valid subcommand: ls | set | rm') + help() + exit(1) + } + } + } + + domains.close() + alias.close() +} + +async function confirmDeploymentRemoval(alias, _alias) { + const time = chalk.gray(ms(new Date() - new Date(_alias.created)) + ' ago') + const _sourceUrl = _alias.deployment + ? chalk.underline(_alias.deployment.url) + : null + const tbl = table( + [ + [ + _alias.uid, + ...(_sourceUrl ? [_sourceUrl] : []), + chalk.underline(_alias.alias), + time + ] + ], + { hsep: ' '.repeat(6) } + ) + + const msg = + '> The following alias will be removed permanently\n' + + ` ${tbl} \nAre you sure?` + + return promptBool(msg, { + trailing: '\n' + }) +} + +function findAlias(alias, list) { + let key + let val + + if (/\./.test(alias)) { + val = toHost(alias) + key = 'alias' + } else { + val = alias + key = 'uid' + } + + const _alias = list.find(d => { + if (d[key] === val) { + if (debug) { + console.log(`> [debug] matched alias ${d.uid} by ${key} ${val}`) + } + + return true + } + + // Match prefix + if (`${val}.now.sh` === d.alias) { + if (debug) { + console.log(`> [debug] matched alias ${d.uid} by url ${d.host}`) + } + + return true + } + + return false + }) + + return _alias +} + +async function updatePathAlias(alias, aliasName, rules, domains) { + const start = new Date() + const res = await alias.updatePathBasedroutes( + String(aliasName), + rules, + domains + ) + const elapsed = ms(new Date() - start) + if (res.error) { + const err = new Error(res.error.message) + err.userError = true + throw err + } else { + console.log( + `${chalk.cyan( + '> Success!' + )} ${res.ruleCount} rules configured for ${chalk.underline( + res.alias + )} [${elapsed}]` + ) + } +} diff --git a/src/providers/sh/commands/bin/scale.js b/src/providers/sh/commands/bin/scale.js new file mode 100644 index 0000000..07f2542 --- /dev/null +++ b/src/providers/sh/commands/bin/scale.js @@ -0,0 +1,392 @@ +#!/usr/bin/env node + +// Packages +const chalk = require('chalk') +const isURL = require('is-url') +const minimist = require('minimist') +const ms = require('ms') +const printf = require('printf') +require('epipebomb')() +const supportsColor = require('supports-color') + +// Ours +const cfg = require('../lib/cfg') +const { handleError, error } = require('../lib/error') +const NowScale = require('../lib/scale') +const login = require('../lib/login') +const exit = require('../lib/utils/exit') +const logo = require('../lib/utils/output/logo') +const info = require('../lib/scale-info') +const sort = require('../lib/sort-deployments') +const success = require('../lib/utils/output/success') + +let id +let scaleArg +let optionalScaleArg + +// Options +const help = () => { + console.log(` + ${chalk.bold(`${logo} now scale`)} ls + ${chalk.bold(`${logo} now scale`)} + ${chalk.bold(`${logo} now scale`)} [max] + + ${chalk.dim('Options:')} + + -h, --help Output usage information + -c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline( + 'FILE' + )} Config file + -d, --debug Debug mode [off] + + ${chalk.dim('Examples:')} + + ${chalk.gray('–')} Create a deployment with 3 instances, never sleeps: + + ${chalk.cyan('$ now scale my-deployment-ntahoeato.now.sh 3')} + + ${chalk.gray('–')} Create an automatically scaling deployment: + + ${chalk.cyan('$ now scale my-deployment-ntahoeato.now.sh 1 5')} + + ${chalk.gray( + '–' + )} Create an automatically scaling deployment without specifying max: + + ${chalk.cyan('$ now scale my-deployment-ntahoeato.now.sh 1 auto')} + + ${chalk.gray( + '–' + )} Create an automatically scaling deployment without specifying min or max: + + ${chalk.cyan('$ now scale my-deployment-ntahoeato.now.sh auto')} + + ${chalk.gray( + '–' + )} Create a deployment that is always active and never "sleeps": + + ${chalk.cyan('$ now scale my-deployment-ntahoeato.now.sh 1')} + `) +} + +// Options +const debug = false +const apiUrl = 'https://api.zeit.co' + +let argv + +const main = async ctx => { + argv = minimist(ctx.argv.slice(2), { + string: ['config', 'token'], + boolean: ['help', 'debug'], + alias: { help: 'h', config: 'c', debug: 'd', token: 't' } + }) + + argv._ = argv._.slice(1) + + id = argv._[0] + scaleArg = argv._[1] + optionalScaleArg = argv._[2] + + if (argv.help) { + help() + process.exit(0) + } + + const config = await cfg.read({ token: argv.token }) + + let token + try { + token = config.token || (await login(apiUrl)) + } catch (err) { + error(`Authentication error – ${err.message}`) + exit(1) + } + + try { + await run({ token, config }) + } catch (err) { + if (err.userError) { + error(err.message) + } else { + error(`Unknown error: ${err}\n${err.stack}`) + } + exit(1) + } +} + +module.exports = async ctx => { + try { + await main(ctx) + } catch (err) { + handleError(err) + process.exit(1) + } +} + +function guessParams() { + if (Number.isInteger(scaleArg) && !optionalScaleArg) { + return { min: scaleArg, max: scaleArg } + } else if (Number.isInteger(scaleArg) && Number.isInteger(optionalScaleArg)) { + return { min: scaleArg, max: optionalScaleArg } + } else if (Number.isInteger(scaleArg) && optionalScaleArg === 'auto') { + return { min: scaleArg, max: 'auto' } + } else if ( + (!scaleArg && !optionalScaleArg) || + (scaleArg === 'auto' && !optionalScaleArg) + ) { + return { min: 1, max: 'auto' } + } + help() + process.exit(1) +} + +function isHostNameOrId(str) { + return ( + /(https?:\/\/)?((?:(?=[a-z0-9-]{1,63}\.)(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\.)+[a-z]{2,63})/.test( + str + ) || str.length === 28 + ) +} + +async function run({ token, config: { currentTeam } }) { + const scale = new NowScale({ apiUrl, token, debug, currentTeam }) + const start = Date.now() + + if (id === 'ls') { + await list(scale) + process.exit(0) + } else if (id === 'info') { + await info(scale) + process.exit(0) + } else if (id && isHostNameOrId(id)) { + // Normalize URL by removing slash from the end + if (isURL(id)) { + id = id.replace(/^https:\/\//i, '') + if (id.slice(-1) === '/') { + id = id.slice(0, -1) + } + } + } else { + error('Please specify a deployment: now scale ') + help() + exit(1) + } + + const deployments = await scale.list() + + let match = deployments.find(d => { + // `url` should match the hostname of the deployment + let u = id.replace(/^https:\/\//i, '') + + if (u.indexOf('.') === -1) { + // `.now.sh` domain is implied if just the subdomain is given + u += '.now.sh' + } + return d.uid === id || d.name === id || d.url === u + }) + + if (!match) { + // Maybe it's an alias + const aliasDeployment = (await scale.listAliases()).find( + e => e.alias === id + ) + if (!aliasDeployment) { + error(`Could not find any deployments matching ${id}`) + return process.exit(1) + } + match = deployments.find(d => { + return d.uid === aliasDeployment.deploymentId + }) + } + + const { min, max } = guessParams() + + if ( + !(Number.isInteger(min) || min === 'auto') && + !(Number.isInteger(max) || max === 'auto') + ) { + help() + return exit(1) + } + + if (match.type === 'STATIC') { + if (min === 0 && max === 0) { + error("Static deployments can't be FROZEN. Use `now rm` to remove") + return process.exit(1) + } + console.log('> Static deployments are automatically scaled!') + return process.exit(0) + } + + const { + max: currentMax, + min: currentMin, + current: currentCurrent + } = match.scale + if ( + max === currentMax && + min === currentMin && + Number.isInteger(min) && + currentCurrent >= min && + Number.isInteger(max) && + currentCurrent <= max + ) { + // Nothing to do, let's print the rules + printScaleingRules(match.url, currentCurrent, min, max) + return + } + + if ((match.state === 'FROZEN' || match.scale.current === 0) && min > 0) { + console.log( + `> Deployment is currently in 0 replicas, preparing deployment for scaling...` + ) + if (match.scale.max < 1) { + await scale.setScale(match.uid, { min: 0, max: 1 }) + } + await scale.unfreeze(match) + } + + const { min: newMin, max: newMax } = await scale.setScale(match.uid, { + min, + max + }) + + const elapsed = ms(new Date() - start) + + const currentReplicas = match.scale.current + printScaleingRules(match.url, currentReplicas, newMin, newMax, elapsed) + await info(scale, match.url) + + scale.close() +} +function printScaleingRules(url, currentReplicas, min, max, elapsed) { + const log = console.log + success( + `Configured scaling rules ${chalk.gray(elapsed ? '[' + elapsed + ']' : '')}` + ) + log() + log( + `${chalk.bold(url)} (${chalk.gray(currentReplicas)} ${chalk.gray( + 'current' + )})` + ) + log(printf('%6s %s', 'min', chalk.bold(min))) + log(printf('%6s %s', 'max', chalk.bold(max))) + log(printf('%6s %s', 'auto', chalk.bold(min === max ? '✖' : '✔'))) + log() +} + +async function list(scale) { + let deployments + try { + const app = argv._[1] + deployments = await scale.list(app) + } catch (err) { + handleError(err) + process.exit(1) + } + + scale.close() + + const apps = new Map() + + for (const dep of deployments) { + const deps = apps.get(dep.name) || [] + apps.set(dep.name, deps.concat(dep)) + } + + const sorted = await sort([...apps]) + + const timeNow = new Date() + const urlLength = + deployments.reduce((acc, i) => { + return Math.max(acc, (i.url && i.url.length) || 0) + }, 0) + 5 + + for (const app of sorted) { + const depls = argv.all ? app[1] : app[1].slice(0, 5) + console.log( + `${chalk.bold(app[0])} ${chalk.gray( + '(' + depls.length + ' of ' + app[1].length + ' total)' + )}` + ) + console.log() + const urlSpec = `%-${urlLength}s` + console.log( + printf( + ` ${chalk.grey(urlSpec + ' %8s %8s %8s %8s %8s')}`, + 'url', + 'cur', + 'min', + 'max', + 'auto', + 'age' + ) + ) + for (const instance of depls) { + if (!instance.scale) { + let spec + if (supportsColor) { + spec = ` %-${urlLength + 10}s %8s %8s %8s %8s %8s` + } else { + spec = ` %-${urlLength + 1}s %8s %8s %8s %8s %8s` + } + const infinite = '∞' + console.log( + printf( + spec, + chalk.underline(instance.url), + infinite, + 1, + infinite, + '✔', + ms(timeNow - instance.created) + ) + ) + } else if (instance.scale.current > 0) { + let spec + if (supportsColor) { + spec = ` %-${urlLength + 10}s %8s %8s %8s %8s %8s` + } else { + spec = ` %-${urlLength + 1}s %8s %8s %8s %8s %8s` + } + console.log( + printf( + spec, + chalk.underline(instance.url), + instance.scale.current, + instance.scale.min, + instance.scale.max, + instance.scale.max === instance.scale.min ? '✖' : '✔', + ms(timeNow - instance.created) + ) + ) + } else { + let spec + if (supportsColor) { + spec = ` %-${urlLength + 10}s ${chalk.gray('%8s %8s %8s %8s %8s')}` + } else { + spec = ` %-${urlLength + 1}s ${chalk.gray('%8s %8s %8s %8s %8s')}` + } + console.log( + printf( + spec, + chalk.underline(instance.url), + instance.scale.current, + instance.scale.min, + instance.scale.max, + instance.scale.max === instance.scale.min ? '✖' : '✔', + ms(timeNow - instance.created) + ) + ) + } + } + console.log() + } +} + +process.on('uncaughtException', err => { + handleError(err) + exit(1) +}) diff --git a/src/providers/sh/index.js b/src/providers/sh/index.js index 1a71f2d..6645c8c 100644 --- a/src/providers/sh/index.js +++ b/src/providers/sh/index.js @@ -1,6 +1,6 @@ module.exports = { title: 'now.sh', - subcommands: new Set(['login', 'deploy', 'ls']), + subcommands: new Set(['login', 'deploy', 'ls', 'alias', 'scale']), get deploy() { return require('./deploy') }, @@ -9,5 +9,11 @@ module.exports = { }, get ls() { return require('./commands/bin/list') + }, + get alias() { + return require('./commands/bin/alias') + }, + get scale() { + return require('./commands/bin/scale') } }