Add endpoints to create, delete and list images, and also list containers

This commit is contained in:
Pablo Carranza Velez 2016-05-11 14:44:05 +00:00
parent 8145f5632d
commit 8101d08433
3 changed files with 147 additions and 48 deletions

View File

@ -7,6 +7,7 @@ bodyParser = require 'body-parser'
request = require 'request'
config = require './config'
device = require './device'
dockerUtils = require './docker-utils'
_ = require 'lodash'
privateAppEnvVars = [
@ -16,7 +17,7 @@ privateAppEnvVars = [
module.exports = (application) ->
api = express()
api.use(bodyParser())
parseBody = bodyParser()
api.use (req, res, next) ->
utils.getOrGenerateSecret('api')
.then (secret) ->
@ -37,7 +38,7 @@ module.exports = (application) ->
setTimeout(utils.blink.pattern.stop, 15000)
res.sendStatus(200)
api.post '/v1/update', (req, res) ->
api.post '/v1/update', parseBody, (req, res) ->
utils.mixpanelTrack('Update notification')
application.update(req.body.force)
res.sendStatus(204)
@ -52,7 +53,7 @@ module.exports = (application) ->
request.post(config.gosuperAddress + '/v1/shutdown')
.pipe(res)
api.post '/v1/purge', (req, res) ->
api.post '/v1/purge', parseBody, (req, res) ->
appId = req.body.appId
utils.mixpanelTrack('Purge /data', appId)
if !appId?
@ -82,7 +83,7 @@ module.exports = (application) ->
utils.disableCheck(true)
res.sendStatus(204)
api.post '/v1/restart', (req, res) ->
api.post '/v1/restart', parseBody, (req, res) ->
appId = req.body.appId
force = req.body.force
utils.mixpanelTrack('Restart container', appId)
@ -101,7 +102,7 @@ module.exports = (application) ->
.catch (err) ->
res.status(503).send(err?.message or err or 'Unknown error')
api.post '/v1/apps/:appId/stop', (req, res) ->
api.post '/v1/apps/:appId/stop', parseBody, (req, res) ->
{ appId } = req.params
{ force } = req.body
utils.mixpanelTrack('Stop container', appId)
@ -166,4 +167,13 @@ module.exports = (application) ->
api.get '/v1/device', (req, res) ->
res.json(device.getState())
api.post '/v1/images/create', dockerUtils.createImage
api.delete '/v1/images/:name', dockerUtils.deleteImage
api.get '/v1/images', dockerUtils.listImages
api.post '/v1/containers/create', parseBody, dockerUtils.createContainer
api.post '/v1/containers/:id/start', dockerUtils.startContainer
api.post '/v1/containers/:id/stop', dockerUtils.stopContainer
api.delete '/v1/containers/:name', dockerUtils.deleteContainer
api.get '/v1/containers', dockerUtils.listContainers
return api

View File

@ -40,6 +40,13 @@ knex.init = Promise.all([
addColumn('app', 'appId', 'string')
]
knex.schema.hasTable('image')
.then (exists) ->
if not exists
knex.schema.createTable 'image', (t) ->
t.increments('id').primary()
t.string('repoTag')
])
module.exports = knex

View File

@ -9,7 +9,7 @@ knex = require './db'
TypedError = require 'typed-error'
{ request } = require './request'
fs = Promise.promisifyAll require 'fs'
Lock = require 'rwlock'
class OutOfSyncError extends TypedError
docker = Promise.promisifyAll(new Docker(socketPath: config.dockerSocket))
@ -149,6 +149,13 @@ dockerSync = (imgSrc, imgDest, rsyncDiff, conf) ->
docker.getImage(destId).tagAsync({ repo, tag, force: true })
do ->
_lock = new Lock()
_writeLock = Promise.promisify(_lock.async.writeLock)
lockImages = ->
_writeLock('images')
.disposer (release) ->
release()
# Keep track of the images being fetched, so we don't clean them up whilst fetching.
imagesBeingFetched = 0
exports.fetchImageWithProgress = (image, onProgress) ->
@ -162,51 +169,126 @@ do ->
# If there is no tag then mark it as latest
supervisorTag += ':latest'
exports.cleanupContainersAndImages = ->
Promise.join(
knex('app').select()
.map (app) ->
app.imageId + ':latest'
docker.listImagesAsync()
(apps, images) ->
imageTags = _.map(images, 'RepoTags')
supervisorTags = _.filter imageTags, (tags) ->
_.contains(tags, supervisorTag)
appTags = _.filter imageTags, (tags) ->
_.any tags, (tag) ->
_.contains(apps, tag)
supervisorTags = _.flatten(supervisorTags)
appTags = _.flatten(appTags)
return { images, supervisorTags, appTags }
)
.then ({ images, supervisorTags, appTags }) ->
# Cleanup containers first, so that they don't block image removal.
docker.listContainersAsync(all: true)
.filter (containerInfo) ->
# Do not remove user apps.
if _.contains(appTags, containerInfo.Image)
return false
if !_.contains(supervisorTags, containerInfo.Image)
return true
return containerHasExited(containerInfo.Id)
.map (containerInfo) ->
docker.getContainer(containerInfo.Id).removeAsync()
Promise.using lockImages(), ->
Promise.join(
knex('image').select('repoTag')
.map (image) ->
image.repoTag
knex('app').select()
.map (app) ->
app.imageId + ':latest'
docker.listImagesAsync()
(localTags, apps, images) ->
imageTags = _.map(images, 'RepoTags')
supervisorTags = _.filter imageTags, (tags) ->
_.contains(tags, supervisorTag)
appTags = _.filter imageTags, (tags) ->
_.any tags, (tag) ->
_.contains(apps, tag)
locallyCreatedTags = _.filter imageTags, (tags) ->
_.any tags, (tag) ->
_.contains(localTags, tag)
supervisorTags = _.flatten(supervisorTags)
appTags = _.flatten(appTags)
locallyCreatedTags = _.flatten(locallyCreatedTags)
return { images, supervisorTags, appTags, locallyCreatedTags }
)
.then ({ images, supervisorTags, appTags, locallyCreatedTags }) ->
# Cleanup containers first, so that they don't block image removal.
docker.listContainersAsync(all: true)
.filter (containerInfo) ->
# Do not remove user apps.
if _.contains(appTags, containerInfo.Image)
return false
if _.contains(locallyCreatedTags, containerInfo.Image)
return false
if !_.contains(supervisorTags, containerInfo.Image)
return true
return containerHasExited(containerInfo.Id)
.map (containerInfo) ->
docker.getContainer(containerInfo.Id).removeAsync()
.then ->
console.log('Deleted container:', containerInfo.Id, containerInfo.Image)
.catch(_.noop)
.then ->
console.log('Deleted container:', containerInfo.Id, containerInfo.Image)
.catch(_.noop)
.then ->
# And then clean up the images, as long as we aren't currently trying to fetch any.
return if imagesBeingFetched > 0
imagesToClean = _.reject images, (image) ->
_.any image.RepoTags, (tag) ->
return _.contains(appTags, tag) or _.contains(supervisorTags, tag)
Promise.map imagesToClean, (image) ->
Promise.map image.RepoTags.concat(image.Id), (tag) ->
docker.getImage(tag).removeAsync()
.then ->
console.log('Deleted image:', tag, image.Id, image.RepoTags)
.catch(_.noop)
# And then clean up the images, as long as we aren't currently trying to fetch any.
return if imagesBeingFetched > 0
imagesToClean = _.reject images, (image) ->
_.any image.RepoTags, (tag) ->
return _.contains(appTags, tag) or _.contains(supervisorTags, tag) or _.contains(locallyCreatedTags, tag)
Promise.map imagesToClean, (image) ->
Promise.map image.RepoTags.concat(image.Id), (tag) ->
docker.getImage(tag).removeAsync()
.then ->
console.log('Deleted image:', tag, image.Id, image.RepoTags)
.catch(_.noop)
containerHasExited = (id) ->
docker.getContainer(id).inspectAsync()
.then (data) ->
return not data.State.Running
buildRepoTag = (repo, tag, registry) ->
repoTag = ''
if registry?
repoTag += registry + '/'
repoTag += repo
if tag?
repoTag += ':' + tag
else
repoTag += ':latest'
return repoTag
sanitizeQuery = (query) ->
_.omit(query, 'apikey')
exports.createImage = (req, res) ->
{ registry, repo, tag, fromImage, fromSrc } = req.query
if fromSrc != '-'
res.status('422').send('Using a fromSrc other than "-" is not supported')
return
repoTag = buildRepoTag(repo, tag, registry)
Promise.using lockImages(), ->
knex('image').insert({ repoTag })
.then ->
docker.importImageAsync(req, { repo, tag, registry })
.then (stream) ->
stream.pipe(res)
.catch (err) ->
res.status(500).send(err?.message or err or 'Unknown error')
exports.deleteImage = (req, res) ->
Promise.using lockImages(), ->
knex('image').select().where('repoTag', req.params.name)
.then (images) ->
throw new Error('Only images created via the Supervisor can be deleted.') if images.length == 0
knex('image').where('repoTag', req.params.name).delete()
.then ->
docker.getImage(req.params.name).removeAsync(sanitizeQuery(req.query))
.then (data) ->
res.json(data)
.catch (err) ->
res.status(500).send(err?.message or err or 'Unknown error')
exports.listImages = (req, res) ->
docker.listImagesAsync(sanitizeQuery(req.query))
.then (images) ->
res.json(images)
.catch (err) ->
res.status(500).send(err?.message or err or 'Unknown error')
exports.createContainer = (req, res) ->
res.status(500).send('Not implemented')
exports.startContainer = (req, res) ->
res.status(500).send('Not implemented')
exports.stopContainer = (req, res) ->
res.status(500).send('Not implemented')
exports.deleteContainer = (req, res) ->
res.status(500).send('Not implemented')
exports.listContainers = (req, res) ->
docker.listContainersAsync(sanitizeQuery(req.query))
.then (containers) ->
res.json(containers)
.catch (err) ->
res.status(500).send(err?.message or err or 'Unknown error')