Issues #389 and #390: Remove /host_run/dbus and /host/var/lib/connman bind mounts for non-ResinOS-1.X devices

On ResinOS 2.X the default mounts should not include the previously deprecated host_run, and there's no connman which makes the connman mount confusing.
This is a breaking change as it is not backwards-compatible on non-ResinOS instances of the supervisor.

Change-Type: major
Signed-off-by: Pablo Carranza Velez <pablo@resin.io>
This commit is contained in:
Pablo Carranza Velez 2017-03-06 13:52:35 -03:00 committed by Pablo Carranza Vélez
parent f7c702b845
commit b64ed9568c
4 changed files with 162 additions and 147 deletions

View File

@ -214,8 +214,8 @@ fetch = (app, setDeviceUpdateState = true) ->
throw err
shouldMountKmod = (image) ->
device.getOSVersion().then (osVersion) ->
return false if not /^Resin OS 1./.test(osVersion)
device.isResinOSv1().then (isV1) ->
return false if not isV1
Promise.using docker.imageRootDirMounted(image), (rootDir) ->
utils.getOSVersion(rootDir + '/etc/os-release')
.then (version) ->
@ -225,112 +225,113 @@ shouldMountKmod = (image) ->
return false
application.start = start = (app) ->
volumes = utils.defaultVolumes
binds = utils.defaultBinds(app.appId)
alreadyStarted = false
Promise.try ->
# Parse the env vars before trying to access them, that's because they have to be stringified for knex..
return [ JSON.parse(app.env), JSON.parse(app.config) ]
.spread (env, conf) ->
if env.PORT?
portList = env.PORT
.split(',')
.map((port) -> port.trim())
.filter(isValidPort)
device.isResinOSv1().then (isV1) ->
volumes = utils.defaultVolumes(isV1)
binds = utils.defaultBinds(app.appId, isV1)
alreadyStarted = false
Promise.try ->
# Parse the env vars before trying to access them, that's because they have to be stringified for knex..
return [ JSON.parse(app.env), JSON.parse(app.config) ]
.spread (env, conf) ->
if env.PORT?
portList = env.PORT
.split(',')
.map((port) -> port.trim())
.filter(isValidPort)
if app.containerId?
# If we have a container id then check it exists and if so use it.
container = docker.getContainer(app.containerId)
containerPromise = container.inspectAsync().return(container)
else
containerPromise = Promise.rejected()
if app.containerId?
# If we have a container id then check it exists and if so use it.
container = docker.getContainer(app.containerId)
containerPromise = container.inspectAsync().return(container)
else
containerPromise = Promise.rejected()
# If there is no existing container then create one instead.
containerPromise.catch ->
fetch(app)
.then (imageInfo) ->
logSystemEvent(logTypes.installApp, app)
device.updateState(status: 'Installing')
# If there is no existing container then create one instead.
containerPromise.catch ->
fetch(app)
.then (imageInfo) ->
logSystemEvent(logTypes.installApp, app)
device.updateState(status: 'Installing')
ports = {}
portBindings = {}
if portList?
portList.forEach (port) ->
ports[port + '/tcp'] = {}
portBindings[port + '/tcp'] = [ HostPort: port ]
ports = {}
portBindings = {}
if portList?
portList.forEach (port) ->
ports[port + '/tcp'] = {}
portBindings[port + '/tcp'] = [ HostPort: port ]
if imageInfo?.Config?.Cmd
cmd = imageInfo.Config.Cmd
else
cmd = [ '/bin/bash', '-c', '/start' ]
if imageInfo?.Config?.Cmd
cmd = imageInfo.Config.Cmd
else
cmd = [ '/bin/bash', '-c', '/start' ]
restartPolicy = createRestartPolicy({ name: conf['RESIN_APP_RESTART_POLICY'], maximumRetryCount: conf['RESIN_APP_RESTART_RETRIES'] })
shouldMountKmod(app.imageId)
.then (shouldMount) ->
binds.push('/bin/kmod:/bin/kmod:ro') if shouldMount
docker.createContainerAsync(
Image: app.imageId
Cmd: cmd
Tty: true
Volumes: volumes
Env: _.map env, (v, k) -> k + '=' + v
ExposedPorts: ports
HostConfig:
Privileged: true
NetworkMode: 'host'
PortBindings: portBindings
Binds: binds
RestartPolicy: restartPolicy
)
.tap ->
logSystemEvent(logTypes.installAppSuccess, app)
restartPolicy = createRestartPolicy({ name: conf['RESIN_APP_RESTART_POLICY'], maximumRetryCount: conf['RESIN_APP_RESTART_RETRIES'] })
shouldMountKmod(app.imageId)
.then (shouldMount) ->
binds.push('/bin/kmod:/bin/kmod:ro') if shouldMount
docker.createContainerAsync(
Image: app.imageId
Cmd: cmd
Tty: true
Volumes: volumes
Env: _.map env, (v, k) -> k + '=' + v
ExposedPorts: ports
HostConfig:
Privileged: true
NetworkMode: 'host'
PortBindings: portBindings
Binds: binds
RestartPolicy: restartPolicy
)
.tap ->
logSystemEvent(logTypes.installAppSuccess, app)
.catch (err) ->
logSystemEvent(logTypes.installAppError, app, err)
throw err
.tap (container) ->
logSystemEvent(logTypes.startApp, app)
device.updateState(status: 'Starting')
container.startAsync()
.catch (err) ->
logSystemEvent(logTypes.installAppError, app, err)
throw err
.tap (container) ->
logSystemEvent(logTypes.startApp, app)
device.updateState(status: 'Starting')
container.startAsync()
.catch (err) ->
statusCode = '' + err.statusCode
# 304 means the container was already started, precisely what we want :)
if statusCode is '304'
alreadyStarted = true
return
statusCode = '' + err.statusCode
# 304 means the container was already started, precisely what we want :)
if statusCode is '304'
alreadyStarted = true
return
if statusCode is '500' and err.json.trim().match(/exec format error$/)
# Provide a friendlier error message for "exec format error"
device.getDeviceType()
.then (deviceType) ->
throw new Error("Application architecture incompatible with #{deviceType}: exec format error")
else
# rethrow the same error
throw err
.catch (err) ->
# If starting the container failed, we remove it so that it doesn't litter
container.removeAsync(v: true)
if statusCode is '500' and err.json.trim().match(/exec format error$/)
# Provide a friendlier error message for "exec format error"
device.getDeviceType()
.then (deviceType) ->
throw new Error("Application architecture incompatible with #{deviceType}: exec format error")
else
# rethrow the same error
throw err
.catch (err) ->
# If starting the container failed, we remove it so that it doesn't litter
container.removeAsync(v: true)
.then ->
app.containerId = null
knex('app').update(app).where(appId: app.appId)
.finally ->
logSystemEvent(logTypes.startAppError, app, err)
throw err
.then ->
app.containerId = null
knex('app').update(app).where(appId: app.appId)
.finally ->
logSystemEvent(logTypes.startAppError, app, err)
throw err
.then ->
app.containerId = container.id
device.updateState(commit: app.commit)
logger.attach(app)
.tap (container) ->
# Update the app info, only if starting the container worked.
knex('app').update(app).where(appId: app.appId)
.then (affectedRows) ->
knex('app').insert(app) if affectedRows == 0
.tap ->
if alreadyStarted
logSystemEvent(logTypes.startAppNoop, app)
else
logSystemEvent(logTypes.startAppSuccess, app)
.finally ->
device.updateState(status: 'Idle')
app.containerId = container.id
device.updateState(commit: app.commit)
logger.attach(app)
.tap (container) ->
# Update the app info, only if starting the container worked.
knex('app').update(app).where(appId: app.appId)
.then (affectedRows) ->
knex('app').insert(app) if affectedRows == 0
.tap ->
if alreadyStarted
logSystemEvent(logTypes.startAppNoop, app)
else
logSystemEvent(logTypes.startAppSuccess, app)
.finally ->
device.updateState(status: 'Idle')
validRestartPolicies = [ 'no', 'always', 'on-failure', 'unless-stopped' ]
# Construct a restart policy based on its name and maximumRetryCount.

