mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-03-21 11:35:18 +00:00
Put preloaded apps in DB, and use promise for when boostrap is done
This commit is contained in:
parent
0bfd329ebc
commit
9d2c142c36
@ -19,16 +19,14 @@ knex.init.then ->
|
||||
utils.mixpanelProperties.uuid = uuid
|
||||
|
||||
api = require './api'
|
||||
application = require './application'
|
||||
application = require('./application')(uuid)
|
||||
device = require './device'
|
||||
randomstring = require 'randomstring'
|
||||
|
||||
console.log('Starting API server..')
|
||||
|
||||
secret = config.forceApiSecret ? randomstring.generate()
|
||||
api(secret).listen(config.listenPort)
|
||||
|
||||
initialStateUpdate = ->
|
||||
bootstrap.done.then ->
|
||||
console.log('Starting API server..')
|
||||
api(secret).listen(config.listenPort)
|
||||
# Let API know what version we are, and our api connection info.
|
||||
console.log('Updating supervisor version and api info')
|
||||
device.updateState(
|
||||
@ -40,11 +38,6 @@ knex.init.then ->
|
||||
download_progress: null
|
||||
)
|
||||
|
||||
if bootstrap.bootstrapped
|
||||
initialStateUpdate()
|
||||
else
|
||||
bootstrap.on('done', initialStateUpdate)
|
||||
|
||||
console.log('Starting Apps..')
|
||||
knex('app').select()
|
||||
.then (apps) ->
|
||||
|
@ -16,13 +16,6 @@ bootstrap = require './bootstrap'
|
||||
|
||||
{ docker } = dockerUtils
|
||||
|
||||
knex('config').select('value').where(key: 'uuid').then ([ uuid ]) ->
|
||||
logger.init(
|
||||
dockerSocket: config.dockerSocket
|
||||
pubnub: config.pubnub
|
||||
channel: "device-#{uuid.value}-logs"
|
||||
)
|
||||
|
||||
logTypes =
|
||||
stopApp:
|
||||
eventName: 'Application kill'
|
||||
@ -87,7 +80,9 @@ logSystemEvent = (logType, app, error) ->
|
||||
utils.mixpanelTrack(logType.eventName, {app, error})
|
||||
return
|
||||
|
||||
exports.kill = kill = (app) ->
|
||||
application = {}
|
||||
|
||||
application.kill = kill = (app) ->
|
||||
logSystemEvent(logTypes.stopApp, app)
|
||||
device.updateState(status: 'Stopping')
|
||||
container = docker.getContainer(app.containerId)
|
||||
@ -140,7 +135,7 @@ fetch = (app) ->
|
||||
logSystemEvent(logTypes.downloadAppError, app, err)
|
||||
throw err
|
||||
|
||||
exports.start = start = (app) ->
|
||||
application.start = start = (app) ->
|
||||
Promise.try ->
|
||||
# Parse the env vars before trying to access them, that's because they have to be stringified for knex..
|
||||
JSON.parse(app.env)
|
||||
@ -256,12 +251,12 @@ lockPath = (app) ->
|
||||
return "/mnt/root/resin-data/#{appId}/resin-updates.lock"
|
||||
|
||||
# At boot, all apps should be unlocked *before* start to prevent a deadlock
|
||||
exports.unlockAndStart = unlockAndStart = (app) ->
|
||||
application.unlockAndStart = unlockAndStart = (app) ->
|
||||
lockFile.unlockAsync(lockPath(app))
|
||||
.then ->
|
||||
start(app)
|
||||
|
||||
exports.lockUpdates = lockUpdates = do ->
|
||||
application.lockUpdates = lockUpdates = do ->
|
||||
_lock = new Lock()
|
||||
_writeLock = Promise.promisify(_lock.async.writeLock)
|
||||
return (app, force) ->
|
||||
@ -293,145 +288,153 @@ updateStatus =
|
||||
state: UPDATE_IDLE
|
||||
failed: 0
|
||||
forceNext: false
|
||||
exports.update = update = (force) ->
|
||||
return if !bootstrap.bootstrapped
|
||||
application.update = update = (force) ->
|
||||
if updateStatus.state isnt UPDATE_IDLE
|
||||
# Mark an update required after the current.
|
||||
updateStatus.forceNext = force
|
||||
updateStatus.state = UPDATE_REQUIRED
|
||||
return
|
||||
updateStatus.state = UPDATE_UPDATING
|
||||
Promise.all([
|
||||
knex('config').select('value').where(key: 'apiKey')
|
||||
knex('config').select('value').where(key: 'uuid')
|
||||
knex('app').select()
|
||||
])
|
||||
.then ([ [ apiKey ], [ uuid ], apps ]) ->
|
||||
apiKey = apiKey.value
|
||||
uuid = uuid.value
|
||||
bootstrap.done.then ->
|
||||
Promise.all([
|
||||
knex('config').select('value').where(key: 'apiKey')
|
||||
knex('config').select('value').where(key: 'uuid')
|
||||
knex('app').select()
|
||||
])
|
||||
.then ([ [ apiKey ], [ uuid ], apps ]) ->
|
||||
apiKey = apiKey.value
|
||||
uuid = uuid.value
|
||||
|
||||
deviceId = device.getID()
|
||||
deviceId = device.getID()
|
||||
|
||||
remoteApps = cachedResinApi.get
|
||||
resource: 'application'
|
||||
options:
|
||||
select: [
|
||||
'id'
|
||||
'git_repository'
|
||||
'commit'
|
||||
]
|
||||
filter:
|
||||
commit: $ne: null
|
||||
device:
|
||||
uuid: uuid
|
||||
customOptions:
|
||||
apikey: apiKey
|
||||
remoteApps = cachedResinApi.get
|
||||
resource: 'application'
|
||||
options:
|
||||
select: [
|
||||
'id'
|
||||
'git_repository'
|
||||
'commit'
|
||||
]
|
||||
filter:
|
||||
commit: $ne: null
|
||||
device:
|
||||
uuid: uuid
|
||||
customOptions:
|
||||
apikey: apiKey
|
||||
|
||||
Promise.join deviceId, remoteApps, (deviceId, remoteApps) ->
|
||||
return Promise.map remoteApps, (remoteApp) ->
|
||||
getEnvironment(remoteApp.id, deviceId, apiKey)
|
||||
.then (environment) ->
|
||||
remoteApp.environment_variable = environment
|
||||
return remoteApp
|
||||
.then (remoteApps) ->
|
||||
remoteAppEnvs = {}
|
||||
remoteApps = _.map remoteApps, (app) ->
|
||||
env =
|
||||
RESIN_DEVICE_UUID: uuid
|
||||
RESIN: '1'
|
||||
USER: 'root'
|
||||
Promise.join deviceId, remoteApps, (deviceId, remoteApps) ->
|
||||
return Promise.map remoteApps, (remoteApp) ->
|
||||
getEnvironment(remoteApp.id, deviceId, apiKey)
|
||||
.then (environment) ->
|
||||
remoteApp.environment_variable = environment
|
||||
return remoteApp
|
||||
.then (remoteApps) ->
|
||||
remoteAppEnvs = {}
|
||||
remoteApps = _.map remoteApps, (app) ->
|
||||
env =
|
||||
RESIN_DEVICE_UUID: uuid
|
||||
RESIN: '1'
|
||||
USER: 'root'
|
||||
|
||||
if app.environment_variable?
|
||||
_.extend(env, app.environment_variable)
|
||||
remoteAppEnvs[app.id] = env
|
||||
return {
|
||||
appId: '' + app.id
|
||||
commit: app.commit
|
||||
imageId: "#{config.registryEndpoint}/#{path.basename(app.git_repository, '.git')}/#{app.commit}"
|
||||
env: JSON.stringify(env) # The env has to be stored as a JSON string for knex
|
||||
}
|
||||
if app.environment_variable?
|
||||
_.extend(env, app.environment_variable)
|
||||
remoteAppEnvs[app.id] = env
|
||||
return {
|
||||
appId: '' + app.id
|
||||
commit: app.commit
|
||||
imageId: "#{config.registryEndpoint}/#{path.basename(app.git_repository, '.git')}/#{app.commit}"
|
||||
env: JSON.stringify(env) # The env has to be stored as a JSON string for knex
|
||||
}
|
||||
|
||||
remoteApps = _.indexBy(remoteApps, 'appId')
|
||||
remoteAppIds = _.keys(remoteApps)
|
||||
remoteApps = _.indexBy(remoteApps, 'appId')
|
||||
remoteAppIds = _.keys(remoteApps)
|
||||
|
||||
apps = _.indexBy(apps, 'appId')
|
||||
localApps = _.mapValues apps, (app) ->
|
||||
_.pick(app, [ 'appId', 'commit', 'imageId', 'env' ])
|
||||
localAppIds = _.keys(localApps)
|
||||
apps = _.indexBy(apps, 'appId')
|
||||
localApps = _.mapValues apps, (app) ->
|
||||
_.pick(app, [ 'appId', 'commit', 'imageId', 'env' ])
|
||||
localAppIds = _.keys(localApps)
|
||||
|
||||
toBeRemoved = _.difference(localAppIds, remoteAppIds)
|
||||
toBeInstalled = _.difference(remoteAppIds, localAppIds)
|
||||
toBeRemoved = _.difference(localAppIds, remoteAppIds)
|
||||
toBeInstalled = _.difference(remoteAppIds, localAppIds)
|
||||
|
||||
toBeUpdated = _.intersection(remoteAppIds, localAppIds)
|
||||
toBeUpdated = _.filter toBeUpdated, (appId) ->
|
||||
return !_.isEqual(remoteApps[appId], localApps[appId])
|
||||
toBeUpdated = _.intersection(remoteAppIds, localAppIds)
|
||||
toBeUpdated = _.filter toBeUpdated, (appId) ->
|
||||
return !_.isEqual(remoteApps[appId], localApps[appId])
|
||||
|
||||
toBeDownloaded = _.filter toBeUpdated, (appId) ->
|
||||
return !_.isEqual(remoteApps[appId].imageId, localApps[appId].imageId)
|
||||
toBeDownloaded = _.union(toBeDownloaded, toBeInstalled)
|
||||
toBeDownloaded = _.filter toBeUpdated, (appId) ->
|
||||
return !_.isEqual(remoteApps[appId].imageId, localApps[appId].imageId)
|
||||
toBeDownloaded = _.union(toBeDownloaded, toBeInstalled)
|
||||
|
||||
allAppIds = _.union(localAppIds, remoteAppIds)
|
||||
allAppIds = _.union(localAppIds, remoteAppIds)
|
||||
|
||||
Promise.map allAppIds, (appId) ->
|
||||
Promise.try ->
|
||||
fetch(remoteApps[appId]) if _.includes(toBeDownloaded, appId)
|
||||
.then ->
|
||||
if _.includes(toBeRemoved, appId)
|
||||
Promise.using lockUpdates(apps[appId], force), ->
|
||||
# We get the app from the DB again in case someone restarted it
|
||||
# (which would have changed its containerId)
|
||||
knex('app').select().where({ appId })
|
||||
.then ([ app ]) ->
|
||||
if !app?
|
||||
throw new Error('App not found')
|
||||
kill(app)
|
||||
.then ->
|
||||
knex('app').where('appId', appId).delete()
|
||||
.catch (err) ->
|
||||
logSystemEvent(logTypes.updateAppError, app, err)
|
||||
throw err
|
||||
else if _.includes(toBeInstalled, appId)
|
||||
app = remoteApps[appId]
|
||||
start(app)
|
||||
else if _.includes(toBeUpdated, appId)
|
||||
localApp = apps[appId]
|
||||
app = remoteApps[appId]
|
||||
logSystemEvent(logTypes.updateApp, app) if localApp.imageId == app.imageId
|
||||
forceThisApp = remoteAppEnvs[appId]['RESIN_OVERRIDE_LOCK'] == '1'
|
||||
Promise.using lockUpdates(localApp, force || forceThisApp), ->
|
||||
knex('app').select().where({ appId })
|
||||
.then ([ localApp ]) ->
|
||||
if !localApp?
|
||||
throw new Error('App not found')
|
||||
kill(localApp)
|
||||
.then ->
|
||||
start(app)
|
||||
.catch (err) ->
|
||||
logSystemEvent(logTypes.updateAppError, app, err)
|
||||
throw err
|
||||
.catch(_.identity)
|
||||
.filter(_.isError)
|
||||
.then (failures) ->
|
||||
throw new Error(joinErrorMessages(failures)) if failures.length > 0
|
||||
.then ->
|
||||
updateStatus.failed = 0
|
||||
# We cleanup here as we want a point when we have a consistent apps/images state, rather than potentially at a
|
||||
# point where we might clean up an image we still want.
|
||||
dockerUtils.cleanupContainersAndImages()
|
||||
.catch (err) ->
|
||||
updateStatus.failed++
|
||||
if updateStatus.state is UPDATE_REQUIRED
|
||||
console.log('Updating failed, but there is already another update scheduled immediately: ', err)
|
||||
return
|
||||
delayTime = Math.min(updateStatus.failed * 500, 30000)
|
||||
# If there was an error then schedule another attempt briefly in the future.
|
||||
console.log('Scheduling another update attempt due to failure: ', delayTime, err)
|
||||
setTimeout(update, delayTime, force)
|
||||
.finally ->
|
||||
device.updateState(status: 'Idle')
|
||||
if updateStatus.state is UPDATE_REQUIRED
|
||||
# If an update is required then schedule it
|
||||
setTimeout(update, 1, updateStatus.forceNext)
|
||||
.finally ->
|
||||
# Set the updating as finished in its own block, so it never has to worry about other code stopping this.
|
||||
updateStatus.state = UPDATE_IDLE
|
||||
Promise.map allAppIds, (appId) ->
|
||||
Promise.try ->
|
||||
fetch(remoteApps[appId]) if _.includes(toBeDownloaded, appId)
|
||||
.then ->
|
||||
if _.includes(toBeRemoved, appId)
|
||||
Promise.using lockUpdates(apps[appId], force), ->
|
||||
# We get the app from the DB again in case someone restarted it
|
||||
# (which would have changed its containerId)
|
||||
knex('app').select().where({ appId })
|
||||
.then ([ app ]) ->
|
||||
if !app?
|
||||
throw new Error('App not found')
|
||||
kill(app)
|
||||
.then ->
|
||||
knex('app').where('appId', appId).delete()
|
||||
.catch (err) ->
|
||||
logSystemEvent(logTypes.updateAppError, app, err)
|
||||
throw err
|
||||
else if _.includes(toBeInstalled, appId)
|
||||
app = remoteApps[appId]
|
||||
start(app)
|
||||
else if _.includes(toBeUpdated, appId)
|
||||
localApp = apps[appId]
|
||||
app = remoteApps[appId]
|
||||
logSystemEvent(logTypes.updateApp, app) if localApp.imageId == app.imageId
|
||||
forceThisApp = remoteAppEnvs[appId]['RESIN_OVERRIDE_LOCK'] == '1'
|
||||
Promise.using lockUpdates(localApp, force || forceThisApp), ->
|
||||
knex('app').select().where({ appId })
|
||||
.then ([ localApp ]) ->
|
||||
if !localApp?
|
||||
throw new Error('App not found')
|
||||
kill(localApp)
|
||||
.then ->
|
||||
start(app)
|
||||
.catch (err) ->
|
||||
logSystemEvent(logTypes.updateAppError, app, err)
|
||||
throw err
|
||||
.catch(_.identity)
|
||||
.filter(_.isError)
|
||||
.then (failures) ->
|
||||
throw new Error(joinErrorMessages(failures)) if failures.length > 0
|
||||
.then ->
|
||||
updateStatus.failed = 0
|
||||
# We cleanup here as we want a point when we have a consistent apps/images state, rather than potentially at a
|
||||
# point where we might clean up an image we still want.
|
||||
dockerUtils.cleanupContainersAndImages()
|
||||
.catch (err) ->
|
||||
updateStatus.failed++
|
||||
if updateStatus.state is UPDATE_REQUIRED
|
||||
console.log('Updating failed, but there is already another update scheduled immediately: ', err)
|
||||
return
|
||||
delayTime = Math.min(updateStatus.failed * 500, 30000)
|
||||
# If there was an error then schedule another attempt briefly in the future.
|
||||
console.log('Scheduling another update attempt due to failure: ', delayTime, err)
|
||||
setTimeout(update, delayTime, force)
|
||||
.finally ->
|
||||
device.updateState(status: 'Idle')
|
||||
if updateStatus.state is UPDATE_REQUIRED
|
||||
# If an update is required then schedule it
|
||||
setTimeout(update, 1, updateStatus.forceNext)
|
||||
.finally ->
|
||||
# Set the updating as finished in its own block, so it never has to worry about other code stopping this.
|
||||
updateStatus.state = UPDATE_IDLE
|
||||
|
||||
module.exports = (uuid) ->
|
||||
logger.init(
|
||||
dockerSocket: config.dockerSocket
|
||||
pubnub: config.pubnub
|
||||
channel: "device-#{uuid}-logs"
|
||||
)
|
||||
return application
|
||||
|
@ -5,18 +5,26 @@ utils = require './utils'
|
||||
deviceRegister = require 'resin-register-device'
|
||||
{ resinApi } = require './request'
|
||||
fs = Promise.promisifyAll(require('fs'))
|
||||
crypto = require 'crypto'
|
||||
appConfig = require './config'
|
||||
EventEmitter = require('events').EventEmitter
|
||||
|
||||
module.exports = do ->
|
||||
configPath = '/boot/config.json'
|
||||
appsPath = '/boot/apps.json'
|
||||
userConfig = {}
|
||||
|
||||
bootstrapper = new EventEmitter()
|
||||
|
||||
loadPreloadedApps = ->
|
||||
#To-Do
|
||||
knex('app').truncate()
|
||||
.then ->
|
||||
fs.readFileAsync(appsPath, 'utf8')
|
||||
.then(JSON.parse)
|
||||
.then (apps) ->
|
||||
Promise.map apps, (app) ->
|
||||
app.env = JSON.stringify(app.env)
|
||||
knex('app').insert(app)
|
||||
.catch (err) ->
|
||||
utils.mixpanelTrack('Loading preloaded apps failed', {error: err})
|
||||
|
||||
bootstrap = ->
|
||||
Promise.try ->
|
||||
@ -46,7 +54,7 @@ module.exports = do ->
|
||||
])
|
||||
])
|
||||
.tap ->
|
||||
doneBootstrapping()
|
||||
bootstrapper.doneBootstrapping()
|
||||
|
||||
readConfigAndEnsureUUID = ->
|
||||
# Load config file
|
||||
@ -73,16 +81,17 @@ module.exports = do ->
|
||||
utils.mixpanelTrack('Device bootstrap failed, retrying', {error: err, delay: config.bootstrapRetryDelay})
|
||||
setTimeout(bootstrapOrRetry, config.bootstrapRetryDelay)
|
||||
|
||||
doneBootstrapping = ->
|
||||
bootstrapper.bootstrapped = true
|
||||
bootstrapper.emit('done')
|
||||
bootstrapper.done = new Promise (resolve) ->
|
||||
bootstrapper.doneBootstrapping = ->
|
||||
bootstrapper.bootstrapped = true
|
||||
resolve(userConfig)
|
||||
|
||||
bootstrapper.bootstrapped = false
|
||||
bootstrapper.startBootstrapping = ->
|
||||
knex('config').select('value').where(key: 'uuid')
|
||||
.then ([ uuid ]) ->
|
||||
if uuid?.value
|
||||
doneBootstrapping()
|
||||
bootstrapper.doneBootstrapping()
|
||||
return uuid.value
|
||||
console.log('New device detected. Bootstrapping..')
|
||||
readConfigAndEnsureUUID()
|
||||
|
Loading…
x
Reference in New Issue
Block a user