balena-supervisor/src/api.coffee

270 lines
9.0 KiB
CoffeeScript

Promise = require 'bluebird'
utils = require './utils'
express = require 'express'
bodyParser = require 'body-parser'
bufferEq = require 'buffer-equal-constant-time'
config = require './config'
device = require './device'
dockerUtils = require './docker-utils'
_ = require 'lodash'
compose = require './compose'
proxyvisor = require './proxyvisor'
module.exports = (application) ->
api = express()
unparsedRouter = express.Router()
parsedRouter = express.Router()
parsedRouter.use(bodyParser())
api.use (req, res, next) ->
queryKey = req.query.apikey
header = req.get('Authorization') ? ''
match = header.match(/^ApiKey (\w+)$/)
headerKey = match?[1]
utils.getOrGenerateSecret('api')
.then (secret) ->
if queryKey? && bufferEq(new Buffer(queryKey), new Buffer(secret))
next()
else if headerKey? && bufferEq(new Buffer(headerKey), new Buffer(secret))
next()
else
res.sendStatus(401)
.catch (err) ->
# This should never happen...
res.status(503).send('Invalid API key in supervisor')
unparsedRouter.get '/ping', (req, res) ->
res.send('OK')
unparsedRouter.post '/v1/blink', (req, res) ->
utils.mixpanelTrack('Device blink')
utils.blink.pattern.start()
setTimeout(utils.blink.pattern.stop, 15000)
res.sendStatus(200)
parsedRouter.post '/v1/update', (req, res) ->
utils.mixpanelTrack('Update notification')
application.update(req.body.force)
res.sendStatus(204)
parsedRouter.post '/v1/reboot', (req, res) ->
force = req.body.force
Promise.map utils.getKnexApps(), (theApp) ->
Promise.using application.lockUpdates(theApp.appId, force), ->
# There's a slight chance the app changed after the previous select
# So we fetch it again now the lock is acquired
utils.getKnexApp(theApp.appId)
.then (app) ->
application.kill(app, removeContainer: false) if app?
.then ->
new Promise (resolve, reject) ->
application.logSystemMessage('Rebooting', {}, 'Reboot')
utils.gosuper.post('/v1/reboot')
.on('error', reject)
.on('response', -> resolve())
.pipe(res)
.catch (err) ->
if err instanceof application.UpdatesLockedError
status = 423
else
status = 500
res.status(status).send(err?.message or err or 'Unknown error')
parsedRouter.post '/v1/shutdown', (req, res) ->
force = req.body.force
Promise.map utils.getKnexApps(), (theApp) ->
Promise.using application.lockUpdates(theApp.appId, force), ->
# There's a slight chance the app changed after the previous select
# So we fetch it again now the lock is acquired
utils.getKnexApp(theApp.appId)
.then (app) ->
application.kill(app, removeContainer: false) if app?
.then ->
new Promise (resolve, reject) ->
application.logSystemMessage('Shutting down', {}, 'Shutdown')
utils.gosuper.post('/v1/shutdown')
.on('error', reject)
.on('response', -> resolve())
.pipe(res)
.catch (err) ->
if err instanceof application.UpdatesLockedError
status = 423
else
status = 500
res.status(status).send(err?.message or err or 'Unknown error')
parsedRouter.post '/v1/purge', (req, res) ->
appId = req.body.appId
application.logSystemMessage('Purging /data', { appId }, 'Purge /data')
if !appId?
return res.status(400).send('Missing app id')
Promise.using application.lockUpdates(appId, true), ->
utils.getKnexApp(appId)
.then (app) ->
application.kill(app)
.then ->
new Promise (resolve, reject) ->
utils.gosuper.post('/v1/purge', { json: true, body: applicationId: appId })
.on('error', reject)
.on('response', -> resolve())
.pipe(res)
.then ->
application.logSystemMessage('Purged /data', { appId }, 'Purge /data success')
.finally ->
application.start(app)
.catch (err) ->
status = 503
if err instanceof utils.AppNotFoundError
errMsg = "App not found: an app needs to be installed for purge to work.
If you've recently moved this device from another app,
please push an app and wait for it to be installed first."
err = new Error(errMsg)
status = 400
application.logSystemMessage("Error purging /data: #{err}", { appId, error: err }, 'Purge /data error')
res.status(status).send(err?.message or err or 'Unknown error')
unparsedRouter.post '/v1/tcp-ping', (req, res) ->
utils.disableCheck(false)
res.sendStatus(204)
unparsedRouter.delete '/v1/tcp-ping', (req, res) ->
utils.disableCheck(true)
res.sendStatus(204)
parsedRouter.post '/v1/restart', (req, res) ->
appId = req.body.appId
force = req.body.force
utils.mixpanelTrack('Restart container', appId)
if !appId?
return res.status(400).send('Missing app id')
Promise.using application.lockUpdates(appId, force), ->
utils.getKnexApp(appId)
.then (app) ->
application.kill(app)
.then ->
application.start(app)
.then ->
res.status(200).send('OK')
.catch utils.AppNotFoundError, (e) ->
return res.status(400).send(e.message)
.catch (err) ->
res.status(503).send(err?.message or err or 'Unknown error')
parsedRouter.post '/v1/apps/:appId/stop', (req, res) ->
{ appId } = req.params
{ force } = req.body
utils.mixpanelTrack('Stop container', appId)
if !appId?
return res.status(400).send('Missing app id')
Promise.using application.lockUpdates(appId, force), ->
utils.getKnexApp(appId)
.tap (app) ->
application.kill(app, removeContainer: false)
.then (app) ->
res.json(_.pick(app, 'containerId'))
.catch utils.AppNotFoundError, (e) ->
return res.status(400).send(e.message)
.catch (err) ->
res.status(503).send(err?.message or err or 'Unknown error')
unparsedRouter.post '/v1/apps/:appId/start', (req, res) ->
{ appId } = req.params
utils.mixpanelTrack('Start container', appId)
if !appId?
return res.status(400).send('Missing app id')
Promise.using application.lockUpdates(appId), ->
utils.getKnexApp(appId)
.tap (app) ->
application.start(app)
.then (app) ->
res.json(_.pick(app, 'containerId'))
.catch utils.AppNotFoundError, (e) ->
return res.status(400).send(e.message)
.catch (err) ->
res.status(503).send(err?.message or err or 'Unknown error')
unparsedRouter.get '/v1/apps/:appId', (req, res) ->
{ appId } = req.params
utils.mixpanelTrack('GET app', appId)
if !appId?
return res.status(400).send('Missing app id')
Promise.using application.lockUpdates(appId, true), ->
columns = [ 'appId', 'containerId', 'commit', 'imageId', 'env' ]
utils.getKnexApp(appId, columns)
.then (app) ->
# Don't return keys on the endpoint
app.env = _.omit(JSON.parse(app.env), config.privateAppEnvVars)
# Don't return data that will be of no use to the user
res.json(app)
.catch utils.AppNotFoundError, (e) ->
return res.status(400).send(e.message)
.catch (err) ->
res.status(503).send(err?.message or err or 'Unknown error')
# Expires the supervisor's API key and generates a new one.
# It also communicates the new key to the Resin API.
unparsedRouter.post '/v1/regenerate-api-key', (req, res) ->
utils.newSecret('api')
.then (secret) ->
device.updateState(api_secret: secret)
res.status(200).send(secret)
.catch (err) ->
res.status(503).send(err?.message or err or 'Unknown error')
unparsedRouter.get '/v1/device', (req, res) ->
res.json(device.getState())
unparsedRouter.post '/v1/images/create', dockerUtils.createImage
unparsedRouter.post '/v1/images/load', dockerUtils.loadImage
unparsedRouter.delete '/v1/images/*', dockerUtils.deleteImage
unparsedRouter.get '/v1/images', dockerUtils.listImages
parsedRouter.post '/v1/containers/create', dockerUtils.createContainer
parsedRouter.post '/v1/containers/update', dockerUtils.updateContainer
parsedRouter.post '/v1/containers/:id/start', dockerUtils.startContainer
unparsedRouter.post '/v1/containers/:id/stop', dockerUtils.stopContainer
unparsedRouter.delete '/v1/containers/:id', dockerUtils.deleteContainer
unparsedRouter.get '/v1/containers', dockerUtils.listContainers
unparsedRouter.post '/v1/apps/:appId/compose/up', (req, res) ->
appId = req.params.appId
onStatus = (status) ->
status = JSON.stringify(status) if _.isObject(status)
res.write(status)
utils.getKnexApp(appId)
.then (app) ->
res.status(200)
compose.up(appId, onStatus)
.catch (err) ->
console.log('Error on compose up:', err, err.stack)
.finally ->
res.end()
.catch utils.AppNotFoundError, (e) ->
return res.status(400).send(e.message)
.catch (err) ->
res.status(503).send(err?.message or err or 'Unknown error')
unparsedRouter.post '/v1/apps/:appId/compose/down', (req, res) ->
appId = req.params.appId
onStatus = (status) ->
status = JSON.stringify(status) if _.isObject(status)
res.write(status)
utils.getKnexApp(appId)
.then (app) ->
res.status(200)
compose.down(appId, onStatus)
.catch (err) ->
console.log('Error on compose down:', err, err.stack)
.finally ->
res.end()
.catch utils.AppNotFoundError, (e) ->
return res.status(400).send(e.message)
.catch (err) ->
res.status(503).send(err?.message or err or 'Unknown error')
api.use(unparsedRouter)
api.use(parsedRouter)
api.use(proxyvisor.router)
return api