mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-03-11 15:04:18 +00:00
Use container name instead of id to identify apps, and avoid duplicated containers
By storing the container name before creating the container, we avoid problems if the supervisor crashes or the device reboots between creating a container and storing its id. Change-Type: patch Signed-off-by: Pablo Carranza Velez <pablo@resin.io>
This commit is contained in:
parent
15b9943f6d
commit
bd34a19a79
@ -154,10 +154,12 @@ module.exports = (application) ->
|
|||||||
return res.status(400).send('Missing app id')
|
return res.status(400).send('Missing app id')
|
||||||
Promise.using application.lockUpdates(appId, force), ->
|
Promise.using application.lockUpdates(appId, force), ->
|
||||||
utils.getKnexApp(appId)
|
utils.getKnexApp(appId)
|
||||||
.tap (app) ->
|
.then (app) ->
|
||||||
|
application.getContainerId(app)
|
||||||
|
.then (containerId) ->
|
||||||
application.kill(app, removeContainer: false)
|
application.kill(app, removeContainer: false)
|
||||||
.then (app) ->
|
.then (app) ->
|
||||||
res.json(_.pick(app, 'containerId'))
|
res.json({ containerId })
|
||||||
.catch utils.AppNotFoundError, (e) ->
|
.catch utils.AppNotFoundError, (e) ->
|
||||||
return res.status(400).send(e.message)
|
return res.status(400).send(e.message)
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
@ -172,8 +174,10 @@ module.exports = (application) ->
|
|||||||
utils.getKnexApp(appId)
|
utils.getKnexApp(appId)
|
||||||
.tap (app) ->
|
.tap (app) ->
|
||||||
application.start(app)
|
application.start(app)
|
||||||
.then (app) ->
|
.then ->
|
||||||
res.json(_.pick(app, 'containerId'))
|
application.getContainerId(app)
|
||||||
|
.then (containerId) ->
|
||||||
|
res.json({ containerId })
|
||||||
.catch utils.AppNotFoundError, (e) ->
|
.catch utils.AppNotFoundError, (e) ->
|
||||||
return res.status(400).send(e.message)
|
return res.status(400).send(e.message)
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
@ -185,13 +189,16 @@ module.exports = (application) ->
|
|||||||
if !appId?
|
if !appId?
|
||||||
return res.status(400).send('Missing app id')
|
return res.status(400).send('Missing app id')
|
||||||
Promise.using application.lockUpdates(appId, true), ->
|
Promise.using application.lockUpdates(appId, true), ->
|
||||||
columns = [ 'appId', 'containerId', 'commit', 'imageId', 'env' ]
|
columns = [ 'appId', 'containerName', 'commit', 'imageId', 'env' ]
|
||||||
utils.getKnexApp(appId, columns)
|
utils.getKnexApp(appId, columns)
|
||||||
.then (app) ->
|
.then (app) ->
|
||||||
|
application.getContainerId(app)
|
||||||
|
.then (containerId) ->
|
||||||
# Don't return keys on the endpoint
|
# Don't return keys on the endpoint
|
||||||
app.env = _.omit(JSON.parse(app.env), config.privateAppEnvVars)
|
app.env = _.omit(JSON.parse(app.env), config.privateAppEnvVars)
|
||||||
|
app.containerId = containerId
|
||||||
# Don't return data that will be of no use to the user
|
# Don't return data that will be of no use to the user
|
||||||
res.json(app)
|
res.json(_.omit(app, [ 'containerName' ]))
|
||||||
.catch utils.AppNotFoundError, (e) ->
|
.catch utils.AppNotFoundError, (e) ->
|
||||||
return res.status(400).send(e.message)
|
return res.status(400).send(e.message)
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
|
@ -18,6 +18,7 @@ proxyvisor = require './proxyvisor'
|
|||||||
{ checkInt, checkTruthy } = require './lib/validation'
|
{ checkInt, checkTruthy } = require './lib/validation'
|
||||||
osRelease = require './lib/os-release'
|
osRelease = require './lib/os-release'
|
||||||
deviceConfig = require './device-config'
|
deviceConfig = require './device-config'
|
||||||
|
randomHexString = require './lib/random-hex-string'
|
||||||
|
|
||||||
class UpdatesLockedError extends TypedError
|
class UpdatesLockedError extends TypedError
|
||||||
ImageNotFoundError = (err) ->
|
ImageNotFoundError = (err) ->
|
||||||
@ -155,7 +156,7 @@ logSpecialAction = (action, value, success) ->
|
|||||||
application.kill = kill = (app, { updateDB = true, removeContainer = true } = {}) ->
|
application.kill = kill = (app, { updateDB = true, removeContainer = true } = {}) ->
|
||||||
logSystemEvent(logTypes.stopApp, app)
|
logSystemEvent(logTypes.stopApp, app)
|
||||||
device.updateState(status: 'Stopping')
|
device.updateState(status: 'Stopping')
|
||||||
container = docker.getContainer(app.containerId)
|
container = docker.getContainer(app.containerName)
|
||||||
container.stop(t: 10)
|
container.stop(t: 10)
|
||||||
.then ->
|
.then ->
|
||||||
container.remove(v: true) if removeContainer
|
container.remove(v: true) if removeContainer
|
||||||
@ -183,7 +184,7 @@ application.kill = kill = (app, { updateDB = true, removeContainer = true } = {}
|
|||||||
.tap ->
|
.tap ->
|
||||||
logSystemEvent(logTypes.stopAppSuccess, app)
|
logSystemEvent(logTypes.stopAppSuccess, app)
|
||||||
if removeContainer && updateDB
|
if removeContainer && updateDB
|
||||||
app.containerId = null
|
app.containerName = null
|
||||||
knex('app').update(app).where(appId: app.appId)
|
knex('app').update(app).where(appId: app.appId)
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
logSystemEvent(logTypes.stopAppError, app, err)
|
logSystemEvent(logTypes.stopAppError, app, err)
|
||||||
@ -255,6 +256,12 @@ isExecFormatError = (err) ->
|
|||||||
message = err.json.trim()
|
message = err.json.trim()
|
||||||
/exec format error$/.test(message)
|
/exec format error$/.test(message)
|
||||||
|
|
||||||
|
generateContainerName = (app) ->
|
||||||
|
randomHexString.generate()
|
||||||
|
.then (randomString) ->
|
||||||
|
sanitisedAppName = app.name.replace(/[^a-zA-Z0-9]+/g, '_')
|
||||||
|
return "#{sanitisedAppName}_#{randomString}"
|
||||||
|
|
||||||
application.start = start = (app) ->
|
application.start = start = (app) ->
|
||||||
device.isResinOSv1().then (isV1) ->
|
device.isResinOSv1().then (isV1) ->
|
||||||
volumes = utils.defaultVolumes(isV1)
|
volumes = utils.defaultVolumes(isV1)
|
||||||
@ -270,9 +277,9 @@ application.start = start = (app) ->
|
|||||||
.map((port) -> port.trim())
|
.map((port) -> port.trim())
|
||||||
.filter(isValidPort)
|
.filter(isValidPort)
|
||||||
|
|
||||||
if app.containerId?
|
if app.containerName?
|
||||||
# If we have a container id then check it exists and if so use it.
|
# If we have a container id then check it exists and if so use it.
|
||||||
container = docker.getContainer(app.containerId)
|
container = docker.getContainer(app.containerName)
|
||||||
containerPromise = container.inspect().return(container)
|
containerPromise = container.inspect().return(container)
|
||||||
else
|
else
|
||||||
containerPromise = Promise.rejected()
|
containerPromise = Promise.rejected()
|
||||||
@ -283,7 +290,13 @@ application.start = start = (app) ->
|
|||||||
.then (imageInfo) ->
|
.then (imageInfo) ->
|
||||||
logSystemEvent(logTypes.installApp, app)
|
logSystemEvent(logTypes.installApp, app)
|
||||||
device.updateState(status: 'Installing')
|
device.updateState(status: 'Installing')
|
||||||
|
generateContainerName(app)
|
||||||
|
.tap (name) ->
|
||||||
|
app.containerName = name
|
||||||
|
knex('app').update(app).where(appId: app.appId)
|
||||||
|
.then (affectedRows) ->
|
||||||
|
knex('app').insert(app) if affectedRows == 0
|
||||||
|
.then (name) ->
|
||||||
ports = {}
|
ports = {}
|
||||||
portBindings = {}
|
portBindings = {}
|
||||||
if portList?
|
if portList?
|
||||||
@ -301,6 +314,7 @@ application.start = start = (app) ->
|
|||||||
.then (shouldMount) ->
|
.then (shouldMount) ->
|
||||||
binds.push('/bin/kmod:/bin/kmod:ro') if shouldMount
|
binds.push('/bin/kmod:/bin/kmod:ro') if shouldMount
|
||||||
docker.createContainer(
|
docker.createContainer(
|
||||||
|
name: name
|
||||||
Image: app.imageId
|
Image: app.imageId
|
||||||
Cmd: cmd
|
Cmd: cmd
|
||||||
Tty: true
|
Tty: true
|
||||||
@ -341,21 +355,20 @@ application.start = start = (app) ->
|
|||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
# If starting the container failed, we remove it so that it doesn't litter
|
# If starting the container failed, we remove it so that it doesn't litter
|
||||||
container.remove(v: true)
|
container.remove(v: true)
|
||||||
|
.finally ->
|
||||||
|
throw err
|
||||||
.then ->
|
.then ->
|
||||||
app.containerId = null
|
device.updateState(commit: app.commit)
|
||||||
|
logger.attach(app)
|
||||||
|
.catch (err) ->
|
||||||
|
# If creating or starting the app failed, we soft-delete it so that
|
||||||
|
# the next update cycle doesn't think the app is up to date
|
||||||
|
app.containerName = null
|
||||||
|
app.markedForDeletion = true
|
||||||
knex('app').update(app).where(appId: app.appId)
|
knex('app').update(app).where(appId: app.appId)
|
||||||
.finally ->
|
.finally ->
|
||||||
logSystemEvent(logTypes.startAppError, app, err)
|
logSystemEvent(logTypes.startAppError, app, err)
|
||||||
throw err
|
throw err
|
||||||
.then ->
|
|
||||||
app.containerId = container.id
|
|
||||||
device.updateState(commit: app.commit)
|
|
||||||
logger.attach(app)
|
|
||||||
.tap (container) ->
|
|
||||||
# Update the app info, only if starting the container worked.
|
|
||||||
knex('app').update(app).where(appId: app.appId)
|
|
||||||
.then (affectedRows) ->
|
|
||||||
knex('app').insert(app) if affectedRows == 0
|
|
||||||
.tap ->
|
.tap ->
|
||||||
if alreadyStarted
|
if alreadyStarted
|
||||||
logSystemEvent(logTypes.startAppNoop, app)
|
logSystemEvent(logTypes.startAppNoop, app)
|
||||||
@ -765,7 +778,7 @@ application.update = update = (force, scheduled = false) ->
|
|||||||
app = localApps[appId]
|
app = localApps[appId]
|
||||||
Promise.using lockUpdates(app, force), ->
|
Promise.using lockUpdates(app, force), ->
|
||||||
# We get the app from the DB again in case someone restarted it
|
# We get the app from the DB again in case someone restarted it
|
||||||
# (which would have changed its containerId)
|
# (which would have changed its containerName)
|
||||||
utils.getKnexApp(appId)
|
utils.getKnexApp(appId)
|
||||||
.then(kill)
|
.then(kill)
|
||||||
.then ->
|
.then ->
|
||||||
@ -848,6 +861,8 @@ application.update = update = (force, scheduled = false) ->
|
|||||||
device.updateState(status: 'Idle')
|
device.updateState(status: 'Idle')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
sanitiseContainerName = (name) -> name.replace(/^\//, '')
|
||||||
|
|
||||||
listenToEvents = do ->
|
listenToEvents = do ->
|
||||||
appHasDied = {}
|
appHasDied = {}
|
||||||
return ->
|
return ->
|
||||||
@ -859,14 +874,14 @@ listenToEvents = do ->
|
|||||||
parser.on 'error', (err) ->
|
parser.on 'error', (err) ->
|
||||||
console.error('Error on docker events JSON stream:', err, err.stack)
|
console.error('Error on docker events JSON stream:', err, err.stack)
|
||||||
parser.on 'data', (data) ->
|
parser.on 'data', (data) ->
|
||||||
if data?.Type? && data.Type == 'container' && data.status in ['die', 'start']
|
if data?.Type? && data.Type == 'container' && data.status in ['die', 'start'] && data.Actor?.Attributes?.Name?
|
||||||
knex('app').select().where({ containerId: data.id })
|
knex('app').select().where({ containerName: sanitiseContainerName(data.Actor.Attributes.Name) })
|
||||||
.then ([ app ]) ->
|
.then ([ app ]) ->
|
||||||
if app?
|
if app?
|
||||||
if data.status == 'die'
|
if data.status == 'die'
|
||||||
logSystemEvent(logTypes.appExit, app)
|
logSystemEvent(logTypes.appExit, app)
|
||||||
appHasDied[app.containerId] = true
|
appHasDied[app.containerName] = true
|
||||||
else if data.status == 'start' and appHasDied[app.containerId]
|
else if data.status == 'start' and appHasDied[app.containerName]
|
||||||
logSystemEvent(logTypes.appRestart, app)
|
logSystemEvent(logTypes.appRestart, app)
|
||||||
logger.attach(app)
|
logger.attach(app)
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
@ -878,8 +893,34 @@ listenToEvents = do ->
|
|||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
console.error('Error listening to events:', err, err.stack)
|
console.error('Error listening to events:', err, err.stack)
|
||||||
|
|
||||||
|
migrateContainerIdApps = ->
|
||||||
|
knex.schema.hasColumn('app', 'containerId')
|
||||||
|
.then (exists) ->
|
||||||
|
return if not exists
|
||||||
|
knex('app').whereNotNull('containerId').select()
|
||||||
|
.then (apps) ->
|
||||||
|
return if !apps? or apps.length == 0
|
||||||
|
Promise.map apps, (app) ->
|
||||||
|
docker.getContainer(app.containerId).inspect()
|
||||||
|
.catchReturn({ Name: null })
|
||||||
|
.then (container) ->
|
||||||
|
app.containerName = sanitiseContainerName(container.Name)
|
||||||
|
app.containerId = null
|
||||||
|
knex('app').update(app).where({ id: app.id })
|
||||||
|
.then ->
|
||||||
|
knex.schema.table 'app', (table) ->
|
||||||
|
table.dropColumn('containerId')
|
||||||
|
|
||||||
|
application.getContainerId = (app) ->
|
||||||
|
docker.getContainer(app.containerName).inspect()
|
||||||
|
.catchReturn({ Id: null })
|
||||||
|
.then (container) ->
|
||||||
|
return container.Id
|
||||||
|
|
||||||
application.initialize = ->
|
application.initialize = ->
|
||||||
listenToEvents()
|
listenToEvents()
|
||||||
|
migrateContainerIdApps()
|
||||||
|
.then ->
|
||||||
getAndApplyDeviceConfig()
|
getAndApplyDeviceConfig()
|
||||||
.then ->
|
.then ->
|
||||||
knex('app').whereNot(markedForDeletion: true).orWhereNull('markedForDeletion').select()
|
knex('app').whereNot(markedForDeletion: true).orWhereNull('markedForDeletion').select()
|
||||||
|
@ -40,7 +40,7 @@ knex.init = Promise.all([
|
|||||||
knex.schema.createTable 'app', (t) ->
|
knex.schema.createTable 'app', (t) ->
|
||||||
t.increments('id').primary()
|
t.increments('id').primary()
|
||||||
t.string('name')
|
t.string('name')
|
||||||
t.string('containerId')
|
t.string('containerName')
|
||||||
t.string('commit')
|
t.string('commit')
|
||||||
t.string('imageId')
|
t.string('imageId')
|
||||||
t.string('appId')
|
t.string('appId')
|
||||||
@ -52,6 +52,7 @@ knex.init = Promise.all([
|
|||||||
Promise.all [
|
Promise.all [
|
||||||
addColumn('app', 'commit', 'string')
|
addColumn('app', 'commit', 'string')
|
||||||
addColumn('app', 'appId', 'string')
|
addColumn('app', 'appId', 'string')
|
||||||
|
addColumn('app', 'containerName', 'string')
|
||||||
addColumn('app', 'config', 'json')
|
addColumn('app', 'config', 'json')
|
||||||
addColumn('app', 'markedForDeletion', 'boolean')
|
addColumn('app', 'markedForDeletion', 'boolean')
|
||||||
]
|
]
|
||||||
|
@ -80,19 +80,19 @@ exports.log = ->
|
|||||||
do ->
|
do ->
|
||||||
_lock = new Lock()
|
_lock = new Lock()
|
||||||
_writeLock = Promise.promisify(_lock.async.writeLock)
|
_writeLock = Promise.promisify(_lock.async.writeLock)
|
||||||
loggerLock = (containerId) ->
|
loggerLock = (containerName) ->
|
||||||
_writeLock(containerId)
|
_writeLock(containerName)
|
||||||
.disposer (release) ->
|
.disposer (release) ->
|
||||||
release()
|
release()
|
||||||
|
|
||||||
attached = {}
|
attached = {}
|
||||||
exports.attach = (app) ->
|
exports.attach = (app) ->
|
||||||
Promise.using loggerLock(app.containerId), ->
|
Promise.using loggerLock(app.containerName), ->
|
||||||
if !attached[app.containerId]
|
if !attached[app.containerName]
|
||||||
docker.getContainer(app.containerId)
|
docker.getContainer(app.containerName)
|
||||||
.logs({ follow: true, stdout: true, stderr: true, timestamps: true })
|
.logs({ follow: true, stdout: true, stderr: true, timestamps: true })
|
||||||
.then (stream) ->
|
.then (stream) ->
|
||||||
attached[app.containerId] = true
|
attached[app.containerName] = true
|
||||||
stream.pipe(es.split())
|
stream.pipe(es.split())
|
||||||
.on 'data', (logLine) ->
|
.on 'data', (logLine) ->
|
||||||
space = logLine.indexOf(' ')
|
space = logLine.indexOf(' ')
|
||||||
@ -101,6 +101,6 @@ do ->
|
|||||||
publish(msg)
|
publish(msg)
|
||||||
.on 'error', (err) ->
|
.on 'error', (err) ->
|
||||||
console.error('Error on container logs', err, err.stack)
|
console.error('Error on container logs', err, err.stack)
|
||||||
attached[app.containerId] = false
|
attached[app.containerName] = false
|
||||||
.on 'end', ->
|
.on 'end', ->
|
||||||
attached[app.containerId] = false
|
attached[app.containerName] = false
|
||||||
|
Loading…
x
Reference in New Issue
Block a user