mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-05-02 00:52:57 +00:00
Various improvements and fixes to how compositions are handled
Change the way we get the network gateway to set up the supervisor API address. Added support for cap_add, cap_drop and devices. Some fixes like missing fat arrows and removing leftover code. Signed-off-by: Pablo Carranza Velez <pablo@resin.io>
This commit is contained in:
parent
5f651c71f7
commit
0d27658a87
@ -597,15 +597,22 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
return dbApp
|
return dbApp
|
||||||
|
|
||||||
createTargetService: (service, opts) ->
|
createTargetService: (service, opts) ->
|
||||||
|
NotFoundErr = (err) -> err.statusCode == 404
|
||||||
serviceOpts = {
|
serviceOpts = {
|
||||||
serviceName: service.serviceName
|
serviceName: service.serviceName
|
||||||
}
|
}
|
||||||
_.assign(serviceOpts, opts)
|
_.assign(serviceOpts, opts)
|
||||||
|
Promise.join(
|
||||||
@images.inspectByName(service.image)
|
@images.inspectByName(service.image)
|
||||||
.catchReturn(undefined)
|
.catchReturn(undefined)
|
||||||
.then (imageInfo) ->
|
@docker.getNetworkGateway(service.network_mode ? service.appId)
|
||||||
|
.catchReturn(NotFoundErr, null)
|
||||||
|
.catchReturn(@docker.InvalidNetGatewayError, null)
|
||||||
|
(imageInfo, apiHostForNetwork) ->
|
||||||
serviceOpts.imageInfo = imageInfo
|
serviceOpts.imageInfo = imageInfo
|
||||||
|
serviceOpts.supervisorApiHost = apiHostForNetwork if apiHostForNetwork?
|
||||||
return new Service(service, serviceOpts)
|
return new Service(service, serviceOpts)
|
||||||
|
)
|
||||||
|
|
||||||
normaliseAndExtendAppFromDB: (app) =>
|
normaliseAndExtendAppFromDB: (app) =>
|
||||||
Promise.join(
|
Promise.join(
|
||||||
@ -619,8 +626,10 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
}
|
}
|
||||||
_.assign(configOpts, opts)
|
_.assign(configOpts, opts)
|
||||||
volumes = JSON.parse(app.volumes)
|
volumes = JSON.parse(app.volumes)
|
||||||
_.forEach volumes, (v) ->
|
volumes = _.mapValues volumes, (volumeConfig) ->
|
||||||
v.labels ?= {}
|
volumeConfig ?= {}
|
||||||
|
volumeConfig.labels ?= {}
|
||||||
|
return volumeConfig
|
||||||
Promise.map(JSON.parse(app.services), (service) => @createTargetService(service, configOpts))
|
Promise.map(JSON.parse(app.services), (service) => @createTargetService(service, configOpts))
|
||||||
.then (services) ->
|
.then (services) ->
|
||||||
# If a named volume is defined in a service, we add it app-wide so that we can track it and purge it
|
# If a named volume is defined in a service, we add it app-wide so that we can track it and purge it
|
||||||
|
@ -26,7 +26,7 @@ module.exports = class Networks
|
|||||||
.then (networks) ->
|
.then (networks) ->
|
||||||
_.filter(networks, (v) -> v.appId == appId)
|
_.filter(networks, (v) -> v.appId == appId)
|
||||||
|
|
||||||
get: (name) ->
|
get: (name) =>
|
||||||
@docker.getNetwork(name).inspect()
|
@docker.getNetwork(name).inspect()
|
||||||
.then (network) ->
|
.then (network) ->
|
||||||
return @format(network)
|
return @format(network)
|
||||||
@ -45,7 +45,7 @@ module.exports = class Networks
|
|||||||
@logger.logSystemEvent(logTypes.createNetworkError, { network: { name }, error: err })
|
@logger.logSystemEvent(logTypes.createNetworkError, { network: { name }, error: err })
|
||||||
throw err
|
throw err
|
||||||
|
|
||||||
remove: ({ name }) ->
|
remove: ({ name }) =>
|
||||||
@logger.logSystemEvent(logTypes.removeNetwork, { network: { name } })
|
@logger.logSystemEvent(logTypes.removeNetwork, { network: { name } })
|
||||||
@docker.getNetwork(name).remove()
|
@docker.getNetwork(name).remove()
|
||||||
.catch (err) =>
|
.catch (err) =>
|
||||||
|
@ -124,7 +124,7 @@ module.exports = class ServiceManager extends EventEmitter
|
|||||||
alreadyStarted = true
|
alreadyStarted = true
|
||||||
return
|
return
|
||||||
|
|
||||||
if statusCode is '500' and err.json.trim().match(/exec format error$/)
|
if statusCode is '500' and err.message?.trim?()?.match(/exec format error$/)
|
||||||
# Provide a friendlier error message for "exec format error"
|
# Provide a friendlier error message for "exec format error"
|
||||||
@config.get('deviceType')
|
@config.get('deviceType')
|
||||||
.then (deviceType) ->
|
.then (deviceType) ->
|
||||||
@ -132,7 +132,7 @@ module.exports = class ServiceManager extends EventEmitter
|
|||||||
else
|
else
|
||||||
# rethrow the same error
|
# rethrow the same error
|
||||||
throw err
|
throw err
|
||||||
.catch (err) ->
|
.catch (err) =>
|
||||||
# If starting the container failed, we remove it so that it doesn't litter
|
# If starting the container failed, we remove it so that it doesn't litter
|
||||||
container.remove(v: true)
|
container.remove(v: true)
|
||||||
.finally =>
|
.finally =>
|
||||||
@ -177,6 +177,8 @@ module.exports = class ServiceManager extends EventEmitter
|
|||||||
getByDockerContainerId: (containerId) =>
|
getByDockerContainerId: (containerId) =>
|
||||||
@docker.getContainer(containerId).inspect()
|
@docker.getContainer(containerId).inspect()
|
||||||
.then (container) ->
|
.then (container) ->
|
||||||
|
if !container.Config.Labels['io.resin.supervised']?
|
||||||
|
return null
|
||||||
return Service.fromContainer(container)
|
return Service.fromContainer(container)
|
||||||
.catchReturn(null)
|
.catchReturn(null)
|
||||||
|
|
||||||
|
@ -34,6 +34,13 @@ defaultBinds = (appId, serviceName) ->
|
|||||||
"#{updateLock.lockPath(appId, serviceName)}:/tmp/resin"
|
"#{updateLock.lockPath(appId, serviceName)}:/tmp/resin"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
formatDevices = (devices) ->
|
||||||
|
return _.map devices, (device) ->
|
||||||
|
[ PathOnHost, PathInContainer, CgroupPermissions ] = device.split(':')
|
||||||
|
PathInContainer ?= PathOnHost
|
||||||
|
CgroupPermissions ?= 'rwm'
|
||||||
|
return { PathOnHost, PathInContainer, CgroupPermissions }
|
||||||
|
|
||||||
module.exports = class Service
|
module.exports = class Service
|
||||||
constructor: (serviceProperties, opts = {}) ->
|
constructor: (serviceProperties, opts = {}) ->
|
||||||
{
|
{
|
||||||
@ -61,6 +68,9 @@ module.exports = class Service
|
|||||||
@cap_drop
|
@cap_drop
|
||||||
@commit
|
@commit
|
||||||
@status
|
@status
|
||||||
|
@devices
|
||||||
|
@exposedPorts
|
||||||
|
@portBindings
|
||||||
} = serviceProperties
|
} = serviceProperties
|
||||||
@privileged ?= false
|
@privileged ?= false
|
||||||
@volumes ?= []
|
@volumes ?= []
|
||||||
@ -71,6 +81,9 @@ module.exports = class Service
|
|||||||
@expose ?= []
|
@expose ?= []
|
||||||
@cap_add ?= []
|
@cap_add ?= []
|
||||||
@cap_drop ?= []
|
@cap_drop ?= []
|
||||||
|
@devices ?= []
|
||||||
|
@exposedPorts ?= {}
|
||||||
|
@portBindings ?= {}
|
||||||
@network_mode ?= @appId.toString()
|
@network_mode ?= @appId.toString()
|
||||||
if @releaseId?
|
if @releaseId?
|
||||||
@releaseId = @releaseId.toString()
|
@releaseId = @releaseId.toString()
|
||||||
@ -83,7 +96,9 @@ module.exports = class Service
|
|||||||
@extendEnvVars(opts)
|
@extendEnvVars(opts)
|
||||||
@extendLabels(opts.imageInfo)
|
@extendLabels(opts.imageInfo)
|
||||||
@extendAndSanitiseVolumes(opts.imageInfo)
|
@extendAndSanitiseVolumes(opts.imageInfo)
|
||||||
|
@extendAndSanitiseExposedPorts(opts.imageInfo)
|
||||||
|
{ @exposedPorts, @portBindings } = @getPortsAndPortBindings()
|
||||||
|
@devices = formatDevices(@devices)
|
||||||
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'])
|
||||||
@ -131,6 +146,17 @@ module.exports = class Service
|
|||||||
@labels['io.resin.commit'] = @commit
|
@labels['io.resin.commit'] = @commit
|
||||||
return @labels
|
return @labels
|
||||||
|
|
||||||
|
extendAndSanitiseExposedPorts: (imageInfo) =>
|
||||||
|
@expose = _.clone(@expose)
|
||||||
|
@expose = _.map(@expose, String)
|
||||||
|
if imageInfo?.Config?.ExposedPorts?
|
||||||
|
_.forEach imageInfo.Config.ExposedPorts, (v, k) =>
|
||||||
|
port = k.match(/^([0-9]*)\/tcp$/)?[1]
|
||||||
|
if port? and !_.find(@expose, port)
|
||||||
|
@expose.push(port)
|
||||||
|
|
||||||
|
return @expose
|
||||||
|
|
||||||
extendAndSanitiseVolumes: (imageInfo) =>
|
extendAndSanitiseVolumes: (imageInfo) =>
|
||||||
volumes = []
|
volumes = []
|
||||||
_.forEach @volumes, (vol) ->
|
_.forEach @volumes, (vol) ->
|
||||||
@ -224,23 +250,27 @@ module.exports = class Service
|
|||||||
containerId: container.Id
|
containerId: container.Id
|
||||||
cap_add: container.HostConfig.CapAdd
|
cap_add: container.HostConfig.CapAdd
|
||||||
cap_drop: container.HostConfig.CapDrop
|
cap_drop: container.HostConfig.CapDrop
|
||||||
|
devices: container.HostConfig.Devices
|
||||||
status
|
status
|
||||||
|
exposedPorts: container.Config.ExposedPorts
|
||||||
|
portBindings: container.HostConfig.PortBindings
|
||||||
}
|
}
|
||||||
return new Service(service)
|
return new Service(service)
|
||||||
|
|
||||||
# TODO: map ports for any of the possible formats "container:host/protocol", port ranges, etc.
|
# TODO: map ports for any of the possible formats "container:host/protocol", port ranges, etc.
|
||||||
getPortsAndPortBindings: =>
|
getPortsAndPortBindings: =>
|
||||||
ports = {}
|
exposedPorts = {}
|
||||||
portBindings = {}
|
portBindings = {}
|
||||||
if @ports?
|
if @ports?
|
||||||
_.forEach @ports, (port) ->
|
_.forEach @ports, (port) ->
|
||||||
[ hostPort, containerPort ] = port.split(':')
|
[ hostPort, containerPort ] = port.toString().split(':')
|
||||||
ports[containerPort + '/tcp'] = {}
|
containerPort ?= hostPort
|
||||||
portBindings[containerPort + '/tcp'] = [ HostPort: hostPort ]
|
exposedPorts[containerPort + '/tcp'] = {}
|
||||||
|
portBindings[containerPort + '/tcp'] = [ { HostIp: '', HostPort: hostPort } ]
|
||||||
if @expose?
|
if @expose?
|
||||||
_.forEach @expose, (port) ->
|
_.forEach @expose, (port) ->
|
||||||
ports[port + '/tcp'] = {}
|
exposedPorts[port + '/tcp'] = {}
|
||||||
return { ports, portBindings }
|
return { exposedPorts, portBindings }
|
||||||
|
|
||||||
getBindsAndVolumes: =>
|
getBindsAndVolumes: =>
|
||||||
binds = []
|
binds = []
|
||||||
@ -254,7 +284,6 @@ module.exports = class Service
|
|||||||
return { binds, volumes }
|
return { binds, volumes }
|
||||||
|
|
||||||
toContainerConfig: =>
|
toContainerConfig: =>
|
||||||
{ ports, portBindings } = @getPortsAndPortBindings()
|
|
||||||
{ binds, volumes } = @getBindsAndVolumes()
|
{ binds, volumes } = @getBindsAndVolumes()
|
||||||
|
|
||||||
conf = {
|
conf = {
|
||||||
@ -265,16 +294,17 @@ module.exports = class Service
|
|||||||
Tty: true
|
Tty: true
|
||||||
Volumes: volumes
|
Volumes: volumes
|
||||||
Env: _.map @environment, (v, k) -> k + '=' + v
|
Env: _.map @environment, (v, k) -> k + '=' + v
|
||||||
ExposedPorts: ports
|
ExposedPorts: @exposedPorts
|
||||||
Labels: @labels
|
Labels: @labels
|
||||||
HostConfig:
|
HostConfig:
|
||||||
Privileged: @privileged
|
Privileged: @privileged
|
||||||
NetworkMode: @network_mode
|
NetworkMode: @network_mode
|
||||||
PortBindings: portBindings
|
PortBindings: @portBindings
|
||||||
Binds: binds
|
Binds: binds
|
||||||
RestartPolicy: @restartPolicy
|
RestartPolicy: @restartPolicy
|
||||||
CapAdd: @cap_add
|
CapAdd: @cap_add
|
||||||
CapDrop: @cap_drop
|
CapDrop: @cap_drop
|
||||||
|
Devices: @devices
|
||||||
}
|
}
|
||||||
# If network mode is the default network for this app, add alias for serviceName
|
# If network mode is the default network for this app, add alias for serviceName
|
||||||
if @network_mode == @appId.toString()
|
if @network_mode == @appId.toString()
|
||||||
@ -296,13 +326,14 @@ module.exports = class Service
|
|||||||
'restartPolicy'
|
'restartPolicy'
|
||||||
'labels'
|
'labels'
|
||||||
'environment'
|
'environment'
|
||||||
'cap_add'
|
'portBindings'
|
||||||
'cap_drop'
|
'exposedPorts'
|
||||||
]
|
]
|
||||||
arraysToCompare = [
|
arraysToCompare = [
|
||||||
'ports'
|
|
||||||
'expose'
|
|
||||||
'volumes'
|
'volumes'
|
||||||
|
'devices'
|
||||||
|
'cap_add'
|
||||||
|
'cap_drop'
|
||||||
]
|
]
|
||||||
return _.isEqual(_.pick(this, propertiesToCompare), _.pick(otherService, propertiesToCompare)) and
|
return _.isEqual(_.pick(this, propertiesToCompare), _.pick(otherService, propertiesToCompare)) and
|
||||||
_.every arraysToCompare, (property) =>
|
_.every arraysToCompare, (property) =>
|
||||||
|
@ -101,8 +101,8 @@ module.exports = class Volumes
|
|||||||
@logger.logSystemEvent(logTypes.removeVolumeError, { volume: { name }, error: err })
|
@logger.logSystemEvent(logTypes.removeVolumeError, { volume: { name }, error: err })
|
||||||
|
|
||||||
isEqualConfig: (current, target) ->
|
isEqualConfig: (current, target) ->
|
||||||
currentOpts = current.driver_opts ? {}
|
currentOpts = current?.driver_opts ? {}
|
||||||
targetOpts = target.driver_opts ? {}
|
targetOpts = target?.driver_opts ? {}
|
||||||
currentLabels = current.labels ? {}
|
currentLabels = current?.labels ? {}
|
||||||
targetLabels = target.labels ? {}
|
targetLabels = target?.labels ? {}
|
||||||
return _.isEqual(currentLabels, targetLabels) and _.isEqual(currentOpts, targetOpts)
|
return _.isEqual(currentLabels, targetLabels) and _.isEqual(currentOpts, targetOpts)
|
||||||
|
@ -36,9 +36,7 @@ module.exports = class EventTracker
|
|||||||
|
|
||||||
properties = _.cloneDeep(properties)
|
properties = _.cloneDeep(properties)
|
||||||
|
|
||||||
# Don't log private env vars (e.g. api keys)
|
# Don't log private env vars (e.g. api keys) or other secrets - use a whitelist to mask what we send
|
||||||
if properties?.service?.environment?
|
|
||||||
delete properties.service.environment
|
|
||||||
properties = mask(properties, mixpanelMask)
|
properties = mask(properties, mixpanelMask)
|
||||||
@_logEvent('Event:', ev, JSON.stringify(properties))
|
@_logEvent('Event:', ev, JSON.stringify(properties))
|
||||||
return if !@_client?
|
return if !@_client?
|
||||||
|
@ -3,6 +3,7 @@ DockerToolbelt = require 'docker-toolbelt'
|
|||||||
{ DockerProgress } = require 'docker-progress'
|
{ DockerProgress } = require 'docker-progress'
|
||||||
Promise = require 'bluebird'
|
Promise = require 'bluebird'
|
||||||
dockerDelta = require 'docker-delta'
|
dockerDelta = require 'docker-delta'
|
||||||
|
TypedError = require 'typed-error'
|
||||||
_ = require 'lodash'
|
_ = require 'lodash'
|
||||||
{ request, resumable } = require './request'
|
{ request, resumable } = require './request'
|
||||||
{ envArrayToObject } = require './conversions'
|
{ envArrayToObject } = require './conversions'
|
||||||
@ -30,6 +31,7 @@ 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
|
return this
|
||||||
|
|
||||||
getRepoAndTag: (image) =>
|
getRepoAndTag: (image) =>
|
||||||
@ -127,9 +129,14 @@ module.exports = class DockerUtils extends DockerToolbelt
|
|||||||
# Maybe switch to just looking at the docker0 interface?
|
# Maybe switch to just looking at the docker0 interface?
|
||||||
# For now we do a hacky thing using the Subnet property...
|
# For now we do a hacky thing using the Subnet property...
|
||||||
defaultBridgeGateway: =>
|
defaultBridgeGateway: =>
|
||||||
@getNetwork('bridge').inspect()
|
@getNetworkGateway('bridge')
|
||||||
.then (netInfo) ->
|
.catchReturn(@InvalidNetGatewayError, '172.17.0.1')
|
||||||
|
|
||||||
|
getNetworkGateway: (netName) =>
|
||||||
|
return Promise.resolve('127.0.0.1') if netName == 'host'
|
||||||
|
@getNetwork(netName).inspect()
|
||||||
|
.then (netInfo) =>
|
||||||
conf = netInfo?.IPAM?.Config?[0]
|
conf = netInfo?.IPAM?.Config?[0]
|
||||||
return conf.Gateway if conf?.Gateway?
|
return conf.Gateway if conf?.Gateway?
|
||||||
return conf.Subnet.replace('.0/16', '.1') if _.endsWith(conf?.Subnet, '.0/16')
|
return conf.Subnet.replace('.0/16', '.1') if _.endsWith(conf?.Subnet, '.0/16')
|
||||||
return '172.17.0.1'
|
throw new @InvalidNetGatewayError("Cannot determine network gateway for #{netName}")
|
||||||
|
@ -6,6 +6,11 @@ checkAndAddIptablesRule = (rule) ->
|
|||||||
.catch ->
|
.catch ->
|
||||||
childProcess.execAsync("iptables -A #{rule}")
|
childProcess.execAsync("iptables -A #{rule}")
|
||||||
|
|
||||||
|
checkAndInsertIptablesRule = (rule) ->
|
||||||
|
childProcess.execAsync("iptables -C #{rule}")
|
||||||
|
.catch ->
|
||||||
|
childProcess.execAsync("iptables -I #{rule}")
|
||||||
|
|
||||||
exports.rejectOnAllInterfacesExcept = (allowedInterfaces, port) ->
|
exports.rejectOnAllInterfacesExcept = (allowedInterfaces, port) ->
|
||||||
Promise.each allowedInterfaces, (iface) ->
|
Promise.each allowedInterfaces, (iface) ->
|
||||||
checkAndAddIptablesRule("INPUT -p tcp --dport #{port} -i #{iface} -j ACCEPT")
|
checkAndAddIptablesRule("INPUT -p tcp --dport #{port} -i #{iface} -j ACCEPT")
|
||||||
@ -14,3 +19,12 @@ exports.rejectOnAllInterfacesExcept = (allowedInterfaces, port) ->
|
|||||||
.catch ->
|
.catch ->
|
||||||
# On systems without REJECT support, fall back to DROP
|
# On systems without REJECT support, fall back to DROP
|
||||||
checkAndAddIptablesRule("INPUT -p tcp --dport #{port} -j DROP")
|
checkAndAddIptablesRule("INPUT -p tcp --dport #{port} -j DROP")
|
||||||
|
|
||||||
|
acceptSubnetRule = (allowedSubnet, port) ->
|
||||||
|
return "INPUT -p tcp --dport #{port} -s #{allowedSubnet} -j ACCEPT"
|
||||||
|
|
||||||
|
exports.addAcceptedSubnet = (allowedSubnet, port) ->
|
||||||
|
checkAndInsertIptablesRule(acceptSubnetRule(allowedSubnet, port))
|
||||||
|
|
||||||
|
exports.isSubnetAccepted = (allowedSubnet, port) ->
|
||||||
|
childProcess.execAsync("iptables -C #{acceptSubnetRule(allowedSubnet, port)}")
|
||||||
|
@ -133,12 +133,9 @@ module.exports = class Logger
|
|||||||
if obj.error?
|
if obj.error?
|
||||||
# Report the message from the original cause to the user.
|
# Report the message from the original cause to the user.
|
||||||
errMessage = obj.error.message
|
errMessage = obj.error.message
|
||||||
if _.isEmpty(errMessage)
|
|
||||||
errMessage = obj.error.json
|
|
||||||
if _.isEmpty(errMessage)
|
|
||||||
errMessage = obj.error.reason
|
|
||||||
if _.isEmpty(errMessage)
|
if _.isEmpty(errMessage)
|
||||||
errMessage = 'Unknown cause'
|
errMessage = 'Unknown cause'
|
||||||
|
console.log('Warning: invalid error message', obj.error)
|
||||||
message += " due to '#{errMessage}'"
|
message += " due to '#{errMessage}'"
|
||||||
@logSystemMessage(message, obj, logType.eventName)
|
@logSystemMessage(message, obj, logType.eventName)
|
||||||
return
|
return
|
||||||
|
Loading…
x
Reference in New Issue
Block a user