View File

@ -224,3 +224,8 @@ do ->
exports.getOSVersion = memoizePromise ->
utils.getOSVersion(config.hostOsVersionPath)
exports.isResinOSv1 = memoizePromise ->
exports.getOSVersion().then (osVersion) ->
return true if /^Resin OS 1./.test(osVersion)
return false

View File

@ -265,42 +265,45 @@ do ->
docker.modem.dialAsync = Promise.promisify(docker.modem.dial)
createContainer = (options, internalId) ->
Promise.using writeLockImages(), ->
knex('image').select().where('repoTag', options.Image)
.then (images) ->
throw new Error('Only images created via the Supervisor can be used for creating containers.') if images.length == 0
knex.transaction (tx) ->
Promise.try ->
return internalId if internalId?
tx.insert({}, 'id').into('container')
.then ([ id ]) ->
return id
.then (id) ->
options.HostConfig ?= {}
options.Volumes ?= {}
_.assign(options.Volumes, utils.defaultVolumes)
options.HostConfig.Binds = utils.defaultBinds("containers/#{id}")
query = ''
query = "name=#{options.Name}&" if options.Name?
optsf =
path: "/containers/create?#{query}"
method: 'POST'
options: options
statusCodes:
200: true
201: true
404: 'no such container'
406: 'impossible to attach'
500: 'server error'
utils.validateKeys(options, utils.validContainerOptions)
.then ->
utils.validateKeys(options.HostConfig, utils.validHostConfigOptions)
.then ->
docker.modem.dialAsync(optsf)
.then (data) ->
containerId = data.Id
tx('container').update({ containerId }).where({ id })
.return(data)
exports.createContainer = (req, res) ->
Promise.join(
knex('image').select().where('repoTag', options.Image)
device.isResinOSv1()
(images, isV1) ->
throw new Error('Only images created via the Supervisor can be used for creating containers.') if images.length == 0
knex.transaction (tx) ->
Promise.try ->
return internalId if internalId?
tx.insert({}, 'id').into('container')
.then ([ id ]) ->
return id
.then (id) ->
options.HostConfig ?= {}
options.Volumes ?= {}
_.assign(options.Volumes, utils.defaultVolumes(isV1))
options.HostConfig.Binds = utils.defaultBinds("containers/#{id}", isV1)
query = ''
query = "name=#{options.Name}&" if options.Name?
optsf =
path: "/containers/create?#{query}"
method: 'POST'
options: options
statusCodes:
200: true
201: true
404: 'no such container'
406: 'impossible to attach'
500: 'server error'
utils.validateKeys(options, utils.validContainerOptions)
.then ->
utils.validateKeys(options.HostConfig, utils.validHostConfigOptions)
.then ->
docker.modem.dialAsync(optsf)
.then (data) ->
containerId = data.Id
tx('container').update({ containerId }).where({ id })
.return(data)
)
exports.createContainer = (req, res) ->
createContainer(req.body)
.then (data) ->
res.json(data)

