heroku/heroku-apps

View on GitHub
src/commands/ps/index.js

Summary

Maintainability
B
6 hrs
Test Coverage
'use strict'

let cli = require('heroku-cli-util')
let co = require('co')
let time = require('../../time')
const {truncate, sortBy, reduce, forEach} = require('lodash')

// gets the process number from a string like web.19 => 19
let getProcessNum = (s) => parseInt(s.split('.', 2)[1])

function printExtended (dynos) {
  const trunc = (s) => truncate(s, {length: 35, omission: '…'})

  dynos = sortBy(dynos, ['type'], (a) => getProcessNum(a.name))
  cli.table(dynos, {
    columns: [
      {key: 'id', label: 'ID'},
      {key: 'name', label: 'Process'},
      {key: 'state', label: 'State', format: (state, row) => `${state} ${time.ago(new Date(row.updated_at))}`},
      {key: 'extended.region', label: 'Region'},
      {key: 'extended.instance', label: 'Instance'},
      {key: 'extended.ip', label: 'IP'},
      {key: 'extended.port', label: 'Port'},
      {key: 'extended.az', label: 'AZ'},
      {key: 'release.version', label: 'Release'},
      {key: 'command', label: 'Command', format: trunc},
      {key: 'extended.route', label: 'Route'},
      {key: 'size', label: 'Size'}
    ]
  })
}

function * printAccountQuota (context, heroku, app, account) {
  if (app.process_tier !== 'free') {
    return
  }

  if (app.owner.id !== account.id) {
    return
  }

  let quota = yield heroku.request({
    path: `/accounts/${account.id}/actions/get-quota`,
    headers: {Accept: 'application/vnd.heroku+json; version=3.account-quotas'}
  })
    .then(function (data) {
    // very temporary fix, the person who can fix this is on vacation
      if (data.id === 'not_found') {
        return null
      }
      return data
    })
    .catch(function () {
      return null
    })

  if (!quota) return

  let remaining, percentage
  if (quota.account_quota === 0) {
    remaining = 0
    percentage = 0
  } else {
    remaining = quota.account_quota - quota.quota_used
    percentage = Math.floor(remaining / quota.account_quota * 100)
  }

  let remainingMinutes = remaining / 60
  let hours = Math.floor(remainingMinutes / 60)
  let minutes = Math.floor(remainingMinutes % 60)

  cli.log(`Free dyno hours quota remaining this month: ${hours}h ${minutes}m (${percentage}%)`)
  cli.log('For more information on dyno sleeping and how to upgrade, see:')
  cli.log('https://devcenter.heroku.com/articles/dyno-sleeping')
  cli.log()
}

function printDynos (dynos) {
  let dynosByCommand = reduce(dynos, function (dynosByCommand, dyno) {
    let since = time.ago(new Date(dyno.updated_at))
    let size = dyno.size || '1X'

    if (dyno.type === 'run') {
      let key = 'run: one-off processes'
      if (dynosByCommand[key] === undefined) dynosByCommand[key] = []
      dynosByCommand[key].push(`${dyno.name} (${size}): ${dyno.state} ${since}: ${dyno.command}`)
    } else {
      let key = `${cli.color.green(dyno.type)} (${cli.color.cyan(size)}): ${dyno.command}`
      if (dynosByCommand[key] === undefined) dynosByCommand[key] = []
      let state = dyno.state === 'up' ? cli.color.green(dyno.state) : cli.color.yellow(dyno.state)
      let item = `${dyno.name}: ${cli.color.green(state)} ${cli.color.dim(since)}`
      dynosByCommand[key].push(item)
    }
    return dynosByCommand
  }, {})
  forEach(dynosByCommand, function (dynos, key) {
    cli.styledHeader(`${key} (${cli.color.yellow(dynos.length)})`)
    dynos = dynos.sort((a, b) => getProcessNum(a) - getProcessNum(b))
    for (let dyno of dynos) cli.log(dyno)
    cli.log()
  })
}

function * run (context, heroku) {
  const {app, flags, args} = context
  const types = args
  const {json, extended} = flags
  const suffix = extended ? '?extended=true' : ''

  let promises = {
    dynos: heroku.request({path: `/apps/${app}/dynos${suffix}`})
  }

  promises.appInfo = heroku.request({
    path: `/apps/${context.app}`,
    headers: {Accept: 'application/vnd.heroku+json; version=3.process-tier'}
  })
  promises.accountInfo = heroku.request({path: '/account'})

  let {dynos, appInfo, accountInfo} = yield promises
  const shielded = appInfo.space && appInfo.space.shield

  if (shielded) {
    dynos.forEach(d => {
      d.size = d.size.replace('Private-', 'Shield-')
    })
  }

  if (types.length > 0) {
    dynos = dynos.filter(dyno => types.find(t => dyno.type === t))
    types.forEach(t => {
      if (!dynos.find(d => d.type === t)) {
        throw new Error(`No ${cli.color.cyan(t)} dynos on ${cli.color.app(app)}`)
      }
    })
  }

  let compare = function (a, b) {
    let comparison = 0
    if (a > b) {
      comparison = 1
    } else if (b > a) {
      comparison = -1
    }
    return comparison
  }

  dynos = dynos.sort((a, b) => compare(a.name, b.name))
  if (json) cli.styledJSON(dynos)
  else if (extended) printExtended(dynos)
  else {
    yield printAccountQuota(context, heroku, appInfo, accountInfo)
    if (dynos.length === 0) cli.log(`No dynos on ${cli.color.app(app)}`)
    else printDynos(dynos)
  }
}

module.exports = {
  topic: 'ps',
  description: 'list dynos for an app',
  variableArgs: true,
  usage: 'ps [TYPE [TYPE ...]]',
  flags: [
    {name: 'json', description: 'display as json'},
    {name: 'extended', char: 'x', hidden: true}
  ],
  examples: `$ heroku ps
=== run: one-off dyno
run.1: up for 5m: bash

=== web: bundle exec thin start -p $PORT
web.1: created for 30s

$ heroku ps run # specifying types
=== run: one-off dyno
run.1: up for 5m: bash`,
  needsAuth: true,
  needsApp: true,
  run: cli.command(co.wrap(run))
}