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:
Pablo Carranza Velez 2017-11-10 02:43:13 -08:00
parent 5f651c71f7
commit 0d27658a87
9 changed files with 97 additions and 39 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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