View File

@ -242,26 +242,32 @@ exports.getOSVersion = (path) ->
console.log('Could not get OS Version: ', err, err.stack)
return undefined
exports.defaultVolumes = {
'/data': {}
'/lib/modules': {}
'/lib/firmware': {}
'/host/var/lib/connman': {}
'/host/run/dbus': {}
}
exports.defaultVolumes = (includeV1Volumes) ->
volumes = {
'/data': {}
'/lib/modules': {}
'/lib/firmware': {}
'/host/run/dbus': {}
}
if includeV1Volumes
volumes['/host/var/lib/connman'] = {}
volumes['/host_run/dbus'] = {}
return volumes
exports.getDataPath = (identifier) ->
return config.dataPath + '/' + identifier
exports.defaultBinds = (dataPath) ->
return [
exports.defaultBinds = (dataPath, includeV1Binds) ->
binds = [
exports.getDataPath(dataPath) + ':/data'
'/lib/modules:/lib/modules'
'/lib/firmware:/lib/firmware'
'/run/dbus:/host_run/dbus'
'/run/dbus:/host/run/dbus'
'/var/lib/connman:/host/var/lib/connman'
]
if includeV1Binds
binds.push('/run/dbus:/host_run/dbus')
binds.push('/var/lib/connman:/host/var/lib/connman')
return binds
exports.validComposeOptions = [
'command'