mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-30 02:28:53 +00:00
Add endpoints to create, delete and list images, and also list containers
This commit is contained in:
parent
8145f5632d
commit
8101d08433
@ -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
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
Loading…
Reference in New Issue
Block a user