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:
Pablo Carranza Velez 2017-10-17 16:56:29 -07:00
parent 15b9943f6d
commit bd34a19a79
4 changed files with 117 additions and 68 deletions

View File

@ -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) ->

View File

@ -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()

View File

@ -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')
] ]

View File

@ -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