mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-23 15:32:24 +00:00
Various bugfixes and sytlistic improvements
* Use the correct defaults for the delta config variables that have them * Only mount /lib/firmware and /lib/modules if they exist on the host * hardcode-migrations.js: Nicer line separation * APIBinder: switch to using a header for authentication, and keep credentials saved in the API clients * Fix hrtime measurements in milliseconds * Do not uses classes for routers * compose: properly initialize networkMode to the first entry in networks if there is one * Fix some details regarding defaults in validation and service Signed-off-by: Pablo Carranza Velez <pablo@resin.io>
This commit is contained in:
parent
ec22bfcb29
commit
58b167b43d
@ -2,6 +2,8 @@
|
|||||||
// This hack makes the migrations directory a constant so that at least we can use webpack contexts for the
|
// This hack makes the migrations directory a constant so that at least we can use webpack contexts for the
|
||||||
// require.
|
// require.
|
||||||
module.exports = function (source) {
|
module.exports = function (source) {
|
||||||
return source.toString().replace("require(directory + '/' + name);", "require('./migrations/' + name);")
|
return source
|
||||||
.replace("require(_path2.default.join(this._absoluteConfigDir(), name));", "require('./migrations/' + name);")
|
.toString()
|
||||||
|
.replace("require(directory + '/' + name);", "require('./migrations/' + name);")
|
||||||
|
.replace("require(_path2.default.join(this._absoluteConfigDir(), name));", "require('./migrations/' + name);")
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ Promise = require 'bluebird'
|
|||||||
_ = require 'lodash'
|
_ = require 'lodash'
|
||||||
url = require 'url'
|
url = require 'url'
|
||||||
TypedError = require 'typed-error'
|
TypedError = require 'typed-error'
|
||||||
PlatformAPI = require 'pinejs-client'
|
PinejsClient = require 'pinejs-client'
|
||||||
deviceRegister = require 'resin-register-device'
|
deviceRegister = require 'resin-register-device'
|
||||||
express = require 'express'
|
express = require 'express'
|
||||||
bodyParser = require 'body-parser'
|
bodyParser = require 'body-parser'
|
||||||
@ -18,17 +18,16 @@ ExchangeKeyError = class ExchangeKeyError extends TypedError
|
|||||||
REPORT_SUCCESS_DELAY = 1000
|
REPORT_SUCCESS_DELAY = 1000
|
||||||
REPORT_RETRY_DELAY = 5000
|
REPORT_RETRY_DELAY = 5000
|
||||||
|
|
||||||
class APIBinderRouter
|
createAPIBinderRouter = (apiBinder) ->
|
||||||
constructor: (@apiBinder) ->
|
router = express.Router()
|
||||||
{ @eventTracker } = @apiBinder
|
router.use(bodyParser.urlencoded(extended: true))
|
||||||
@router = express.Router()
|
router.use(bodyParser.json())
|
||||||
@router.use(bodyParser.urlencoded(extended: true))
|
router.post '/v1/update', (req, res) ->
|
||||||
@router.use(bodyParser.json())
|
apiBinder.eventTracker.track('Update notification')
|
||||||
@router.post '/v1/update', (req, res) =>
|
if apiBinder.readyForUpdates
|
||||||
@eventTracker.track('Update notification')
|
apiBinder.getAndSetTargetState(req.body.force)
|
||||||
if @apiBinder.readyForUpdates
|
res.sendStatus(204)
|
||||||
@apiBinder.getAndSetTargetState(req.body.force)
|
return router
|
||||||
res.sendStatus(204)
|
|
||||||
|
|
||||||
module.exports = class APIBinder
|
module.exports = class APIBinder
|
||||||
constructor: ({ @config, @db, @deviceState, @eventTracker }) ->
|
constructor: ({ @config, @db, @deviceState, @eventTracker }) ->
|
||||||
@ -41,8 +40,7 @@ module.exports = class APIBinder
|
|||||||
@_targetStateInterval = null
|
@_targetStateInterval = null
|
||||||
@reportPending = false
|
@reportPending = false
|
||||||
@stateReportErrors = 0
|
@stateReportErrors = 0
|
||||||
@_router = new APIBinderRouter(this)
|
@router = createAPIBinderRouter(this)
|
||||||
@router = @_router.router
|
|
||||||
_lock = new Lock()
|
_lock = new Lock()
|
||||||
@_writeLock = Promise.promisify(_lock.async.writeLock)
|
@_writeLock = Promise.promisify(_lock.async.writeLock)
|
||||||
@readyForUpdates = false
|
@readyForUpdates = false
|
||||||
@ -52,7 +50,9 @@ module.exports = class APIBinder
|
|||||||
.then (conf) =>
|
.then (conf) =>
|
||||||
if conf.offlineMode
|
if conf.offlineMode
|
||||||
return true
|
return true
|
||||||
stateFetchHealthy = process.hrtime(@lastTargetStateFetch)[0] < 2 * conf.appUpdatePollInterval
|
timeSinceLastFetch = process.hrtime(@lastTargetStateFetch)
|
||||||
|
timeSinceLastFetchMs = timeSinceLastFetch[0] * 1000 + timeSinceLastFetch[1] / 1e6
|
||||||
|
stateFetchHealthy = timeSinceLastFetchMs < 2 * conf.appUpdatePollInterval
|
||||||
stateReportHealthy = !conf.connectivityCheckEnabled or !@deviceState.connected or @stateReportErrors < 3
|
stateReportHealthy = !conf.connectivityCheckEnabled or !@deviceState.connected or @stateReportErrors < 3
|
||||||
return stateFetchHealthy and stateReportHealthy
|
return stateFetchHealthy and stateReportHealthy
|
||||||
|
|
||||||
@ -61,19 +61,22 @@ module.exports = class APIBinder
|
|||||||
release()
|
release()
|
||||||
|
|
||||||
init: (startServices = true) ->
|
init: (startServices = true) ->
|
||||||
@config.getMany([ 'offlineMode', 'resinApiEndpoint', 'bootstrapRetryDelay' ])
|
@config.getMany([ 'offlineMode', 'resinApiEndpoint', 'bootstrapRetryDelay', 'currentApiKey' ])
|
||||||
.then ({ offlineMode, resinApiEndpoint, bootstrapRetryDelay }) =>
|
.then ({ offlineMode, resinApiEndpoint, bootstrapRetryDelay, currentApiKey }) =>
|
||||||
if offlineMode
|
if offlineMode
|
||||||
console.log('Offline Mode is set, skipping API binder initialization')
|
console.log('Offline Mode is set, skipping API binder initialization')
|
||||||
return
|
return
|
||||||
baseUrl = url.resolve(resinApiEndpoint, '/v4/')
|
baseUrl = url.resolve(resinApiEndpoint, '/v4/')
|
||||||
@resinApi = new PlatformAPI
|
passthrough = _.cloneDeep(requestOpts)
|
||||||
|
passthrough.headers ?= {}
|
||||||
|
passthrough.headers.Authorization = "Bearer #{currentApiKey}"
|
||||||
|
@resinApi = new PinejsClient
|
||||||
apiPrefix: baseUrl
|
apiPrefix: baseUrl
|
||||||
passthrough: requestOpts
|
passthrough: passthrough
|
||||||
baseUrlLegacy = url.resolve(resinApiEndpoint, '/v2/')
|
baseUrlLegacy = url.resolve(resinApiEndpoint, '/v2/')
|
||||||
@resinApiLegacy = new PlatformAPI
|
@resinApiLegacy = new PinejsClient
|
||||||
apiPrefix: baseUrlLegacy
|
apiPrefix: baseUrlLegacy
|
||||||
passthrough: requestOpts
|
passthrough: passthrough
|
||||||
@cachedResinApi = @resinApi.clone({}, cache: {})
|
@cachedResinApi = @resinApi.clone({}, cache: {})
|
||||||
if !startServices
|
if !startServices
|
||||||
return
|
return
|
||||||
@ -100,8 +103,8 @@ module.exports = class APIBinder
|
|||||||
options:
|
options:
|
||||||
filter:
|
filter:
|
||||||
uuid: uuid
|
uuid: uuid
|
||||||
customOptions:
|
passthrough:
|
||||||
apikey: apiKey
|
headers: Authorization: "Bearer: #{apiKey}"
|
||||||
.get(0)
|
.get(0)
|
||||||
.catchReturn(null)
|
.catchReturn(null)
|
||||||
.timeout(timeout)
|
.timeout(timeout)
|
||||||
@ -121,20 +124,21 @@ module.exports = class APIBinder
|
|||||||
return device
|
return device
|
||||||
# If it's not valid/doesn't exist then we try to use the user/provisioning api key for the exchange
|
# If it's not valid/doesn't exist then we try to use the user/provisioning api key for the exchange
|
||||||
@fetchDevice(opts.uuid, opts.provisioningApiKey, opts.apiTimeout)
|
@fetchDevice(opts.uuid, opts.provisioningApiKey, opts.apiTimeout)
|
||||||
.then (device) ->
|
.tap (device) ->
|
||||||
if not device?
|
if not device?
|
||||||
throw new ExchangeKeyError("Couldn't fetch device with provisioning key")
|
throw new ExchangeKeyError("Couldn't fetch device with provisioning key")
|
||||||
# We found the device, we can try to register a working device key for it
|
# We found the device, we can try to register a working device key for it
|
||||||
request.postAsync("#{opts.apiEndpoint}/api-key/device/#{device.id}/device-key?apikey=#{opts.provisioningApiKey}", {
|
request.postAsync("#{opts.apiEndpoint}/api-key/device/#{device.id}/device-key", {
|
||||||
json: true
|
json: true
|
||||||
body:
|
body:
|
||||||
apiKey: opts.deviceApiKey
|
apiKey: opts.deviceApiKey
|
||||||
|
headers:
|
||||||
|
Authorization: "Bearer #{opts.provisioningApiKey}"
|
||||||
})
|
})
|
||||||
.spread (res, body) ->
|
.spread (res, body) ->
|
||||||
if res.statusCode != 200
|
if res.statusCode != 200
|
||||||
throw new ExchangeKeyError("Couldn't register device key with provisioning key")
|
throw new ExchangeKeyError("Couldn't register device key with provisioning key")
|
||||||
.timeout(opts.apiTimeout)
|
.timeout(opts.apiTimeout)
|
||||||
.return(device)
|
|
||||||
|
|
||||||
_exchangeKeyAndGetDeviceOrRegenerate: (opts) =>
|
_exchangeKeyAndGetDeviceOrRegenerate: (opts) =>
|
||||||
@_exchangeKeyAndGetDevice(opts)
|
@_exchangeKeyAndGetDevice(opts)
|
||||||
@ -167,6 +171,7 @@ module.exports = class APIBinder
|
|||||||
console.log('Device is registered but we still have an apiKey, attempting key exchange')
|
console.log('Device is registered but we still have an apiKey, attempting key exchange')
|
||||||
@_exchangeKeyAndGetDevice(opts)
|
@_exchangeKeyAndGetDevice(opts)
|
||||||
.then ({ id }) =>
|
.then ({ id }) =>
|
||||||
|
@resinApi.passthrough.headers.Authorization = "Bearer #{opts.deviceApiKey}"
|
||||||
configToUpdate = {
|
configToUpdate = {
|
||||||
registered_at: opts.registered_at
|
registered_at: opts.registered_at
|
||||||
deviceId: id
|
deviceId: id
|
||||||
@ -200,7 +205,6 @@ module.exports = class APIBinder
|
|||||||
@config.getMany([
|
@config.getMany([
|
||||||
'offlineMode'
|
'offlineMode'
|
||||||
'provisioned'
|
'provisioned'
|
||||||
'currentApiKey'
|
|
||||||
'apiTimeout'
|
'apiTimeout'
|
||||||
'userId'
|
'userId'
|
||||||
'deviceId'
|
'deviceId'
|
||||||
@ -221,8 +225,6 @@ module.exports = class APIBinder
|
|||||||
@resinApi.post
|
@resinApi.post
|
||||||
resource: 'device'
|
resource: 'device'
|
||||||
body: device
|
body: device
|
||||||
customOptions:
|
|
||||||
apikey: conf.currentApiKey
|
|
||||||
.timeout(conf.apiTimeout)
|
.timeout(conf.apiTimeout)
|
||||||
|
|
||||||
# This uses resin API v2 for now, as the proxyvisor expects to be able to patch the device's commit
|
# This uses resin API v2 for now, as the proxyvisor expects to be able to patch the device's commit
|
||||||
@ -230,7 +232,6 @@ module.exports = class APIBinder
|
|||||||
@config.getMany([
|
@config.getMany([
|
||||||
'offlineMode'
|
'offlineMode'
|
||||||
'provisioned'
|
'provisioned'
|
||||||
'currentApiKey'
|
|
||||||
'apiTimeout'
|
'apiTimeout'
|
||||||
])
|
])
|
||||||
.then (conf) =>
|
.then (conf) =>
|
||||||
@ -242,8 +243,6 @@ module.exports = class APIBinder
|
|||||||
resource: 'device'
|
resource: 'device'
|
||||||
id: id
|
id: id
|
||||||
body: updatedFields
|
body: updatedFields
|
||||||
customOptions:
|
|
||||||
apikey: conf.currentApiKey
|
|
||||||
.timeout(conf.apiTimeout)
|
.timeout(conf.apiTimeout)
|
||||||
|
|
||||||
# Creates the necessary config vars in the API to match the current device state,
|
# Creates the necessary config vars in the API to match the current device state,
|
||||||
@ -252,8 +251,8 @@ module.exports = class APIBinder
|
|||||||
Promise.join(
|
Promise.join(
|
||||||
@deviceState.getCurrentForComparison()
|
@deviceState.getCurrentForComparison()
|
||||||
@getTargetState()
|
@getTargetState()
|
||||||
@config.getMany([ 'currentApiKey', 'deviceId' ])
|
@config.get('deviceId')
|
||||||
(currentState, targetState, conf) =>
|
(currentState, targetState, deviceId) =>
|
||||||
currentConfig = currentState.local.config
|
currentConfig = currentState.local.config
|
||||||
targetConfig = targetState.local.config
|
targetConfig = targetState.local.config
|
||||||
Promise.mapSeries _.toPairs(currentConfig), ([ key, value ]) =>
|
Promise.mapSeries _.toPairs(currentConfig), ([ key, value ]) =>
|
||||||
@ -263,14 +262,12 @@ module.exports = class APIBinder
|
|||||||
if !targetConfig[key]?
|
if !targetConfig[key]?
|
||||||
envVar = {
|
envVar = {
|
||||||
value
|
value
|
||||||
device: conf.deviceId
|
device: deviceId
|
||||||
name: key
|
name: key
|
||||||
}
|
}
|
||||||
@resinApi.post
|
@resinApi.post
|
||||||
resource: 'device_config_variable'
|
resource: 'device_config_variable'
|
||||||
body: envVar
|
body: envVar
|
||||||
customOptions:
|
|
||||||
apikey: conf.currentApiKey
|
|
||||||
)
|
)
|
||||||
.then =>
|
.then =>
|
||||||
@config.set({ initialConfigReported: 'true' })
|
@config.set({ initialConfigReported: 'true' })
|
||||||
@ -284,13 +281,13 @@ module.exports = class APIBinder
|
|||||||
@reportInitialConfig(retryDelay)
|
@reportInitialConfig(retryDelay)
|
||||||
|
|
||||||
getTargetState: =>
|
getTargetState: =>
|
||||||
@config.getMany([ 'uuid', 'currentApiKey', 'resinApiEndpoint', 'apiTimeout' ])
|
@config.getMany([ 'uuid', 'resinApiEndpoint', 'apiTimeout' ])
|
||||||
.then ({ uuid, currentApiKey, resinApiEndpoint, apiTimeout }) =>
|
.then ({ uuid, resinApiEndpoint, apiTimeout }) =>
|
||||||
endpoint = url.resolve(resinApiEndpoint, "/device/v2/#{uuid}/state")
|
endpoint = url.resolve(resinApiEndpoint, "/device/v2/#{uuid}/state")
|
||||||
|
|
||||||
requestParams = _.extend
|
requestParams = _.extend
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
url: "#{endpoint}?&apikey=#{currentApiKey}"
|
url: "#{endpoint}"
|
||||||
, @cachedResinApi.passthrough
|
, @cachedResinApi.passthrough
|
||||||
|
|
||||||
@cachedResinApi._request(requestParams)
|
@cachedResinApi._request(requestParams)
|
||||||
@ -312,11 +309,10 @@ module.exports = class APIBinder
|
|||||||
@lastTargetStateFetch = process.hrtime()
|
@lastTargetStateFetch = process.hrtime()
|
||||||
|
|
||||||
_pollTargetState: =>
|
_pollTargetState: =>
|
||||||
if @_targetStateInterval?
|
|
||||||
clearInterval(@_targetStateInterval)
|
|
||||||
@_targetStateInterval = null
|
|
||||||
@config.get('appUpdatePollInterval')
|
@config.get('appUpdatePollInterval')
|
||||||
.then (appUpdatePollInterval) =>
|
.then (appUpdatePollInterval) =>
|
||||||
|
if @_targetStateInterval?
|
||||||
|
clearInterval(@_targetStateInterval)
|
||||||
@_targetStateInterval = setInterval(@getAndSetTargetState, appUpdatePollInterval)
|
@_targetStateInterval = setInterval(@getAndSetTargetState, appUpdatePollInterval)
|
||||||
@getAndSetTargetState()
|
@getAndSetTargetState()
|
||||||
return
|
return
|
||||||
@ -336,20 +332,20 @@ module.exports = class APIBinder
|
|||||||
dependent: _.omitBy @stateForReport.dependent, (val, key) =>
|
dependent: _.omitBy @stateForReport.dependent, (val, key) =>
|
||||||
_.isEqual(@lastReportedState.dependent[key], val)
|
_.isEqual(@lastReportedState.dependent[key], val)
|
||||||
}
|
}
|
||||||
return _.pickBy(diff, (val) -> !_.isEmpty(val))
|
return _.pickBy(diff, _.negate(_.isEmpty))
|
||||||
|
|
||||||
_sendReportPatch: (stateDiff, conf) =>
|
_sendReportPatch: (stateDiff, conf) =>
|
||||||
endpoint = url.resolve(conf.resinApiEndpoint, "/device/v2/#{conf.uuid}/state")
|
endpoint = url.resolve(conf.resinApiEndpoint, "/device/v2/#{conf.uuid}/state")
|
||||||
requestParams = _.extend
|
requestParams = _.extend
|
||||||
method: 'PATCH'
|
method: 'PATCH'
|
||||||
url: "#{endpoint}?&apikey=#{conf.currentApiKey}"
|
url: "#{endpoint}"
|
||||||
body: stateDiff
|
body: stateDiff
|
||||||
, @cachedResinApi.passthrough
|
, @cachedResinApi.passthrough
|
||||||
|
|
||||||
@cachedResinApi._request(requestParams)
|
@cachedResinApi._request(requestParams)
|
||||||
|
|
||||||
_report: =>
|
_report: =>
|
||||||
@config.getMany([ 'currentApiKey', 'deviceId', 'apiTimeout', 'resinApiEndpoint', 'uuid' ])
|
@config.getMany([ 'deviceId', 'apiTimeout', 'resinApiEndpoint', 'uuid' ])
|
||||||
.then (conf) =>
|
.then (conf) =>
|
||||||
stateDiff = @_getStateDiff()
|
stateDiff = @_getStateDiff()
|
||||||
if _.size(stateDiff) is 0
|
if _.size(stateDiff) is 0
|
||||||
|
@ -3,10 +3,11 @@ _ = require 'lodash'
|
|||||||
EventEmitter = require 'events'
|
EventEmitter = require 'events'
|
||||||
express = require 'express'
|
express = require 'express'
|
||||||
bodyParser = require 'body-parser'
|
bodyParser = require 'body-parser'
|
||||||
|
fs = Promise.promisifyAll(require('fs'))
|
||||||
|
path = require 'path'
|
||||||
|
|
||||||
constants = require './lib/constants'
|
constants = require './lib/constants'
|
||||||
|
|
||||||
process.env.DOCKER_HOST ?= "unix://#{constants.dockerSocket}"
|
|
||||||
Docker = require './lib/docker-utils'
|
Docker = require './lib/docker-utils'
|
||||||
updateLock = require './lib/update-lock'
|
updateLock = require './lib/update-lock'
|
||||||
{ checkTruthy, checkInt, checkString } = require './lib/validation'
|
{ checkTruthy, checkInt, checkString } = require './lib/validation'
|
||||||
@ -20,11 +21,8 @@ Volumes = require './compose/volumes'
|
|||||||
|
|
||||||
Proxyvisor = require './proxyvisor'
|
Proxyvisor = require './proxyvisor'
|
||||||
|
|
||||||
serviceAction = (action, serviceId, current, target, options) ->
|
serviceAction = (action, serviceId, current, target, options = {}) ->
|
||||||
obj = { action, serviceId, current, target }
|
return { action, serviceId, current, target, options }
|
||||||
if options?
|
|
||||||
obj.options = options
|
|
||||||
return obj
|
|
||||||
|
|
||||||
# TODO: move this to an Image class?
|
# TODO: move this to an Image class?
|
||||||
imageForService = (service) ->
|
imageForService = (service) ->
|
||||||
@ -44,234 +42,193 @@ fetchAction = (service) ->
|
|||||||
image: imageForService(service)
|
image: imageForService(service)
|
||||||
serviceId: service.serviceId
|
serviceId: service.serviceId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pathExistsOnHost = (p) ->
|
||||||
|
fs.statAsync(path.join(constants.rootMountPoint, p))
|
||||||
|
.return(true)
|
||||||
|
.catchReturn(false)
|
||||||
|
|
||||||
|
appNotFoundMsg = "App not found: an app needs to be installed for this endpoint to work.
|
||||||
|
If you've recently moved this device from another app,
|
||||||
|
please push an app and wait for it to be installed first."
|
||||||
|
|
||||||
# TODO: implement additional v2 endpoints
|
# TODO: implement additional v2 endpoints
|
||||||
# v1 endpoins only work for single-container apps as they assume the app has a single service.
|
# Some v1 endpoins only work for single-container apps as they assume the app has a single service.
|
||||||
class ApplicationManagerRouter
|
createApplicationManagerRouter = (applications) ->
|
||||||
constructor: (@applications) ->
|
{ eventTracker, deviceState, _lockingIfNecessary, logger } = applications
|
||||||
{ @proxyvisor, @eventTracker, @deviceState, @_lockingIfNecessary, @logger } = @applications
|
router = express.Router()
|
||||||
@router = express.Router()
|
router.use(bodyParser.urlencoded(extended: true))
|
||||||
@router.use(bodyParser.urlencoded(extended: true))
|
router.use(bodyParser.json())
|
||||||
@router.use(bodyParser.json())
|
|
||||||
|
|
||||||
doRestart = (appId, force) =>
|
doRestart = (appId, force) ->
|
||||||
@_lockingIfNecessary appId, { force }, =>
|
_lockingIfNecessary appId, { force }, ->
|
||||||
@deviceState.getCurrentForComparison()
|
deviceState.getCurrentForComparison()
|
||||||
.then (currentState) =>
|
.then (currentState) ->
|
||||||
app = currentState.local.apps[appId]
|
app = currentState.local.apps[appId]
|
||||||
imageIds = _.map(app.services, 'imageId')
|
imageIds = _.map(app.services, 'imageId')
|
||||||
@applications.clearTargetVolatileForServices(imageIds)
|
applications.clearTargetVolatileForServices(imageIds)
|
||||||
stoppedApp = _.cloneDeep(app)
|
stoppedApp = _.cloneDeep(app)
|
||||||
stoppedApp.services = []
|
stoppedApp.services = []
|
||||||
currentState.local.apps[appId] = stoppedApp
|
currentState.local.apps[appId] = stoppedApp
|
||||||
@deviceState.pausingApply =>
|
deviceState.pausingApply ->
|
||||||
@deviceState.applyIntermediateTarget(currentState, { skipLock: true })
|
deviceState.applyIntermediateTarget(currentState, { skipLock: true })
|
||||||
.then =>
|
.then ->
|
||||||
currentState.local.apps[appId] = app
|
currentState.local.apps[appId] = app
|
||||||
@deviceState.applyIntermediateTarget(currentState, { skipLock: true })
|
deviceState.applyIntermediateTarget(currentState, { skipLock: true })
|
||||||
.finally =>
|
.finally ->
|
||||||
@deviceState.triggerApplyTarget()
|
deviceState.triggerApplyTarget()
|
||||||
|
|
||||||
doPurge = (appId, force) =>
|
doPurge = (appId, force) ->
|
||||||
@logger.logSystemMessage("Purging data for app #{appId}", { appId }, 'Purge data')
|
logger.logSystemMessage("Purging data for app #{appId}", { appId }, 'Purge data')
|
||||||
@_lockingIfNecessary appId, { force }, =>
|
_lockingIfNecessary appId, { force }, ->
|
||||||
@deviceState.getCurrentForComparison()
|
deviceState.getCurrentForComparison()
|
||||||
.then (currentState) =>
|
.then (currentState) ->
|
||||||
app = currentState.local.apps[appId]
|
app = currentState.local.apps[appId]
|
||||||
purgedApp = _.cloneDeep(app)
|
if !app?
|
||||||
purgedApp.services = []
|
throw new Error(appNotFoundMsg)
|
||||||
purgedApp.volumes = {}
|
purgedApp = _.cloneDeep(app)
|
||||||
currentState.local.apps[appId] = purgedApp
|
purgedApp.services = []
|
||||||
@deviceState.pausingApply =>
|
purgedApp.volumes = {}
|
||||||
@deviceState.applyIntermediateTarget(currentState, { skipLock: true })
|
currentState.local.apps[appId] = purgedApp
|
||||||
.then =>
|
deviceState.pausingApply ->
|
||||||
currentState.local.apps[appId] = app
|
deviceState.applyIntermediateTarget(currentState, { skipLock: true })
|
||||||
@deviceState.applyIntermediateTarget(currentState, { skipLock: true })
|
.then ->
|
||||||
.finally =>
|
currentState.local.apps[appId] = app
|
||||||
@deviceState.triggerApplyTarget()
|
deviceState.applyIntermediateTarget(currentState, { skipLock: true })
|
||||||
.tap =>
|
.finally ->
|
||||||
@logger.logSystemMessage('Purged data', { appId }, 'Purge data success')
|
deviceState.triggerApplyTarget()
|
||||||
.catch (err) =>
|
.tap ->
|
||||||
@logger.logSystemMessage("Error purging data: #{err}", { appId, error: err }, 'Purge data error')
|
logger.logSystemMessage('Purged data', { appId }, 'Purge data success')
|
||||||
throw err
|
.tapCatch (err) ->
|
||||||
|
logger.logSystemMessage("Error purging data: #{err}", { appId, error: err }, 'Purge data error')
|
||||||
|
|
||||||
@router.post '/v1/restart', (req, res) =>
|
router.post '/v1/restart', (req, res) ->
|
||||||
appId = checkInt(req.body.appId)
|
appId = checkInt(req.body.appId)
|
||||||
force = checkTruthy(req.body.force)
|
force = checkTruthy(req.body.force)
|
||||||
@eventTracker.track('Restart container (v1)', { appId })
|
eventTracker.track('Restart container (v1)', { appId })
|
||||||
if !appId?
|
if !appId?
|
||||||
return res.status(400).send('Missing app id')
|
return res.status(400).send('Missing app id')
|
||||||
doRestart(appId, force)
|
doRestart(appId, force)
|
||||||
.then ->
|
.then ->
|
||||||
res.status(200).send('OK')
|
res.status(200).send('OK')
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
res.status(503).send(err?.message or err or 'Unknown error')
|
res.status(503).send(err?.message or err or 'Unknown error')
|
||||||
|
|
||||||
@router.post '/v1/apps/:appId/stop', (req, res) =>
|
v1StopOrStart = (req, res, action) ->
|
||||||
appId = checkInt(req.params.appId)
|
appId = checkInt(req.params.appId)
|
||||||
force = checkTruthy(req.body.force)
|
force = checkTruthy(req.body.force)
|
||||||
if !appId?
|
if !appId?
|
||||||
return res.status(400).send('Missing app id')
|
return res.status(400).send('Missing app id')
|
||||||
@applications.getCurrentApp(appId)
|
applications.getCurrentApp(appId)
|
||||||
.then (app) =>
|
.then (app) ->
|
||||||
|
service = app?.services?[0]
|
||||||
|
if !service?
|
||||||
|
return res.status(400).send('App not found')
|
||||||
|
if app.services.length > 1
|
||||||
|
return res.status(400).send('Some v1 endpoints are only allowed on single-container apps')
|
||||||
|
applications.setTargetVolatileForService(service.imageId, running: action != 'stop')
|
||||||
|
applications.executeStepAction(serviceAction(action, service.serviceId, service), { force })
|
||||||
|
.then (service) ->
|
||||||
|
res.status(200).json({ containerId: service.containerId })
|
||||||
|
.catch (err) ->
|
||||||
|
res.status(503).send(err?.message or err or 'Unknown error')
|
||||||
|
|
||||||
|
router.post '/v1/apps/:appId/stop', (req, res) ->
|
||||||
|
v1StopOrStart(req, res, 'stop')
|
||||||
|
|
||||||
|
router.post '/v1/apps/:appId/start', (req, res) ->
|
||||||
|
v1StopOrStart(req, res, 'start')
|
||||||
|
|
||||||
|
router.get '/v1/apps/:appId', (req, res) ->
|
||||||
|
appId = checkInt(req.params.appId)
|
||||||
|
eventTracker.track('GET app (v1)', appId)
|
||||||
|
if !appId?
|
||||||
|
return res.status(400).send('Missing app id')
|
||||||
|
Promise.join(
|
||||||
|
applications.getCurrentApp(appId)
|
||||||
|
applications.getStatus()
|
||||||
|
(app, status) ->
|
||||||
service = app?.services?[0]
|
service = app?.services?[0]
|
||||||
if !service?
|
if !service?
|
||||||
return res.status(400).send('App not found')
|
return res.status(400).send('App not found')
|
||||||
if app.services.length > 1
|
if app.services.length > 1
|
||||||
return res.status(400).send('Some v1 endpoints are only allowed on single-container apps')
|
return res.status(400).send('Some v1 endpoints are only allowed on single-container apps')
|
||||||
@applications.setTargetVolatileForService(service.imageId, running: false)
|
# Don't return data that will be of no use to the user
|
||||||
@applications.executeStepAction(serviceAction('stop', service.serviceId, service), { force })
|
appToSend = {
|
||||||
.then (service) ->
|
appId
|
||||||
res.status(200).json({ containerId: service.containerId })
|
containerId: service.containerId
|
||||||
.catch (err) ->
|
env: _.omit(service.environment, constants.privateAppEnvVars)
|
||||||
res.status(503).send(err?.message or err or 'Unknown error')
|
releaseId: service.releaseId
|
||||||
|
imageId: service.image
|
||||||
|
}
|
||||||
|
if status.commit?
|
||||||
|
appToSend.commit = status.commit
|
||||||
|
res.json(appToSend)
|
||||||
|
)
|
||||||
|
.catch (err) ->
|
||||||
|
res.status(503).send(err?.message or err or 'Unknown error')
|
||||||
|
|
||||||
@router.post '/v1/apps/:appId/start', (req, res) =>
|
router.post '/v1/purge', (req, res) ->
|
||||||
appId = checkInt(req.params.appId)
|
appId = checkInt(req.body.appId)
|
||||||
force = checkTruthy(req.body.force)
|
force = checkTruthy(req.body.force)
|
||||||
if !appId?
|
if !appId?
|
||||||
return res.status(400).send('Missing app id')
|
errMsg = 'Invalid or missing appId'
|
||||||
@applications.getCurrentApp(appId)
|
return res.status(400).send(errMsg)
|
||||||
.then (app) =>
|
doPurge(appId, force)
|
||||||
service = app?.services?[0]
|
.then ->
|
||||||
|
res.status(200).json(Data: 'OK', Error: '')
|
||||||
|
.catch (err) ->
|
||||||
|
res.status(503).send(err?.message or err or 'Unknown error')
|
||||||
|
|
||||||
|
router.post '/v2/applications/:appId/purge', (req, res) ->
|
||||||
|
{ force } = req.body
|
||||||
|
{ appId } = req.params
|
||||||
|
doPurge(appId, force)
|
||||||
|
.then ->
|
||||||
|
res.status(200).send('OK')
|
||||||
|
.catch (err) ->
|
||||||
|
res.status(503).send(err?.message or err or 'Unknown error')
|
||||||
|
|
||||||
|
handleServiceAction = (req, res, action) ->
|
||||||
|
{ imageId, force } = req.body
|
||||||
|
{ appId } = req.params
|
||||||
|
_lockingIfNecessary appId, { force }, ->
|
||||||
|
applications.getCurrentApp(appId)
|
||||||
|
.then (app) ->
|
||||||
|
if !app?
|
||||||
|
return res.status(404).send(appNotFoundMsg)
|
||||||
|
service = _.find(app.services, { imageId })
|
||||||
if !service?
|
if !service?
|
||||||
return res.status(400).send('App not found')
|
errMsg = 'Service not found, a container must exist for this endpoint to work.'
|
||||||
if app.services.length > 1
|
return res.status(404).send(errMsg)
|
||||||
return res.status(400).send('Some v1 endpoints are only allowed on single-container apps')
|
applications.setTargetVolatileForService(service.imageId, running: action != 'stop')
|
||||||
@applications.setTargetVolatileForService(service.imageId, running: true)
|
applications.executeStepAction(serviceAction(action, service.serviceId, service, service, { wait: true }), { skipLock: true })
|
||||||
@applications.executeStepAction(serviceAction('start', service.serviceId, null, service), { force })
|
.then ->
|
||||||
.then (service) ->
|
res.status(200).send('OK')
|
||||||
res.status(200).json({ containerId: service.containerId })
|
.catch (err) ->
|
||||||
.catch (err) ->
|
res.status(503).send(err?.message or err or 'Unknown error')
|
||||||
res.status(503).send(err?.message or err or 'Unknown error')
|
|
||||||
|
|
||||||
@router.get '/v1/apps/:appId', (req, res) =>
|
router.post '/v2/applications/:appId/restart-service', (req, res) ->
|
||||||
appId = checkInt(req.params.appId)
|
handleServiceAction(req, res, 'restart')
|
||||||
@eventTracker.track('GET app (v1)', appId)
|
|
||||||
if !appId?
|
|
||||||
return res.status(400).send('Missing app id')
|
|
||||||
Promise.join(
|
|
||||||
@applications.getCurrentApp(appId)
|
|
||||||
@applications.getStatus()
|
|
||||||
(app, status) ->
|
|
||||||
service = app?.services?[0]
|
|
||||||
if !service?
|
|
||||||
return res.status(400).send('App not found')
|
|
||||||
if app.services.length > 1
|
|
||||||
return res.status(400).send('Some v1 endpoints are only allowed on single-container apps')
|
|
||||||
# Don't return data that will be of no use to the user
|
|
||||||
appToSend = {
|
|
||||||
appId
|
|
||||||
containerId: service.containerId
|
|
||||||
env: _.omit(service.environment, constants.privateAppEnvVars)
|
|
||||||
releaseId: service.releaseId
|
|
||||||
imageId: service.image
|
|
||||||
}
|
|
||||||
if status.commit?
|
|
||||||
appToSend.commit = status.commit
|
|
||||||
res.json(appToSend)
|
|
||||||
)
|
|
||||||
.catch (err) ->
|
|
||||||
res.status(503).send(err?.message or err or 'Unknown error')
|
|
||||||
|
|
||||||
@router.post '/v1/purge', (req, res) ->
|
router.post '/v2/applications/:appId/stop-service', (req, res) ->
|
||||||
appId = checkInt(req.body.appId)
|
handleServiceAction(req, res, 'stop')
|
||||||
force = checkTruthy(req.body.force)
|
|
||||||
if !appId?
|
|
||||||
errMsg = "App not found: an app needs to be installed for purge to work.
|
|
||||||
If you've recently moved this device from another app,
|
|
||||||
please push an app and wait for it to be installed first."
|
|
||||||
return res.status(400).send(errMsg)
|
|
||||||
doPurge(appId, force)
|
|
||||||
.then ->
|
|
||||||
res.status(200).json(Data: 'OK', Error: '')
|
|
||||||
.catch (err) ->
|
|
||||||
res.status(503).send(err?.message or err or 'Unknown error')
|
|
||||||
|
|
||||||
@router.post '/v2/applications/:appId/purge', (req, res) ->
|
router.post '/v2/applications/:appId/start-service', (req, res) ->
|
||||||
{ force } = req.body
|
handleServiceAction(req, res, 'start')
|
||||||
{ appId } = req.params
|
|
||||||
doPurge(appId, force)
|
|
||||||
.then ->
|
|
||||||
res.status(200).send('OK')
|
|
||||||
.catch (err) ->
|
|
||||||
res.status(503).send(err?.message or err or 'Unknown error')
|
|
||||||
|
|
||||||
@router.post '/v2/applications/:appId/restart-service', (req, res) =>
|
router.post '/v2/applications/:appId/restart', (req, res) ->
|
||||||
{ imageId, force } = req.body
|
{ force } = req.body
|
||||||
{ appId } = req.params
|
{ appId } = req.params
|
||||||
@_lockingIfNecessary appId, { force }, =>
|
doRestart(appId, force)
|
||||||
@applications.getCurrentApp(appId)
|
.then ->
|
||||||
.then (app) =>
|
res.status(200).send('OK')
|
||||||
if !app?
|
.catch (err) ->
|
||||||
errMsg = "App not found: an app needs to be installed for restart-service to work.
|
res.status(503).send(err?.message or err or 'Unknown error')
|
||||||
If you've recently moved this device from another app,
|
|
||||||
please push an app and wait for it to be installed first."
|
|
||||||
return res.status(404).send(errMsg)
|
|
||||||
service = _.find(app.services, (s) -> s.imageId == imageId)
|
|
||||||
if !service?
|
|
||||||
errMsg = 'Service not found, a container must exist for service restart to work.'
|
|
||||||
return res.status(404).send(errMsg)
|
|
||||||
@applications.setTargetVolatileForService(service.imageId, running: true)
|
|
||||||
@applications.executeStepAction(serviceAction('restart', service.serviceId, service, service), { skipLock: true })
|
|
||||||
.then ->
|
|
||||||
res.status(200).send('OK')
|
|
||||||
.catch (err) ->
|
|
||||||
res.status(503).send(err?.message or err or 'Unknown error')
|
|
||||||
|
|
||||||
@router.post '/v2/applications/:appId/stop-service', (req, res) =>
|
router.use(applications.proxyvisor.router)
|
||||||
{ imageId, force } = req.body
|
|
||||||
{ appId } = req.params
|
|
||||||
@_lockingIfNecessary appId, { force }, =>
|
|
||||||
@applications.getCurrentApp(appId)
|
|
||||||
.then (app) =>
|
|
||||||
if !app?
|
|
||||||
errMsg = "App not found: an app needs to be installed for stop-service to work.
|
|
||||||
If you've recently moved this device from another app,
|
|
||||||
please push an app and wait for it to be installed first."
|
|
||||||
return res.status(404).send(errMsg)
|
|
||||||
service = _.find(app.services, (s) -> s.imageId == imageId)
|
|
||||||
if !service?
|
|
||||||
errMsg = 'Service not found, a container must exist for service stop to work.'
|
|
||||||
return res.status(404).send(errMsg)
|
|
||||||
@applications.setTargetVolatileForService(service.imageId, running: false)
|
|
||||||
@applications.executeStepAction(serviceAction('stop', service.serviceId, service, service, { wait: true }), { skipLock: true })
|
|
||||||
.then ->
|
|
||||||
res.status(200).send('OK')
|
|
||||||
.catch (err) ->
|
|
||||||
res.status(503).send(err?.message or err or 'Unknown error')
|
|
||||||
|
|
||||||
@router.post '/v2/applications/:appId/start-service', (req, res) =>
|
return router
|
||||||
{ imageId, force } = req.body
|
|
||||||
{ appId } = req.params
|
|
||||||
@_lockingIfNecessary appId, { force }, =>
|
|
||||||
@applications.getCurrentApp(appId)
|
|
||||||
.then (app) =>
|
|
||||||
if !app?
|
|
||||||
errMsg = "App not found: an app needs to be installed for stop-service to work.
|
|
||||||
If you've recently moved this device from another app,
|
|
||||||
please push an app and wait for it to be installed first."
|
|
||||||
return res.status(404).send(errMsg)
|
|
||||||
service = _.find(app.services, (s) -> s.imageId == imageId)
|
|
||||||
if !service?
|
|
||||||
errMsg = 'Service not found, a container must exist for service start to work.'
|
|
||||||
return res.status(404).send(errMsg)
|
|
||||||
@applications.setTargetVolatileForService(service.imageId, running: true)
|
|
||||||
@applications.executeStepAction(serviceAction('start', service.serviceId, service, service), { skipLock: true })
|
|
||||||
.then ->
|
|
||||||
res.status(200).send('OK')
|
|
||||||
.catch (err) ->
|
|
||||||
res.status(503).send(err?.message or err or 'Unknown error')
|
|
||||||
|
|
||||||
@router.post '/v2/applications/:appId/restart', (req, res) ->
|
|
||||||
{ force } = req.body
|
|
||||||
{ appId } = req.params
|
|
||||||
doRestart(appId, force)
|
|
||||||
.then ->
|
|
||||||
res.status(200).send('OK')
|
|
||||||
.catch (err) ->
|
|
||||||
res.status(503).send(err?.message or err or 'Unknown error')
|
|
||||||
|
|
||||||
@router.use(@proxyvisor.router)
|
|
||||||
|
|
||||||
module.exports = class ApplicationManager extends EventEmitter
|
module.exports = class ApplicationManager extends EventEmitter
|
||||||
constructor: ({ @logger, @config, @db, @eventTracker, @deviceState }) ->
|
constructor: ({ @logger, @config, @db, @eventTracker, @deviceState }) ->
|
||||||
@ -304,8 +261,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
@services.updateMetadata(step.current, step.target)
|
@services.updateMetadata(step.current, step.target)
|
||||||
restart: (step, { force = false, skipLock = false } = {}) =>
|
restart: (step, { force = false, skipLock = false } = {}) =>
|
||||||
@_lockingIfNecessary step.current.appId, { force, skipLock: skipLock or step.options?.skipLock }, =>
|
@_lockingIfNecessary step.current.appId, { force, skipLock: skipLock or step.options?.skipLock }, =>
|
||||||
Promise.try =>
|
@services.kill(step.current, { wait: true })
|
||||||
@services.kill(step.current, { wait: true })
|
|
||||||
.then =>
|
.then =>
|
||||||
delete @_containerStarted[step.current.containerId]
|
delete @_containerStarted[step.current.containerId]
|
||||||
.then =>
|
.then =>
|
||||||
@ -331,8 +287,13 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
opts.deltaSource = @bestDeltaSource(step.image, availableImages)
|
opts.deltaSource = @bestDeltaSource(step.image, availableImages)
|
||||||
@images.triggerFetch step.image, opts, (success) =>
|
@images.triggerFetch step.image, opts, (success) =>
|
||||||
@fetchesInProgress -= 1
|
@fetchesInProgress -= 1
|
||||||
@timeSpentFetching += process.hrtime(startTime)[0]
|
elapsed = process.hrtime(startTime)
|
||||||
|
elapsedMs = elapsed[0] * 1000 + elapsed[1] / 1e6
|
||||||
|
@timeSpentFetching += elapsedMs
|
||||||
if success
|
if success
|
||||||
|
# update_downloaded is true if *any* image has been downloaded,
|
||||||
|
# and it's relevant mostly for the legacy GET /v1/device endpoint
|
||||||
|
# that assumes a single-container app
|
||||||
@reportCurrentState(update_downloaded: true)
|
@reportCurrentState(update_downloaded: true)
|
||||||
)
|
)
|
||||||
removeImage: (step) =>
|
removeImage: (step) =>
|
||||||
@ -351,9 +312,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
@networks.ensureSupervisorNetwork()
|
@networks.ensureSupervisorNetwork()
|
||||||
}
|
}
|
||||||
@validActions = _.keys(@actionExecutors).concat(@proxyvisor.validActions)
|
@validActions = _.keys(@actionExecutors).concat(@proxyvisor.validActions)
|
||||||
@_router = new ApplicationManagerRouter(this)
|
@router = createApplicationManagerRouter(this)
|
||||||
@router = @_router.router
|
|
||||||
|
|
||||||
@images.on('change', @reportCurrentState)
|
@images.on('change', @reportCurrentState)
|
||||||
@services.on('change', @reportCurrentState)
|
@services.on('change', @reportCurrentState)
|
||||||
|
|
||||||
@ -372,7 +331,6 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
@services.listenToEvents()
|
@services.listenToEvents()
|
||||||
|
|
||||||
# Returns the status of applications and their services
|
# Returns the status of applications and their services
|
||||||
# TODO: discuss: I think commit could be deduced by the UI looking at the image_installs on the API?
|
|
||||||
getStatus: =>
|
getStatus: =>
|
||||||
Promise.join(
|
Promise.join(
|
||||||
@services.getStatus()
|
@services.getStatus()
|
||||||
@ -382,11 +340,13 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
apps = {}
|
apps = {}
|
||||||
dependent = {}
|
dependent = {}
|
||||||
releaseId = null
|
releaseId = null
|
||||||
|
creationTimesAndReleases = {}
|
||||||
# We iterate over the current running services and add them to the current state
|
# We iterate over the current running services and add them to the current state
|
||||||
# of the app they belong to.
|
# of the app they belong to.
|
||||||
for service in services
|
for service in services
|
||||||
appId = service.appId
|
appId = service.appId
|
||||||
apps[appId] ?= {}
|
apps[appId] ?= {}
|
||||||
|
creationTimesAndReleases[appId] = {}
|
||||||
apps[appId].services ?= {}
|
apps[appId].services ?= {}
|
||||||
# We only send commit if all services have the same release, and it matches the target release
|
# We only send commit if all services have the same release, and it matches the target release
|
||||||
if !releaseId?
|
if !releaseId?
|
||||||
@ -395,11 +355,11 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
releaseId = false
|
releaseId = false
|
||||||
if !apps[appId].services[service.imageId]?
|
if !apps[appId].services[service.imageId]?
|
||||||
apps[appId].services[service.imageId] = _.pick(service, [ 'status', 'releaseId' ])
|
apps[appId].services[service.imageId] = _.pick(service, [ 'status', 'releaseId' ])
|
||||||
|
creationTimesAndReleases[appId][service.imageId] = _.pick(service, [ 'createdAt', 'releaseId' ])
|
||||||
apps[appId].services[service.imageId].download_progress = null
|
apps[appId].services[service.imageId].download_progress = null
|
||||||
else
|
else
|
||||||
# There's two containers with the same imageId, so this has to be a handover
|
# There's two containers with the same imageId, so this has to be a handover
|
||||||
previousReleaseId = apps[appId].services[service.imageId].releaseId
|
apps[appId].services[service.imageId].releaseId = _.minBy([ creationTimesAndReleases[appId][service.imageId], service ], 'createdAt').releaseId
|
||||||
apps[appId].services[service.imageId].releaseId = Math.max(previousReleaseId, service.releaseId)
|
|
||||||
apps[appId].services[service.imageId].status = 'Handing over'
|
apps[appId].services[service.imageId].status = 'Handing over'
|
||||||
|
|
||||||
for image in images
|
for image in images
|
||||||
@ -409,14 +369,13 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
apps[appId].services ?= {}
|
apps[appId].services ?= {}
|
||||||
apps[appId].services[image.imageId] ?= _.pick(image, [ 'status', 'releaseId' ])
|
apps[appId].services[image.imageId] ?= _.pick(image, [ 'status', 'releaseId' ])
|
||||||
apps[appId].services[image.imageId].download_progress = image.downloadProgress
|
apps[appId].services[image.imageId].download_progress = image.downloadProgress
|
||||||
|
else if image.imageId?
|
||||||
|
dependent[appId] ?= {}
|
||||||
|
dependent[appId].images ?= {}
|
||||||
|
dependent[appId].images[image.imageId] = _.pick(image, [ 'status' ])
|
||||||
|
dependent[appId].images[image.imageId].download_progress = image.downloadProgress
|
||||||
else
|
else
|
||||||
if image.imageId?
|
console.log('Ignoring legacy dependent image', image)
|
||||||
dependent[appId] ?= {}
|
|
||||||
dependent[appId].images ?= {}
|
|
||||||
dependent[appId].images[image.imageId] = _.pick(image, [ 'status' ])
|
|
||||||
dependent[appId].images[image.imageId].download_progress = image.downloadProgress
|
|
||||||
else
|
|
||||||
console.log('Ignoring legacy dependent image', image)
|
|
||||||
|
|
||||||
obj = { local: apps, dependent }
|
obj = { local: apps, dependent }
|
||||||
if releaseId and targetApps[0]?.releaseId == releaseId
|
if releaseId and targetApps[0]?.releaseId == releaseId
|
||||||
@ -454,8 +413,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
@services.getAll()
|
@services.getAll()
|
||||||
@networks.getAll()
|
@networks.getAll()
|
||||||
@volumes.getAll()
|
@volumes.getAll()
|
||||||
(services, networks, volumes) =>
|
@_buildApps
|
||||||
return @_buildApps(services, networks, volumes)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
getCurrentApp: (appId) =>
|
getCurrentApp: (appId) =>
|
||||||
@ -463,9 +421,8 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
@services.getAllByAppId(appId)
|
@services.getAllByAppId(appId)
|
||||||
@networks.getAllByAppId(appId)
|
@networks.getAllByAppId(appId)
|
||||||
@volumes.getAllByAppId(appId)
|
@volumes.getAllByAppId(appId)
|
||||||
(services, networks, volumes) =>
|
@_buildApps
|
||||||
return @_buildApps(services, networks, volumes)[appId]
|
).get(appId)
|
||||||
)
|
|
||||||
|
|
||||||
getTargetApp: (appId) =>
|
getTargetApp: (appId) =>
|
||||||
@db.models('app').where({ appId }).select()
|
@db.models('app').where({ appId }).select()
|
||||||
@ -486,7 +443,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
|
|
||||||
toBeRemoved = _.difference(currentServiceIds, targetServiceIds)
|
toBeRemoved = _.difference(currentServiceIds, targetServiceIds)
|
||||||
for serviceId in toBeRemoved
|
for serviceId in toBeRemoved
|
||||||
servicesToRemove = _.filter(currentServices, (s) -> s.serviceId == serviceId)
|
servicesToRemove = _.filter(currentServices, { serviceId })
|
||||||
for service in servicesToRemove
|
for service in servicesToRemove
|
||||||
removePairs.push({
|
removePairs.push({
|
||||||
current: service
|
current: service
|
||||||
@ -496,7 +453,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
|
|
||||||
toBeInstalled = _.difference(targetServiceIds, currentServiceIds)
|
toBeInstalled = _.difference(targetServiceIds, currentServiceIds)
|
||||||
for serviceId in toBeInstalled
|
for serviceId in toBeInstalled
|
||||||
serviceToInstall = _.find(targetServices, (s) -> s.serviceId == serviceId)
|
serviceToInstall = _.find(targetServices, { serviceId })
|
||||||
if serviceToInstall?
|
if serviceToInstall?
|
||||||
installPairs.push({
|
installPairs.push({
|
||||||
current: null
|
current: null
|
||||||
@ -508,8 +465,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
currentServicesPerId = {}
|
currentServicesPerId = {}
|
||||||
targetServicesPerId = _.keyBy(targetServices, 'serviceId')
|
targetServicesPerId = _.keyBy(targetServices, 'serviceId')
|
||||||
for serviceId in toBeMaybeUpdated
|
for serviceId in toBeMaybeUpdated
|
||||||
currentServiceContainers = _.filter currentServices, (service) ->
|
currentServiceContainers = _.filter(currentServices, { serviceId })
|
||||||
return service.serviceId == serviceId
|
|
||||||
if currentServiceContainers.length > 1
|
if currentServiceContainers.length > 1
|
||||||
currentServicesPerId[serviceId] = _.maxBy(currentServiceContainers, 'createdAt')
|
currentServicesPerId[serviceId] = _.maxBy(currentServiceContainers, 'createdAt')
|
||||||
# All but the latest container for this service are spurious and should be removed
|
# All but the latest container for this service are spurious and should be removed
|
||||||
@ -525,12 +481,14 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
# Returns true if a service matches its target except it should be running and it is not, but we've
|
# Returns true if a service matches its target except it should be running and it is not, but we've
|
||||||
# already started it before. In this case it means it just exited so we don't want to start it again.
|
# already started it before. In this case it means it just exited so we don't want to start it again.
|
||||||
alreadyStarted = (serviceId) =>
|
alreadyStarted = (serviceId) =>
|
||||||
currentServicesPerId[serviceId].isEqualExceptForRunningState(targetServicesPerId[serviceId]) and
|
return (
|
||||||
|
currentServicesPerId[serviceId].isEqualExceptForRunningState(targetServicesPerId[serviceId]) and
|
||||||
targetServicesPerId[serviceId].running and
|
targetServicesPerId[serviceId].running and
|
||||||
@_containerStarted[currentServicesPerId[serviceId].containerId]
|
@_containerStarted[currentServicesPerId[serviceId].containerId]
|
||||||
|
)
|
||||||
|
|
||||||
needUpdate = _.filter toBeMaybeUpdated, (serviceId) ->
|
needUpdate = _.filter toBeMaybeUpdated, (serviceId) ->
|
||||||
return !currentServicesPerId[serviceId].isEqual(targetServicesPerId[serviceId]) and !alreadyStarted(serviceId)
|
!currentServicesPerId[serviceId].isEqual(targetServicesPerId[serviceId]) and !alreadyStarted(serviceId)
|
||||||
for serviceId in needUpdate
|
for serviceId in needUpdate
|
||||||
updatePairs.push({
|
updatePairs.push({
|
||||||
current: currentServicesPerId[serviceId]
|
current: currentServicesPerId[serviceId]
|
||||||
@ -599,26 +557,24 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
name = _.split(volume, ':')[0]
|
name = _.split(volume, ':')[0]
|
||||||
_.some volumePairs, (pair) ->
|
_.some volumePairs, (pair) ->
|
||||||
"#{service.appId}_#{pair.current?.name}" == name
|
"#{service.appId}_#{pair.current?.name}" == name
|
||||||
if hasVolume
|
return hasVolume
|
||||||
return true
|
|
||||||
return false
|
|
||||||
|
|
||||||
# TODO: account for volumes-from, networks-from, links, etc
|
# TODO: account for volumes-from, networks-from, links, etc
|
||||||
# TODO: support networks instead of only networkMode
|
# TODO: support networks instead of only networkMode
|
||||||
_dependenciesMetForServiceStart: (target, networkPairs, volumePairs, pendingPairs) ->
|
_dependenciesMetForServiceStart: (target, networkPairs, volumePairs, pendingPairs) ->
|
||||||
# for dependsOn, check no install or update pairs have that service
|
# for dependsOn, check no install or update pairs have that service
|
||||||
dependencyUnmet = _.some target.dependsOn ? [], (dependency) ->
|
dependencyUnmet = _.some target.dependsOn ? [], (dependency) ->
|
||||||
_.find(pendingPairs, (pair) -> pair.target?.serviceName == dependency)?
|
_.some(pendingPairs, (pair) -> pair.target?.serviceName == dependency)
|
||||||
if dependencyUnmet
|
if dependencyUnmet
|
||||||
return false
|
return false
|
||||||
# for networks and volumes, check no network pairs have that volume name
|
# for networks and volumes, check no network pairs have that volume name
|
||||||
if _.find(networkPairs, (pair) -> "#{target.appId}_#{pair.target?.name}" == target.networkMode)?
|
if _.some(networkPairs, (pair) -> "#{target.appId}_#{pair.target?.name}" == target.networkMode)
|
||||||
return false
|
return false
|
||||||
volumeUnmet = _.some target.volumes, (volumeDefinition) ->
|
volumeUnmet = _.some target.volumes, (volumeDefinition) ->
|
||||||
[ sourceName, destName ] = volumeDefinition.split(':')
|
[ sourceName, destName ] = volumeDefinition.split(':')
|
||||||
if !destName? # If this is not a named volume, ignore it
|
if !destName? # If this is not a named volume, ignore it
|
||||||
return false
|
return false
|
||||||
return _.find(volumePairs, (pair) -> "#{target.appId}_#{pair.target?.name}" == sourceName)?
|
return _.some(volumePairs, (pair) -> "#{target.appId}_#{pair.target?.name}" == sourceName)
|
||||||
return !volumeUnmet
|
return !volumeUnmet
|
||||||
|
|
||||||
# Unless the update strategy requires an early kill (i.e. kill-then-download, delete-then-download), we only want
|
# Unless the update strategy requires an early kill (i.e. kill-then-download, delete-then-download), we only want
|
||||||
@ -627,8 +583,8 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
_dependenciesMetForServiceKill: (target, targetApp, availableImages) =>
|
_dependenciesMetForServiceKill: (target, targetApp, availableImages) =>
|
||||||
if target.dependsOn?
|
if target.dependsOn?
|
||||||
for dependency in target.dependsOn
|
for dependency in target.dependsOn
|
||||||
dependencyService = _.find(targetApp.services, (s) -> s.serviceName == dependency)
|
dependencyService = _.find(targetApp.services, serviceName: dependency)
|
||||||
if !_.find(availableImages, (image) => image.dockerImageId == dependencyService.image or @images.isSameImage(image, { name: dependencyService.imageName }))?
|
if !_.some(availableImages, (image) => image.dockerImageId == dependencyService.image or @images.isSameImage(image, { name: dependencyService.imageName }))
|
||||||
return false
|
return false
|
||||||
return true
|
return true
|
||||||
|
|
||||||
@ -644,7 +600,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
# we have to kill them before removing the network/volume (e.g. when we're only updating the network config)
|
# we have to kill them before removing the network/volume (e.g. when we're only updating the network config)
|
||||||
steps = []
|
steps = []
|
||||||
for dependency in dependencies
|
for dependency in dependencies
|
||||||
if dependency.status != 'Stopping' and !_.some(changingPairs, (pair) -> pair.serviceId == dependency.serviceId)
|
if dependency.status != 'Stopping' and !_.some(changingPairs, serviceId: dependency.serviceId)
|
||||||
steps.push(serviceAction('kill', dependency.serviceId, dependency))
|
steps.push(serviceAction('kill', dependency.serviceId, dependency))
|
||||||
return steps
|
return steps
|
||||||
else if target?
|
else if target?
|
||||||
@ -696,7 +652,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
'hand-over': (current, target, needsDownload, dependenciesMetForStart, dependenciesMetForKill, needsSpecialKill, timeout) ->
|
'hand-over': (current, target, needsDownload, dependenciesMetForStart, dependenciesMetForKill, needsSpecialKill, timeout) ->
|
||||||
if needsDownload
|
if needsDownload
|
||||||
return fetchAction(target)
|
return fetchAction(target)
|
||||||
else if needsSpecialKill && dependenciesMetForKill()
|
else if needsSpecialKill and dependenciesMetForKill()
|
||||||
return serviceAction('kill', target.serviceId, current, target)
|
return serviceAction('kill', target.serviceId, current, target)
|
||||||
else if dependenciesMetForStart()
|
else if dependenciesMetForStart()
|
||||||
return serviceAction('handover', target.serviceId, current, target, timeout: timeout)
|
return serviceAction('handover', target.serviceId, current, target, timeout: timeout)
|
||||||
@ -747,8 +703,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
else
|
else
|
||||||
# Create the default network for the target app
|
# Create the default network for the target app
|
||||||
targetApp.networks['default'] ?= {}
|
targetApp.networks['default'] ?= {}
|
||||||
if !currentApp?
|
currentApp ?= emptyApp
|
||||||
currentApp = emptyApp
|
|
||||||
if currentApp.services?.length == 1 and targetApp.services?.length == 1 and
|
if currentApp.services?.length == 1 and targetApp.services?.length == 1 and
|
||||||
targetApp.services[0].serviceName == currentApp.services[0].serviceName and
|
targetApp.services[0].serviceName == currentApp.services[0].serviceName and
|
||||||
checkTruthy(currentApp.services[0].labels['io.resin.legacy-container'])
|
checkTruthy(currentApp.services[0].labels['io.resin.legacy-container'])
|
||||||
@ -828,10 +783,15 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
@config.get('extendedEnvOptions')
|
@config.get('extendedEnvOptions')
|
||||||
@docker.getNetworkGateway(constants.supervisorNetworkInterface)
|
@docker.getNetworkGateway(constants.supervisorNetworkInterface)
|
||||||
.catchReturn('127.0.0.1')
|
.catchReturn('127.0.0.1')
|
||||||
(opts, supervisorApiHost) =>
|
Promise.props({
|
||||||
|
firmware: pathExistsOnHost('/lib/firmware')
|
||||||
|
modules: pathExistsOnHost('/lib/modules')
|
||||||
|
})
|
||||||
|
(opts, supervisorApiHost, hostPathExists) =>
|
||||||
configOpts = {
|
configOpts = {
|
||||||
appName: app.name
|
appName: app.name
|
||||||
supervisorApiHost
|
supervisorApiHost
|
||||||
|
hostPathExists
|
||||||
}
|
}
|
||||||
_.assign(configOpts, opts)
|
_.assign(configOpts, opts)
|
||||||
volumes = JSON.parse(app.volumes)
|
volumes = JSON.parse(app.volumes)
|
||||||
@ -931,10 +891,10 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
allImagesForTargetApp = (app) -> _.map(app.services, imageForService)
|
allImagesForTargetApp = (app) -> _.map(app.services, imageForService)
|
||||||
allImagesForCurrentApp = (app) ->
|
allImagesForCurrentApp = (app) ->
|
||||||
_.map app.services, (service) ->
|
_.map app.services, (service) ->
|
||||||
_.omit(_.find(available, (image) -> image.dockerImageId == service.image and image.imageId == service.imageId), [ 'dockerImageId', 'id' ])
|
_.omit(_.find(available, { dockerImageId: service.image, imageId: service.imageId }), [ 'dockerImageId', 'id' ])
|
||||||
availableWithoutIds = _.map(available, (image) -> _.omit(image, [ 'dockerImageId', 'id' ]))
|
availableWithoutIds = _.map(available, (image) -> _.omit(image, [ 'dockerImageId', 'id' ]))
|
||||||
currentImages = _.flatten(_.map(current.local.apps, allImagesForCurrentApp))
|
currentImages = _.flatMap(current.local.apps, allImagesForCurrentApp)
|
||||||
targetImages = _.flatten(_.map(target.local.apps, allImagesForTargetApp))
|
targetImages = _.flatMap(target.local.apps, allImagesForTargetApp)
|
||||||
availableAndUnused = _.filter availableWithoutIds, (image) ->
|
availableAndUnused = _.filter availableWithoutIds, (image) ->
|
||||||
!_.some currentImages.concat(targetImages), (imageInUse) -> _.isEqual(image, imageInUse)
|
!_.some currentImages.concat(targetImages), (imageInUse) -> _.isEqual(image, imageInUse)
|
||||||
imagesToDownload = _.filter targetImages, (targetImage) =>
|
imagesToDownload = _.filter targetImages, (targetImage) =>
|
||||||
@ -942,13 +902,13 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
# Images that are available but we don't have them in the DB with the exact metadata:
|
# Images that are available but we don't have them in the DB with the exact metadata:
|
||||||
imagesToSave = _.filter targetImages, (targetImage) =>
|
imagesToSave = _.filter targetImages, (targetImage) =>
|
||||||
_.some(available, (availableImage) => @images.isSameImage(availableImage, targetImage)) and
|
_.some(available, (availableImage) => @images.isSameImage(availableImage, targetImage)) and
|
||||||
!_.find(availableWithoutIds, (img) -> _.isEqual(img, targetImage))?
|
!_.some(availableWithoutIds, (img) -> _.isEqual(img, targetImage))
|
||||||
|
|
||||||
deltaSources = _.map imagesToDownload, (image) =>
|
deltaSources = _.map imagesToDownload, (image) =>
|
||||||
return @bestDeltaSource(image, available)
|
return @bestDeltaSource(image, available)
|
||||||
proxyvisorImages = @proxyvisor.imagesInUse(current, target)
|
proxyvisorImages = @proxyvisor.imagesInUse(current, target)
|
||||||
imagesToRemove = _.filter availableAndUnused, (image) =>
|
imagesToRemove = _.filter availableAndUnused, (image) =>
|
||||||
notUsedForDelta = !_.some deltaSources, (deltaSource) -> deltaSource == image.name
|
notUsedForDelta = !_.includes(deltaSources, image.name)
|
||||||
notUsedByProxyvisor = !_.some proxyvisorImages, (proxyvisorImage) => @images.isSameImage(image, { name: proxyvisorImage })
|
notUsedByProxyvisor = !_.some proxyvisorImages, (proxyvisorImage) => @images.isSameImage(image, { name: proxyvisorImage })
|
||||||
return notUsedForDelta and notUsedByProxyvisor
|
return notUsedForDelta and notUsedByProxyvisor
|
||||||
return { imagesToSave, imagesToRemove }
|
return { imagesToSave, imagesToRemove }
|
||||||
@ -986,7 +946,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
if !ignoreImages and delta and newDownloads > 0
|
if !ignoreImages and delta and newDownloads > 0
|
||||||
downloadsToBlock = downloading.length + newDownloads - constants.maxDeltaDownloads
|
downloadsToBlock = downloading.length + newDownloads - constants.maxDeltaDownloads
|
||||||
while downloadsToBlock > 0
|
while downloadsToBlock > 0
|
||||||
_.pull(nextSteps, _.find(nextSteps, (s) -> s.action == 'fetch'))
|
_.pull(nextSteps, _.find(nextSteps, action: 'fetch'))
|
||||||
downloadsToBlock -= 1
|
downloadsToBlock -= 1
|
||||||
if !ignoreImages and _.isEmpty(nextSteps) and !_.isEmpty(downloading)
|
if !ignoreImages and _.isEmpty(nextSteps) and !_.isEmpty(downloading)
|
||||||
nextSteps.push({ action: 'noop' })
|
nextSteps.push({ action: 'noop' })
|
||||||
@ -1002,7 +962,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
|
|
||||||
_lockingIfNecessary: (appId, { force = false, skipLock = false } = {}, fn) =>
|
_lockingIfNecessary: (appId, { force = false, skipLock = false } = {}, fn) =>
|
||||||
if skipLock
|
if skipLock
|
||||||
return Promise.try( -> fn())
|
return Promise.try(fn)
|
||||||
@config.get('lockOverride')
|
@config.get('lockOverride')
|
||||||
.then (lockOverride) ->
|
.then (lockOverride) ->
|
||||||
return checkTruthy(lockOverride) or force
|
return checkTruthy(lockOverride) or force
|
||||||
@ -1026,7 +986,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
(cleanupNeeded, availableImages, downloading, supervisorNetworkReady, conf) =>
|
(cleanupNeeded, availableImages, downloading, supervisorNetworkReady, conf) =>
|
||||||
@_inferNextSteps(cleanupNeeded, availableImages, downloading, supervisorNetworkReady, currentState, targetState, ignoreImages, conf)
|
@_inferNextSteps(cleanupNeeded, availableImages, downloading, supervisorNetworkReady, currentState, targetState, ignoreImages, conf)
|
||||||
.then (nextSteps) =>
|
.then (nextSteps) =>
|
||||||
if ignoreImages and _.some(nextSteps, (step) -> step.action == 'fetch')
|
if ignoreImages and _.some(nextSteps, action: 'fetch')
|
||||||
throw new Error('Cannot fetch images while executing an API action')
|
throw new Error('Cannot fetch images while executing an API action')
|
||||||
@proxyvisor.getRequiredSteps(availableImages, downloading, currentState, targetState, nextSteps)
|
@proxyvisor.getRequiredSteps(availableImages, downloading, currentState, targetState, nextSteps)
|
||||||
.then (proxyvisorSteps) ->
|
.then (proxyvisorSteps) ->
|
||||||
|
@ -21,7 +21,7 @@ validation = require '../lib/validation'
|
|||||||
# }
|
# }
|
||||||
|
|
||||||
hasDigest = (name) ->
|
hasDigest = (name) ->
|
||||||
name?.split?('@')?[1]? ? false
|
name?.split?('@')?[1]?
|
||||||
|
|
||||||
module.exports = class Images extends EventEmitter
|
module.exports = class Images extends EventEmitter
|
||||||
constructor: ({ @docker, @logger, @db }) ->
|
constructor: ({ @docker, @logger, @db }) ->
|
||||||
@ -136,10 +136,10 @@ module.exports = class Images extends EventEmitter
|
|||||||
# Image has a regular tag, so we might have to remove unnecessary tags
|
# Image has a regular tag, so we might have to remove unnecessary tags
|
||||||
@docker.getImage(img.dockerImageId).inspect()
|
@docker.getImage(img.dockerImageId).inspect()
|
||||||
.then (dockerImg) =>
|
.then (dockerImg) =>
|
||||||
differentTags = _.filter(imagesFromDB, (dbImage) -> dbImage.name != img.name)
|
differentTags = _.reject(imagesFromDB, name: img.name)
|
||||||
if dockerImg.RepoTags.length > 1 and
|
if dockerImg.RepoTags.length > 1 and
|
||||||
_.some(dockerImg.RepoTags, (tag) -> tag == img.name) and
|
_.includes(dockerImg.RepoTags, img.name) and
|
||||||
_.some(dockerImg.RepoTags, (tag) -> _.some(differentTags, (dbImage) -> dbImage.name == tag))
|
_.some(dockerImg.RepoTags, (tag) -> _.some(differentTags, name: tag))
|
||||||
@docker.getImage(img.name).remove(noprune: true)
|
@docker.getImage(img.name).remove(noprune: true)
|
||||||
.return(false)
|
.return(false)
|
||||||
else
|
else
|
||||||
@ -155,9 +155,8 @@ module.exports = class Images extends EventEmitter
|
|||||||
|
|
||||||
remove: (image) =>
|
remove: (image) =>
|
||||||
@_removeImageIfNotNeeded(image)
|
@_removeImageIfNotNeeded(image)
|
||||||
.catch (err) =>
|
.tapCatch (err) =>
|
||||||
@logger.logSystemEvent(logTypes.deleteImageError, { image, error: err })
|
@logger.logSystemEvent(logTypes.deleteImageError, { image, error: err })
|
||||||
throw err
|
|
||||||
|
|
||||||
getByDockerId: (id) =>
|
getByDockerId: (id) =>
|
||||||
@db.models('image').where(dockerImageId: id).first()
|
@db.models('image').where(dockerImageId: id).first()
|
||||||
@ -167,7 +166,7 @@ module.exports = class Images extends EventEmitter
|
|||||||
.then(@remove)
|
.then(@remove)
|
||||||
|
|
||||||
getNormalisedTags: (image) ->
|
getNormalisedTags: (image) ->
|
||||||
Promise.map(image.RepoTags ? [], (tag) => @normalise(tag))
|
Promise.map(image.RepoTags ? [], @normalise)
|
||||||
|
|
||||||
_withImagesFromDockerAndDB: (callback) =>
|
_withImagesFromDockerAndDB: (callback) =>
|
||||||
Promise.join(
|
Promise.join(
|
||||||
@ -194,7 +193,7 @@ module.exports = class Images extends EventEmitter
|
|||||||
|
|
||||||
getDownloadingImageIds: =>
|
getDownloadingImageIds: =>
|
||||||
Promise.try =>
|
Promise.try =>
|
||||||
return _.map(_.keys(_.pickBy(@volatileState, (s) -> s.status == 'Downloading')), validation.checkInt)
|
return _.map(_.keys(_.pickBy(@volatileState, status: 'Downloading')), validation.checkInt)
|
||||||
|
|
||||||
cleanupDatabase: =>
|
cleanupDatabase: =>
|
||||||
@_withImagesFromDockerAndDB (dockerImages, supervisedImages) =>
|
@_withImagesFromDockerAndDB (dockerImages, supervisedImages) =>
|
||||||
@ -252,7 +251,6 @@ module.exports = class Images extends EventEmitter
|
|||||||
.then =>
|
.then =>
|
||||||
toCleanup = _.filter _.uniq(images), (image) =>
|
toCleanup = _.filter _.uniq(images), (image) =>
|
||||||
!@imageCleanupFailures[image]? or Date.now() - @imageCleanupFailures[image] > constants.imageCleanupErrorIgnoreTimeout
|
!@imageCleanupFailures[image]? or Date.now() - @imageCleanupFailures[image] > constants.imageCleanupErrorIgnoreTimeout
|
||||||
#console.log(toCleanup)
|
|
||||||
return toCleanup
|
return toCleanup
|
||||||
inspectByName: (imageName) =>
|
inspectByName: (imageName) =>
|
||||||
@docker.getImage(imageName).inspect()
|
@docker.getImage(imageName).inspect()
|
||||||
|
@ -47,16 +47,14 @@ module.exports = class Networks
|
|||||||
'io.resin.supervised': 'true'
|
'io.resin.supervised': 'true'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch (err) =>
|
.tapCatch (err) =>
|
||||||
@logger.logSystemEvent(logTypes.createNetworkError, { network: { name, appId }, error: err })
|
@logger.logSystemEvent(logTypes.createNetworkError, { network: { name, appId }, error: err })
|
||||||
throw err
|
|
||||||
|
|
||||||
remove: ({ name, appId }) =>
|
remove: ({ name, appId }) =>
|
||||||
@logger.logSystemEvent(logTypes.removeNetwork, { network: { name, appId } })
|
@logger.logSystemEvent(logTypes.removeNetwork, { network: { name, appId } })
|
||||||
@docker.getNetwork("#{appId}_#{name}").remove()
|
@docker.getNetwork("#{appId}_#{name}").remove()
|
||||||
.catch (err) =>
|
.tapCatch (err) =>
|
||||||
@logger.logSystemEvent(logTypes.removeNetworkError, { network: { name, appId }, error: err })
|
@logger.logSystemEvent(logTypes.removeNetworkError, { network: { name, appId }, error: err })
|
||||||
throw err
|
|
||||||
|
|
||||||
supervisorNetworkReady: =>
|
supervisorNetworkReady: =>
|
||||||
# For mysterious reasons sometimes the balena/docker network exists
|
# For mysterious reasons sometimes the balena/docker network exists
|
||||||
@ -80,8 +78,7 @@ module.exports = class Networks
|
|||||||
removeIt()
|
removeIt()
|
||||||
else
|
else
|
||||||
fs.statAsync("/sys/class/net/#{constants.supervisorNetworkInterface}")
|
fs.statAsync("/sys/class/net/#{constants.supervisorNetworkInterface}")
|
||||||
.catch ENOENT, ->
|
.catch(ENOENT, removeIt)
|
||||||
removeIt()
|
|
||||||
.catch NotFoundError, =>
|
.catch NotFoundError, =>
|
||||||
console.log('Creating supervisor0 network')
|
console.log('Creating supervisor0 network')
|
||||||
@docker.createNetwork({
|
@docker.createNetwork({
|
||||||
|
@ -66,7 +66,6 @@ module.exports = class ServiceManager extends EventEmitter
|
|||||||
throw err
|
throw err
|
||||||
.tap =>
|
.tap =>
|
||||||
delete @containerHasDied[containerId]
|
delete @containerHasDied[containerId]
|
||||||
.tap =>
|
|
||||||
@logger.logSystemEvent(logTypes.stopServiceSuccess, { service })
|
@logger.logSystemEvent(logTypes.stopServiceSuccess, { service })
|
||||||
.catch (err) =>
|
.catch (err) =>
|
||||||
@logger.logSystemEvent(logTypes.stopServiceError, { service, error: err })
|
@logger.logSystemEvent(logTypes.stopServiceError, { service, error: err })
|
||||||
@ -101,13 +100,11 @@ module.exports = class ServiceManager extends EventEmitter
|
|||||||
.tap (container) =>
|
.tap (container) =>
|
||||||
service.containerId = container.id
|
service.containerId = container.id
|
||||||
Promise.map nets, ({ name, endpointConfig }) =>
|
Promise.map nets, ({ name, endpointConfig }) =>
|
||||||
console.log('Connecting ' + container.id + ' to network ' + name)
|
|
||||||
@docker.getNetwork(name).connect({ Container: container.id, EndpointConfig: endpointConfig })
|
@docker.getNetwork(name).connect({ Container: container.id, EndpointConfig: endpointConfig })
|
||||||
.tap =>
|
.tap =>
|
||||||
@logger.logSystemEvent(logTypes.installServiceSuccess, { service })
|
@logger.logSystemEvent(logTypes.installServiceSuccess, { service })
|
||||||
.catch (err) =>
|
.tapCatch (err) =>
|
||||||
@logger.logSystemEvent(logTypes.installServiceError, { service, error: err })
|
@logger.logSystemEvent(logTypes.installServiceError, { service, error: err })
|
||||||
throw err
|
|
||||||
.finally =>
|
.finally =>
|
||||||
@reportChange(mockContainerId)
|
@reportChange(mockContainerId)
|
||||||
|
|
||||||
@ -135,12 +132,12 @@ module.exports = class ServiceManager extends EventEmitter
|
|||||||
else
|
else
|
||||||
# rethrow the same error
|
# rethrow the same error
|
||||||
throw err
|
throw err
|
||||||
.catch (err) =>
|
.tapCatch (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 =>
|
.catch(_.noop)
|
||||||
|
.then =>
|
||||||
@logger.logSystemEvent(logTypes.startServiceError, { service, error: err })
|
@logger.logSystemEvent(logTypes.startServiceError, { service, error: err })
|
||||||
throw err
|
|
||||||
.then =>
|
.then =>
|
||||||
@logger.attach(@docker, container.id, service.serviceId)
|
@logger.attach(@docker, container.id, service.serviceId)
|
||||||
.tap =>
|
.tap =>
|
||||||
@ -176,7 +173,7 @@ module.exports = class ServiceManager extends EventEmitter
|
|||||||
.then (services) =>
|
.then (services) =>
|
||||||
status = _.clone(@volatileState)
|
status = _.clone(@volatileState)
|
||||||
for service in services
|
for service in services
|
||||||
status[service.containerId] ?= _.pick(service, [ 'appId', 'imageId', 'status', 'releaseId', 'commit' ])
|
status[service.containerId] ?= _.pick(service, [ 'appId', 'imageId', 'status', 'releaseId', 'commit', 'createdAt' ])
|
||||||
return _.values(status)
|
return _.values(status)
|
||||||
|
|
||||||
getByDockerContainerId: (containerId) =>
|
getByDockerContainerId: (containerId) =>
|
||||||
@ -233,7 +230,7 @@ module.exports = class ServiceManager extends EventEmitter
|
|||||||
@docker.getEvents(filters: type: [ 'container' ])
|
@docker.getEvents(filters: type: [ 'container' ])
|
||||||
.then (stream) =>
|
.then (stream) =>
|
||||||
stream.on 'error', (err) ->
|
stream.on 'error', (err) ->
|
||||||
console.error('Error on docker events stream:', err, err.stack)
|
console.error('Error on docker events stream:', err)
|
||||||
parser = JSONStream.parse()
|
parser = JSONStream.parse()
|
||||||
parser.on 'data', (data) =>
|
parser.on 'data', (data) =>
|
||||||
if data?.status in ['die', 'start']
|
if data?.status in ['die', 'start']
|
||||||
@ -257,9 +254,8 @@ module.exports = class ServiceManager extends EventEmitter
|
|||||||
parser
|
parser
|
||||||
.on 'error', (err) ->
|
.on 'error', (err) ->
|
||||||
console.error('Error on docker events stream', err)
|
console.error('Error on docker events stream', err)
|
||||||
reject()
|
reject(err)
|
||||||
.on 'end', ->
|
.on 'end', ->
|
||||||
console.error('Docker events stream ended, restarting listener')
|
|
||||||
resolve()
|
resolve()
|
||||||
stream.pipe(parser)
|
stream.pipe(parser)
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
|
@ -24,55 +24,33 @@ parseMemoryNumber = (numAsString, defaultVal) ->
|
|||||||
# Construct a restart policy based on its name.
|
# Construct a restart policy based on its name.
|
||||||
# The default policy (if name is not a valid policy) is "always".
|
# The default policy (if name is not a valid policy) is "always".
|
||||||
createRestartPolicy = (name) ->
|
createRestartPolicy = (name) ->
|
||||||
if not (name in validRestartPolicies)
|
if name not in validRestartPolicies
|
||||||
name = 'always'
|
name = 'always'
|
||||||
return { Name: name, MaximumRetryCount: 0 }
|
return { Name: name, MaximumRetryCount: 0 }
|
||||||
|
|
||||||
getCommand = (service, imageInfo) ->
|
getCommand = (service, imageInfo) ->
|
||||||
cmd = null
|
cmd = service.command ? imageInfo?.Config?.Cmd ? null
|
||||||
if service.command?
|
|
||||||
cmd = service.command
|
|
||||||
else if imageInfo?.Config?.Cmd?
|
|
||||||
cmd = imageInfo.Config.Cmd
|
|
||||||
if _.isString(cmd)
|
if _.isString(cmd)
|
||||||
cmd = parseCommand(cmd)
|
cmd = parseCommand(cmd)
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
getEntrypoint = (service, imageInfo) ->
|
getEntrypoint = (service, imageInfo) ->
|
||||||
entry = null
|
entry = service.entrypoint ? imageInfo?.Config?.Entrypoint ? null
|
||||||
if service.entrypoint?
|
|
||||||
entry = service.entrypoint
|
|
||||||
else if imageInfo?.Config?.Entrypoint?
|
|
||||||
entry = imageInfo.Config.Entrypoint
|
|
||||||
if _.isString(entry)
|
if _.isString(entry)
|
||||||
entry = parseCommand(entry)
|
entry = parseCommand(entry)
|
||||||
return entry
|
return entry
|
||||||
|
|
||||||
getStopSignal = (service, imageInfo) ->
|
getStopSignal = (service, imageInfo) ->
|
||||||
sig = null
|
sig = service.stopSignal ? imageInfo?.Config?.StopSignal ? null
|
||||||
if service.stop_signal?
|
|
||||||
sig = service.stop_signal
|
|
||||||
else if imageInfo?.Config?.StopSignal?
|
|
||||||
sig = imageInfo.Config.StopSignal
|
|
||||||
if sig? and !_.isString(sig) # In case the YAML was parsed as a number
|
if sig? and !_.isString(sig) # In case the YAML was parsed as a number
|
||||||
sig = sig.toString()
|
sig = sig.toString()
|
||||||
return sig
|
return sig
|
||||||
|
|
||||||
getUser = (service, imageInfo) ->
|
getUser = (service, imageInfo) ->
|
||||||
user = ''
|
return service.user ? imageInfo?.Config?.User ? ''
|
||||||
if service.user?
|
|
||||||
user = service.user
|
|
||||||
else if imageInfo?.Config?.User?
|
|
||||||
user = imageInfo.Config.User
|
|
||||||
return user
|
|
||||||
|
|
||||||
getWorkingDir = (service, imageInfo) ->
|
getWorkingDir = (service, imageInfo) ->
|
||||||
workingDir = ''
|
return service.workingDir ? imageInfo?.Config?.WorkingDir ? ''
|
||||||
if service.workingDir?
|
|
||||||
workingDir = service.workingDir
|
|
||||||
else if imageInfo?.Config?.WorkingDir?
|
|
||||||
workingDir = imageInfo.Config.WorkingDir
|
|
||||||
return workingDir
|
|
||||||
|
|
||||||
buildHealthcheckTest = (test) ->
|
buildHealthcheckTest = (test) ->
|
||||||
if _.isString(test)
|
if _.isString(test)
|
||||||
@ -101,13 +79,11 @@ overrideHealthcheckFromCompose = (serviceHealthcheck, imageHealthcheck = {}) ->
|
|||||||
return imageHealthcheck
|
return imageHealthcheck
|
||||||
|
|
||||||
getHealthcheck = (service, imageInfo) ->
|
getHealthcheck = (service, imageInfo) ->
|
||||||
healthcheck = null
|
healthcheck = imageInfo?.Config?.Healthcheck ? null
|
||||||
if imageInfo?.Config?.Healthcheck?
|
|
||||||
healthcheck = imageInfo.Config.Healthcheck
|
|
||||||
if service.healthcheck?
|
if service.healthcheck?
|
||||||
healthcheck = overrideHealthcheckFromCompose(service.healthcheck, healthcheck)
|
healthcheck = overrideHealthcheckFromCompose(service.healthcheck, healthcheck)
|
||||||
# Set invalid healthchecks back to null
|
# Set invalid healthchecks back to null
|
||||||
if healthcheck and (!healthcheck.Test? or _.isEqual(healthcheck.Test, []))
|
if healthcheck? and (!healthcheck.Test? or _.isEqual(healthcheck.Test, []))
|
||||||
healthcheck = null
|
healthcheck = null
|
||||||
return healthcheck
|
return healthcheck
|
||||||
|
|
||||||
@ -128,7 +104,8 @@ formatDevices = (devices) ->
|
|||||||
|
|
||||||
# TODO: Support configuration for "networks"
|
# TODO: Support configuration for "networks"
|
||||||
module.exports = class Service
|
module.exports = class Service
|
||||||
constructor: (serviceProperties, opts = {}) ->
|
constructor: (props, opts = {}) ->
|
||||||
|
serviceProperties = _.mapKeys(props, (v, k) -> _.camelCase(k))
|
||||||
{
|
{
|
||||||
@image
|
@image
|
||||||
@imageName
|
@imageName
|
||||||
@ -194,7 +171,7 @@ module.exports = class Service
|
|||||||
@macAddress
|
@macAddress
|
||||||
@user
|
@user
|
||||||
@workingDir
|
@workingDir
|
||||||
} = _.mapKeys(serviceProperties, (v, k) -> _.camelCase(k))
|
} = serviceProperties
|
||||||
|
|
||||||
@networks ?= {}
|
@networks ?= {}
|
||||||
@privileged ?= false
|
@privileged ?= false
|
||||||
@ -255,15 +232,18 @@ module.exports = class Service
|
|||||||
|
|
||||||
# If the service has no containerId, it is a target service and has to be normalised and extended
|
# If the service has no containerId, it is a target service and has to be normalised and extended
|
||||||
if !@containerId?
|
if !@containerId?
|
||||||
@networkMode ?= 'default'
|
if !@networkMode?
|
||||||
|
if !_.isEmpty(@networks)
|
||||||
|
@networkMode = _.keys(@networks)[0]
|
||||||
|
else
|
||||||
|
@networkMode = 'default'
|
||||||
if @networkMode not in [ 'host', 'bridge', 'none' ]
|
if @networkMode not in [ 'host', 'bridge', 'none' ]
|
||||||
@networkMode = "#{@appId}_#{@networkMode}"
|
@networkMode = "#{@appId}_#{@networkMode}"
|
||||||
|
|
||||||
@networks = _.mapKeys @networks, (v, k) =>
|
@networks = _.mapKeys @networks, (v, k) =>
|
||||||
if k not in [ 'host', 'bridge', 'none' ]
|
if k not in [ 'host', 'bridge', 'none' ]
|
||||||
return "#{@appId}_#{k}"
|
return "#{@appId}_#{k}"
|
||||||
else
|
return k
|
||||||
return k
|
|
||||||
|
|
||||||
@networks[@networkMode] ?= {}
|
@networks[@networkMode] ?= {}
|
||||||
|
|
||||||
@ -282,14 +262,11 @@ module.exports = class Service
|
|||||||
@devices = formatDevices(@devices)
|
@devices = formatDevices(@devices)
|
||||||
@addFeaturesFromLabels(opts)
|
@addFeaturesFromLabels(opts)
|
||||||
if @dns?
|
if @dns?
|
||||||
if !Array.isArray(@dns)
|
@dns = _.castArray(@dns)
|
||||||
@dns = [ @dns ]
|
|
||||||
if @dnsSearch?
|
if @dnsSearch?
|
||||||
if !Array.isArray(@dnsSearch)
|
@dnsSearch = _.castArray(@dnsSearch)
|
||||||
@dnsSearch = [ @dns ]
|
|
||||||
if @tmpfs?
|
if @tmpfs?
|
||||||
if !Array.isArray(@tmpfs)
|
@tmpfs = _.castArray(@tmpfs)
|
||||||
@tmpfs = [ @tmpfs ]
|
|
||||||
|
|
||||||
@nanoCpus = Math.round(Number(@cpus) * 10 ** 9)
|
@nanoCpus = Math.round(Number(@cpus) * 10 ** 9)
|
||||||
|
|
||||||
@ -330,9 +307,9 @@ module.exports = class Service
|
|||||||
addFeaturesFromLabels: (opts) =>
|
addFeaturesFromLabels: (opts) =>
|
||||||
if checkTruthy(@labels['io.resin.features.dbus'])
|
if checkTruthy(@labels['io.resin.features.dbus'])
|
||||||
@volumes.push('/run/dbus:/host/run/dbus')
|
@volumes.push('/run/dbus:/host/run/dbus')
|
||||||
if checkTruthy(@labels['io.resin.features.kernel-modules'])
|
if checkTruthy(@labels['io.resin.features.kernel-modules']) and opts.hostPathExists.modules
|
||||||
@volumes.push('/lib/modules:/lib/modules')
|
@volumes.push('/lib/modules:/lib/modules')
|
||||||
if checkTruthy(@labels['io.resin.features.firmware'])
|
if checkTruthy(@labels['io.resin.features.firmware']) and opts.hostPathExists.firmware
|
||||||
@volumes.push('/lib/firmware:/lib/firmware')
|
@volumes.push('/lib/firmware:/lib/firmware')
|
||||||
if checkTruthy(@labels['io.resin.features.balena-socket'])
|
if checkTruthy(@labels['io.resin.features.balena-socket'])
|
||||||
@volumes.push('/var/run/balena.sock:/var/run/balena.sock')
|
@volumes.push('/var/run/balena.sock:/var/run/balena.sock')
|
||||||
@ -388,7 +365,7 @@ module.exports = class Service
|
|||||||
extendAndSanitiseVolumes: (imageInfo) =>
|
extendAndSanitiseVolumes: (imageInfo) =>
|
||||||
volumes = []
|
volumes = []
|
||||||
for vol in @volumes
|
for vol in @volumes
|
||||||
isBind = /:/.test(vol)
|
isBind = _.includes(vol, ':')
|
||||||
if isBind
|
if isBind
|
||||||
[ bindSource, bindDest, mode ] = vol.split(':')
|
[ bindSource, bindDest, mode ] = vol.split(':')
|
||||||
if !path.isAbsolute(bindSource)
|
if !path.isAbsolute(bindSource)
|
||||||
@ -409,7 +386,7 @@ module.exports = class Service
|
|||||||
getNamedVolumes: =>
|
getNamedVolumes: =>
|
||||||
defaults = @defaultBinds()
|
defaults = @defaultBinds()
|
||||||
validVolumes = _.map @volumes, (vol) ->
|
validVolumes = _.map @volumes, (vol) ->
|
||||||
if _.includes(defaults, vol) or !/:/.test(vol)
|
if _.includes(defaults, vol) or !_.includes(vol, ':')
|
||||||
return null
|
return null
|
||||||
bindSource = vol.split(':')[0]
|
bindSource = vol.split(':')[0]
|
||||||
if !path.isAbsolute(bindSource)
|
if !path.isAbsolute(bindSource)
|
||||||
@ -417,7 +394,7 @@ module.exports = class Service
|
|||||||
return m[1]
|
return m[1]
|
||||||
else
|
else
|
||||||
return null
|
return null
|
||||||
return _.filter(validVolumes, (v) -> !_.isNull(v))
|
return _.reject(validVolumes, _.isNil)
|
||||||
|
|
||||||
lockPath: =>
|
lockPath: =>
|
||||||
return updateLock.lockPath(@appId)
|
return updateLock.lockPath(@appId)
|
||||||
@ -562,7 +539,7 @@ module.exports = class Service
|
|||||||
binds = []
|
binds = []
|
||||||
volumes = {}
|
volumes = {}
|
||||||
for vol in @volumes
|
for vol in @volumes
|
||||||
isBind = /:/.test(vol)
|
isBind = _.includes(vol, ':')
|
||||||
if isBind
|
if isBind
|
||||||
binds.push(vol)
|
binds.push(vol)
|
||||||
else
|
else
|
||||||
@ -714,11 +691,11 @@ module.exports = class Service
|
|||||||
|
|
||||||
# This can be very useful for debugging so I'm leaving it commented for now.
|
# This can be very useful for debugging so I'm leaving it commented for now.
|
||||||
# Uncomment to see the services whenever they don't match.
|
# Uncomment to see the services whenever they don't match.
|
||||||
#if !isEq
|
if !isEq
|
||||||
# console.log(JSON.stringify(this, null, 2))
|
console.log(JSON.stringify(this, null, 2))
|
||||||
# console.log(JSON.stringify(otherService, null, 2))
|
console.log(JSON.stringify(otherService, null, 2))
|
||||||
# diff = _.omitBy this, (prop, k) -> _.isEqual(prop, otherService[k])
|
diff = _.omitBy this, (prop, k) -> _.isEqual(prop, otherService[k])
|
||||||
# console.log(JSON.stringify(diff, null, 2))
|
console.log(JSON.stringify(diff, null, 2))
|
||||||
|
|
||||||
return isEq
|
return isEq
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
Promise = require 'bluebird'
|
Promise = require 'bluebird'
|
||||||
_ = require 'lodash'
|
_ = require 'lodash'
|
||||||
fs = Promise.promisifyAll(require('fs'))
|
|
||||||
path = require 'path'
|
path = require 'path'
|
||||||
|
|
||||||
logTypes = require '../lib/log-types'
|
logTypes = require '../lib/log-types'
|
||||||
@ -8,6 +7,7 @@ constants = require '../lib/constants'
|
|||||||
{ checkInt } = require '../lib/validation'
|
{ checkInt } = require '../lib/validation'
|
||||||
{ NotFoundError } = require '../lib/errors'
|
{ NotFoundError } = require '../lib/errors'
|
||||||
{ defaultLegacyVolume } = require '../lib/migration'
|
{ defaultLegacyVolume } = require '../lib/migration'
|
||||||
|
{ safeRename } = require '../lib/fs-utils'
|
||||||
|
|
||||||
module.exports = class Volumes
|
module.exports = class Volumes
|
||||||
constructor: ({ @docker, @logger }) ->
|
constructor: ({ @docker, @logger }) ->
|
||||||
@ -31,18 +31,16 @@ module.exports = class Volumes
|
|||||||
volumes = response.Volumes ? []
|
volumes = response.Volumes ? []
|
||||||
Promise.map volumes, (volume) =>
|
Promise.map volumes, (volume) =>
|
||||||
@docker.getVolume(volume.Name).inspect()
|
@docker.getVolume(volume.Name).inspect()
|
||||||
.then (vol) =>
|
.then(@format)
|
||||||
@format(vol)
|
|
||||||
|
|
||||||
getAllByAppId: (appId) =>
|
getAllByAppId: (appId) =>
|
||||||
@getAll()
|
@getAll()
|
||||||
.then (volumes) ->
|
.then (volumes) ->
|
||||||
_.filter(volumes, (v) -> v.appId == appId)
|
_.filter(volumes, { appId })
|
||||||
|
|
||||||
get: ({ name, appId }) ->
|
get: ({ name, appId }) ->
|
||||||
@docker.getVolume("#{appId}_#{name}").inspect()
|
@docker.getVolume("#{appId}_#{name}").inspect()
|
||||||
.then (volume) =>
|
.then(@format)
|
||||||
return @format(volume)
|
|
||||||
|
|
||||||
defaultLabels: ->
|
defaultLabels: ->
|
||||||
return {
|
return {
|
||||||
@ -67,25 +65,17 @@ module.exports = class Volumes
|
|||||||
Labels: labels
|
Labels: labels
|
||||||
DriverOpts: driverOpts
|
DriverOpts: driverOpts
|
||||||
})
|
})
|
||||||
.catch (err) =>
|
.tapCatch (err) =>
|
||||||
@logger.logSystemEvent(logTypes.createVolumeError, { volume: { name }, error: err })
|
@logger.logSystemEvent(logTypes.createVolumeError, { volume: { name }, error: err })
|
||||||
throw err
|
|
||||||
|
|
||||||
createFromLegacy: (appId) =>
|
createFromLegacy: (appId) =>
|
||||||
name = defaultLegacyVolume()
|
name = defaultLegacyVolume()
|
||||||
@create({ name, appId })
|
@create({ name, appId })
|
||||||
.then (v) ->
|
.call('inspect')
|
||||||
v.inspect()
|
|
||||||
.then (v) ->
|
.then (v) ->
|
||||||
volumePath = path.join(constants.rootMountPoint, v.Mountpoint)
|
volumePath = path.join(constants.rootMountPoint, v.Mountpoint)
|
||||||
legacyPath = path.join(constants.rootMountPoint, constants.dataPath, appId.toString())
|
legacyPath = path.join(constants.rootMountPoint, constants.dataPath, appId.toString())
|
||||||
fs.renameAsync(legacyPath, volumePath)
|
safeRename(legacyPath, volumePath)
|
||||||
.then ->
|
|
||||||
fs.openAsync(path.dirname(volumePath))
|
|
||||||
.then (parent) ->
|
|
||||||
fs.fsyncAsync(parent)
|
|
||||||
.then ->
|
|
||||||
fs.closeAsync(parent)
|
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
@logger.logSystemMessage("Warning: could not migrate legacy /data volume: #{err.message}", { error: err }, 'Volume migration error')
|
@logger.logSystemMessage("Warning: could not migrate legacy /data volume: #{err.message}", { error: err }, 'Volume migration error')
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ Lock = require 'rwlock'
|
|||||||
deviceRegister = require 'resin-register-device'
|
deviceRegister = require 'resin-register-device'
|
||||||
fs = Promise.promisifyAll(require('fs'))
|
fs = Promise.promisifyAll(require('fs'))
|
||||||
EventEmitter = require 'events'
|
EventEmitter = require 'events'
|
||||||
|
path = require 'path'
|
||||||
{ writeAndSyncFile, writeFileAtomic } = require './lib/fs-utils'
|
{ writeAndSyncFile, writeFileAtomic } = require './lib/fs-utils'
|
||||||
osRelease = require './lib/os-release'
|
osRelease = require './lib/os-release'
|
||||||
supervisorVersion = require './lib/supervisor-version'
|
supervisorVersion = require './lib/supervisor-version'
|
||||||
@ -12,10 +12,6 @@ constants = require './lib/constants'
|
|||||||
|
|
||||||
module.exports = class Config extends EventEmitter
|
module.exports = class Config extends EventEmitter
|
||||||
constructor: ({ @db, @configPath }) ->
|
constructor: ({ @db, @configPath }) ->
|
||||||
# These are values that come from env vars or hardcoded defaults and can be resolved synchronously
|
|
||||||
# Defaults needed for both gosuper and node supervisor are declared in entry.sh
|
|
||||||
@constants = constants
|
|
||||||
|
|
||||||
@funcs =
|
@funcs =
|
||||||
version: ->
|
version: ->
|
||||||
Promise.resolve(supervisorVersion)
|
Promise.resolve(supervisorVersion)
|
||||||
@ -40,18 +36,18 @@ module.exports = class Config extends EventEmitter
|
|||||||
# Fall back to checking if an API endpoint was passed via env vars if there's none in config.json (legacy)
|
# Fall back to checking if an API endpoint was passed via env vars if there's none in config.json (legacy)
|
||||||
@get('apiEndpoint')
|
@get('apiEndpoint')
|
||||||
.then (apiEndpoint) ->
|
.then (apiEndpoint) ->
|
||||||
return apiEndpoint ? @constants.apiEndpointFromEnv
|
return apiEndpoint ? constants.apiEndpointFromEnv
|
||||||
|
|
||||||
provisioned: =>
|
provisioned: =>
|
||||||
@getMany([ 'uuid', 'resinApiEndpoint', 'registered_at', 'deviceId' ])
|
@getMany([ 'uuid', 'resinApiEndpoint', 'registered_at', 'deviceId' ])
|
||||||
.then (requiredValues) ->
|
.then (requiredValues) ->
|
||||||
return _.every(_.values(requiredValues), Boolean)
|
return _.every(_.values(requiredValues), Boolean)
|
||||||
|
|
||||||
osVersion: =>
|
osVersion: ->
|
||||||
osRelease.getOSVersion(@constants.hostOSVersionPath)
|
osRelease.getOSVersion(constants.hostOSVersionPath)
|
||||||
|
|
||||||
osVariant: =>
|
osVariant: ->
|
||||||
osRelease.getOSVariant(@constants.hostOSVersionPath)
|
osRelease.getOSVariant(constants.hostOSVersionPath)
|
||||||
|
|
||||||
provisioningOptions: =>
|
provisioningOptions: =>
|
||||||
@getMany([
|
@getMany([
|
||||||
@ -111,9 +107,9 @@ module.exports = class Config extends EventEmitter
|
|||||||
registered_at: { source: 'config.json', mutable: true }
|
registered_at: { source: 'config.json', mutable: true }
|
||||||
applicationId: { source: 'config.json' }
|
applicationId: { source: 'config.json' }
|
||||||
appUpdatePollInterval: { source: 'config.json', mutable: true, default: 60000 }
|
appUpdatePollInterval: { source: 'config.json', mutable: true, default: 60000 }
|
||||||
pubnubSubscribeKey: { source: 'config.json', default: @constants.defaultPubnubSubscribeKey }
|
pubnubSubscribeKey: { source: 'config.json', default: constants.defaultPubnubSubscribeKey }
|
||||||
pubnubPublishKey: { source: 'config.json', default: @constants.defaultPubnubPublishKey }
|
pubnubPublishKey: { source: 'config.json', default: constants.defaultPubnubPublishKey }
|
||||||
mixpanelToken: { source: 'config.json', default: @constants.defaultMixpanelToken }
|
mixpanelToken: { source: 'config.json', default: constants.defaultMixpanelToken }
|
||||||
bootstrapRetryDelay: { source: 'config.json', default: 30000 }
|
bootstrapRetryDelay: { source: 'config.json', default: 30000 }
|
||||||
supervisorOfflineMode: { source: 'config.json', default: false }
|
supervisorOfflineMode: { source: 'config.json', default: false }
|
||||||
hostname: { source: 'config.json', mutable: true }
|
hostname: { source: 'config.json', mutable: true }
|
||||||
@ -141,41 +137,41 @@ module.exports = class Config extends EventEmitter
|
|||||||
loggingEnabled: { source: 'db', mutable: true, default: 'true' }
|
loggingEnabled: { source: 'db', mutable: true, default: 'true' }
|
||||||
connectivityCheckEnabled: { source: 'db', mutable: true, default: 'true' }
|
connectivityCheckEnabled: { source: 'db', mutable: true, default: 'true' }
|
||||||
delta: { source: 'db', mutable: true, default: 'false' }
|
delta: { source: 'db', mutable: true, default: 'false' }
|
||||||
deltaRequestTimeout: { source: 'db', mutable: true, default: '' }
|
deltaRequestTimeout: { source: 'db', mutable: true, default: '30000' }
|
||||||
deltaApplyTimeout: { source: 'db', mutable: true, default: '' }
|
deltaApplyTimeout: { source: 'db', mutable: true, default: '' }
|
||||||
deltaRetryCount: { source: 'db', mutable: true, default: '' }
|
deltaRetryCount: { source: 'db', mutable: true, default: '30' }
|
||||||
deltaRetryInterval: { source: 'db', mutable: true, default: '' }
|
deltaRetryInterval: { source: 'db', mutable: true, default: '10000' }
|
||||||
lockOverride: { source: 'db', mutable: true, default: 'false' }
|
lockOverride: { source: 'db', mutable: true, default: 'false' }
|
||||||
legacyAppsPresent: { source: 'db', mutable: true, default: 'false' }
|
legacyAppsPresent: { source: 'db', mutable: true, default: 'false' }
|
||||||
}
|
}
|
||||||
|
|
||||||
@configJsonCache = {}
|
@configJsonCache = {}
|
||||||
|
|
||||||
@_lock = new Lock()
|
_lock = new Lock()
|
||||||
@_writeLock = Promise.promisify(@_lock.async.writeLock)
|
_writeLock = Promise.promisify(_lock.async.writeLock)
|
||||||
@writeLockConfigJson = =>
|
@writeLockConfigJson = ->
|
||||||
@_writeLock('config.json')
|
_writeLock('config.json')
|
||||||
.disposer (release) ->
|
.disposer (release) ->
|
||||||
release()
|
release()
|
||||||
|
|
||||||
@_readLock = Promise.promisify(@_lock.async.readLock)
|
_readLock = Promise.promisify(_lock.async.readLock)
|
||||||
@readLockConfigJson = =>
|
@readLockConfigJson = ->
|
||||||
@_readLock('config.json')
|
_readLock('config.json')
|
||||||
.disposer (release) ->
|
.disposer (release) ->
|
||||||
release()
|
release()
|
||||||
|
|
||||||
writeConfigJson: =>
|
writeConfigJson: =>
|
||||||
atomicWritePossible = true
|
atomicWritePossible = true
|
||||||
@configJsonPathOnHost()
|
@configJsonPathOnHost()
|
||||||
.catch (err) =>
|
.catch (err) ->
|
||||||
console.error(err.message)
|
console.error(err.message)
|
||||||
atomicWritePossible = false
|
atomicWritePossible = false
|
||||||
return @constants.configJsonNonAtomicPath
|
return constants.configJsonNonAtomicPath
|
||||||
.then (path) =>
|
.then (configPath) =>
|
||||||
if atomicWritePossible
|
if atomicWritePossible
|
||||||
writeFileAtomic(path, JSON.stringify(@configJsonCache))
|
writeFileAtomic(configPath, JSON.stringify(@configJsonCache))
|
||||||
else
|
else
|
||||||
writeAndSyncFile(path, JSON.stringify(@configJsonCache))
|
writeAndSyncFile(configPath, JSON.stringify(@configJsonCache))
|
||||||
|
|
||||||
|
|
||||||
configJsonSet: (keyVals) =>
|
configJsonSet: (keyVals) =>
|
||||||
@ -195,48 +191,46 @@ module.exports = class Config extends EventEmitter
|
|||||||
configJsonRemove: (key) =>
|
configJsonRemove: (key) =>
|
||||||
changed = false
|
changed = false
|
||||||
Promise.using @writeLockConfigJson(), =>
|
Promise.using @writeLockConfigJson(), =>
|
||||||
Promise.try =>
|
if @configJsonCache[key]?
|
||||||
if @configJsonCache[key]?
|
delete @configJsonCache[key]
|
||||||
delete @configJsonCache[key]
|
changed = true
|
||||||
changed = true
|
if changed
|
||||||
.then =>
|
@writeConfigJson()
|
||||||
if changed
|
|
||||||
@writeConfigJson()
|
|
||||||
|
|
||||||
configJsonPathOnHost: =>
|
configJsonPathOnHost: =>
|
||||||
Promise.try =>
|
Promise.try =>
|
||||||
if @configPath?
|
if @configPath?
|
||||||
return @configPath
|
return @configPath
|
||||||
if @constants.configJsonPathOnHost?
|
if constants.configJsonPathOnHost?
|
||||||
return @constants.configJsonPathOnHost
|
return constants.configJsonPathOnHost
|
||||||
osRelease.getOSVersion(@constants.hostOSVersionPath)
|
osRelease.getOSVersion(constants.hostOSVersionPath)
|
||||||
.then (osVersion) =>
|
.then (osVersion) ->
|
||||||
if /^Resin OS 2./.test(osVersion)
|
if /^Resin OS 2./.test(osVersion)
|
||||||
return "#{@constants.bootMountPointFromEnv}/config.json"
|
return path.join(constants.bootMountPointFromEnv, 'config.json')
|
||||||
else if /^Resin OS 1./.test(osVersion)
|
else if /^Resin OS 1./.test(osVersion)
|
||||||
# In Resin OS 1.12, $BOOT_MOUNTPOINT was added and it coincides with config.json's path
|
# In Resin OS 1.12, $BOOT_MOUNTPOINT was added and it coincides with config.json's path
|
||||||
if @constants.bootMountPointFromEnv
|
if constants.bootMountPointFromEnv
|
||||||
return "#{@constants.bootMountPointFromEnv}/config.json"
|
return path.join(constants.bootMountPointFromEnv, 'config.json')
|
||||||
# Older 1.X versions have config.json here
|
# Older 1.X versions have config.json here
|
||||||
return '/mnt/conf/config.json'
|
return '/mnt/conf/config.json'
|
||||||
else
|
else
|
||||||
# In non-resinOS hosts (or older than 1.0.0), if CONFIG_JSON_PATH wasn't passed then we can't do atomic changes
|
# In non-resinOS hosts (or older than 1.0.0), if CONFIG_JSON_PATH wasn't passed then we can't do atomic changes
|
||||||
# (only access to config.json we have is in /boot, which is assumed to be a file bind mount where rename is impossible)
|
# (only access to config.json we have is in /boot, which is assumed to be a file bind mount where rename is impossible)
|
||||||
throw new Error('Could not determine config.json path on host, atomic write will not be possible')
|
throw new Error('Could not determine config.json path on host, atomic write will not be possible')
|
||||||
.then (path) =>
|
.then (pathOnHost) ->
|
||||||
return "#{@constants.rootMountPoint}#{path}"
|
return path.join(constants.rootMountPoint, pathOnHost)
|
||||||
|
|
||||||
configJsonPath: =>
|
configJsonPath: =>
|
||||||
@configJsonPathOnHost()
|
@configJsonPathOnHost()
|
||||||
.catch (err) =>
|
.catch (err) ->
|
||||||
console.error(err.message)
|
console.error(err.message)
|
||||||
return @constants.configJsonNonAtomicPath
|
return constants.configJsonNonAtomicPath
|
||||||
|
|
||||||
readConfigJson: =>
|
readConfigJson: =>
|
||||||
@configJsonPath()
|
@configJsonPath()
|
||||||
.then (path) ->
|
.then (configPath) ->
|
||||||
fs.readFileAsync(path)
|
fs.readFileAsync(configPath)
|
||||||
.then(JSON.parse)
|
.then(JSON.parse)
|
||||||
|
|
||||||
newUniqueKey: ->
|
newUniqueKey: ->
|
||||||
deviceRegister.generateUniqueKey()
|
deviceRegister.generateUniqueKey()
|
||||||
@ -282,7 +276,7 @@ module.exports = class Config extends EventEmitter
|
|||||||
|
|
||||||
getMany: (keys, trx) =>
|
getMany: (keys, trx) =>
|
||||||
# Get the values for several keys in an array
|
# Get the values for several keys in an array
|
||||||
Promise.all(_.map(keys, (key) => @get(key, trx) ))
|
Promise.map(keys, (key) => @get(key, trx))
|
||||||
.then (values) ->
|
.then (values) ->
|
||||||
out = {}
|
out = {}
|
||||||
for key, i in keys
|
for key, i in keys
|
||||||
|
@ -44,10 +44,10 @@ module.exports = class DeviceConfig
|
|||||||
connectivityCheckEnabled: { envVarName: 'RESIN_SUPERVISOR_CONNECTIVITY_CHECK', varType: 'bool', defaultValue: 'true' }
|
connectivityCheckEnabled: { envVarName: 'RESIN_SUPERVISOR_CONNECTIVITY_CHECK', varType: 'bool', defaultValue: 'true' }
|
||||||
loggingEnabled: { envVarName: 'RESIN_SUPERVISOR_LOG_CONTROL', varType: 'bool', defaultValue: 'true' }
|
loggingEnabled: { envVarName: 'RESIN_SUPERVISOR_LOG_CONTROL', varType: 'bool', defaultValue: 'true' }
|
||||||
delta: { envVarName: 'RESIN_SUPERVISOR_DELTA', varType: 'bool', defaultValue: 'false' }
|
delta: { envVarName: 'RESIN_SUPERVISOR_DELTA', varType: 'bool', defaultValue: 'false' }
|
||||||
deltaRequestTimeout: { envVarName: 'RESIN_SUPERVISOR_DELTA_REQUEST_TIMEOUT', varType: 'int' }
|
deltaRequestTimeout: { envVarName: 'RESIN_SUPERVISOR_DELTA_REQUEST_TIMEOUT', varType: 'int', defaultValue: '30000' }
|
||||||
deltaApplyTimeout: { envVarName: 'RESIN_SUPERVISOR_DELTA_APPLY_TIMEOUT', varType: 'int' }
|
deltaApplyTimeout: { envVarName: 'RESIN_SUPERVISOR_DELTA_APPLY_TIMEOUT', varType: 'int' }
|
||||||
deltaRetryCount: { envVarName: 'RESIN_SUPERVISOR_DELTA_RETRY_COUNT', varType: 'int' }
|
deltaRetryCount: { envVarName: 'RESIN_SUPERVISOR_DELTA_RETRY_COUNT', varType: 'int', defaultValue: '30' }
|
||||||
deltaRetryInterval: { envVarName: 'RESIN_SUPERVISOR_DELTA_RETRY_INTERVAL', varType: 'int' }
|
deltaRetryInterval: { envVarName: 'RESIN_SUPERVISOR_DELTA_RETRY_INTERVAL', varType: 'int', defaultValue: '10000' }
|
||||||
lockOverride: { envVarName: 'RESIN_SUPERVISOR_OVERRIDE_LOCK', varType: 'bool', defaultValue: 'false' }
|
lockOverride: { envVarName: 'RESIN_SUPERVISOR_OVERRIDE_LOCK', varType: 'bool', defaultValue: 'false' }
|
||||||
}
|
}
|
||||||
@validKeys = [ 'RESIN_HOST_LOG_TO_DISPLAY', 'RESIN_SUPERVISOR_VPN_CONTROL', 'RESIN_OVERRIDE_LOCK' ].concat(_.map(@configKeys, 'envVarName'))
|
@validKeys = [ 'RESIN_HOST_LOG_TO_DISPLAY', 'RESIN_SUPERVISOR_VPN_CONTROL', 'RESIN_OVERRIDE_LOCK' ].concat(_.map(@configKeys, 'envVarName'))
|
||||||
@ -57,18 +57,16 @@ module.exports = class DeviceConfig
|
|||||||
@config.set(step.target)
|
@config.set(step.target)
|
||||||
.then =>
|
.then =>
|
||||||
@logger.logConfigChange(step.humanReadableTarget, { success: true })
|
@logger.logConfigChange(step.humanReadableTarget, { success: true })
|
||||||
.catch (err) =>
|
.tapCatch (err) =>
|
||||||
@logger.logConfigChange(step.humanReadableTarget, { err })
|
@logger.logConfigChange(step.humanReadableTarget, { err })
|
||||||
throw err
|
|
||||||
setLogToDisplay: (step) =>
|
setLogToDisplay: (step) =>
|
||||||
logValue = { RESIN_HOST_LOG_TO_DISPLAY: step.target }
|
logValue = { RESIN_HOST_LOG_TO_DISPLAY: step.target }
|
||||||
@logger.logConfigChange(logValue)
|
@logger.logConfigChange(logValue)
|
||||||
@setLogToDisplay(step.target)
|
@setLogToDisplay(step.target)
|
||||||
.then =>
|
.then =>
|
||||||
@logger.logConfigChange(logValue, { success: true })
|
@logger.logConfigChange(logValue, { success: true })
|
||||||
.catch (err) =>
|
.tapCatch (err) =>
|
||||||
@logger.logConfigChange(logValue, { err })
|
@logger.logConfigChange(logValue, { err })
|
||||||
throw err
|
|
||||||
setVPNEnabled: (step, { initial = false } = {}) =>
|
setVPNEnabled: (step, { initial = false } = {}) =>
|
||||||
logValue = { RESIN_SUPERVISOR_VPN_CONTROL: step.target }
|
logValue = { RESIN_SUPERVISOR_VPN_CONTROL: step.target }
|
||||||
if !initial
|
if !initial
|
||||||
@ -77,9 +75,8 @@ module.exports = class DeviceConfig
|
|||||||
.then =>
|
.then =>
|
||||||
if !initial
|
if !initial
|
||||||
@logger.logConfigChange(logValue, { success: true })
|
@logger.logConfigChange(logValue, { success: true })
|
||||||
.catch (err) =>
|
.tapCatch (err) =>
|
||||||
@logger.logConfigChange(logValue, { err })
|
@logger.logConfigChange(logValue, { err })
|
||||||
throw err
|
|
||||||
setBootConfig: (step) =>
|
setBootConfig: (step) =>
|
||||||
@config.get('deviceType')
|
@config.get('deviceType')
|
||||||
.then (deviceType) =>
|
.then (deviceType) =>
|
||||||
@ -138,7 +135,7 @@ module.exports = class DeviceConfig
|
|||||||
@logger.logSystemMessage(err, { error: err }, 'Apply boot config error')
|
@logger.logSystemMessage(err, { error: err }, 'Apply boot config error')
|
||||||
throw new Error(err)
|
throw new Error(err)
|
||||||
return true
|
return true
|
||||||
else return false
|
return false
|
||||||
|
|
||||||
getRequiredSteps: (currentState, targetState) =>
|
getRequiredSteps: (currentState, targetState) =>
|
||||||
current = _.clone(currentState.local?.config ? {})
|
current = _.clone(currentState.local?.config ? {})
|
||||||
@ -191,8 +188,7 @@ module.exports = class DeviceConfig
|
|||||||
action: 'reboot'
|
action: 'reboot'
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
.then ->
|
.return(steps)
|
||||||
return steps
|
|
||||||
|
|
||||||
executeStepAction: (step, opts) =>
|
executeStepAction: (step, opts) =>
|
||||||
@actionExecutors[step.action](step, opts)
|
@actionExecutors[step.action](step, opts)
|
||||||
@ -251,7 +247,7 @@ module.exports = class DeviceConfig
|
|||||||
gosuper.get('/v1/log-to-display', { json: true })
|
gosuper.get('/v1/log-to-display', { json: true })
|
||||||
.spread (res, body) ->
|
.spread (res, body) ->
|
||||||
if res.statusCode == 404
|
if res.statusCode == 404
|
||||||
return undefined
|
return
|
||||||
if res.statusCode != 200
|
if res.statusCode != 200
|
||||||
throw new Error("Error getting log to display status: #{res.statusCode} #{body.Error}")
|
throw new Error("Error getting log to display status: #{res.statusCode} #{body.Error}")
|
||||||
return Boolean(body.Data)
|
return Boolean(body.Data)
|
||||||
@ -293,9 +289,8 @@ module.exports = class DeviceConfig
|
|||||||
@logger.logSystemMessage("Applied boot config: #{JSON.stringify(conf)}", {}, 'Apply boot config success')
|
@logger.logSystemMessage("Applied boot config: #{JSON.stringify(conf)}", {}, 'Apply boot config success')
|
||||||
@rebootRequired = true
|
@rebootRequired = true
|
||||||
return true
|
return true
|
||||||
.catch (err) =>
|
.tapCatch (err) =>
|
||||||
@logger.logSystemMessage("Error setting boot config: #{err}", { error: err }, 'Apply boot config error')
|
@logger.logSystemMessage("Error setting boot config: #{err}", { error: err }, 'Apply boot config error')
|
||||||
throw err
|
|
||||||
|
|
||||||
getVPNEnabled: ->
|
getVPNEnabled: ->
|
||||||
gosuper.get('/v1/vpncontrol', { json: true })
|
gosuper.get('/v1/vpncontrol', { json: true })
|
||||||
@ -304,9 +299,8 @@ module.exports = class DeviceConfig
|
|||||||
throw new Error("Error getting vpn status: #{res.statusCode} #{body.Error}")
|
throw new Error("Error getting vpn status: #{res.statusCode} #{body.Error}")
|
||||||
@gosuperHealthy = true
|
@gosuperHealthy = true
|
||||||
return Boolean(body.Data)
|
return Boolean(body.Data)
|
||||||
.catch (err) =>
|
.tapCatch (err) =>
|
||||||
@gosuperHealthy = false
|
@gosuperHealthy = false
|
||||||
throw err
|
|
||||||
|
|
||||||
setVPNEnabled: (val) ->
|
setVPNEnabled: (val) ->
|
||||||
enable = checkTruthy(val) ? true
|
enable = checkTruthy(val) ? true
|
||||||
|
@ -41,78 +41,77 @@ validateState = Promise.method (state) ->
|
|||||||
if state.dependent?
|
if state.dependent?
|
||||||
validateDependentState(state.dependent)
|
validateDependentState(state.dependent)
|
||||||
|
|
||||||
class DeviceStateRouter
|
createDeviceStateRouter = (deviceState) ->
|
||||||
constructor: (@deviceState) ->
|
router = express.Router()
|
||||||
{ @applications, @config } = @deviceState
|
router.use(bodyParser.urlencoded(extended: true))
|
||||||
@router = express.Router()
|
router.use(bodyParser.json())
|
||||||
@router.use(bodyParser.urlencoded(extended: true))
|
|
||||||
@router.use(bodyParser.json())
|
|
||||||
|
|
||||||
@router.post '/v1/reboot', (req, res) =>
|
router.post '/v1/reboot', (req, res) ->
|
||||||
force = validation.checkTruthy(req.body.force)
|
force = validation.checkTruthy(req.body.force)
|
||||||
@deviceState.executeStepAction({ action: 'reboot' }, { force })
|
deviceState.executeStepAction({ action: 'reboot' }, { force })
|
||||||
.then (response) ->
|
.then (response) ->
|
||||||
res.status(202).json(response)
|
res.status(202).json(response)
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
if err instanceof updateLock.UpdatesLockedError
|
if err instanceof updateLock.UpdatesLockedError
|
||||||
status = 423
|
status = 423
|
||||||
else
|
else
|
||||||
status = 500
|
status = 500
|
||||||
res.status(status).json({ Data: '', Error: err?.message or err or 'Unknown error' })
|
res.status(status).json({ Data: '', Error: err?.message or err or 'Unknown error' })
|
||||||
|
|
||||||
@router.post '/v1/shutdown', (req, res) =>
|
router.post '/v1/shutdown', (req, res) ->
|
||||||
force = validation.checkTruthy(req.body.force)
|
force = validation.checkTruthy(req.body.force)
|
||||||
@deviceState.executeStepAction({ action: 'shutdown' }, { force })
|
deviceState.executeStepAction({ action: 'shutdown' }, { force })
|
||||||
.then (response) ->
|
.then (response) ->
|
||||||
res.status(202).json(response)
|
res.status(202).json(response)
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
if err instanceof updateLock.UpdatesLockedError
|
if err instanceof updateLock.UpdatesLockedError
|
||||||
status = 423
|
status = 423
|
||||||
else
|
else
|
||||||
status = 500
|
status = 500
|
||||||
res.status(status).json({ Data: '', Error: err?.message or err or 'Unknown error' })
|
res.status(status).json({ Data: '', Error: err?.message or err or 'Unknown error' })
|
||||||
|
|
||||||
@router.get '/v1/device/host-config', (req, res) ->
|
router.get '/v1/device/host-config', (req, res) ->
|
||||||
hostConfig.get()
|
hostConfig.get()
|
||||||
.then (conf) ->
|
.then (conf) ->
|
||||||
res.json(conf)
|
res.json(conf)
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
res.status(503).send(err?.message or err or 'Unknown error')
|
res.status(503).send(err?.message or err or 'Unknown error')
|
||||||
|
|
||||||
@router.patch '/v1/device/host-config', (req, res) =>
|
router.patch '/v1/device/host-config', (req, res) ->
|
||||||
hostConfig.patch(req.body, @config)
|
hostConfig.patch(req.body, deviceState.config)
|
||||||
.then ->
|
.then ->
|
||||||
res.status(200).send('OK')
|
res.status(200).send('OK')
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
res.status(503).send(err?.message or err or 'Unknown error')
|
res.status(503).send(err?.message or err or 'Unknown error')
|
||||||
|
|
||||||
@router.get '/v1/device', (req, res) =>
|
router.get '/v1/device', (req, res) ->
|
||||||
@deviceState.getStatus()
|
deviceState.getStatus()
|
||||||
.then (state) ->
|
.then (state) ->
|
||||||
stateToSend = _.pick(state.local, [
|
stateToSend = _.pick(state.local, [
|
||||||
'api_port'
|
'api_port'
|
||||||
'ip_address'
|
'ip_address'
|
||||||
'os_version'
|
'os_version'
|
||||||
'supervisor_version'
|
'supervisor_version'
|
||||||
'update_pending'
|
'update_pending'
|
||||||
'update_failed'
|
'update_failed'
|
||||||
'update_downloaded'
|
'update_downloaded'
|
||||||
])
|
])
|
||||||
if state.local.is_on__commit?
|
if state.local.is_on__commit?
|
||||||
stateToSend.commit = state.local.is_on__commit
|
stateToSend.commit = state.local.is_on__commit
|
||||||
# Will produce nonsensical results for multicontainer apps...
|
# Will produce nonsensical results for multicontainer apps...
|
||||||
service = _.toPairs(_.toPairs(state.local.apps)[0]?[1]?.services)[0]?[1]
|
service = _.toPairs(_.toPairs(state.local.apps)[0]?[1]?.services)[0]?[1]
|
||||||
if service?
|
if service?
|
||||||
stateToSend.status = service.status
|
stateToSend.status = service.status
|
||||||
# For backwards compatibility, we adapt Running to the old "Idle"
|
# For backwards compatibility, we adapt Running to the old "Idle"
|
||||||
if stateToSend.status == 'Running'
|
if stateToSend.status == 'Running'
|
||||||
stateToSend.status = 'Idle'
|
stateToSend.status = 'Idle'
|
||||||
stateToSend.download_progress = service.download_progress
|
stateToSend.download_progress = service.download_progress
|
||||||
res.json(stateToSend)
|
res.json(stateToSend)
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
res.status(500).json({ Data: '', Error: err?.message or err or 'Unknown error' })
|
res.status(500).json({ Data: '', Error: err?.message or err or 'Unknown error' })
|
||||||
|
|
||||||
@router.use(@applications.router)
|
router.use(deviceState.applications.router)
|
||||||
|
return router
|
||||||
|
|
||||||
module.exports = class DeviceState extends EventEmitter
|
module.exports = class DeviceState extends EventEmitter
|
||||||
constructor: ({ @db, @config, @eventTracker }) ->
|
constructor: ({ @db, @config, @eventTracker }) ->
|
||||||
@ -131,27 +130,20 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
@lastApplyStart = process.hrtime()
|
@lastApplyStart = process.hrtime()
|
||||||
@scheduledApply = null
|
@scheduledApply = null
|
||||||
@shuttingDown = false
|
@shuttingDown = false
|
||||||
@_router = new DeviceStateRouter(this)
|
@router = createDeviceStateRouter(this)
|
||||||
@router = @_router.router
|
|
||||||
@on 'apply-target-state-end', (err) ->
|
@on 'apply-target-state-end', (err) ->
|
||||||
if err?
|
if err?
|
||||||
console.log("Apply error #{err}")
|
console.log("Apply error #{err}")
|
||||||
else
|
else
|
||||||
console.log('Apply success!')
|
console.log('Apply success!')
|
||||||
#@on 'step-completed', (err) ->
|
|
||||||
# if err?
|
|
||||||
# console.log("Step completed with error #{err}")
|
|
||||||
# else
|
|
||||||
# console.log('Step success!')
|
|
||||||
#@on 'step-error', (err) ->
|
|
||||||
# console.log("Step error #{err}")
|
|
||||||
|
|
||||||
@applications.on('change', @reportCurrentState)
|
@applications.on('change', @reportCurrentState)
|
||||||
|
|
||||||
healthcheck: =>
|
healthcheck: =>
|
||||||
@config.getMany([ 'appUpdatePollInterval', 'offlineMode' ])
|
@config.getMany([ 'appUpdatePollInterval', 'offlineMode' ])
|
||||||
.then (conf) =>
|
.then (conf) =>
|
||||||
cycleTimeWithinInterval = process.hrtime(@lastApplyStart)[0] - @applications.timeSpentFetching < 2 * conf.appUpdatePollInterval
|
cycleTime = process.hrtime(@lastApplyStart)
|
||||||
|
cycleTimeMs = cycleTime[0] * 1000 + cycleTime[1] / 1e6
|
||||||
|
cycleTimeWithinInterval = cycleTimeMs - @applications.timeSpentFetching < 2 * conf.appUpdatePollInterval
|
||||||
applyTargetHealthy = conf.offlineMode or !@applyInProgress or @applications.fetchesInProgress > 0 or cycleTimeWithinInterval
|
applyTargetHealthy = conf.offlineMode or !@applyInProgress or @applications.fetchesInProgress > 0 or cycleTimeWithinInterval
|
||||||
return applyTargetHealthy and @deviceConfig.gosuperHealthy
|
return applyTargetHealthy and @deviceConfig.gosuperHealthy
|
||||||
|
|
||||||
@ -228,7 +220,7 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
@reportCurrentState(
|
@reportCurrentState(
|
||||||
ip_address: addresses.join(' ')
|
ip_address: addresses.join(' ')
|
||||||
)
|
)
|
||||||
, @config.constants.ipAddressUpdateInterval
|
, constants.ipAddressUpdateInterval
|
||||||
|
|
||||||
saveInitialConfig: =>
|
saveInitialConfig: =>
|
||||||
@deviceConfig.getCurrent()
|
@deviceConfig.getCurrent()
|
||||||
@ -331,7 +323,7 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
if _.isArray(stateFromFile)
|
if _.isArray(stateFromFile)
|
||||||
# This is a legacy apps.json
|
# This is a legacy apps.json
|
||||||
stateFromFile = @_convertLegacyAppsJson(stateFromFile)
|
stateFromFile = @_convertLegacyAppsJson(stateFromFile)
|
||||||
images = _.flatten(_.map(stateFromFile.apps, (app, appId) =>
|
images = _.flatMap stateFromFile.apps, (app, appId) =>
|
||||||
_.map app.services, (service, serviceId) =>
|
_.map app.services, (service, serviceId) =>
|
||||||
svc = {
|
svc = {
|
||||||
imageName: service.image
|
imageName: service.image
|
||||||
@ -342,7 +334,6 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
appId
|
appId
|
||||||
}
|
}
|
||||||
return @applications.imageForService(svc)
|
return @applications.imageForService(svc)
|
||||||
))
|
|
||||||
Promise.map images, (img) =>
|
Promise.map images, (img) =>
|
||||||
@applications.images.normalise(img.name)
|
@applications.images.normalise(img.name)
|
||||||
.then (name) =>
|
.then (name) =>
|
||||||
@ -350,12 +341,12 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
@applications.images.save(img)
|
@applications.images.save(img)
|
||||||
.then =>
|
.then =>
|
||||||
@deviceConfig.getCurrent()
|
@deviceConfig.getCurrent()
|
||||||
.then (deviceConf) =>
|
.then (deviceConf) =>
|
||||||
_.defaults(stateFromFile.config, deviceConf)
|
_.defaults(stateFromFile.config, deviceConf)
|
||||||
stateFromFile.name ?= ''
|
stateFromFile.name ?= ''
|
||||||
@setTarget({
|
@setTarget({
|
||||||
local: stateFromFile
|
local: stateFromFile
|
||||||
})
|
})
|
||||||
.catch (err) =>
|
.catch (err) =>
|
||||||
@eventTracker.track('Loading preloaded apps failed', { error: err })
|
@eventTracker.track('Loading preloaded apps failed', { error: err })
|
||||||
|
|
||||||
@ -364,17 +355,18 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
.then =>
|
.then =>
|
||||||
@logger.logSystemMessage('Rebooting', {}, 'Reboot')
|
@logger.logSystemMessage('Rebooting', {}, 'Reboot')
|
||||||
device.reboot()
|
device.reboot()
|
||||||
.tap =>
|
.tap =>
|
||||||
@emit('shutdown')
|
@shuttingDown = true
|
||||||
|
@emitAsync('shutdown')
|
||||||
|
|
||||||
shutdown: (force, skipLock) =>
|
shutdown: (force, skipLock) =>
|
||||||
@applications.stopAll({ force, skipLock })
|
@applications.stopAll({ force, skipLock })
|
||||||
.then =>
|
.then =>
|
||||||
@logger.logSystemMessage('Shutting down', {}, 'Shutdown')
|
@logger.logSystemMessage('Shutting down', {}, 'Shutdown')
|
||||||
device.shutdown()
|
device.shutdown()
|
||||||
.tap =>
|
.tap =>
|
||||||
@shuttingDown = true
|
@shuttingDown = true
|
||||||
@emitAsync('shutdown')
|
@emitAsync('shutdown')
|
||||||
|
|
||||||
executeStepAction: (step, { force, initial, skipLock }) =>
|
executeStepAction: (step, { force, initial, skipLock }) =>
|
||||||
Promise.try =>
|
Promise.try =>
|
||||||
@ -397,27 +389,25 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
if @shuttingDown
|
if @shuttingDown
|
||||||
return
|
return
|
||||||
@executeStepAction(step, { force, initial, skipLock })
|
@executeStepAction(step, { force, initial, skipLock })
|
||||||
.catch (err) =>
|
.tapCatch (err) =>
|
||||||
@emitAsync('step-error', err, step)
|
@emitAsync('step-error', err, step)
|
||||||
throw err
|
|
||||||
.then (stepResult) =>
|
.then (stepResult) =>
|
||||||
@emitAsync('step-completed', null, step, stepResult)
|
@emitAsync('step-completed', null, step, stepResult)
|
||||||
|
|
||||||
applyError: (err, { force, initial, intermediate }) =>
|
applyError: (err, { force, initial, intermediate }) =>
|
||||||
@emitAsync('apply-target-state-error', err)
|
@emitAsync('apply-target-state-error', err)
|
||||||
@emitAsync('apply-target-state-end', err)
|
@emitAsync('apply-target-state-end', err)
|
||||||
if !intermediate
|
if intermediate
|
||||||
@failedUpdates += 1
|
|
||||||
@reportCurrentState(update_failed: true)
|
|
||||||
if @scheduledApply?
|
|
||||||
console.log("Updating failed, but there's another update scheduled immediately: ", err)
|
|
||||||
else
|
|
||||||
delay = Math.min((2 ** @failedUpdates) * 500, 30000)
|
|
||||||
# If there was an error then schedule another attempt briefly in the future.
|
|
||||||
console.log('Scheduling another update attempt due to failure: ', delay, err)
|
|
||||||
@triggerApplyTarget({ force, delay, initial })
|
|
||||||
else
|
|
||||||
throw err
|
throw err
|
||||||
|
@failedUpdates += 1
|
||||||
|
@reportCurrentState(update_failed: true)
|
||||||
|
if @scheduledApply?
|
||||||
|
console.log("Updating failed, but there's another update scheduled immediately: ", err)
|
||||||
|
else
|
||||||
|
delay = Math.min((2 ** @failedUpdates) * 500, 30000)
|
||||||
|
# If there was an error then schedule another attempt briefly in the future.
|
||||||
|
console.log('Scheduling another update attempt due to failure: ', delay, err)
|
||||||
|
@triggerApplyTarget({ force, delay, initial })
|
||||||
|
|
||||||
applyTarget: ({ force = false, initial = false, intermediate = false, skipLock = false } = {}) =>
|
applyTarget: ({ force = false, initial = false, intermediate = false, skipLock = false } = {}) =>
|
||||||
nextDelay = 200
|
nextDelay = 200
|
||||||
@ -453,8 +443,7 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
nextDelay = 1000
|
nextDelay = 1000
|
||||||
Promise.map steps, (step) =>
|
Promise.map steps, (step) =>
|
||||||
@applyStep(step, { force, initial, intermediate, skipLock })
|
@applyStep(step, { force, initial, intermediate, skipLock })
|
||||||
.then ->
|
.delay(nextDelay)
|
||||||
Promise.delay(nextDelay)
|
|
||||||
.then =>
|
.then =>
|
||||||
@applyTarget({ force, initial, intermediate, skipLock })
|
@applyTarget({ force, initial, intermediate, skipLock })
|
||||||
.catch (err) =>
|
.catch (err) =>
|
||||||
@ -497,13 +486,13 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
@lastApplyStart = process.hrtime()
|
@lastApplyStart = process.hrtime()
|
||||||
console.log('Applying target state')
|
console.log('Applying target state')
|
||||||
@applyTarget({ force, initial })
|
@applyTarget({ force, initial })
|
||||||
.finally =>
|
.finally =>
|
||||||
@applyInProgress = false
|
@applyInProgress = false
|
||||||
@reportCurrentState()
|
@reportCurrentState()
|
||||||
if @scheduledApply?
|
if @scheduledApply?
|
||||||
@triggerApplyTarget(@scheduledApply)
|
@triggerApplyTarget(@scheduledApply)
|
||||||
@scheduledApply = null
|
@scheduledApply = null
|
||||||
return
|
return null
|
||||||
|
|
||||||
applyIntermediateTarget: (intermediateTarget, { force = false, skipLock = false } = {}) =>
|
applyIntermediateTarget: (intermediateTarget, { force = false, skipLock = false } = {}) =>
|
||||||
@intermediateTarget = _.cloneDeep(intermediateTarget)
|
@intermediateTarget = _.cloneDeep(intermediateTarget)
|
||||||
|
@ -27,6 +27,7 @@ module.exports = class EventTracker
|
|||||||
if properties instanceof Error
|
if properties instanceof Error
|
||||||
properties = error: properties
|
properties = error: properties
|
||||||
|
|
||||||
|
properties = _.cloneDeep(properties)
|
||||||
# If the properties has an error argument that is an Error object then it treats it nicely,
|
# If the properties has an error argument that is an Error object then it treats it nicely,
|
||||||
# rather than letting it become `{}`
|
# rather than letting it become `{}`
|
||||||
if properties.error instanceof Error
|
if properties.error instanceof Error
|
||||||
@ -34,8 +35,6 @@ module.exports = class EventTracker
|
|||||||
message: properties.error.message
|
message: properties.error.message
|
||||||
stack: properties.error.stack
|
stack: properties.error.stack
|
||||||
|
|
||||||
properties = _.cloneDeep(properties)
|
|
||||||
|
|
||||||
# Don't log private env vars (e.g. api keys) or other secrets - use a whitelist to mask what we send
|
# Don't log private env vars (e.g. api keys) or other secrets - use a whitelist to mask what we send
|
||||||
properties = mask(properties, mixpanelMask)
|
properties = mask(properties, mixpanelMask)
|
||||||
@_logEvent('Event:', ev, JSON.stringify(properties))
|
@_logEvent('Event:', ev, JSON.stringify(properties))
|
||||||
|
@ -37,3 +37,5 @@ module.exports =
|
|||||||
ipAddressUpdateInterval: 30 * 1000
|
ipAddressUpdateInterval: 30 * 1000
|
||||||
imageCleanupErrorIgnoreTimeout: 3600 * 1000
|
imageCleanupErrorIgnoreTimeout: 3600 * 1000
|
||||||
maxDeltaDownloads: 3
|
maxDeltaDownloads: 3
|
||||||
|
|
||||||
|
process.env.DOCKER_HOST ?= "unix://#{module.exports.dockerSocket}"
|
||||||
|
@ -4,6 +4,5 @@ exports.envArrayToObject = (env) ->
|
|||||||
# env is an array of strings that say 'key=value'
|
# env is an array of strings that say 'key=value'
|
||||||
toPair = (keyVal) ->
|
toPair = (keyVal) ->
|
||||||
m = keyVal.match(/^([^=]+)=(.*)$/)
|
m = keyVal.match(/^([^=]+)=(.*)$/)
|
||||||
[ _unused, key, val ] = m
|
return m[1..]
|
||||||
return [ key, val ]
|
|
||||||
_.fromPairs(_.map(env, toPair))
|
_.fromPairs(_.map(env, toPair))
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
gosuper = require './gosuper'
|
gosuper = require './gosuper'
|
||||||
|
|
||||||
exports.reboot = ->
|
gosuperAction = (action) ->
|
||||||
gosuper.post('/v1/reboot', { json: true })
|
gosuper.post("/v1/#{action}", { json: true })
|
||||||
.spread (res, body) ->
|
.spread (res, body) ->
|
||||||
if res.statusCode != 202
|
if res.statusCode != 202
|
||||||
throw new Error(body.Error)
|
throw new Error(body.Error)
|
||||||
return body
|
return body
|
||||||
|
|
||||||
|
exports.reboot = ->
|
||||||
|
gosuperAction('reboot')
|
||||||
|
|
||||||
exports.shutdown = ->
|
exports.shutdown = ->
|
||||||
gosuper.post('/v1/shutdown', { json: true })
|
gosuperAction('shutdown')
|
||||||
.spread (res, body) ->
|
|
||||||
if res.statusCode != 202
|
|
||||||
throw new Error(body.Error)
|
|
||||||
return body
|
|
||||||
|
@ -31,8 +31,8 @@ module.exports = class DockerUtils extends DockerToolbelt
|
|||||||
super(opts)
|
super(opts)
|
||||||
@dockerProgress = new DockerProgress(dockerToolbelt: this)
|
@dockerProgress = new DockerProgress(dockerToolbelt: this)
|
||||||
@supervisorTagPromise = @normaliseImageName(constants.supervisorImage)
|
@supervisorTagPromise = @normaliseImageName(constants.supervisorImage)
|
||||||
@InvalidNetGatewayError = class InvalidNetGatewayError extends TypedError
|
|
||||||
return this
|
InvalidNetGatewayError: class InvalidNetGatewayError extends TypedError
|
||||||
|
|
||||||
getRepoAndTag: (image) =>
|
getRepoAndTag: (image) =>
|
||||||
@getRegistryAndName(image)
|
@getRegistryAndName(image)
|
||||||
@ -71,14 +71,14 @@ module.exports = class DockerUtils extends DockerToolbelt
|
|||||||
url = "#{tokenEndpoint}?service=#{dstInfo.registry}&scope=repository:#{dstInfo.imageName}:pull&scope=repository:#{srcInfo.imageName}:pull"
|
url = "#{tokenEndpoint}?service=#{dstInfo.registry}&scope=repository:#{dstInfo.imageName}:pull&scope=repository:#{srcInfo.imageName}:pull"
|
||||||
request.getAsync(url, opts)
|
request.getAsync(url, opts)
|
||||||
.get(1)
|
.get(1)
|
||||||
.then (b) ->
|
.then (responseBody) ->
|
||||||
opts =
|
opts =
|
||||||
followRedirect: false
|
followRedirect: false
|
||||||
timeout: requestTimeout
|
timeout: requestTimeout
|
||||||
|
|
||||||
if b?.token?
|
if responseBody?.token?
|
||||||
opts.auth =
|
opts.auth =
|
||||||
bearer: b.token
|
bearer: responseBody.token
|
||||||
sendImmediately: true
|
sendImmediately: true
|
||||||
new Promise (resolve, reject) ->
|
new Promise (resolve, reject) ->
|
||||||
request.get("#{deltaEndpoint}/api/v2/delta?src=#{deltaSource}&dest=#{imgDest}", opts)
|
request.get("#{deltaEndpoint}/api/v2/delta?src=#{deltaSource}&dest=#{imgDest}", opts)
|
||||||
@ -116,8 +116,7 @@ module.exports = class DockerUtils extends DockerToolbelt
|
|||||||
getImageEnv: (id) ->
|
getImageEnv: (id) ->
|
||||||
@getImage(id).inspect()
|
@getImage(id).inspect()
|
||||||
.get('Config').get('Env')
|
.get('Config').get('Env')
|
||||||
.then (env) ->
|
.then(envArrayToObject)
|
||||||
envArrayToObject(env)
|
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
console.log('Error getting env from image', err, err.stack)
|
console.log('Error getting env from image', err, err.stack)
|
||||||
return {}
|
return {}
|
||||||
|
@ -2,3 +2,4 @@
|
|||||||
|
|
||||||
exports.NotFoundError = (err) -> checkInt(err.statusCode) is 404
|
exports.NotFoundError = (err) -> checkInt(err.statusCode) is 404
|
||||||
exports.ENOENT = (err) -> err.code is 'ENOENT'
|
exports.ENOENT = (err) -> err.code is 'ENOENT'
|
||||||
|
exports.EEXIST = (err) -> err.code is 'EEXIST'
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
Promise = require 'bluebird'
|
Promise = require 'bluebird'
|
||||||
fs = Promise.promisifyAll(require('fs'))
|
fs = Promise.promisifyAll(require('fs'))
|
||||||
|
path = require 'path'
|
||||||
|
|
||||||
exports.writeAndSyncFile = (path, data) ->
|
exports.writeAndSyncFile = (path, data) ->
|
||||||
fs.openAsync(path, 'w')
|
fs.openAsync(path, 'w')
|
||||||
@ -14,3 +15,10 @@ exports.writeFileAtomic = (path, data) ->
|
|||||||
exports.writeAndSyncFile("#{path}.new", data)
|
exports.writeAndSyncFile("#{path}.new", data)
|
||||||
.then ->
|
.then ->
|
||||||
fs.renameAsync("#{path}.new", path)
|
fs.renameAsync("#{path}.new", path)
|
||||||
|
|
||||||
|
exports.safeRename = (src, dest) ->
|
||||||
|
fs.renameAsync(src, dest)
|
||||||
|
.then ->
|
||||||
|
fs.openAsync(path.dirname(dest))
|
||||||
|
.tap(fs.fsyncAsync)
|
||||||
|
.then(fs.closeAsync)
|
||||||
|
@ -3,13 +3,13 @@ childProcess = Promise.promisifyAll(require('child_process'))
|
|||||||
|
|
||||||
clearAndAppendIptablesRule = (rule) ->
|
clearAndAppendIptablesRule = (rule) ->
|
||||||
childProcess.execAsync("iptables -D #{rule}")
|
childProcess.execAsync("iptables -D #{rule}")
|
||||||
.catch(->)
|
.catchReturn()
|
||||||
.then ->
|
.then ->
|
||||||
childProcess.execAsync("iptables -A #{rule}")
|
childProcess.execAsync("iptables -A #{rule}")
|
||||||
|
|
||||||
clearAndInsertIptablesRule = (rule) ->
|
clearAndInsertIptablesRule = (rule) ->
|
||||||
childProcess.execAsync("iptables -D #{rule}")
|
childProcess.execAsync("iptables -D #{rule}")
|
||||||
.catch(->)
|
.catchReturn()
|
||||||
.then ->
|
.then ->
|
||||||
childProcess.execAsync("iptables -I #{rule}")
|
childProcess.execAsync("iptables -I #{rule}")
|
||||||
|
|
||||||
|
@ -19,16 +19,9 @@ exports.singleToMulticontainerApp = (app) ->
|
|||||||
}
|
}
|
||||||
defaultVolume = exports.defaultLegacyVolume()
|
defaultVolume = exports.defaultLegacyVolume()
|
||||||
newApp.volumes[defaultVolume] = {}
|
newApp.volumes[defaultVolume] = {}
|
||||||
updateStrategy = conf['RESIN_SUPERVISOR_UPDATE_STRATEGY']
|
updateStrategy = conf['RESIN_SUPERVISOR_UPDATE_STRATEGY'] ? 'download-then-kill'
|
||||||
if !updateStrategy?
|
handoverTimeout = conf['RESIN_SUPERVISOR_HANDOVER_TIMEOUT'] ? ''
|
||||||
updateStrategy = 'download-then-kill'
|
restartPolicy = conf['RESIN_APP_RESTART_POLICY'] ? 'always'
|
||||||
handoverTimeout = conf['RESIN_SUPERVISOR_HANDOVER_TIMEOUT']
|
|
||||||
if !handoverTimeout?
|
|
||||||
handoverTimeout = ''
|
|
||||||
restartPolicy = conf['RESIN_APP_RESTART_POLICY']
|
|
||||||
if !restartPolicy?
|
|
||||||
restartPolicy = 'always'
|
|
||||||
|
|
||||||
newApp.services = {
|
newApp.services = {
|
||||||
'1': {
|
'1': {
|
||||||
appId: appId
|
appId: appId
|
||||||
|
@ -39,25 +39,23 @@ exports.lock = do ->
|
|||||||
Promise.map _.clone(locksTaken), (lockName) ->
|
Promise.map _.clone(locksTaken), (lockName) ->
|
||||||
_.pull(locksTaken, lockName)
|
_.pull(locksTaken, lockName)
|
||||||
lockFile.unlockAsync(lockName)
|
lockFile.unlockAsync(lockName)
|
||||||
.finally ->
|
.finally(release)
|
||||||
release()
|
|
||||||
_writeLock(appId)
|
_writeLock(appId)
|
||||||
.tap (release) ->
|
.tap (release) ->
|
||||||
theLockDir = path.join(constants.rootMountPoint, baseLockPath(appId))
|
theLockDir = path.join(constants.rootMountPoint, baseLockPath(appId))
|
||||||
fs.readdirAsync(theLockDir)
|
fs.readdirAsync(theLockDir)
|
||||||
.catch ENOENT, -> []
|
.catchReturn(ENOENT, [])
|
||||||
.mapSeries (serviceName) ->
|
.mapSeries (serviceName) ->
|
||||||
tmpLockName = lockFileOnHost(appId, serviceName)
|
tmpLockName = lockFileOnHost(appId, serviceName)
|
||||||
Promise.try ->
|
Promise.try ->
|
||||||
lockFile.unlockAsync(tmpLockName) if force == true
|
lockFile.unlockAsync(tmpLockName) if force == true
|
||||||
.then ->
|
.then ->
|
||||||
lockFile.lockAsync(tmpLockName)
|
lockFile.lockAsync(tmpLockName)
|
||||||
.then ->
|
.then ->
|
||||||
locksTaken.push(tmpLockName)
|
locksTaken.push(tmpLockName)
|
||||||
.catch ENOENT, _.noop
|
.catchReturn(ENOENT, null)
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
dispose(release)
|
dispose(release)
|
||||||
.finally ->
|
.throw(new exports.UpdatesLockedError("Updates are locked: #{err.message}"))
|
||||||
throw new exports.UpdatesLockedError("Updates are locked: #{err.message}")
|
|
||||||
.disposer(dispose)
|
.disposer(dispose)
|
||||||
Promise.using takeTheLock(), -> fn()
|
Promise.using takeTheLock(), -> fn()
|
||||||
|
@ -14,7 +14,7 @@ exports.checkInt = checkInt = (s, options = {}) ->
|
|||||||
exports.checkString = (s) ->
|
exports.checkString = (s) ->
|
||||||
# Make sure `s` exists and is not an empty string, or 'null' or 'undefined'.
|
# Make sure `s` exists and is not an empty string, or 'null' or 'undefined'.
|
||||||
# This might happen if the parsing of config.json on the host using jq is wrong (it is buggy in some versions).
|
# This might happen if the parsing of config.json on the host using jq is wrong (it is buggy in some versions).
|
||||||
if !s? or !_.isString(s) or s == 'null' or s == 'undefined' or s == ''
|
if !s? or !_.isString(s) or s in [ 'null', 'undefined', '' ]
|
||||||
return
|
return
|
||||||
return s
|
return s
|
||||||
|
|
||||||
@ -28,11 +28,11 @@ exports.isValidShortText = isValidShortText = (t) ->
|
|||||||
_.isString(t) and t.length <= 255
|
_.isString(t) and t.length <= 255
|
||||||
exports.isValidEnv = isValidEnv = (obj) ->
|
exports.isValidEnv = isValidEnv = (obj) ->
|
||||||
_.isObject(obj) and _.every obj, (val, key) ->
|
_.isObject(obj) and _.every obj, (val, key) ->
|
||||||
isValidShortText(key) and /^[a-zA-Z_]+[a-zA-Z0-9_]*$/.test(key) and _.isString(val)
|
isValidShortText(key) and /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key) and _.isString(val)
|
||||||
|
|
||||||
exports.isValidLabelsObject = isValidLabelsObject = (obj) ->
|
exports.isValidLabelsObject = isValidLabelsObject = (obj) ->
|
||||||
_.isObject(obj) and _.every obj, (val, key) ->
|
_.isObject(obj) and _.every obj, (val, key) ->
|
||||||
isValidShortText(key) and /^[a-zA-Z_]+[a-zA-Z0-9\.\-]*$/.test(key) and _.isString(val)
|
isValidShortText(key) and /^[a-zA-Z][a-zA-Z0-9\.\-]*$/.test(key) and _.isString(val)
|
||||||
|
|
||||||
undefinedOrValidEnv = (val) ->
|
undefinedOrValidEnv = (val) ->
|
||||||
if val? and !isValidEnv(val)
|
if val? and !isValidEnv(val)
|
||||||
@ -41,39 +41,54 @@ undefinedOrValidEnv = (val) ->
|
|||||||
|
|
||||||
exports.isValidDependentAppsObject = (apps) ->
|
exports.isValidDependentAppsObject = (apps) ->
|
||||||
return false if !_.isObject(apps)
|
return false if !_.isObject(apps)
|
||||||
return false if !_.every apps, (val, appId) ->
|
return _.every apps, (val, appId) ->
|
||||||
|
val = _.defaults(_.clone(val), { config: undefined, environment: undefined, commit: undefined, image: undefined })
|
||||||
return false if !isValidShortText(appId) or !checkInt(appId)?
|
return false if !isValidShortText(appId) or !checkInt(appId)?
|
||||||
return false if !isValidShortText(val.name)
|
return _.conformsTo(val, {
|
||||||
return false if val.commit? and (!isValidShortText(val.image) or !isValidShortText(val.commit))
|
name: isValidShortText
|
||||||
return undefinedOrValidEnv(val.config) and undefinedOrValidEnv(val.environment)
|
image: (i) -> !val.commit? or isValidShortText(i)
|
||||||
return true
|
commit: (c) -> !c? or isValidShortText(c)
|
||||||
|
config: undefinedOrValidEnv
|
||||||
|
environment: undefinedOrValidEnv
|
||||||
|
})
|
||||||
|
|
||||||
isValidService = (service, serviceId) ->
|
isValidService = (service, serviceId) ->
|
||||||
return false if !isValidEnv(service.environment)
|
|
||||||
return false if !isValidShortText(service.serviceName)
|
|
||||||
return false if !isValidShortText(service.image)
|
|
||||||
return false if !isValidShortText(serviceId) or !checkInt(serviceId)
|
return false if !isValidShortText(serviceId) or !checkInt(serviceId)
|
||||||
return false if !checkInt(service.imageId)?
|
return _.conformsTo(service, {
|
||||||
return false if !isValidLabelsObject(service.labels)
|
serviceName: isValidShortText
|
||||||
return true
|
image: isValidShortText
|
||||||
|
environment: isValidEnv
|
||||||
|
imageId: (i) -> checkInt(i)?
|
||||||
|
labels: isValidLabelsObject
|
||||||
|
})
|
||||||
|
|
||||||
exports.isValidAppsObject = (obj) ->
|
exports.isValidAppsObject = (obj) ->
|
||||||
return false if !_.isObject(obj)
|
return false if !_.isObject(obj)
|
||||||
return false if !_.every obj, (val, appId) ->
|
return _.every obj, (val, appId) ->
|
||||||
return false if !isValidShortText(appId) or !checkInt(appId)?
|
return false if !isValidShortText(appId) or !checkInt(appId)?
|
||||||
return false if !isValidShortText(val.name) or (val.releaseId? and !checkInt(val.releaseId)?)
|
return _.conformsTo(_.defaults(_.clone(val), { releaseId: undefined }), {
|
||||||
return false if !_.isObject(val.services)
|
name: isValidShortText
|
||||||
return false if !_.every(val.services, isValidService)
|
releaseId: (r) -> !r? or checkInt(r)?
|
||||||
return true
|
services: (s) -> _.isObject(s) and _.every(s, isValidService)
|
||||||
return true
|
})
|
||||||
|
|
||||||
exports.isValidDependentDevicesObject = (devices) ->
|
exports.isValidDependentDevicesObject = (devices) ->
|
||||||
return false if !_.isObject(devices)
|
return false if !_.isObject(devices)
|
||||||
return false if !_.every devices, (val, uuid) ->
|
return _.every devices, (val, uuid) ->
|
||||||
return false if !isValidShortText(uuid)
|
return false if !isValidShortText(uuid)
|
||||||
return false if !isValidShortText(val.name)
|
return _.conformsTo(val, {
|
||||||
return false if !_.isObject(val.apps) or _.isEmpty(val.apps)
|
name: isValidShortText
|
||||||
return false if !_.every val.apps, (app) ->
|
apps: (a) ->
|
||||||
return undefinedOrValidEnv(app.config) and undefinedOrValidEnv(app.environment)
|
return (
|
||||||
return true
|
_.isObject(a) and
|
||||||
return true
|
!_.isEmpty(a) and
|
||||||
|
_.every a, (app) ->
|
||||||
|
app = _.defaults(_.clone(app), { config: undefined, environment: undefined })
|
||||||
|
_.conformsTo(app, { config: undefinedOrValidEnv, environment: undefinedOrValidEnv })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
exports.validStringOrUndefined = (s) ->
|
||||||
|
_.isUndefined(s) or (_.isString(s) and !_.isEmpty(s))
|
||||||
|
exports.validObjectOrUndefined = (o) ->
|
||||||
|
_.isUndefined(o) or _.isObject(o)
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
// history without actually adding drop statements (mostly just becoming unused, but still there).
|
// history without actually adding drop statements (mostly just becoming unused, but still there).
|
||||||
|
|
||||||
exports.up = function (knex, Promise) {
|
exports.up = function (knex, Promise) {
|
||||||
let addColumn = function (table, column, type) {
|
const addColumn = function (table, column, type) {
|
||||||
return knex.schema.hasColumn(table, column)
|
return knex.schema.hasColumn(table, column)
|
||||||
.then((exists) => {
|
.then((exists) => {
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
@ -16,7 +16,7 @@ exports.up = function (knex, Promise) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
let dropColumn = function (table, column) {
|
const dropColumn = function (table, column) {
|
||||||
return knex.schema.hasColumn(table, column)
|
return knex.schema.hasColumn(table, column)
|
||||||
.then((exists) => {
|
.then((exists) => {
|
||||||
if (exists) {
|
if (exists) {
|
||||||
@ -24,7 +24,7 @@ exports.up = function (knex, Promise) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
let createTableOrRun = function (tableName, tableCreator, runIfTableExists) {
|
const createTableOrRun = function (tableName, tableCreator, runIfTableExists) {
|
||||||
return knex.schema.hasTable(tableName)
|
return knex.schema.hasTable(tableName)
|
||||||
.then((exists) => {
|
.then((exists) => {
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
@ -34,7 +34,7 @@ exports.up = function (knex, Promise) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
let dropTable = function (tableName) {
|
const dropTable = function (tableName) {
|
||||||
return knex.schema.hasTable(tableName)
|
return knex.schema.hasTable(tableName)
|
||||||
.then((exists) => {
|
.then((exists) => {
|
||||||
if (exists) {
|
if (exists) {
|
||||||
@ -132,5 +132,5 @@ exports.up = function (knex, Promise) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exports.down = function(knex, Promise) {
|
exports.down = function(knex, Promise) {
|
||||||
return Promise.try(() => { throw new Error('Not implemented') })
|
return Promise.reject(new Error('Not implemented'))
|
||||||
}
|
}
|
||||||
|
@ -15,16 +15,16 @@ var tryParse = function (obj) {
|
|||||||
var singleToMulticontainerApp = function (app) {
|
var singleToMulticontainerApp = function (app) {
|
||||||
// From *very* old supervisors, env or config may be null
|
// From *very* old supervisors, env or config may be null
|
||||||
// so we ignore errors parsing them
|
// so we ignore errors parsing them
|
||||||
let conf = tryParse(app.config)
|
const conf = tryParse(app.config)
|
||||||
let env = tryParse(app.env)
|
const env = tryParse(app.env)
|
||||||
let environment = {}
|
const environment = {}
|
||||||
let appId = parseInt(app.appId)
|
const appId = parseInt(app.appId)
|
||||||
for (let key in env) {
|
for (let key in env) {
|
||||||
if (!/^RESIN_/.test(key)) {
|
if (!/^RESIN_/.test(key)) {
|
||||||
environment[key] = env[key]
|
environment[key] = env[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let newApp = {
|
const newApp = {
|
||||||
appId: appId,
|
appId: appId,
|
||||||
commit: app.commit,
|
commit: app.commit,
|
||||||
name: app.name,
|
name: app.name,
|
||||||
@ -32,7 +32,7 @@ var singleToMulticontainerApp = function (app) {
|
|||||||
networks: {},
|
networks: {},
|
||||||
volumes: {}
|
volumes: {}
|
||||||
}
|
}
|
||||||
let defaultVolume = defaultLegacyVolume(appId)
|
const defaultVolume = defaultLegacyVolume(appId)
|
||||||
newApp.volumes[defaultVolume] = {}
|
newApp.volumes[defaultVolume] = {}
|
||||||
let updateStrategy = conf['RESIN_SUPERVISOR_UPDATE_STRATEGY']
|
let updateStrategy = conf['RESIN_SUPERVISOR_UPDATE_STRATEGY']
|
||||||
if (updateStrategy == null) {
|
if (updateStrategy == null) {
|
||||||
@ -79,7 +79,7 @@ var singleToMulticontainerApp = function (app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var jsonifyAppFields = function (app) {
|
var jsonifyAppFields = function (app) {
|
||||||
let newApp = Object.assign({}, app)
|
const newApp = Object.assign({}, app)
|
||||||
newApp.services = JSON.stringify(app.services)
|
newApp.services = JSON.stringify(app.services)
|
||||||
newApp.networks = JSON.stringify(app.networks)
|
newApp.networks = JSON.stringify(app.networks)
|
||||||
newApp.volumes = JSON.stringify(app.volumes)
|
newApp.volumes = JSON.stringify(app.volumes)
|
||||||
@ -87,7 +87,7 @@ var jsonifyAppFields = function (app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var imageForApp = function (app) {
|
var imageForApp = function (app) {
|
||||||
let service = app.services[0]
|
const service = app.services[0]
|
||||||
return {
|
return {
|
||||||
name: service.image,
|
name: service.image,
|
||||||
appId: service.appId,
|
appId: service.appId,
|
||||||
@ -147,7 +147,7 @@ exports.up = function (knex, Promise) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.map((app) => {
|
.map((app) => {
|
||||||
let migratedApp = singleToMulticontainerApp(app)
|
const migratedApp = singleToMulticontainerApp(app)
|
||||||
return knex('app').insert(jsonifyAppFields(migratedApp))
|
return knex('app').insert(jsonifyAppFields(migratedApp))
|
||||||
.then(() => knex('image').insert(imageForApp(migratedApp)))
|
.then(() => knex('image').insert(imageForApp(migratedApp)))
|
||||||
})
|
})
|
||||||
@ -160,9 +160,8 @@ exports.up = function (knex, Promise) {
|
|||||||
.then((deviceConf) => {
|
.then((deviceConf) => {
|
||||||
return knex.schema.dropTable('deviceConfig')
|
return knex.schema.dropTable('deviceConfig')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
let values = JSON.parse(deviceConf[0].values)
|
const values = JSON.parse(deviceConf[0].values)
|
||||||
let promises = []
|
const configKeys = {
|
||||||
let configKeys = {
|
|
||||||
'RESIN_SUPERVISOR_POLL_INTERVAL': 'appUpdatePollInterval',
|
'RESIN_SUPERVISOR_POLL_INTERVAL': 'appUpdatePollInterval',
|
||||||
'RESIN_SUPERVISOR_LOCAL_MODE': 'localMode',
|
'RESIN_SUPERVISOR_LOCAL_MODE': 'localMode',
|
||||||
'RESIN_SUPERVISOR_CONNECTIVITY_CHECK': 'connectivityCheckEnabled',
|
'RESIN_SUPERVISOR_CONNECTIVITY_CHECK': 'connectivityCheckEnabled',
|
||||||
@ -174,12 +173,11 @@ exports.up = function (knex, Promise) {
|
|||||||
'RESIN_SUPERVISOR_DELTA_RETRY_INTERVAL': 'deltaRequestTimeout',
|
'RESIN_SUPERVISOR_DELTA_RETRY_INTERVAL': 'deltaRequestTimeout',
|
||||||
'RESIN_SUPERVISOR_OVERRIDE_LOCK': 'lockOverride'
|
'RESIN_SUPERVISOR_OVERRIDE_LOCK': 'lockOverride'
|
||||||
}
|
}
|
||||||
for (let envVarName in values) {
|
return Promise.map(Object.keys(values), (envVarName) => {
|
||||||
if (configKeys[envVarName] != null) {
|
if (configKeys[envVarName] != null) {
|
||||||
promises.push(knex('config').insert({ key: configKeys[envVarName], value: values[envVarName]}))
|
return knex('config').insert({ key: configKeys[envVarName], value: values[envVarName]})
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
return Promise.all(promises)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@ -222,7 +220,7 @@ exports.up = function (knex, Promise) {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return Promise.map(dependentApps, (app) => {
|
return Promise.map(dependentApps, (app) => {
|
||||||
let newApp = {
|
const newApp = {
|
||||||
appId: parseInt(app.appId),
|
appId: parseInt(app.appId),
|
||||||
parentApp: parseInt(app.parentAppId),
|
parentApp: parseInt(app.parentAppId),
|
||||||
image: app.imageId,
|
image: app.imageId,
|
||||||
@ -232,7 +230,7 @@ exports.up = function (knex, Promise) {
|
|||||||
config: JSON.stringify(tryParse(app.config)),
|
config: JSON.stringify(tryParse(app.config)),
|
||||||
environment: JSON.stringify(tryParse(app.environment))
|
environment: JSON.stringify(tryParse(app.environment))
|
||||||
}
|
}
|
||||||
let image = imageForDependentApp(newApp)
|
const image = imageForDependentApp(newApp)
|
||||||
return knex('image').insert(image)
|
return knex('image').insert(image)
|
||||||
.then(() => knex('dependentApp').insert(newApp))
|
.then(() => knex('dependentApp').insert(newApp))
|
||||||
.then(() => knex('dependentAppTarget').insert(newApp))
|
.then(() => knex('dependentAppTarget').insert(newApp))
|
||||||
@ -276,7 +274,7 @@ exports.up = function (knex, Promise) {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return Promise.map(dependentDevices, (device) => {
|
return Promise.map(dependentDevices, (device) => {
|
||||||
let newDevice = Object.assign({}, device)
|
const newDevice = Object.assign({}, device)
|
||||||
newDevice.appId = parseInt(device.appId)
|
newDevice.appId = parseInt(device.appId)
|
||||||
newDevice.deviceId = parseInt(device.deviceId)
|
newDevice.deviceId = parseInt(device.deviceId)
|
||||||
if (device.is_managed_by != null) {
|
if (device.is_managed_by != null) {
|
||||||
@ -289,7 +287,7 @@ exports.up = function (knex, Promise) {
|
|||||||
if (newDevice.markedForDeletion == null) {
|
if (newDevice.markedForDeletion == null) {
|
||||||
newDevice.markedForDeletion = false
|
newDevice.markedForDeletion = false
|
||||||
}
|
}
|
||||||
let deviceTarget = {
|
const deviceTarget = {
|
||||||
uuid: device.uuid,
|
uuid: device.uuid,
|
||||||
name: device.name,
|
name: device.name,
|
||||||
apps: {}
|
apps: {}
|
||||||
@ -307,5 +305,5 @@ exports.up = function (knex, Promise) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exports.down = function(knex, Promise) {
|
exports.down = function(knex, Promise) {
|
||||||
return Promise.try(() => { throw new Error('Not implemented') })
|
return Promise.reject(new Error('Not implemented'))
|
||||||
}
|
}
|
@ -1,10 +1,10 @@
|
|||||||
// Adds a dockerImageId column to the image table to identify images downloaded with deltas
|
// Adds a dockerImageId column to the image table to identify images downloaded with deltas
|
||||||
exports.up = function (knex, Promise) {
|
exports.up = function (knex, Promise) {
|
||||||
return knex.schema.table('image', (t) => {
|
return knex.schema.table('image', (t) => {
|
||||||
t.string('dockerImageId')
|
t.string('dockerImageId')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.down = function(knex, Promise) {
|
exports.down = function(knex, Promise) {
|
||||||
return Promise.try(() => { throw new Error('Not implemented') })
|
return Promise.reject(new Error('Not implemented'))
|
||||||
}
|
}
|
@ -8,6 +8,7 @@ fs = Promise.promisifyAll(require('fs'))
|
|||||||
constants = require './lib/constants'
|
constants = require './lib/constants'
|
||||||
{ checkTruthy } = require './lib/validation'
|
{ checkTruthy } = require './lib/validation'
|
||||||
blink = require './lib/blink'
|
blink = require './lib/blink'
|
||||||
|
{ EEXIST } = require './lib/errors'
|
||||||
|
|
||||||
networkPattern =
|
networkPattern =
|
||||||
blinks: 4
|
blinks: 4
|
||||||
@ -40,9 +41,6 @@ vpnStatusInotifyCallback = ->
|
|||||||
.catch ->
|
.catch ->
|
||||||
pauseConnectivityCheck = false
|
pauseConnectivityCheck = false
|
||||||
|
|
||||||
# Use the following to catch EEXIST errors
|
|
||||||
EEXIST = (err) -> err.code is 'EEXIST'
|
|
||||||
|
|
||||||
exports.startConnectivityCheck = _.once (apiEndpoint, enable, onChangeCallback) ->
|
exports.startConnectivityCheck = _.once (apiEndpoint, enable, onChangeCallback) ->
|
||||||
exports.enableConnectivityCheck(enable)
|
exports.enableConnectivityCheck(enable)
|
||||||
if !apiEndpoint?
|
if !apiEndpoint?
|
||||||
@ -62,8 +60,7 @@ exports.startConnectivityCheck = _.once (apiEndpoint, enable, onChangeCallback)
|
|||||||
port: parsedUrl.port ? (if parsedUrl.protocol is 'https:' then 443 else 80)
|
port: parsedUrl.port ? (if parsedUrl.protocol is 'https:' then 443 else 80)
|
||||||
interval: 10 * 1000
|
interval: 10 * 1000
|
||||||
(connected) ->
|
(connected) ->
|
||||||
if onChangeCallback?
|
onChangeCallback?(connected)
|
||||||
onChangeCallback(connected)
|
|
||||||
if connected
|
if connected
|
||||||
console.log('Internet Connectivity: OK')
|
console.log('Internet Connectivity: OK')
|
||||||
blink.pattern.stop()
|
blink.pattern.stop()
|
||||||
@ -91,9 +88,9 @@ exports.getIPAddresses = ->
|
|||||||
# - the docker network for the supervisor API (supervisor0)
|
# - the docker network for the supervisor API (supervisor0)
|
||||||
# - custom docker network bridges (br- + 12 hex characters)
|
# - custom docker network bridges (br- + 12 hex characters)
|
||||||
_.flatten(_.map(_.omitBy(os.networkInterfaces(), (interfaceFields, interfaceName) ->
|
_.flatten(_.map(_.omitBy(os.networkInterfaces(), (interfaceFields, interfaceName) ->
|
||||||
/^(balena[0-9]+)|(docker[0-9]+)|(rce[0-9]+)|(tun[0-9]+)|(resin-vpn)|(lo)|(resin-dns)|(supervisor0)|(br-[0-9a-f]{12})$/.test(interfaceName))
|
/^(?:balena|docker|rce|tun)[0-9]+|tun[0-9]+|resin-vpn|lo|resin-dns|supervisor0|br-[0-9a-f]{12}$/.test(interfaceName))
|
||||||
, (validInterfaces) ->
|
, (validInterfaces) ->
|
||||||
_.map(_.omitBy(validInterfaces, (a) -> a.family != 'IPv4' ), 'address'))
|
_.map(_.pickBy(validInterfaces, family: 'IPv4'), 'address'))
|
||||||
)
|
)
|
||||||
|
|
||||||
exports.startIPAddressUpdate = do ->
|
exports.startIPAddressUpdate = do ->
|
||||||
|
@ -4,7 +4,7 @@ express = require 'express'
|
|||||||
fs = Promise.promisifyAll require 'fs'
|
fs = Promise.promisifyAll require 'fs'
|
||||||
{ request } = require './lib/request'
|
{ request } = require './lib/request'
|
||||||
constants = require './lib/constants'
|
constants = require './lib/constants'
|
||||||
{ checkInt } = require './lib/validation'
|
{ checkInt, validStringOrUndefined, validObjectOrUndefined } = require './lib/validation'
|
||||||
path = require 'path'
|
path = require 'path'
|
||||||
mkdirp = Promise.promisify(require('mkdirp'))
|
mkdirp = Promise.promisify(require('mkdirp'))
|
||||||
bodyParser = require 'body-parser'
|
bodyParser = require 'body-parser'
|
||||||
@ -22,12 +22,6 @@ parseDeviceFields = (device) ->
|
|||||||
device.targetEnvironment = JSON.parse(device.targetEnvironment ? '{}')
|
device.targetEnvironment = JSON.parse(device.targetEnvironment ? '{}')
|
||||||
return _.omit(device, 'markedForDeletion', 'logs_channel')
|
return _.omit(device, 'markedForDeletion', 'logs_channel')
|
||||||
|
|
||||||
# TODO move to lib/validation
|
|
||||||
validStringOrUndefined = (s) ->
|
|
||||||
_.isUndefined(s) or !_.isEmpty(s)
|
|
||||||
validObjectOrUndefined = (o) ->
|
|
||||||
_.isUndefined(o) or _.isObject(o)
|
|
||||||
|
|
||||||
tarDirectory = (appId) ->
|
tarDirectory = (appId) ->
|
||||||
return "/data/dependent-assets/#{appId}"
|
return "/data/dependent-assets/#{appId}"
|
||||||
|
|
||||||
@ -54,11 +48,9 @@ cleanupTars = (appId, commit) ->
|
|||||||
.catchReturn([])
|
.catchReturn([])
|
||||||
.then (files) ->
|
.then (files) ->
|
||||||
if fileToKeep?
|
if fileToKeep?
|
||||||
files = _.filter files, (file) ->
|
files = _.reject(files, fileToKeep)
|
||||||
return file isnt fileToKeep
|
|
||||||
Promise.map files, (file) ->
|
Promise.map files, (file) ->
|
||||||
if !fileToKeep? or (file isnt fileToKeep)
|
fs.unlinkAsync(path.join(dir, file))
|
||||||
fs.unlinkAsync(path.join(dir, file))
|
|
||||||
|
|
||||||
formatTargetAsState = (device) ->
|
formatTargetAsState = (device) ->
|
||||||
return {
|
return {
|
||||||
@ -76,168 +68,168 @@ formatCurrentAsState = (device) ->
|
|||||||
config: device.config
|
config: device.config
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProxyvisorRouter
|
createProxyvisorRouter = (proxyvisor) ->
|
||||||
constructor: (@proxyvisor) ->
|
{ db } = proxyvisor
|
||||||
{ @config, @logger, @db, @docker } = @proxyvisor
|
router = express.Router()
|
||||||
@router = express.Router()
|
router.use(bodyParser.urlencoded(extended: true))
|
||||||
@router.use(bodyParser.urlencoded(extended: true))
|
router.use(bodyParser.json())
|
||||||
@router.use(bodyParser.json())
|
router.get '/v1/devices', (req, res) ->
|
||||||
@router.get '/v1/devices', (req, res) =>
|
db.models('dependentDevice').select()
|
||||||
@db.models('dependentDevice').select()
|
.map(parseDeviceFields)
|
||||||
.map(parseDeviceFields)
|
.then (devices) ->
|
||||||
.then (devices) ->
|
res.json(devices)
|
||||||
res.json(devices)
|
.catch (err) ->
|
||||||
.catch (err) ->
|
res.status(503).send(err?.message or err or 'Unknown error')
|
||||||
res.status(503).send(err?.message or err or 'Unknown error')
|
|
||||||
|
|
||||||
@router.post '/v1/devices', (req, res) =>
|
router.post '/v1/devices', (req, res) ->
|
||||||
{ appId, device_type } = req.body
|
{ appId, device_type } = req.body
|
||||||
|
|
||||||
if !appId? or _.isNaN(parseInt(appId)) or parseInt(appId) <= 0
|
if !appId? or _.isNaN(parseInt(appId)) or parseInt(appId) <= 0
|
||||||
res.status(400).send('appId must be a positive integer')
|
res.status(400).send('appId must be a positive integer')
|
||||||
|
return
|
||||||
|
device_type = 'generic' if !device_type?
|
||||||
|
d =
|
||||||
|
belongs_to__application: req.body.appId
|
||||||
|
device_type: device_type
|
||||||
|
proxyvisor.apiBinder.provisionDependentDevice(d)
|
||||||
|
.then (dev) ->
|
||||||
|
# If the response has id: null then something was wrong in the request
|
||||||
|
# but we don't know precisely what.
|
||||||
|
if !dev.id?
|
||||||
|
res.status(400).send('Provisioning failed, invalid appId or credentials')
|
||||||
return
|
return
|
||||||
device_type = 'generic' if !device_type?
|
deviceForDB = {
|
||||||
d =
|
uuid: dev.uuid
|
||||||
belongs_to__application: req.body.appId
|
appId
|
||||||
device_type: device_type
|
device_type: dev.device_type
|
||||||
@proxyvisor.apiBinder.provisionDependentDevice(d)
|
deviceId: dev.id
|
||||||
.then (dev) =>
|
name: dev.name
|
||||||
# If the response has id: null then something was wrong in the request
|
status: dev.status
|
||||||
# but we don't know precisely what.
|
logs_channel: dev.logs_channel
|
||||||
if !dev.id?
|
|
||||||
res.status(400).send('Provisioning failed, invalid appId or credentials')
|
|
||||||
return
|
|
||||||
deviceForDB = {
|
|
||||||
uuid: dev.uuid
|
|
||||||
appId
|
|
||||||
device_type: dev.device_type
|
|
||||||
deviceId: dev.id
|
|
||||||
name: dev.name
|
|
||||||
status: dev.status
|
|
||||||
logs_channel: dev.logs_channel
|
|
||||||
}
|
|
||||||
@db.models('dependentDevice').insert(deviceForDB)
|
|
||||||
.then ->
|
|
||||||
res.status(201).send(dev)
|
|
||||||
.catch (err) ->
|
|
||||||
console.error("Error on #{req.method} #{url.parse(req.url).pathname}", err, err.stack)
|
|
||||||
res.status(503).send(err?.message or err or 'Unknown error')
|
|
||||||
|
|
||||||
@router.get '/v1/devices/:uuid', (req, res) =>
|
|
||||||
uuid = req.params.uuid
|
|
||||||
@db.models('dependentDevice').select().where({ uuid })
|
|
||||||
.then ([ device ]) ->
|
|
||||||
return res.status(404).send('Device not found') if !device?
|
|
||||||
return res.status(410).send('Device deleted') if device.markedForDeletion
|
|
||||||
res.json(parseDeviceFields(device))
|
|
||||||
.catch (err) ->
|
|
||||||
console.error("Error on #{req.method} #{url.parse(req.url).pathname}", err, err.stack)
|
|
||||||
res.status(503).send(err?.message or err or 'Unknown error')
|
|
||||||
|
|
||||||
@router.post '/v1/devices/:uuid/logs', (req, res) =>
|
|
||||||
uuid = req.params.uuid
|
|
||||||
m = {
|
|
||||||
message: req.body.message
|
|
||||||
timestamp: req.body.timestamp or Date.now()
|
|
||||||
}
|
}
|
||||||
m.isSystem = req.body.isSystem if req.body.isSystem?
|
db.models('dependentDevice').insert(deviceForDB)
|
||||||
|
.then ->
|
||||||
|
res.status(201).send(dev)
|
||||||
|
.catch (err) ->
|
||||||
|
console.error("Error on #{req.method} #{url.parse(req.url).pathname}", err, err.stack)
|
||||||
|
res.status(503).send(err?.message or err or 'Unknown error')
|
||||||
|
|
||||||
@db.models('dependentDevice').select().where({ uuid })
|
router.get '/v1/devices/:uuid', (req, res) ->
|
||||||
.then ([ device ]) =>
|
uuid = req.params.uuid
|
||||||
return res.status(404).send('Device not found') if !device?
|
db.models('dependentDevice').select().where({ uuid })
|
||||||
return res.status(410).send('Device deleted') if device.markedForDeletion
|
.then ([ device ]) ->
|
||||||
@logger.log(m, { channel: "device-#{device.logs_channel}-logs" })
|
return res.status(404).send('Device not found') if !device?
|
||||||
res.status(202).send('OK')
|
return res.status(410).send('Device deleted') if device.markedForDeletion
|
||||||
.catch (err) ->
|
res.json(parseDeviceFields(device))
|
||||||
console.error("Error on #{req.method} #{url.parse(req.url).pathname}", err, err.stack)
|
.catch (err) ->
|
||||||
res.status(503).send(err?.message or err or 'Unknown error')
|
console.error("Error on #{req.method} #{url.parse(req.url).pathname}", err, err.stack)
|
||||||
|
res.status(503).send(err?.message or err or 'Unknown error')
|
||||||
|
|
||||||
@router.put '/v1/devices/:uuid', (req, res) =>
|
router.post '/v1/devices/:uuid/logs', (req, res) ->
|
||||||
uuid = req.params.uuid
|
uuid = req.params.uuid
|
||||||
{ status, is_online, commit, releaseId, environment, config } = req.body
|
m = {
|
||||||
validateDeviceFields = ->
|
message: req.body.message
|
||||||
if isDefined(is_online) and !_.isBoolean(is_online)
|
timestamp: req.body.timestamp or Date.now()
|
||||||
return 'is_online must be a boolean'
|
}
|
||||||
if !validStringOrUndefined(status)
|
m.isSystem = req.body.isSystem if req.body.isSystem?
|
||||||
return 'status must be a non-empty string'
|
|
||||||
if !validStringOrUndefined(commit)
|
|
||||||
return 'commit must be a non-empty string'
|
|
||||||
if !validStringOrUndefined(releaseId)
|
|
||||||
return 'commit must be a non-empty string'
|
|
||||||
if !validObjectOrUndefined(environment)
|
|
||||||
return 'environment must be an object'
|
|
||||||
if !validObjectOrUndefined(config)
|
|
||||||
return 'config must be an object'
|
|
||||||
return null
|
|
||||||
requestError = validateDeviceFields()
|
|
||||||
if requestError?
|
|
||||||
res.status(400).send(requestError)
|
|
||||||
return
|
|
||||||
|
|
||||||
environment = JSON.stringify(environment) if isDefined(environment)
|
db.models('dependentDevice').select().where({ uuid })
|
||||||
config = JSON.stringify(config) if isDefined(config)
|
.then ([ device ]) ->
|
||||||
|
return res.status(404).send('Device not found') if !device?
|
||||||
|
return res.status(410).send('Device deleted') if device.markedForDeletion
|
||||||
|
proxyvisor.logger.log(m, { channel: "device-#{device.logs_channel}-logs" })
|
||||||
|
res.status(202).send('OK')
|
||||||
|
.catch (err) ->
|
||||||
|
console.error("Error on #{req.method} #{url.parse(req.url).pathname}", err, err.stack)
|
||||||
|
res.status(503).send(err?.message or err or 'Unknown error')
|
||||||
|
|
||||||
fieldsToUpdateOnDB = _.pickBy({ status, is_online, commit, releaseId, config, environment }, isDefined)
|
router.put '/v1/devices/:uuid', (req, res) ->
|
||||||
fieldsToUpdateOnAPI = _.pick(fieldsToUpdateOnDB, 'status', 'is_online', 'commit', 'releaseId')
|
uuid = req.params.uuid
|
||||||
|
{ status, is_online, commit, releaseId, environment, config } = req.body
|
||||||
|
validateDeviceFields = ->
|
||||||
|
if isDefined(is_online) and !_.isBoolean(is_online)
|
||||||
|
return 'is_online must be a boolean'
|
||||||
|
if !validStringOrUndefined(status)
|
||||||
|
return 'status must be a non-empty string'
|
||||||
|
if !validStringOrUndefined(commit)
|
||||||
|
return 'commit must be a non-empty string'
|
||||||
|
if !validStringOrUndefined(releaseId)
|
||||||
|
return 'commit must be a non-empty string'
|
||||||
|
if !validObjectOrUndefined(environment)
|
||||||
|
return 'environment must be an object'
|
||||||
|
if !validObjectOrUndefined(config)
|
||||||
|
return 'config must be an object'
|
||||||
|
return null
|
||||||
|
requestError = validateDeviceFields()
|
||||||
|
if requestError?
|
||||||
|
res.status(400).send(requestError)
|
||||||
|
return
|
||||||
|
|
||||||
if _.isEmpty(fieldsToUpdateOnDB)
|
environment = JSON.stringify(environment) if isDefined(environment)
|
||||||
res.status(400).send('At least one device attribute must be updated')
|
config = JSON.stringify(config) if isDefined(config)
|
||||||
return
|
|
||||||
|
|
||||||
@db.models('dependentDevice').select().where({ uuid })
|
fieldsToUpdateOnDB = _.pickBy({ status, is_online, commit, releaseId, config, environment }, isDefined)
|
||||||
.then ([ device ]) =>
|
fieldsToUpdateOnAPI = _.pick(fieldsToUpdateOnDB, 'status', 'is_online', 'commit', 'releaseId')
|
||||||
return res.status(404).send('Device not found') if !device?
|
|
||||||
return res.status(410).send('Device deleted') if device.markedForDeletion
|
|
||||||
throw new Error('Device is invalid') if !device.deviceId?
|
|
||||||
Promise.try =>
|
|
||||||
if !_.isEmpty(fieldsToUpdateOnAPI)
|
|
||||||
@proxyvisor.apiBinder.patchDevice(device.deviceId, fieldsToUpdateOnAPI)
|
|
||||||
.then =>
|
|
||||||
@db.models('dependentDevice').update(fieldsToUpdateOnDB).where({ uuid })
|
|
||||||
.then =>
|
|
||||||
@db.models('dependentDevice').select().where({ uuid })
|
|
||||||
.then ([ device ]) ->
|
|
||||||
res.json(parseDeviceFields(device))
|
|
||||||
.catch (err) ->
|
|
||||||
console.error("Error on #{req.method} #{url.parse(req.url).pathname}", err, err.stack)
|
|
||||||
res.status(503).send(err?.message or err or 'Unknown error')
|
|
||||||
|
|
||||||
@router.get '/v1/dependent-apps/:appId/assets/:commit', (req, res) =>
|
if _.isEmpty(fieldsToUpdateOnDB)
|
||||||
@db.models('dependentApp').select().where(_.pick(req.params, 'appId', 'commit'))
|
res.status(400).send('At least one device attribute must be updated')
|
||||||
.then ([ app ]) =>
|
return
|
||||||
return res.status(404).send('Not found') if !app
|
|
||||||
dest = tarPath(app.appId, app.commit)
|
|
||||||
fs.lstatAsync(dest)
|
|
||||||
.catch =>
|
|
||||||
Promise.using @docker.imageRootDirMounted(app.image), (rootDir) ->
|
|
||||||
getTarArchive(rootDir + '/assets', dest)
|
|
||||||
.then ->
|
|
||||||
res.sendFile(dest)
|
|
||||||
.catch (err) ->
|
|
||||||
console.error("Error on #{req.method} #{url.parse(req.url).pathname}", err, err.stack)
|
|
||||||
res.status(503).send(err?.message or err or 'Unknown error')
|
|
||||||
|
|
||||||
@router.get '/v1/dependent-apps', (req, res) =>
|
db.models('dependentDevice').select().where({ uuid })
|
||||||
@db.models('dependentApp').select()
|
.then ([ device ]) ->
|
||||||
.map (app) ->
|
return res.status(404).send('Device not found') if !device?
|
||||||
return {
|
return res.status(410).send('Device deleted') if device.markedForDeletion
|
||||||
id: parseInt(app.appId)
|
throw new Error('Device is invalid') if !device.deviceId?
|
||||||
commit: app.commit
|
Promise.try ->
|
||||||
name: app.name
|
if !_.isEmpty(fieldsToUpdateOnAPI)
|
||||||
config: JSON.parse(app.config ? '{}')
|
proxyvisor.apiBinder.patchDevice(device.deviceId, fieldsToUpdateOnAPI)
|
||||||
}
|
.then ->
|
||||||
.then (apps) ->
|
db.models('dependentDevice').update(fieldsToUpdateOnDB).where({ uuid })
|
||||||
res.json(apps)
|
.then ->
|
||||||
.catch (err) ->
|
db.models('dependentDevice').select().where({ uuid })
|
||||||
console.error("Error on #{req.method} #{url.parse(req.url).pathname}", err, err.stack)
|
.then ([ device ]) ->
|
||||||
res.status(503).send(err?.message or err or 'Unknown error')
|
res.json(parseDeviceFields(device))
|
||||||
|
.catch (err) ->
|
||||||
|
console.error("Error on #{req.method} #{url.parse(req.url).pathname}", err, err.stack)
|
||||||
|
res.status(503).send(err?.message or err or 'Unknown error')
|
||||||
|
|
||||||
|
router.get '/v1/dependent-apps/:appId/assets/:commit', (req, res) ->
|
||||||
|
db.models('dependentApp').select().where(_.pick(req.params, 'appId', 'commit'))
|
||||||
|
.then ([ app ]) ->
|
||||||
|
return res.status(404).send('Not found') if !app
|
||||||
|
dest = tarPath(app.appId, app.commit)
|
||||||
|
fs.lstatAsync(dest)
|
||||||
|
.catch ->
|
||||||
|
Promise.using proxyvisor.docker.imageRootDirMounted(app.image), (rootDir) ->
|
||||||
|
getTarArchive(rootDir + '/assets', dest)
|
||||||
|
.then ->
|
||||||
|
res.sendFile(dest)
|
||||||
|
.catch (err) ->
|
||||||
|
console.error("Error on #{req.method} #{url.parse(req.url).pathname}", err, err.stack)
|
||||||
|
res.status(503).send(err?.message or err or 'Unknown error')
|
||||||
|
|
||||||
|
router.get '/v1/dependent-apps', (req, res) ->
|
||||||
|
db.models('dependentApp').select()
|
||||||
|
.map (app) ->
|
||||||
|
return {
|
||||||
|
id: parseInt(app.appId)
|
||||||
|
commit: app.commit
|
||||||
|
name: app.name
|
||||||
|
config: JSON.parse(app.config ? '{}')
|
||||||
|
}
|
||||||
|
.then (apps) ->
|
||||||
|
res.json(apps)
|
||||||
|
.catch (err) ->
|
||||||
|
console.error("Error on #{req.method} #{url.parse(req.url).pathname}", err, err.stack)
|
||||||
|
res.status(503).send(err?.message or err or 'Unknown error')
|
||||||
|
|
||||||
|
return router
|
||||||
|
|
||||||
module.exports = class Proxyvisor
|
module.exports = class Proxyvisor
|
||||||
constructor: ({ @config, @logger, @db, @docker, @images, @applications }) ->
|
constructor: ({ @config, @logger, @db, @docker, @images, @applications }) ->
|
||||||
@acknowledgedState = {}
|
@acknowledgedState = {}
|
||||||
@lastRequestForDevice = {}
|
@lastRequestForDevice = {}
|
||||||
@_router = new ProxyvisorRouter(this)
|
@router = createProxyvisorRouter(this)
|
||||||
@router = @_router.router
|
|
||||||
@actionExecutors = {
|
@actionExecutors = {
|
||||||
updateDependentTargets: (step) =>
|
updateDependentTargets: (step) =>
|
||||||
@config.getMany([ 'currentApiKey', 'apiTimeout' ])
|
@config.getMany([ 'currentApiKey', 'apiTimeout' ])
|
||||||
@ -460,7 +452,7 @@ module.exports = class Proxyvisor
|
|||||||
return images
|
return images
|
||||||
|
|
||||||
_imageAvailable: (image, available) ->
|
_imageAvailable: (image, available) ->
|
||||||
_.some(available, (availableImage) -> availableImage.name == image)
|
_.some(available, name: image)
|
||||||
|
|
||||||
_getHookStep: (currentDevices, appId) =>
|
_getHookStep: (currentDevices, appId) =>
|
||||||
hookStep = {
|
hookStep = {
|
||||||
@ -572,7 +564,7 @@ module.exports = class Proxyvisor
|
|||||||
for appId in allAppIds
|
for appId in allAppIds
|
||||||
devicesForApp = (devices) ->
|
devicesForApp = (devices) ->
|
||||||
_.filter devices, (d) ->
|
_.filter devices, (d) ->
|
||||||
_.includes(_.keys(d.apps), appId)
|
_.has(d.apps, appId)
|
||||||
|
|
||||||
currentDevices = devicesForApp(current.dependent.devices)
|
currentDevices = devicesForApp(current.dependent.devices)
|
||||||
targetDevices = devicesForApp(target.dependent.devices)
|
targetDevices = devicesForApp(target.dependent.devices)
|
||||||
@ -590,8 +582,7 @@ module.exports = class Proxyvisor
|
|||||||
.then (parentApp) =>
|
.then (parentApp) =>
|
||||||
Promise.map parentApp?.services ? [], (service) =>
|
Promise.map parentApp?.services ? [], (service) =>
|
||||||
@docker.getImageEnv(service.image)
|
@docker.getImageEnv(service.image)
|
||||||
.then (imageEnv) ->
|
.get('RESIN_DEPENDENT_DEVICES_HOOK_ADDRESS')
|
||||||
return imageEnv.RESIN_DEPENDENT_DEVICES_HOOK_ADDRESS
|
|
||||||
.then (imageHookAddresses) ->
|
.then (imageHookAddresses) ->
|
||||||
for addr in imageHookAddresses
|
for addr in imageHookAddresses
|
||||||
return addr if addr?
|
return addr if addr?
|
||||||
|
@ -7,6 +7,8 @@ APIBinder = require './api-binder'
|
|||||||
DeviceState = require './device-state'
|
DeviceState = require './device-state'
|
||||||
SupervisorAPI = require './supervisor-api'
|
SupervisorAPI = require './supervisor-api'
|
||||||
|
|
||||||
|
constants = require './lib/constants'
|
||||||
|
|
||||||
startupConfigFields = [
|
startupConfigFields = [
|
||||||
'uuid'
|
'uuid'
|
||||||
'listenPort'
|
'listenPort'
|
||||||
@ -44,7 +46,7 @@ module.exports = class Supervisor extends EventEmitter
|
|||||||
.then =>
|
.then =>
|
||||||
# initialize API
|
# initialize API
|
||||||
console.log('Starting API server')
|
console.log('Starting API server')
|
||||||
@api.listen(@config.constants.allowedInterfaces, conf.listenPort, conf.apiTimeout)
|
@api.listen(constants.allowedInterfaces, conf.listenPort, conf.apiTimeout)
|
||||||
@deviceState.on('shutdown', => @api.stop())
|
@deviceState.on('shutdown', => @api.stop())
|
||||||
.then =>
|
.then =>
|
||||||
@apiBinder.init() # this will first try to provision if it's a new device
|
@apiBinder.init() # this will first try to provision if it's a new device
|
||||||
|
Loading…
Reference in New Issue
Block a user