hubot-heroku
Version:
Run heroku commands via hubot without direct access to Heroku
220 lines (164 loc) • 7.18 kB
text/coffeescript
# Description:
# Exposes Heroku commands to hubot
#
# Dependencies:
# "heroku-client": "^1.9.0"
# "hubot-auth": "^1.2.0"
#
# Configuration:
# HUBOT_HEROKU_API_KEY
#
# Commands:
# hubot heroku info <app> - Returns useful information about the app
# hubot heroku dynos <app> - Lists all dynos and their status
# hubot heroku releases <app> - Latest 10 releases
# hubot heroku rollback <app> <version> - Rollback to a release
# hubot heroku restart <app> <dyno> - Restarts the specified app or dyno/s (e.g. worker or web.2)
# hubot heroku migrate <app> - Runs migrations. Remember to restart the app =)
# hubot heroku config <app> - Get config keys for the app. Values not given for security
# hubot heroku config:set <app> <KEY=value> - Set KEY to value. Case sensitive and overrides present key
# hubot heroku config:unset <app> <KEY> - Unsets KEY, does not throw error if key is not present
#
# Author:
# daemonsy
Heroku = require('heroku-client')
heroku = new Heroku(token: process.env.HUBOT_HEROKU_API_KEY)
_ = require('lodash')
mapper = require('../heroku-response-mapper')
moment = require('moment')
useAuth = (process.env.HUBOT_HEROKU_USE_AUTH or '').trim().toLowerCase() is 'true'
module.exports = (robot) ->
auth = (msg, appName) ->
role = "heroku-#{appName}"
hasRole = robot.auth.hasRole(msg.envelope.user, role)
isAdmin = robot.auth.hasRole(msg.envelope.user, 'admin')
if useAuth and not (hasRole or isAdmin)
msg.reply "Access denied. You must have this role to use this command: #{role}"
return false
return true
respondToUser = (robotMessage, error, successMessage) ->
if error
robotMessage.reply "Shucks. An error occurred. #{error.statusCode} - #{error.body.message}"
else
robotMessage.reply successMessage
rpad = (string, width, padding = ' ') ->
if (width <= string.length) then string else rpad(width, string + padding, padding)
objectToMessage = (object) ->
output = []
maxLength = 0
keys = Object.keys(object)
keys.forEach (key) ->
maxLength = key.length if key.length > maxLength
keys.forEach (key) ->
output.push "#{rpad(key, maxLength)} : #{object[key]}"
output.join("\n")
# App Info
robot.respond /heroku info (.*)/i, (msg) ->
appName = msg.match[1]
return unless auth(msg, appName)
msg.reply "Getting information about #{appName}"
heroku.apps(appName).info (error, info) ->
respondToUser(msg, error, "\n" + objectToMessage(mapper.info(info)))
# Dynos
robot.respond /heroku dynos (.*)/i, (msg) ->
appName = msg.match[1]
return unless auth(msg, appName)
msg.reply "Getting dynos of #{appName}"
heroku.apps(appName).dynos().list (error, dynos) ->
output = []
if dynos
output.push "Dynos of #{appName}"
lastFormation = ""
for dyno in dynos
currentFormation = "#{dyno.type}.#{dyno.size}"
unless currentFormation is lastFormation
output.push "" if lastFormation
output.push "=== #{dyno.type} (#{dyno.size}): `#{dyno.command}`"
lastFormation = currentFormation
updatedAt = moment(dyno.updated_at)
updatedTime = updatedAt.utc().format('YYYY/MM/DD HH:mm:ss')
timeAgo = updatedAt.fromNow()
output.push "#{dyno.name}: #{dyno.state} #{updatedTime} (~ #{timeAgo})"
respondToUser(msg, error, output.join("\n"))
# Releases
robot.respond /heroku releases (.*)$/i, (msg) ->
appName = msg.match[1]
return unless auth(msg, appName)
msg.reply "Getting releases for #{appName}"
heroku.apps(appName).releases().list (error, releases) ->
output = []
if releases
output.push "Recent releases of #{appName}"
for release in releases.sort((a, b) -> b.version - a.version)[0..9]
output.push "v#{release.version} - #{release.description} - #{release.user.email} - #{release.created_at}"
respondToUser(msg, error, output.join("\n"))
# Rollback
robot.respond /heroku rollback (.*) (.*)$/i, (msg) ->
appName = msg.match[1]
version = msg.match[2]
return unless auth(msg, appName)
if version.match(/v\d+$/)
msg.reply "Telling Heroku to rollback to #{version}"
app = heroku.apps(appName)
app.releases().list (error, releases) ->
release = _.find releases, (release) ->
"v#{release.version}" == version
return msg.reply "Version #{version} not found for #{appName} :(" unless release
app.releases().rollback release: release.id, (error, release) ->
respondToUser(msg, error, "Success! v#{release.version} -> Rollback to #{version}")
# Restart
robot.respond /heroku restart ([\w-]+)\s?(\w+(?:\.\d+)?)?/i, (msg) ->
appName = msg.match[1]
dynoName = msg.match[2]
dynoNameText = if dynoName then ' '+dynoName else ''
return unless auth(msg, appName)
msg.reply "Telling Heroku to restart #{appName}#{dynoNameText}"
unless dynoName
heroku.apps(appName).dynos().restartAll (error, app) ->
respondToUser(msg, error, "Heroku: Restarting #{appName}")
else
heroku.apps(appName).dynos(dynoName).restart (error, app) ->
respondToUser(msg, error, "Heroku: Restarting #{appName}#{dynoNameText}")
# Migration
robot.respond /heroku migrate (.*)/i, (msg) ->
appName = msg.match[1]
return unless auth(msg, appName)
msg.reply "Telling Heroku to migrate #{appName}"
heroku.apps(appName).dynos().create
command: "rake db:migrate"
attach: false
, (error, dyno) ->
respondToUser(msg, error, "Heroku: Running migrations for #{appName}")
heroku.apps(appName).logSessions().create
dyno: dyno.name
tail: true
, (error, session) ->
respondToUser(msg, error, "View logs at: #{session.logplex_url}")
# Config Vars
robot.respond /heroku config (.*)$/i, (msg) ->
appName = msg.match[1]
return unless auth(msg, appName)
msg.reply "Getting config keys for #{appName}"
heroku.apps(appName).configVars().info (error, configVars) ->
listOfKeys = configVars && Object.keys(configVars).join(", ")
respondToUser(msg, error, listOfKeys)
robot.respond /heroku config:set (.*) (\w+)=('([\s\S]+)'|"([\s\S]+)"|([\s\S]+\b))/im, (msg) ->
keyPair = {}
appName = msg.match[1]
key = msg.match[2]
value = msg.match[4] || msg.match[5] || msg.match[6] # :sad_panda:
return unless auth(msg, appName)
msg.reply "Setting config #{key} => #{value}"
keyPair[key] = value
heroku.apps(appName).configVars().update keyPair, (error, configVars) ->
respondToUser(msg, error, "Heroku: #{key} is set to #{configVars[key]}")
robot.respond /heroku config:unset (.*) (\w+)$/i, (msg) ->
keyPair = {}
appName = msg.match[1]
key = msg.match[2]
value = msg.match[3]
return unless auth(msg, appName)
msg.reply "Unsetting config #{key}"
keyPair[key] = null
heroku.apps(appName).configVars().update keyPair, (error, response) ->
respondToUser(msg, error, "Heroku: #{key} has been unset")