mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-20 22:23:13 +00:00
compose/service: Convert module to typescript and add network config
Stability improvements; * Printing of unsupported compose fields * Added a lot of tests * All compose configuration has a default value, enabling better comparison Change-type: minor Signed-off-by: Cameron Diver <cameron@resin.io>
This commit is contained in:
parent
e0231f15e9
commit
892d227cc2
@ -36,6 +36,7 @@
|
||||
"@types/mz": "0.0.32",
|
||||
"@types/node": "^10.3.1",
|
||||
"@types/rwlock": "^5.0.2",
|
||||
"@types/shell-quote": "^1.6.0",
|
||||
"JSONStream": "^1.1.2",
|
||||
"blinking": "~0.0.2",
|
||||
"bluebird": "^3.5.0",
|
||||
@ -46,6 +47,7 @@
|
||||
"coffee-script": "~1.11.0",
|
||||
"copy-webpack-plugin": "^4.2.3",
|
||||
"dbus-native": "^0.2.5",
|
||||
"deep-object-diff": "^1.1.0",
|
||||
"docker-delta": "^2.1.0",
|
||||
"docker-progress": "^2.7.2",
|
||||
"docker-toolbelt": "^3.3.2",
|
||||
|
@ -14,7 +14,7 @@ updateLock = require './lib/update-lock'
|
||||
{ NotFoundError } = require './lib/errors'
|
||||
|
||||
ServiceManager = require './compose/service-manager'
|
||||
Service = require './compose/service'
|
||||
{ Service } = require './compose/service'
|
||||
Images = require './compose/images'
|
||||
{ NetworkManager } = require './compose/network-manager'
|
||||
{ Network } = require './compose/network'
|
||||
@ -89,12 +89,12 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
.then =>
|
||||
delete @_containerStarted[step.current.containerId]
|
||||
if step.options?.removeImage
|
||||
@images.removeByDockerId(step.current.image)
|
||||
@images.removeByDockerId(step.current.config.image)
|
||||
remove: (step) =>
|
||||
# Only called for dead containers, so no need to take locks or anything
|
||||
@services.remove(step.current)
|
||||
updateMetadata: (step, { force = false, skipLock = false } = {}) =>
|
||||
skipLock or= checkTruthy(step.current.labels['io.resin.legacy-container'])
|
||||
skipLock or= checkTruthy(step.current.config.labels['io.resin.legacy-container'])
|
||||
@_lockingIfNecessary step.current.appId, { force, skipLock: skipLock or step.options?.skipLock }, =>
|
||||
@services.updateMetadata(step.current, step.target)
|
||||
restart: (step, { force = false, skipLock = false } = {}) =>
|
||||
@ -144,10 +144,12 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
@images.cleanup()
|
||||
createNetworkOrVolume: (step) =>
|
||||
if step.model is 'network'
|
||||
# TODO: These step targets should be the actual compose objects,
|
||||
# rather than recreating them
|
||||
Network.fromComposeObject({ @docker, @logger },
|
||||
step.target.name,
|
||||
step.appId,
|
||||
step.target
|
||||
step.target.config
|
||||
).create()
|
||||
else
|
||||
@volumes.create(step.target)
|
||||
@ -156,7 +158,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
Network.fromComposeObject({ @docker, @logger },
|
||||
step.current.name,
|
||||
step.appId,
|
||||
step.current
|
||||
step.current.config
|
||||
).remove()
|
||||
else
|
||||
@volumes.remove(step.current)
|
||||
@ -329,6 +331,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
currentServiceContainers = _.filter(currentServices, { serviceId })
|
||||
if currentServiceContainers.length > 1
|
||||
currentServicesPerId[serviceId] = _.maxBy(currentServiceContainers, 'createdAt')
|
||||
|
||||
# All but the latest container for this service are spurious and should be removed
|
||||
for service in _.without(currentServiceContainers, currentServicesPerId[serviceId])
|
||||
removePairs.push({
|
||||
@ -344,12 +347,13 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
alreadyStarted = (serviceId) =>
|
||||
return (
|
||||
currentServicesPerId[serviceId].isEqualExceptForRunningState(targetServicesPerId[serviceId]) and
|
||||
targetServicesPerId[serviceId].running and
|
||||
targetServicesPerId[serviceId].config.running and
|
||||
@_containerStarted[currentServicesPerId[serviceId].containerId]
|
||||
)
|
||||
|
||||
needUpdate = _.filter toBeMaybeUpdated, (serviceId) ->
|
||||
!currentServicesPerId[serviceId].isEqual(targetServicesPerId[serviceId]) and !alreadyStarted(serviceId)
|
||||
|
||||
for serviceId in needUpdate
|
||||
updatePairs.push({
|
||||
current: currentServicesPerId[serviceId]
|
||||
@ -399,7 +403,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
opts,
|
||||
name,
|
||||
appId,
|
||||
target[name].config
|
||||
target[name]
|
||||
)
|
||||
return !currentNet.isEqualConfig(targetNet)
|
||||
else
|
||||
@ -488,13 +492,14 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
|
||||
_nextStepsForNetwork: ({ current, target }, currentApp, changingPairs) =>
|
||||
dependencyComparisonFn = (service, current) ->
|
||||
service.networkMode == "#{service.appId}_#{current?.name}"
|
||||
service.config.networkMode == "#{service.appId}_#{current?.name}"
|
||||
|
||||
@_nextStepsForNetworkOrVolume({ current, target }, currentApp, changingPairs, dependencyComparisonFn, 'network')
|
||||
|
||||
_nextStepsForVolume: ({ current, target }, currentApp, changingPairs) ->
|
||||
# Check none of the currentApp.services use this network or volume
|
||||
dependencyComparisonFn = (service, current) ->
|
||||
_.some service.volumes, (volumeDefinition) ->
|
||||
_.some service.config.volumes, (volumeDefinition) ->
|
||||
[ sourceName, destName ] = volumeDefinition.split(':')
|
||||
destName? and sourceName == "#{service.appId}_#{current?.name}"
|
||||
@_nextStepsForNetworkOrVolume({ current, target }, currentApp, changingPairs, dependencyComparisonFn, 'volume')
|
||||
@ -503,7 +508,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
_updateContainerStep: (current, target) ->
|
||||
if current.releaseId != target.releaseId or current.imageId != target.imageId
|
||||
return serviceAction('updateMetadata', target.serviceId, current, target)
|
||||
else if target.running
|
||||
else if target.config.running
|
||||
return serviceAction('start', target.serviceId, current, target)
|
||||
else
|
||||
return serviceAction('stop', target.serviceId, current, target)
|
||||
@ -551,7 +556,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
return serviceAction('remove', current.serviceId, current)
|
||||
|
||||
needsDownload = !_.some availableImages, (image) =>
|
||||
image.dockerImageId == target?.image or @images.isSameImage(image, { name: target.imageName })
|
||||
image.dockerImageId == target?.config.image or @images.isSameImage(image, { name: target.imageName })
|
||||
|
||||
# This service needs an image download but it's currently downloading, so we wait
|
||||
if needsDownload and target?.imageId in downloading
|
||||
@ -566,18 +571,18 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
# even if its strategy is handover
|
||||
needsSpecialKill = @_hasCurrentNetworksOrVolumes(current, networkPairs, volumePairs)
|
||||
|
||||
if current?.isSameContainer(target)
|
||||
if current?.isEqualConfig(target)
|
||||
# We're only stopping/starting it
|
||||
return @_updateContainerStep(current, target)
|
||||
else if !current?
|
||||
# Either this is a new service, or the current one has already been killed
|
||||
return @_fetchOrStartStep(current, target, needsDownload, dependenciesMetForStart)
|
||||
else
|
||||
strategy = checkString(target.labels['io.resin.update.strategy'])
|
||||
strategy = checkString(target.config.labels['io.resin.update.strategy'])
|
||||
validStrategies = [ 'download-then-kill', 'kill-then-download', 'delete-then-download', 'hand-over' ]
|
||||
if !_.includes(validStrategies, strategy)
|
||||
strategy = 'download-then-kill'
|
||||
timeout = checkInt(target.labels['io.resin.update.handover-timeout'])
|
||||
timeout = checkInt(target.config.labels['io.resin.update.handover-timeout'])
|
||||
return @_strategySteps[strategy](current, target, needsDownload, dependenciesMetForStart, dependenciesMetForKill, needsSpecialKill, timeout)
|
||||
|
||||
_nextStepsForAppUpdate: (currentApp, targetApp, availableImages = [], downloading = []) =>
|
||||
@ -590,12 +595,12 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
currentApp ?= emptyApp
|
||||
if currentApp.services?.length == 1 and targetApp.services?.length == 1 and
|
||||
targetApp.services[0].serviceName == currentApp.services[0].serviceName and
|
||||
checkTruthy(currentApp.services[0].labels['io.resin.legacy-container'])
|
||||
checkTruthy(currentApp.services[0].config.labels['io.resin.legacy-container'])
|
||||
# This is a legacy preloaded app or container, so we didn't have things like serviceId.
|
||||
# We hack a few things to avoid an unnecessary restart of the preloaded app
|
||||
# (but ensuring it gets updated if it actually changed)
|
||||
targetApp.services[0].labels['io.resin.legacy-container'] = currentApp.services[0].labels['io.resin.legacy-container']
|
||||
targetApp.services[0].labels['io.resin.service-id'] = currentApp.services[0].labels['io.resin.service-id']
|
||||
targetApp.services[0].config.labels['io.resin.legacy-container'] = currentApp.services[0].labels['io.resin.legacy-container']
|
||||
targetApp.services[0].config.labels['io.resin.service-id'] = currentApp.services[0].labels['io.resin.service-id']
|
||||
targetApp.services[0].serviceId = currentApp.services[0].serviceId
|
||||
|
||||
appId = targetApp.appId ? currentApp.appId
|
||||
@ -609,6 +614,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
steps.push(serviceAction('kill', pair.current.serviceId, pair.current, null))
|
||||
else
|
||||
steps.push({ action: 'noop' })
|
||||
|
||||
# next step for install pairs in download - start order, but start requires dependencies, networks and volumes met
|
||||
# next step for update pairs in order by update strategy. start requires dependencies, networks and volumes met.
|
||||
for pair in installPairs.concat(updatePairs)
|
||||
@ -668,7 +674,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
service.imageName = service.image
|
||||
if imageInfo?.Id?
|
||||
service.image = imageInfo.Id
|
||||
return new Service(service, serviceOpts)
|
||||
return Service.fromComposeObject(service, serviceOpts)
|
||||
|
||||
normaliseAndExtendAppFromDB: (app) =>
|
||||
Promise.join(
|
||||
@ -787,15 +793,19 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
allImagesForTargetApp = (app) -> _.map(app.services, imageForService)
|
||||
allImagesForCurrentApp = (app) ->
|
||||
_.map app.services, (service) ->
|
||||
img = _.find(available, { dockerImageId: service.image, imageId: service.imageId }) ? _.find(available, { dockerImageId: service.image })
|
||||
img = _.find(available, { dockerImageId: service.config.image, imageId: service.imageId }) ? _.find(available, { dockerImageId: service.config.image })
|
||||
return _.omit(img, [ 'dockerImageId', 'id' ])
|
||||
|
||||
availableWithoutIds = _.map(available, (image) -> _.omit(image, [ 'dockerImageId', 'id' ]))
|
||||
currentImages = _.flatMap(current.local.apps, allImagesForCurrentApp)
|
||||
targetImages = _.flatMap(target.local.apps, allImagesForTargetApp)
|
||||
|
||||
availableAndUnused = _.filter availableWithoutIds, (image) ->
|
||||
!_.some currentImages.concat(targetImages), (imageInUse) -> _.isEqual(image, imageInUse)
|
||||
|
||||
imagesToDownload = _.filter targetImages, (targetImage) =>
|
||||
!_.some available, (availableImage) => @images.isSameImage(availableImage, targetImage)
|
||||
|
||||
# Images that are available but we don't have them in the DB with the exact metadata:
|
||||
imagesToSave = _.filter targetImages, (targetImage) =>
|
||||
_.some(available, (availableImage) => @images.isSameImage(availableImage, targetImage)) and
|
||||
@ -804,6 +814,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
deltaSources = _.map imagesToDownload, (image) =>
|
||||
return @bestDeltaSource(image, available)
|
||||
proxyvisorImages = @proxyvisor.imagesInUse(current, target)
|
||||
|
||||
imagesToRemove = _.filter availableAndUnused, (image) =>
|
||||
notUsedForDelta = !_.includes(deltaSources, image.name)
|
||||
notUsedByProxyvisor = !_.some proxyvisorImages, (proxyvisorImage) => @images.isSameImage(image, { name: proxyvisorImage })
|
||||
|
@ -61,6 +61,13 @@ export class PortMap {
|
||||
};
|
||||
}
|
||||
|
||||
public toExposedPortArray(): string[] {
|
||||
const internalRange = this.generatePortRange(this.ports.internalStart, this.ports.internalEnd);
|
||||
return _.map(internalRange, (internal) => {
|
||||
return `${internal}/${this.ports.protocol}`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* fromDockerOpts
|
||||
*
|
||||
|
77
src/compose/sanitise.ts
Normal file
77
src/compose/sanitise.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { ConfigMap, ServiceComposeConfig } from './types/service';
|
||||
|
||||
// TODO: Generate these fields from the interface we define
|
||||
// in service-types.
|
||||
const supportedComposeFields = [
|
||||
'capAdd',
|
||||
'capDrop',
|
||||
'command',
|
||||
'cgroupParent',
|
||||
'devices',
|
||||
'dns',
|
||||
'dnsOpt',
|
||||
'dnsSearch',
|
||||
'tmpfs',
|
||||
'entrypoint',
|
||||
'environment',
|
||||
'expose',
|
||||
'extraHosts',
|
||||
'groupAdd',
|
||||
'healthcheck',
|
||||
'image',
|
||||
'init',
|
||||
'labels',
|
||||
'running',
|
||||
'networkMode',
|
||||
'networks',
|
||||
'pid',
|
||||
'pidsLimit',
|
||||
'ports',
|
||||
'securityOpt',
|
||||
'stopGracePeriod',
|
||||
'stopSignal',
|
||||
'storageOpt',
|
||||
'sysctls',
|
||||
'ulimits',
|
||||
'usernsMode',
|
||||
'volumes',
|
||||
'restart',
|
||||
'cpuShares',
|
||||
'cpuQuota',
|
||||
'cpus',
|
||||
'cpuset',
|
||||
'domainname',
|
||||
'hostname',
|
||||
'ipc',
|
||||
'macAddress',
|
||||
'memLimit',
|
||||
'memReservation',
|
||||
'oomKillDisable',
|
||||
'oomScoreAdj',
|
||||
'privileged',
|
||||
'readOnly',
|
||||
'shmSize',
|
||||
'user',
|
||||
'workingDir',
|
||||
];
|
||||
|
||||
export function sanitiseComposeConfig(
|
||||
composeConfig: ConfigMap,
|
||||
): ServiceComposeConfig {
|
||||
const filtered: string[] = [];
|
||||
const toReturn = _.pickBy(composeConfig, (_v, k) => {
|
||||
const included = _.includes(supportedComposeFields, k);
|
||||
if (!included) {
|
||||
filtered.push(k);
|
||||
}
|
||||
return included;
|
||||
}) as ServiceComposeConfig;
|
||||
|
||||
if (filtered.length > 0) {
|
||||
console.log(`Warning: Ignoring unsupported or unknown compose fields: ${filtered.join(', ')}`);
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
@ -8,7 +8,7 @@ logTypes = require '../lib/log-types'
|
||||
{ checkInt, isValidDeviceName } = require '../lib/validation'
|
||||
constants = require '../lib/constants'
|
||||
|
||||
Service = require './service'
|
||||
{ Service } = require './service'
|
||||
|
||||
{ NotFoundError } = require '../lib/errors'
|
||||
|
||||
@ -100,6 +100,8 @@ module.exports = class ServiceManager extends EventEmitter
|
||||
.then (existingService) =>
|
||||
return @docker.getContainer(existingService.containerId)
|
||||
.catch NotFoundError, =>
|
||||
conf = service.toDockerContainer()
|
||||
nets = service.extraNetworksToJoin()
|
||||
|
||||
@config.get('name')
|
||||
.then (deviceName) =>
|
||||
@ -109,9 +111,9 @@ module.exports = class ServiceManager extends EventEmitter
|
||||
'Please fix the device name.'
|
||||
)
|
||||
|
||||
service.environment['RESIN_DEVICE_NAME_AT_INIT'] = deviceName
|
||||
# TODO: Don't mutate service like this, use an interface
|
||||
service.config.environment['RESIN_DEVICE_NAME_AT_INIT'] = deviceName
|
||||
|
||||
conf = service.toContainerConfig()
|
||||
nets = service.extraNetworksToJoin()
|
||||
|
||||
@logger.logSystemEvent(logTypes.installService, { service })
|
||||
@ -121,7 +123,7 @@ module.exports = class ServiceManager extends EventEmitter
|
||||
.tap (container) =>
|
||||
service.containerId = container.id
|
||||
|
||||
Promise.map nets, ({ name, endpointConfig }) =>
|
||||
Promise.all _.map nets, (endpointConfig, name) =>
|
||||
@docker
|
||||
.getNetwork(name)
|
||||
.connect({ Container: container.id, EndpointConfig: endpointConfig })
|
||||
@ -180,7 +182,7 @@ module.exports = class ServiceManager extends EventEmitter
|
||||
@docker.listContainers({ all: true, filters })
|
||||
.mapSeries (container) =>
|
||||
@docker.getContainer(container.Id).inspect()
|
||||
.then(Service.fromContainer)
|
||||
.then(Service.fromDockerContainer)
|
||||
.then (service) =>
|
||||
if @volatileState[service.containerId]?.status?
|
||||
service.status = @volatileState[service.containerId].status
|
||||
@ -191,7 +193,7 @@ module.exports = class ServiceManager extends EventEmitter
|
||||
# Returns the first container matching a service definition
|
||||
get: (service) =>
|
||||
@getAll("io.resin.service-id=#{service.serviceId}")
|
||||
.filter((currentService) -> currentService.isSameContainer(service))
|
||||
.filter((currentService) -> currentService.isEqualConfig(service))
|
||||
.then (services) ->
|
||||
if services.length == 0
|
||||
e = new Error('Could not find a container matching this service definition')
|
||||
@ -220,7 +222,7 @@ module.exports = class ServiceManager extends EventEmitter
|
||||
.then (container) ->
|
||||
if !container.Config.Labels['io.resin.supervised']?
|
||||
return null
|
||||
return Service.fromContainer(container)
|
||||
return Service.fromDockerContainer(container)
|
||||
|
||||
waitToKill: (service, timeout) ->
|
||||
pollInterval = 100
|
||||
@ -260,7 +262,7 @@ module.exports = class ServiceManager extends EventEmitter
|
||||
.then =>
|
||||
@start(targetService)
|
||||
.then =>
|
||||
@waitToKill(currentService, targetService.labels['io.resin.update.handover-timeout'])
|
||||
@waitToKill(currentService, targetService.config.labels['io.resin.update.handover-timeout'])
|
||||
.then =>
|
||||
@kill(currentService)
|
||||
|
||||
|
@ -1,735 +0,0 @@
|
||||
_ = require 'lodash'
|
||||
path = require 'path'
|
||||
os = require 'os'
|
||||
{ checkTruthy, checkInt } = require '../lib/validation'
|
||||
updateLock = require '../lib/update-lock'
|
||||
constants = require '../lib/constants'
|
||||
conversions = require '../lib/conversions'
|
||||
parseCommand = require('shell-quote').parse
|
||||
Duration = require 'duration-js'
|
||||
{ PortMap } = require './ports'
|
||||
|
||||
validRestartPolicies = [ 'no', 'always', 'on-failure', 'unless-stopped' ]
|
||||
|
||||
parseMemoryNumber = (numAsString, defaultVal) ->
|
||||
m = numAsString?.toString().match(/^([0-9]+)([bkmg]?)b?$/i)
|
||||
if !m? and defaultVal?
|
||||
return parseMemoryNumber(defaultVal)
|
||||
num = m[1]
|
||||
pow = { '': 0, 'b': 0, 'B': 0, 'K': 1, 'k': 1, 'm': 2, 'M': 2, 'g': 3, 'G': 3 }
|
||||
return parseInt(num) * 1024 ** pow[m[2]]
|
||||
|
||||
# Construct a restart policy based on its name.
|
||||
# The default policy (if name is not a valid policy) is "always".
|
||||
createRestartPolicy = (name) ->
|
||||
if name not in validRestartPolicies
|
||||
name = 'always'
|
||||
return { Name: name, MaximumRetryCount: 0 }
|
||||
|
||||
processCommandStr = (s) ->
|
||||
# Escape dollars
|
||||
s.replace(/(\$)/g, '\\$1')
|
||||
|
||||
processCommandParsedArrayElement = (arg) ->
|
||||
if _.isObject(arg)
|
||||
if arg.op == 'glob'
|
||||
return arg.pattern
|
||||
return arg.op
|
||||
return arg
|
||||
|
||||
ensureCommandIsArray = (s) ->
|
||||
if _.isString(s)
|
||||
s = _.map(parseCommand(processCommandStr(s)), processCommandParsedArrayElement)
|
||||
return s
|
||||
|
||||
getCommand = (service, imageInfo) ->
|
||||
cmd = service.command ? imageInfo?.Config?.Cmd ? null
|
||||
return ensureCommandIsArray(cmd)
|
||||
|
||||
getEntrypoint = (service, imageInfo) ->
|
||||
entry = service.entrypoint ? imageInfo?.Config?.Entrypoint ? null
|
||||
return ensureCommandIsArray(entry)
|
||||
|
||||
getStopSignal = (service, imageInfo) ->
|
||||
sig = service.stopSignal ? imageInfo?.Config?.StopSignal ? null
|
||||
if sig? and !_.isString(sig) # In case the YAML was parsed as a number
|
||||
sig = sig.toString()
|
||||
return sig
|
||||
|
||||
getUser = (service, imageInfo) ->
|
||||
return service.user ? imageInfo?.Config?.User ? ''
|
||||
|
||||
getWorkingDir = (service, imageInfo) ->
|
||||
return (service.workingDir ? imageInfo?.Config?.WorkingDir ? '').replace(/(^.+)\/$/, '$1')
|
||||
|
||||
buildHealthcheckTest = (test) ->
|
||||
if _.isString(test)
|
||||
return [ 'CMD-SHELL', test ]
|
||||
else
|
||||
return test
|
||||
|
||||
getNanoseconds = (duration) ->
|
||||
d = new Duration(duration)
|
||||
return d.nanoseconds()
|
||||
|
||||
# Mutates imageHealthcheck
|
||||
overrideHealthcheckFromCompose = (serviceHealthcheck, imageHealthcheck = {}) ->
|
||||
if serviceHealthcheck.disable
|
||||
imageHealthcheck.Test = [ 'NONE' ]
|
||||
else
|
||||
imageHealthcheck.Test = buildHealthcheckTest(serviceHealthcheck.test)
|
||||
if serviceHealthcheck.interval?
|
||||
imageHealthcheck.Interval = getNanoseconds(serviceHealthcheck.interval)
|
||||
if serviceHealthcheck.timeout?
|
||||
imageHealthcheck.Timeout = getNanoseconds(serviceHealthcheck.timeout)
|
||||
if serviceHealthcheck.start_period?
|
||||
imageHealthcheck.StartPeriod = getNanoseconds(serviceHealthcheck.start_period)
|
||||
if serviceHealthcheck.retries?
|
||||
imageHealthcheck.Retries = parseInt(serviceHealthcheck.retries)
|
||||
return imageHealthcheck
|
||||
|
||||
getHealthcheck = (service, imageInfo) ->
|
||||
healthcheck = imageInfo?.Config?.Healthcheck ? null
|
||||
if service.healthcheck?
|
||||
healthcheck = overrideHealthcheckFromCompose(service.healthcheck, healthcheck)
|
||||
# Set invalid healthchecks back to null
|
||||
if healthcheck? and (!healthcheck.Test? or _.isEqual(healthcheck.Test, []))
|
||||
healthcheck = null
|
||||
return healthcheck
|
||||
|
||||
killmePath = (appId, serviceName) ->
|
||||
return updateLock.lockPath(appId, serviceName)
|
||||
|
||||
defaultBinds = (appId, serviceName) ->
|
||||
return [
|
||||
"#{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 }
|
||||
|
||||
# TODO: Support configuration for "networks"
|
||||
module.exports = class Service
|
||||
constructor: (props, opts = {}) ->
|
||||
serviceProperties = _.mapKeys(props, (v, k) -> _.camelCase(k))
|
||||
{
|
||||
@image
|
||||
@imageName
|
||||
@expose
|
||||
@ports
|
||||
@networkMode
|
||||
@privileged
|
||||
@releaseId
|
||||
@imageId
|
||||
@serviceId
|
||||
@appId
|
||||
@serviceName
|
||||
@containerId
|
||||
@running
|
||||
@createdAt
|
||||
@environment
|
||||
@command
|
||||
@entrypoint
|
||||
@labels
|
||||
@volumes
|
||||
@restartPolicy
|
||||
@dependsOn
|
||||
@capAdd
|
||||
@capDrop
|
||||
@status
|
||||
@devices
|
||||
@portMappings
|
||||
@networks
|
||||
@memLimit
|
||||
@memReservation
|
||||
@shmSize
|
||||
@cpuShares
|
||||
@cpuQuota
|
||||
@cpus
|
||||
@cpuset
|
||||
@nanoCpus
|
||||
@domainname
|
||||
@oomKillDisable
|
||||
@oomScoreAdj
|
||||
@dns
|
||||
@dnsSearch
|
||||
@dnsOpt
|
||||
@tmpfs
|
||||
@extraHosts
|
||||
@ulimits
|
||||
@ulimitsArray
|
||||
@stopSignal
|
||||
@stopGracePeriod
|
||||
@init
|
||||
@healthcheck
|
||||
@readOnly
|
||||
@sysctls
|
||||
@hostname
|
||||
@cgroupParent
|
||||
@groupAdd
|
||||
@pid
|
||||
@pidsLimit
|
||||
@securityOpt
|
||||
@storageOpt
|
||||
@usernsMode
|
||||
@ipc
|
||||
@macAddress
|
||||
@user
|
||||
@workingDir
|
||||
} = serviceProperties
|
||||
|
||||
@networks ?= {}
|
||||
@privileged ?= false
|
||||
@volumes ?= []
|
||||
@labels ?= {}
|
||||
@environment ?= {}
|
||||
@running ?= true
|
||||
@ports ?= []
|
||||
@expose ?= []
|
||||
@capAdd ?= []
|
||||
@capDrop ?= []
|
||||
@devices ?= []
|
||||
|
||||
@memLimit = parseMemoryNumber(@memLimit, '0')
|
||||
@memReservation = parseMemoryNumber(@memReservation, '0')
|
||||
@shmSize = parseMemoryNumber(@shmSize, '64m')
|
||||
@cpuShares ?= 0
|
||||
@cpuQuota ?= 0
|
||||
@cpus ?= 0
|
||||
@nanoCpus ?= 0
|
||||
@cpuset ?= ''
|
||||
@domainname ?= ''
|
||||
|
||||
@oomScoreAdj ?= 0
|
||||
@oomKillDisable ?= false
|
||||
@tmpfs ?= []
|
||||
@extraHosts ?= []
|
||||
|
||||
@dns ?= []
|
||||
@dnsSearch ?= []
|
||||
@dnsOpt ?= []
|
||||
@ulimitsArray ?= []
|
||||
@groupAdd ?= []
|
||||
|
||||
@stopSignal ?= null
|
||||
@stopGracePeriod ?= null
|
||||
@healthcheck ?= null
|
||||
@init ?= null
|
||||
@readOnly ?= false
|
||||
@macAddress ?= null
|
||||
|
||||
@sysctls ?= {}
|
||||
|
||||
@hostname ?= ''
|
||||
@cgroupParent ?= ''
|
||||
@pid ?= ''
|
||||
@pidsLimit ?= 0
|
||||
@securityOpt ?= []
|
||||
@storageOpt ?= {}
|
||||
@usernsMode ?= ''
|
||||
@user ?= ''
|
||||
@workingDir ?= ''
|
||||
|
||||
if _.isEmpty(@ipc)
|
||||
@ipc = 'shareable'
|
||||
|
||||
@portMappings ?= @getPortsAndPortBindings()
|
||||
|
||||
# If the service has no containerId, it is a target service and has to be normalised and extended
|
||||
if !@containerId?
|
||||
if !@networkMode?
|
||||
if !_.isEmpty(@networks)
|
||||
@networkMode = _.keys(@networks)[0]
|
||||
else
|
||||
@networkMode = 'default'
|
||||
if @networkMode not in [ 'host', 'bridge', 'none' ]
|
||||
@networkMode = "#{@appId}_#{@networkMode}"
|
||||
|
||||
@networks = _.mapKeys @networks, (v, k) =>
|
||||
if k not in [ 'host', 'bridge', 'none' ]
|
||||
return "#{@appId}_#{k}"
|
||||
return k
|
||||
|
||||
if @networkMode == 'host' and @hostname == ''
|
||||
@hostname = opts.hostnameOnHost
|
||||
|
||||
@networks[@networkMode] ?= {}
|
||||
|
||||
@restartPolicy = createRestartPolicy(serviceProperties.restart)
|
||||
@command = getCommand(serviceProperties, opts.imageInfo)
|
||||
@entrypoint = getEntrypoint(serviceProperties, opts.imageInfo)
|
||||
@stopSignal = getStopSignal(serviceProperties, opts.imageInfo)
|
||||
@healthcheck = getHealthcheck(serviceProperties, opts.imageInfo)
|
||||
@workingDir = getWorkingDir(serviceProperties, opts.imageInfo)
|
||||
@user = getUser(serviceProperties, opts.imageInfo)
|
||||
@extendEnvVars(opts)
|
||||
@extendLabels(opts.imageInfo)
|
||||
@extendAndSanitiseVolumes(opts.imageInfo)
|
||||
@extendAndSanitiseExposedPorts(opts.imageInfo)
|
||||
@devices = formatDevices(@devices)
|
||||
@addFeaturesFromLabels(opts)
|
||||
if @dns?
|
||||
@dns = _.castArray(@dns)
|
||||
if @dnsSearch?
|
||||
@dnsSearch = _.castArray(@dnsSearch)
|
||||
if @tmpfs?
|
||||
@tmpfs = _.castArray(@tmpfs)
|
||||
|
||||
@nanoCpus = Math.round(Number(@cpus) * 10 ** 9)
|
||||
|
||||
@ulimitsArray = _.map @ulimits, (value, name) ->
|
||||
if _.isNumber(value) or _.isString(value)
|
||||
return { Name: name, Soft: checkInt(value), Hard: checkInt(value) }
|
||||
else
|
||||
return { Name: name, Soft: checkInt(value.soft), Hard: checkInt(value.hard) }
|
||||
if @init
|
||||
@init = true
|
||||
|
||||
if @stopGracePeriod?
|
||||
d = new Duration(@stopGracePeriod)
|
||||
@stopGracePeriod = d.seconds()
|
||||
|
||||
@oomKillDisable = Boolean(@oomKillDisable)
|
||||
@readOnly = Boolean(@readOnly)
|
||||
|
||||
if Array.isArray(@sysctls)
|
||||
@sysctls = _.fromPairs(_.map(@sysctls, (v) -> _.split(v, '=')))
|
||||
@sysctls = _.mapValues(@sysctls, String)
|
||||
|
||||
# Avoid problems with yaml parsing numbers as strings
|
||||
for key in [ 'cpuShares', 'cpuQuota', 'oomScoreAdj' ]
|
||||
this[key] = checkInt(this[key])
|
||||
|
||||
_addSupervisorApi: (opts) =>
|
||||
@environment['RESIN_SUPERVISOR_PORT'] = opts.listenPort.toString()
|
||||
@environment['RESIN_SUPERVISOR_API_KEY'] = opts.apiSecret
|
||||
if @networkMode == 'host'
|
||||
@environment['RESIN_SUPERVISOR_HOST'] = '127.0.0.1'
|
||||
@environment['RESIN_SUPERVISOR_ADDRESS'] = "http://127.0.0.1:#{opts.listenPort}"
|
||||
else
|
||||
@environment['RESIN_SUPERVISOR_HOST'] = opts.supervisorApiHost
|
||||
@environment['RESIN_SUPERVISOR_ADDRESS'] = "http://#{opts.supervisorApiHost}:#{opts.listenPort}"
|
||||
@networks[constants.supervisorNetworkInterface] = {}
|
||||
|
||||
addFeaturesFromLabels: (opts) =>
|
||||
if checkTruthy(@labels['io.resin.features.dbus'])
|
||||
@volumes.push('/run/dbus:/host/run/dbus')
|
||||
if checkTruthy(@labels['io.resin.features.kernel-modules']) and opts.hostPathExists.modules
|
||||
@volumes.push('/lib/modules:/lib/modules')
|
||||
if checkTruthy(@labels['io.resin.features.firmware']) and opts.hostPathExists.firmware
|
||||
@volumes.push('/lib/firmware:/lib/firmware')
|
||||
if checkTruthy(@labels['io.resin.features.balena-socket'])
|
||||
@volumes.push('/var/run/balena.sock:/var/run/balena.sock')
|
||||
@environment['DOCKER_HOST'] ?= 'unix:///var/run/balena.sock'
|
||||
if checkTruthy(@labels['io.resin.features.supervisor-api'])
|
||||
@_addSupervisorApi(opts)
|
||||
else
|
||||
# We ensure the user hasn't added "supervisor0" to the service's networks
|
||||
delete @networks[constants.supervisorNetworkInterface]
|
||||
if checkTruthy(@labels['io.resin.features.resin-api'])
|
||||
@environment['RESIN_API_KEY'] = opts.deviceApiKey
|
||||
|
||||
extendEnvVars: ({ imageInfo, uuid, appName, name, version, deviceType, osVersion }) =>
|
||||
newEnv =
|
||||
RESIN_APP_ID: @appId.toString()
|
||||
RESIN_APP_NAME: appName
|
||||
RESIN_SERVICE_NAME: @serviceName
|
||||
RESIN_DEVICE_UUID: uuid
|
||||
RESIN_DEVICE_TYPE: deviceType
|
||||
RESIN_HOST_OS_VERSION: osVersion
|
||||
RESIN_SUPERVISOR_VERSION: version
|
||||
RESIN_APP_LOCK_PATH: '/tmp/resin/resin-updates.lock'
|
||||
RESIN_SERVICE_KILL_ME_PATH: '/tmp/resin/resin-kill-me'
|
||||
RESIN: '1'
|
||||
USER: 'root'
|
||||
if @environment?
|
||||
_.defaults(newEnv, @environment)
|
||||
_.defaults(newEnv, conversions.envArrayToObject(imageInfo?.Config?.Env ? []))
|
||||
@environment = newEnv
|
||||
return @environment
|
||||
|
||||
extendLabels: (imageInfo) =>
|
||||
@labels = _.clone(@labels)
|
||||
_.defaults(@labels, imageInfo?.Config?.Labels ? {})
|
||||
@labels['io.resin.supervised'] = 'true'
|
||||
@labels['io.resin.app-id'] = @appId.toString()
|
||||
@labels['io.resin.service-id'] = @serviceId.toString()
|
||||
@labels['io.resin.service-name'] = @serviceName
|
||||
return @labels
|
||||
|
||||
extendAndSanitiseExposedPorts: (imageInfo) =>
|
||||
@expose = _.map @expose, (p) ->
|
||||
p = new String(p)
|
||||
if /^[0-9]*$/.test(p)
|
||||
p += '/tcp'
|
||||
return p
|
||||
if imageInfo?.Config?.ExposedPorts?
|
||||
for own port, _v of imageInfo.Config.ExposedPorts
|
||||
if !_.find(@expose, port)
|
||||
@expose.push(port)
|
||||
return @expose
|
||||
|
||||
extendAndSanitiseVolumes: (imageInfo) =>
|
||||
volumes = []
|
||||
for vol in @volumes
|
||||
isBind = _.includes(vol, ':')
|
||||
if isBind
|
||||
[ bindSource, bindDest, mode ] = vol.split(':')
|
||||
if !path.isAbsolute(bindSource)
|
||||
# Rewrite named volumes to namespace by appId
|
||||
volDefinition = "#{@appId}_#{bindSource}:#{bindDest}"
|
||||
if mode?
|
||||
volDefinition += ":#{mode}"
|
||||
volumes.push(volDefinition)
|
||||
else
|
||||
console.log("Ignoring invalid bind mount #{vol}")
|
||||
else
|
||||
volumes.push(vol)
|
||||
volumes = volumes.concat(@defaultBinds())
|
||||
volumes = _.union(_.keys(imageInfo?.Config?.Volumes), volumes)
|
||||
@volumes = volumes
|
||||
return @volumes
|
||||
|
||||
getNamedVolumes: =>
|
||||
defaults = @defaultBinds()
|
||||
validVolumes = _.map @volumes, (vol) ->
|
||||
if _.includes(defaults, vol) or !_.includes(vol, ':')
|
||||
return null
|
||||
bindSource = vol.split(':')[0]
|
||||
if !path.isAbsolute(bindSource)
|
||||
m = bindSource.match(/[0-9]+_(.+)/)
|
||||
return m[1]
|
||||
else
|
||||
return null
|
||||
return _.reject(validVolumes, _.isNil)
|
||||
|
||||
lockPath: =>
|
||||
return updateLock.lockPath(@appId)
|
||||
|
||||
killmePath: =>
|
||||
return killmePath(@appId, @serviceName)
|
||||
|
||||
killmeFullPathOnHost: =>
|
||||
return "#{constants.rootMountPoint}#{@killmePath()}/resin-kill-me"
|
||||
|
||||
defaultBinds: =>
|
||||
return defaultBinds(@appId, @serviceName)
|
||||
|
||||
@fromContainer: (container, containerToService) ->
|
||||
if container.State.Running
|
||||
status = 'Running'
|
||||
else if container.State.Status == 'created'
|
||||
status = 'Installed'
|
||||
else if container.State.Status == 'dead'
|
||||
status = 'Dead'
|
||||
else
|
||||
status = 'Stopped'
|
||||
|
||||
boundContainerPorts = []
|
||||
ports = []
|
||||
expose = []
|
||||
for own port, conf of container.HostConfig.PortBindings
|
||||
containerPort = port.match(/^([0-9]*)\/tcp$/)?[1]
|
||||
if containerPort?
|
||||
boundContainerPorts.push(containerPort)
|
||||
hostPort = conf[0]?.HostPort
|
||||
if !_.isEmpty(hostPort)
|
||||
ports.push("#{hostPort}:#{containerPort}")
|
||||
else
|
||||
ports.push(containerPort)
|
||||
for own port, conf of container.Config.ExposedPorts
|
||||
containerPort = port.match(/^([0-9]*)\/tcp$/)?[1]
|
||||
if containerPort? and !_.includes(boundContainerPorts, containerPort)
|
||||
expose.push(containerPort)
|
||||
|
||||
portMappings = PortMap.fromDockerOpts(container.HostConfig.PortBindings)
|
||||
|
||||
appId = checkInt(container.Config.Labels['io.resin.app-id'])
|
||||
serviceId = checkInt(container.Config.Labels['io.resin.service-id'])
|
||||
serviceName = container.Config.Labels['io.resin.service-name']
|
||||
nameComponents = container.Name.match(/.*_(\d+)_(\d+)$/)
|
||||
imageId = checkInt(nameComponents?[1])
|
||||
releaseId = checkInt(nameComponents?[2])
|
||||
|
||||
networkMode = container.HostConfig.NetworkMode
|
||||
if _.startsWith(networkMode, 'container:')
|
||||
networkMode = 'service:' + containerToService[_.replace(networkMode, 'container:', '')]
|
||||
|
||||
hostname = container.Config.Hostname
|
||||
# A hostname equal to the first part of the container ID actually
|
||||
# means no hostname was specified
|
||||
if hostname.length is 12 and container.Id.startsWith(hostname)
|
||||
hostname = ''
|
||||
|
||||
service = {
|
||||
appId: appId
|
||||
serviceId: serviceId
|
||||
serviceName: serviceName
|
||||
imageId: imageId
|
||||
command: container.Config.Cmd
|
||||
entrypoint: container.Config.Entrypoint
|
||||
networkMode: networkMode
|
||||
volumes: _.concat(container.HostConfig.Binds ? [], _.keys(container.Config.Volumes ? {}))
|
||||
image: container.Config.Image
|
||||
environment: conversions.envArrayToObject(container.Config.Env)
|
||||
privileged: container.HostConfig.Privileged
|
||||
releaseId: releaseId
|
||||
labels: container.Config.Labels
|
||||
running: container.State.Running
|
||||
createdAt: new Date(container.Created)
|
||||
restartPolicy: container.HostConfig.RestartPolicy
|
||||
portMappings: portMappings
|
||||
containerId: container.Id
|
||||
capAdd: container.HostConfig.CapAdd
|
||||
capDrop: container.HostConfig.CapDrop
|
||||
devices: container.HostConfig.Devices
|
||||
status
|
||||
exposedPorts: container.Config.ExposedPorts
|
||||
portBindings: container.HostConfig.PortBindings
|
||||
networks: container.NetworkSettings.Networks
|
||||
memLimit: container.HostConfig.Memory
|
||||
memReservation: container.HostConfig.MemoryReservation
|
||||
shmSize: container.HostConfig.ShmSize
|
||||
cpuShares: container.HostConfig.CpuShares
|
||||
cpuQuota: container.HostConfig.CpuQuota
|
||||
nanoCpus: container.HostConfig.NanoCpus
|
||||
cpuset: container.HostConfig.CpusetCpus
|
||||
domainname: container.Config.Domainname
|
||||
oomKillDisable: container.HostConfig.OomKillDisable
|
||||
oomScoreAdj: container.HostConfig.OomScoreAdj
|
||||
dns: container.HostConfig.Dns
|
||||
dnsSearch: container.HostConfig.DnsSearch
|
||||
dnsOpt: container.HostConfig.DnsOpt
|
||||
tmpfs: _.keys(container.HostConfig.Tmpfs ? {})
|
||||
extraHosts: container.HostConfig.ExtraHosts
|
||||
ulimitsArray: container.HostConfig.Ulimits
|
||||
stopSignal: container.Config.StopSignal
|
||||
stopGracePeriod: container.Config.StopTimeout
|
||||
healthcheck: container.Config.Healthcheck
|
||||
init: container.HostConfig.Init
|
||||
readOnly: container.HostConfig.ReadonlyRootfs
|
||||
sysctls: container.HostConfig.Sysctls
|
||||
hostname: hostname
|
||||
cgroupParent: container.HostConfig.CgroupParent
|
||||
groupAdd: container.HostConfig.GroupAdd
|
||||
pid: container.HostConfig.PidMode
|
||||
pidsLimit: container.HostConfig.PidsLimit
|
||||
securityOpt: container.HostConfig.SecurityOpt
|
||||
storageOpt: container.HostConfig.StorageOpt
|
||||
usernsMode: container.HostConfig.UsernsMode
|
||||
ipc: container.HostConfig.IpcMode
|
||||
macAddress: container.Config.MacAddress
|
||||
user: container.Config.User
|
||||
workingDir: container.Config.WorkingDir
|
||||
}
|
||||
# I've seen docker use either 'no' or '' for no restart policy, so we normalise to 'no'.
|
||||
if service.restartPolicy.Name == ''
|
||||
service.restartPolicy.Name = 'no'
|
||||
return new Service(service)
|
||||
|
||||
# TODO: map ports for any of the possible formats "container:host/protocol", port ranges, etc.
|
||||
getPortsAndPortBindings: =>
|
||||
portMaps = _.map @ports, (p) -> new PortMap(p)
|
||||
return PortMap.normalisePortMaps(portMaps)
|
||||
|
||||
generatePortBindings: =>
|
||||
portBindings = {}
|
||||
exposedPorts = {}
|
||||
for portMap in @portMappings
|
||||
ports = portMap.toDockerOpts()
|
||||
_.merge(portBindings, ports.portBindings)
|
||||
_.merge(exposedPorts, ports.exposedPorts)
|
||||
|
||||
# Any additonal exposed ports
|
||||
if @expose?
|
||||
for port in @expose
|
||||
exposedPorts[port] = {}
|
||||
|
||||
return {
|
||||
portBindings,
|
||||
exposedPorts
|
||||
}
|
||||
|
||||
getBindsAndVolumes: =>
|
||||
binds = []
|
||||
volumes = {}
|
||||
for vol in @volumes
|
||||
isBind = _.includes(vol, ':')
|
||||
if isBind
|
||||
binds.push(vol)
|
||||
else
|
||||
volumes[vol] = {}
|
||||
return { binds, volumes }
|
||||
|
||||
toContainerConfig: ->
|
||||
{ binds, volumes } = @getBindsAndVolumes()
|
||||
tmpfs = {}
|
||||
for dir in @tmpfs
|
||||
tmpfs[dir] = ''
|
||||
networkMode = @networkMode
|
||||
if _.startsWith(networkMode, 'service:')
|
||||
networkMode = "container:#{_.replace(networkMode, 'service:', '')}_#{@imageId}_#{@releaseId}"
|
||||
|
||||
# Generate port options
|
||||
{ portBindings, exposedPorts } = @generatePortBindings()
|
||||
|
||||
conf = {
|
||||
name: "#{@serviceName}_#{@imageId}_#{@releaseId}"
|
||||
Image: @image
|
||||
Cmd: @command
|
||||
Entrypoint: @entrypoint
|
||||
Tty: true
|
||||
Volumes: volumes
|
||||
Env: _.map @environment, (v, k) -> k + '=' + v
|
||||
ExposedPorts: exposedPorts
|
||||
Labels: @labels
|
||||
Domainname: @domainname
|
||||
User: @user
|
||||
WorkingDir: @workingDir
|
||||
HostConfig:
|
||||
Memory: @memLimit
|
||||
MemoryReservation: @memReservation
|
||||
ShmSize: @shmSize
|
||||
Privileged: @privileged
|
||||
NetworkMode: networkMode
|
||||
PortBindings: portBindings
|
||||
Binds: binds
|
||||
CapAdd: @capAdd
|
||||
CapDrop: @capDrop
|
||||
Devices: @devices
|
||||
CpuShares: @cpuShares
|
||||
NanoCpus: @nanoCpus
|
||||
CpuQuota: @cpuQuota
|
||||
CpusetCpus: @cpuset
|
||||
OomScoreAdj: @oomScoreAdj
|
||||
OomKillDisable: @oomKillDisable
|
||||
Tmpfs: tmpfs
|
||||
Dns: @dns
|
||||
DnsSearch: @dnsSearch
|
||||
DnsOpt: @dnsOpt
|
||||
Ulimits: @ulimitsArray
|
||||
ReadonlyRootfs: @readOnly
|
||||
Sysctls: @sysctls
|
||||
CgroupParent: @cgroupParent
|
||||
ExtraHosts: @extraHosts
|
||||
GroupAdd: @groupAdd
|
||||
PidMode: @pid
|
||||
PidsLimit: @pidsLimit
|
||||
SecurityOpt: @securityOpt
|
||||
UsernsMode: @usernsMode
|
||||
IpcMode: @ipc
|
||||
}
|
||||
if @stopSignal?
|
||||
conf.StopSignal = @stopSignal
|
||||
if @stopGracePeriod?
|
||||
conf.StopTimeout = @stopGracePeriod
|
||||
if @healthcheck?
|
||||
conf.Healthcheck = @healthcheck
|
||||
if @restartPolicy.Name != 'no'
|
||||
conf.HostConfig.RestartPolicy = @restartPolicy
|
||||
# If network mode is the default network for this app, add alias for serviceName
|
||||
if @networkMode == "#{@appId}_default"
|
||||
conf.NetworkingConfig = {
|
||||
EndpointsConfig: {
|
||||
"#{@appId}_default": {
|
||||
Aliases: [ @serviceName ]
|
||||
}
|
||||
}
|
||||
}
|
||||
if @init
|
||||
conf.HostConfig.Init = true
|
||||
if !_.isEmpty(@hostname)
|
||||
conf.Hostname = @hostname
|
||||
if !_.isEmpty(@storageOpt)
|
||||
conf.HostConfig.StorageOpt = @storageOpt
|
||||
if @macAddress?
|
||||
conf.MacAddress = @macAddress
|
||||
return conf
|
||||
|
||||
# TODO: when we support network configuration properly, return endpointConfig: conf
|
||||
extraNetworksToJoin: ->
|
||||
_.map _.pickBy(@networks, (conf, net) => net != @networkMode), (conf, net) ->
|
||||
return { name: net, endpointConfig: {} }
|
||||
|
||||
# TODO: compare configuration, not only network names
|
||||
hasSameNetworks: (otherService) =>
|
||||
_.isEmpty(_.xor(_.keys(@networks), _.keys(otherService.networks)))
|
||||
|
||||
isSameContainer: (otherService) =>
|
||||
|
||||
propertiesToCompare = [
|
||||
'image'
|
||||
'command'
|
||||
'entrypoint'
|
||||
'networkMode'
|
||||
'privileged'
|
||||
'restartPolicy'
|
||||
'labels'
|
||||
'portMappings'
|
||||
'shmSize'
|
||||
'memLimit'
|
||||
'cpuShares'
|
||||
'cpuQuota'
|
||||
'nanoCpus'
|
||||
'cpuset'
|
||||
'domainname'
|
||||
'oomScoreAdj'
|
||||
'oomKillDisable'
|
||||
'healthcheck'
|
||||
'stopSignal'
|
||||
'stopGracePeriod'
|
||||
'init'
|
||||
'readOnly'
|
||||
'sysctls'
|
||||
'hostname'
|
||||
'cgroupParent'
|
||||
'pid'
|
||||
'pidsLimit'
|
||||
'storageOpt'
|
||||
'usernsMode'
|
||||
'ipc'
|
||||
'macAddress'
|
||||
'user'
|
||||
'workingDir'
|
||||
]
|
||||
arraysToCompare = [
|
||||
'volumes'
|
||||
'devices'
|
||||
'capAdd'
|
||||
'capDrop'
|
||||
'dns'
|
||||
'dnsSearch'
|
||||
'dnsOpt'
|
||||
'tmpfs'
|
||||
'extraHosts'
|
||||
'ulimitsArray'
|
||||
'groupAdd'
|
||||
'securityOpt'
|
||||
]
|
||||
|
||||
equalProps = _.isEqual(_.pick(this, propertiesToCompare), _.pick(otherService, propertiesToCompare))
|
||||
equalEnv = _.isEqual(
|
||||
_.omit(@environment, [ 'RESIN_DEVICE_NAME_AT_INIT' ]),
|
||||
_.omit(otherService.environment, [ 'RESIN_DEVICE_NAME_AT_INIT' ])
|
||||
)
|
||||
|
||||
equalNetworks = @hasSameNetworks(otherService)
|
||||
equalArrays = _.every arraysToCompare, (property) =>
|
||||
_.isEmpty(_.xorWith(this[property], otherService[property], _.isEqual))
|
||||
|
||||
equal = equalProps and equalEnv and equalNetworks and equalArrays
|
||||
|
||||
return equal
|
||||
|
||||
isEqualExceptForRunningState: (otherService) =>
|
||||
return @isSameContainer(otherService) and
|
||||
@releaseId == otherService.releaseId and
|
||||
@imageId == otherService.imageId
|
||||
|
||||
isEqual: (otherService) =>
|
||||
return @isEqualExceptForRunningState(otherService) and
|
||||
@running == otherService.running
|
820
src/compose/service.ts
Normal file
820
src/compose/service.ts
Normal file
@ -0,0 +1,820 @@
|
||||
import { detailedDiff as diff } from 'deep-object-diff';
|
||||
import * as Dockerode from 'dockerode';
|
||||
import Duration = require('duration-js');
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
|
||||
import * as conversions from '../lib/conversions';
|
||||
import { checkInt } from '../lib/validation';
|
||||
import { DockerPortOptions, PortMap } from './ports';
|
||||
import {
|
||||
ConfigMap,
|
||||
DeviceMetadata,
|
||||
DockerDevice,
|
||||
ServiceComposeConfig,
|
||||
ServiceConfig,
|
||||
ServiceConfigArrayField,
|
||||
} from './types/service';
|
||||
import * as ComposeUtils from './utils';
|
||||
|
||||
import * as updateLock from '../lib/update-lock';
|
||||
import { sanitiseComposeConfig } from './sanitise';
|
||||
|
||||
export class Service {
|
||||
|
||||
public appId: number | null;
|
||||
public imageId: number | null;
|
||||
public config: ServiceConfig;
|
||||
public serviceName: string | null;
|
||||
public releaseId: number | null;
|
||||
public serviceId: number | null;
|
||||
public imageName: string | null;
|
||||
public containerId: string | null;
|
||||
|
||||
public dependsOn: string | null;
|
||||
|
||||
public status: string;
|
||||
public createdAt: Date | null;
|
||||
|
||||
private static configArrayFields: ServiceConfigArrayField[] = [
|
||||
'volumes',
|
||||
'devices',
|
||||
'capAdd',
|
||||
'capDrop',
|
||||
'dns',
|
||||
'dnsSearch',
|
||||
'dnsOpt',
|
||||
'tmpfs',
|
||||
'extraHosts',
|
||||
'expose',
|
||||
'ulimitsArray',
|
||||
'groupAdd',
|
||||
'securityOpt',
|
||||
];
|
||||
|
||||
// A list of fields to ignore when comparing container configuration
|
||||
private static omitFields = [
|
||||
'networks',
|
||||
'running',
|
||||
'containerId',
|
||||
// This field is passed at container creation, but is not
|
||||
// reported on a container inspect, so we cannot use it
|
||||
// to compare containers
|
||||
'storageOpt',
|
||||
'cpus',
|
||||
].concat(Service.configArrayFields);
|
||||
|
||||
private constructor() {
|
||||
}
|
||||
|
||||
// The type here is actually ServiceComposeConfig, except that the
|
||||
// keys must be camelCase'd first
|
||||
public static fromComposeObject(
|
||||
appConfig: ConfigMap,
|
||||
options: DeviceMetadata,
|
||||
): Service {
|
||||
const service = new Service();
|
||||
|
||||
appConfig = ComposeUtils.camelCaseConfig(appConfig);
|
||||
|
||||
// Seperate the application information from the docker
|
||||
// container configuration
|
||||
service.imageId = appConfig.imageId;
|
||||
delete appConfig.imageId;
|
||||
service.serviceName = appConfig.serviceName;
|
||||
delete appConfig.serviceName;
|
||||
service.appId = appConfig.appId;
|
||||
delete appConfig.appId;
|
||||
service.releaseId = appConfig.releaseId;
|
||||
delete appConfig.releaseId;
|
||||
service.serviceId = appConfig.serviceId;
|
||||
delete appConfig.serviceId;
|
||||
service.imageName = appConfig.imageName;
|
||||
delete appConfig.imageName;
|
||||
service.dependsOn = appConfig.dependsOn || null;
|
||||
delete appConfig.dependsOn;
|
||||
service.createdAt = appConfig.createdAt;
|
||||
delete appConfig.createdAt;
|
||||
|
||||
// We don't need this value
|
||||
delete appConfig.commit;
|
||||
|
||||
|
||||
// Get rid of any extra values and report them to the user
|
||||
const config = sanitiseComposeConfig(appConfig);
|
||||
|
||||
// Process some values into the correct format, delete them from
|
||||
// the original object, and add them to the defaults object below
|
||||
// We do it using defaults, as the types may be slightly different.
|
||||
// For any types which do not change, we change config[value] directly
|
||||
|
||||
// First process the networks correctly
|
||||
let networks: ServiceConfig['networks'] = { };
|
||||
if (_.isArray(config.networks)) {
|
||||
_.each(config.networks, (name) => {
|
||||
networks[name] = { };
|
||||
});
|
||||
} else if(_.isObject(config.networks)) {
|
||||
networks = config.networks || { };
|
||||
}
|
||||
// Prefix the network entries with the app id
|
||||
networks = _.mapKeys(networks, (_v, k) => `${service.appId}_${k}`);
|
||||
delete config.networks;
|
||||
|
||||
// Check for unsupported networkMode entries
|
||||
if (config.networkMode != null) {
|
||||
if (/service:(\s*)?.+/.test(config.networkMode)) {
|
||||
console.log('Warning: A network_mode referencing a service is not yet supported. Ignoring.');
|
||||
delete config.networkMode;
|
||||
} else if (/container:(\s*)?.+/.test(config.networkMode)) {
|
||||
console.log('Warning: A network_mode referencing a container is not supported. Ignoring.');
|
||||
delete config.networkMode;
|
||||
}
|
||||
}
|
||||
|
||||
// memory strings
|
||||
const memLimit = ComposeUtils.parseMemoryNumber(config.memLimit, '0');
|
||||
const memReservation = ComposeUtils.parseMemoryNumber(config.memReservation, '0');
|
||||
const shmSize = ComposeUtils.parseMemoryNumber(config.shmSize, '64m');
|
||||
delete config.memLimit;
|
||||
delete config.memReservation;
|
||||
delete config.shmSize;
|
||||
|
||||
// time strings
|
||||
let stopGracePeriod = 0;
|
||||
if (config.stopGracePeriod != null) {
|
||||
stopGracePeriod = new Duration(config.stopGracePeriod).seconds();
|
||||
}
|
||||
delete config.stopGracePeriod;
|
||||
|
||||
// ulimits
|
||||
const ulimits: ServiceConfig['ulimits'] = { };
|
||||
_.each(config.ulimits, (limit, name) => {
|
||||
if (_.isNumber(limit)) {
|
||||
ulimits[name] = { soft: limit, hard: limit };
|
||||
return;
|
||||
}
|
||||
ulimits[name] = { soft: limit.soft, hard: limit.hard };
|
||||
});
|
||||
delete config.ulimits;
|
||||
|
||||
// string or array of strings - normalise to an array
|
||||
if (_.isString(config.dns)) {
|
||||
config.dns = [ config.dns ];
|
||||
}
|
||||
|
||||
if (_.isString(config.dnsSearch)) {
|
||||
config.dnsSearch = [ config.dnsSearch ];
|
||||
}
|
||||
|
||||
// Assign network_mode to a default value if necessary
|
||||
if (!config.networkMode) {
|
||||
if (!_.isEmpty(networks)) {
|
||||
config.networkMode = _.keys(networks)[0];
|
||||
} else {
|
||||
config.networkMode = 'default';
|
||||
}
|
||||
}
|
||||
if (
|
||||
config.networkMode !== 'host' &&
|
||||
config.networkMode !== 'bridge' &&
|
||||
config.networkMode !== 'none'
|
||||
) {
|
||||
if (networks[config.networkMode] == null) {
|
||||
// The network mode has not been set explicitly
|
||||
config.networkMode = `${service.appId}_${config.networkMode}`;
|
||||
// If we don't have any networks, we need to
|
||||
// create the default with some default options
|
||||
networks[config.networkMode] = {
|
||||
aliases: [ service.serviceName || '' ],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Add default environment variables and labels
|
||||
config.environment = Service.extendEnvVars(
|
||||
config.environment || { },
|
||||
options,
|
||||
service.appId || 0,
|
||||
service.serviceName || '',
|
||||
);
|
||||
config.labels = Service.extendLabels(
|
||||
config.labels || { },
|
||||
options,
|
||||
service.appId || 0,
|
||||
service.serviceId || 0,
|
||||
service.serviceName || '',
|
||||
);
|
||||
|
||||
// Any other special case handling
|
||||
if (config.networkMode === 'host' && !config.hostname) {
|
||||
config.hostname = options.hostnameOnHost;
|
||||
}
|
||||
config.restart = ComposeUtils.createRestartPolicy(config.restart);
|
||||
config.command = ComposeUtils.getCommand(config.command, options.imageInfo);
|
||||
config.entrypoint = ComposeUtils.getEntryPoint(config.entrypoint, options.imageInfo);
|
||||
config.stopSignal = ComposeUtils.getStopSignal(config.stopSignal, options.imageInfo);
|
||||
config.workingDir = ComposeUtils.getWorkingDir(config.workingDir, options.imageInfo);
|
||||
config.user = ComposeUtils.getUser(config.user, options.imageInfo);
|
||||
|
||||
const healthcheck = ComposeUtils.getHealthcheck(config.healthcheck, options.imageInfo);
|
||||
delete config.healthcheck;
|
||||
|
||||
config.volumes = Service.extendAndSanitiseVolumes(
|
||||
config.volumes,
|
||||
options.imageInfo,
|
||||
service.appId || 0,
|
||||
service.serviceName || '',
|
||||
);
|
||||
|
||||
let portMaps: PortMap[] = [];
|
||||
if (config.ports != null) {
|
||||
portMaps = _.map(config.ports, (p) => new PortMap(p));
|
||||
}
|
||||
delete config.ports;
|
||||
|
||||
// get the exposed ports, both from the image and the compose file
|
||||
let expose: string[] = [];
|
||||
if (config.expose != null) {
|
||||
expose = _.map(config.expose, ComposeUtils.sanitiseExposeFromCompose);
|
||||
}
|
||||
const imageExposedPorts = _.get(options.imageInfo, 'Config.ExposedPorts', { });
|
||||
expose = expose.concat(_.keys(imageExposedPorts));
|
||||
expose = _.uniq(expose);
|
||||
// Also add any exposed ports which are implied from the portMaps
|
||||
const exposedFromPortMappings = _.flatMap(portMaps, (port) => port.toExposedPortArray());
|
||||
expose = expose.concat(exposedFromPortMappings);
|
||||
delete config.expose;
|
||||
|
||||
let devices: DockerDevice[] = [];
|
||||
if (config.devices != null) {
|
||||
devices = _.map(config.devices, ComposeUtils.formatDevice);
|
||||
}
|
||||
delete config.devices;
|
||||
|
||||
// Sanity check the incoming boolean values
|
||||
config.oomKillDisable = Boolean(config.oomKillDisable);
|
||||
config.readOnly = Boolean(config.readOnly);
|
||||
|
||||
if (_.isArray(config.sysctls)) {
|
||||
config.sysctls = _.fromPairs(_.map(config.sysctls, (v) => _.split(v, '=')));
|
||||
}
|
||||
config.sysctls = _.mapValues(config.sysctls, String);
|
||||
|
||||
_.each([ 'cpuShares', 'cpuQuota', 'oomScoreAdj' ], (key)=> {
|
||||
const numVal = checkInt(config[key]);
|
||||
if (numVal) {
|
||||
config[key] = numVal;
|
||||
} else {
|
||||
delete config[key];
|
||||
}
|
||||
});
|
||||
|
||||
if (config.cpus != null) {
|
||||
config.cpus = Math.round(Number(config.cpus) * 10 ** 9);
|
||||
if (_.isNaN(config.cpus)) {
|
||||
console.log('Warning: config.cpus value cannot be parsed. Ignoring.');
|
||||
console.log(` Value: ${config.cpus}`);
|
||||
config.cpus = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
let tmpfs: string[] = [];
|
||||
if (config.tmpfs != null) {
|
||||
if (_.isString(config.tmpfs)) {
|
||||
tmpfs = [ config.tmpfs ];
|
||||
} else {
|
||||
tmpfs = config.tmpfs;
|
||||
}
|
||||
}
|
||||
delete config.tmpfs;
|
||||
|
||||
// Normalise the config before passing it to defaults
|
||||
ComposeUtils.normalizeNullValues(config);
|
||||
|
||||
service.config = _.defaults(config, {
|
||||
portMaps,
|
||||
capAdd: [ ],
|
||||
capDrop:[ ],
|
||||
command: [ ],
|
||||
cgroupParent: '',
|
||||
devices,
|
||||
dnsOpt: [ ],
|
||||
entrypoint: '',
|
||||
extraHosts: [ ],
|
||||
expose,
|
||||
networks,
|
||||
dns: [ ],
|
||||
dnsSearch: [ ],
|
||||
environment: { },
|
||||
labels: { },
|
||||
networkMode: '',
|
||||
ulimits,
|
||||
groupAdd: [ ],
|
||||
healthcheck,
|
||||
pid: '',
|
||||
pidsLimit: 0,
|
||||
securityOpt: [ ],
|
||||
stopGracePeriod,
|
||||
stopSignal: '',
|
||||
storageOpt: { },
|
||||
sysctls: { },
|
||||
tmpfs,
|
||||
usernsMode: '',
|
||||
volumes: [ ],
|
||||
restart: 'always',
|
||||
cpuShares: 0,
|
||||
cpuQuota: 0,
|
||||
cpus: 0,
|
||||
cpuset: '',
|
||||
domainname: '',
|
||||
ipc: 'shareable',
|
||||
macAddress: '',
|
||||
memLimit,
|
||||
memReservation,
|
||||
oomKillDisable: false,
|
||||
oomScoreAdj: 0,
|
||||
privileged: false,
|
||||
readOnly: false,
|
||||
shmSize,
|
||||
hostname: '',
|
||||
user: '',
|
||||
workingDir: '',
|
||||
});
|
||||
|
||||
// Mutate service with extra features
|
||||
ComposeUtils.addFeaturesFromLabels(service, options);
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
public static fromDockerContainer(container: Dockerode.ContainerInspectInfo): Service {
|
||||
const svc = new Service();
|
||||
|
||||
if (container.State.Running) {
|
||||
svc.status = 'Running';
|
||||
} else if(container.State.Status === 'created') {
|
||||
svc.status = 'Installed';
|
||||
} else if(container.State.Status === 'dead') {
|
||||
svc.status = 'Dead';
|
||||
} else {
|
||||
svc.status = container.State.Status;
|
||||
}
|
||||
|
||||
svc.createdAt = new Date(container.Created);
|
||||
svc.containerId = container.Id;
|
||||
|
||||
let hostname = container.Config.Hostname;
|
||||
if (hostname.length === 12 && _.startsWith(container.Id, hostname)) {
|
||||
// A hostname equal to the first part of the container ID actually
|
||||
// means no hostname was specified
|
||||
hostname = '';
|
||||
}
|
||||
|
||||
let networks: ServiceConfig['networks'] = { };
|
||||
if (_.get(container, 'NetworkSettings.Networks', null) != null) {
|
||||
networks = ComposeUtils.dockerNetworkToServiceNetwork(container.NetworkSettings.Networks);
|
||||
}
|
||||
|
||||
const ulimits: ServiceConfig['ulimits'] = { };
|
||||
_.each(container.HostConfig.Ulimits, ({ Name, Soft, Hard }) => {
|
||||
ulimits[Name] = { soft: Soft, hard: Hard };
|
||||
});
|
||||
|
||||
const portMaps = PortMap.fromDockerOpts(container.HostConfig.PortBindings);
|
||||
let expose = _.flatMap(
|
||||
_.flatMap(portMaps, (p) => p.toDockerOpts().exposedPorts),
|
||||
_.keys,
|
||||
);
|
||||
if (container.Config.ExposedPorts != null) {
|
||||
expose = expose.concat(_.map(container.Config.ExposedPorts, (_v, k) => k.toString()));
|
||||
}
|
||||
expose = _.uniq(expose);
|
||||
|
||||
const tmpfs: string[] = [];
|
||||
_.each((container.HostConfig as any).Tmpfs, (_v, key) => {
|
||||
tmpfs.push(key);
|
||||
});
|
||||
|
||||
// We cannot use || for this value, as the empty string is a
|
||||
// valid restart policy but will equate to null in an OR
|
||||
let restart = (container.HostConfig.RestartPolicy || {}).Name;
|
||||
if (restart == null) {
|
||||
restart = 'always';
|
||||
}
|
||||
|
||||
// Define the service config with the same defaults that are used
|
||||
// when creating from a compose object, so comparisons will work
|
||||
// correctly
|
||||
// TODO: We have extended HostConfig interface to keep up with the
|
||||
// missing typings, but we cannot do the same the Config sub-object
|
||||
// as it is not defined as it's own type. We need to either recreate
|
||||
// the entire ContainerInspectInfo object, or upstream the extra
|
||||
// fields to DefinitelyTyped
|
||||
svc.config = {
|
||||
networkMode: container.HostConfig.NetworkMode,
|
||||
|
||||
portMaps,
|
||||
expose,
|
||||
hostname,
|
||||
command: container.Config.Cmd || '',
|
||||
entrypoint: container.Config.Entrypoint || '',
|
||||
volumes: _.concat(container.HostConfig.Binds || [], _.keys(container.Config.Volumes || { })),
|
||||
image: container.Config.Image,
|
||||
environment: conversions.envArrayToObject(container.Config.Env || [ ]),
|
||||
privileged: container.HostConfig.Privileged || false,
|
||||
labels: container.Config.Labels || { },
|
||||
running: container.State.Running,
|
||||
restart,
|
||||
capAdd: container.HostConfig.CapAdd || [ ],
|
||||
capDrop: container.HostConfig.CapDrop || [ ],
|
||||
devices: container.HostConfig.Devices || [ ],
|
||||
networks,
|
||||
memLimit: container.HostConfig.Memory || 0 ,
|
||||
memReservation: container.HostConfig.MemoryReservation || 0,
|
||||
shmSize: container.HostConfig.ShmSize || 0,
|
||||
cpuShares: container.HostConfig.CpuShares || 0,
|
||||
cpuQuota: container.HostConfig.CpuQuota || 0,
|
||||
// Not present on a container inspect
|
||||
cpus: 0,
|
||||
cpuset: container.HostConfig.CpusetCpus || '',
|
||||
domainname: container.Config.Domainname || '',
|
||||
oomKillDisable: container.HostConfig.OomKillDisable || false,
|
||||
oomScoreAdj: container.HostConfig.OomScoreAdj || 0,
|
||||
dns: container.HostConfig.Dns || [ ],
|
||||
dnsSearch: container.HostConfig.DnsSearch || [ ],
|
||||
dnsOpt: container.HostConfig.DnsOptions || [ ],
|
||||
tmpfs,
|
||||
extraHosts: container.HostConfig.ExtraHosts || [ ],
|
||||
ulimits,
|
||||
stopSignal: (container.Config as any).StopSignal || '',
|
||||
stopGracePeriod: (container.Config as any).StopTimeout || 0,
|
||||
healthcheck: ComposeUtils.dockerHealthcheckToServiceHealthcheck(
|
||||
(container.Config as any).Healthcheck || { },
|
||||
),
|
||||
readOnly: container.HostConfig.ReadonlyRootfs || false,
|
||||
sysctls: container.HostConfig.Sysctls || { },
|
||||
cgroupParent: container.HostConfig.CgroupParent || '',
|
||||
groupAdd: container.HostConfig.GroupAdd || [ ],
|
||||
pid: container.HostConfig.PidMode || '',
|
||||
pidsLimit: container.HostConfig.PidsLimit || 0,
|
||||
securityOpt: container.HostConfig.SecurityOpt || [ ],
|
||||
// StorageOpt is present on container creation, but not
|
||||
// when you inspect the container
|
||||
storageOpt: { },
|
||||
usernsMode: container.HostConfig.UsernsMode || '',
|
||||
ipc: container.HostConfig.IpcMode || '',
|
||||
macAddress: (container.Config as any).MacAddress || '',
|
||||
user: container.Config.User || '',
|
||||
workingDir: container.Config.WorkingDir || '',
|
||||
};
|
||||
|
||||
svc.appId = checkInt(container.Config.Labels['io.resin.app-id']) || null;
|
||||
svc.serviceId = checkInt(container.Config.Labels['io.resin.service-id']) || null;
|
||||
svc.serviceName = container.Config.Labels['io.resin.service-name'];
|
||||
const nameMatch = container.Name.match(/.*_(\d+)_(\d+)$/);
|
||||
|
||||
svc.imageId = nameMatch != null ? checkInt(nameMatch[1]) || null : null;
|
||||
svc.releaseId = nameMatch != null ? checkInt(nameMatch[2]) || null : null;
|
||||
svc.containerId = container.Id;
|
||||
|
||||
return svc;
|
||||
}
|
||||
|
||||
public toDockerContainer(): Dockerode.ContainerCreateOptions {
|
||||
const { binds, volumes } = this.getBindsAndVolumes();
|
||||
const { exposedPorts, portBindings } = this.generateExposeAndPorts();
|
||||
|
||||
const tmpFs: Dictionary<''> = { };
|
||||
_.each(this.config.tmpfs, (tmp) => {
|
||||
tmpFs[tmp] = '';
|
||||
});
|
||||
|
||||
return {
|
||||
Tty: true,
|
||||
Cmd: this.config.command,
|
||||
Volumes: volumes,
|
||||
// Typings are wrong here, the docker daemon accepts a string or string[],
|
||||
Entrypoint: this.config.entrypoint as string,
|
||||
Env: conversions.envObjectToArray(this.config.environment),
|
||||
ExposedPorts: exposedPorts,
|
||||
Image: this.config.image,
|
||||
Labels: this.config.labels,
|
||||
NetworkingConfig: ComposeUtils.serviceNetworksToDockerNetworks(this.config.networks),
|
||||
StopSignal: this.config.stopSignal,
|
||||
Domainname: this.config.domainname,
|
||||
Hostname: this.config.hostname,
|
||||
// Typings are wrong here, it says MacAddress is a bool (wtf?) but it is
|
||||
// in fact a string
|
||||
MacAddress: this.config.macAddress as any,
|
||||
User: this.config.user,
|
||||
WorkingDir: this.config.workingDir,
|
||||
HostConfig: {
|
||||
CapAdd: this.config.capAdd,
|
||||
CapDrop: this.config.capDrop,
|
||||
Binds: binds,
|
||||
CgroupParent: this.config.cgroupParent,
|
||||
Devices: this.config.devices,
|
||||
Dns: this.config.dns,
|
||||
DnsOptions: this.config.dnsOpt,
|
||||
DnsSearch: this.config.dnsSearch,
|
||||
PortBindings: portBindings,
|
||||
ExtraHosts: this.config.extraHosts,
|
||||
GroupAdd: this.config.groupAdd,
|
||||
NetworkMode: this.config.networkMode,
|
||||
PidMode: this.config.pid,
|
||||
PidsLimit: this.config.pidsLimit,
|
||||
SecurityOpt: this.config.securityOpt,
|
||||
Sysctls: this.config.sysctls,
|
||||
Ulimits: ComposeUtils.serviceUlimitsToDockerUlimits(this.config.ulimits),
|
||||
RestartPolicy: ComposeUtils.serviceRestartToDockerRestartPolicy(this.config.restart),
|
||||
CpuShares: this.config.cpuShares,
|
||||
CpuQuota: this.config.cpuQuota,
|
||||
// Type missing, and HostConfig isn't defined as a seperate object
|
||||
// so we cannot extend it easily
|
||||
CpusetCpus: this.config.cpuset,
|
||||
Memory: this.config.memLimit,
|
||||
MemoryReservation: this.config.memReservation,
|
||||
OomKillDisable: this.config.oomKillDisable,
|
||||
OomScoreAdj: this.config.oomScoreAdj,
|
||||
Privileged: this.config.privileged,
|
||||
ReadonlyRootfs: this.config.readOnly,
|
||||
ShmSize: this.config.shmSize,
|
||||
Tmpfs: tmpFs,
|
||||
StorageOpt: this.config.storageOpt,
|
||||
UsernsMode: this.config.usernsMode,
|
||||
NanoCpus: this.config.cpus,
|
||||
IpcMode: this.config.ipc,
|
||||
} as Dockerode.ContainerCreateOptions['HostConfig'],
|
||||
Healthcheck: ComposeUtils.serviceHealthcheckToDockerHealthcheck(this.config.healthcheck),
|
||||
StopTimeout: this.config.stopGracePeriod,
|
||||
};
|
||||
}
|
||||
|
||||
public isEqualConfig(service: Service): boolean {
|
||||
// Check all of the networks for any changes
|
||||
let sameNetworks = true;
|
||||
_.each(service.config.networks, (network, name) => {
|
||||
if (this.config.networks[name] == null) {
|
||||
sameNetworks = false;
|
||||
return;
|
||||
}
|
||||
sameNetworks = sameNetworks && this.isSameNetwork(this.config.networks[name], network);
|
||||
});
|
||||
|
||||
// Check the configuration for any changes
|
||||
const thisOmitted = _.omit(this.config, Service.omitFields);
|
||||
const otherOmitted = _.omit(service.config, Service.omitFields);
|
||||
let sameConfig = _.isEqual(
|
||||
thisOmitted,
|
||||
otherOmitted,
|
||||
);
|
||||
const nonArrayEquals = sameConfig;
|
||||
|
||||
// Check for array fields which don't match
|
||||
const differentArrayFields: string[] = [];
|
||||
sameConfig = sameConfig && _.every(Service.configArrayFields, (field: ServiceConfigArrayField) => {
|
||||
return _.isEmpty(
|
||||
_.xorWith(
|
||||
// TODO: The typings here aren't accepted, even though we
|
||||
// know it's fine
|
||||
(this.config as any)[field],
|
||||
(service.config as any)[field],
|
||||
(a, b) => {
|
||||
const eq = _.isEqual(a, b);
|
||||
if (!eq) {
|
||||
differentArrayFields.push(field);
|
||||
}
|
||||
return eq;
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
if (!(sameConfig && sameNetworks)) {
|
||||
// Add some console output for why a service is not matching
|
||||
// so that if we end up in a restart loop, we know exactly why
|
||||
console.log(`Replacing container for service ${this.serviceName} because of config changes:`);
|
||||
if (!nonArrayEquals) {
|
||||
console.log(' Non-array fields: ', JSON.stringify(diff(
|
||||
thisOmitted,
|
||||
otherOmitted,
|
||||
)));
|
||||
}
|
||||
if (differentArrayFields.length > 0) {
|
||||
console.log(' Array Fields: ', differentArrayFields.join(','));
|
||||
}
|
||||
|
||||
if (!sameNetworks) {
|
||||
console.log(' Network changes detected');
|
||||
}
|
||||
|
||||
}
|
||||
return sameNetworks && sameConfig;
|
||||
}
|
||||
|
||||
public extraNetworksToJoin(): ServiceConfig['networks'] {
|
||||
return _.omit(this.config.networks, this.config.networkMode);
|
||||
}
|
||||
|
||||
public isEqualExceptForRunningState(service: Service): boolean {
|
||||
return this.isEqualConfig(service) &&
|
||||
this.releaseId === service.releaseId &&
|
||||
this.imageId === service.imageId;
|
||||
}
|
||||
|
||||
public isEqual(service: Service): boolean {
|
||||
return this.isEqualExceptForRunningState(service) &&
|
||||
this.config.running === service.config.running;
|
||||
}
|
||||
|
||||
public getNamedVolumes() {
|
||||
const defaults = Service.defaultBinds(this.appId || 0, this.serviceName || '');
|
||||
const validVolumes = _.map(this.config.volumes, (volume) => {
|
||||
if (_.includes(defaults, volume) || !_.includes(volume, ':')) {
|
||||
return null;
|
||||
}
|
||||
const bindSource = volume.split(':')[0];
|
||||
if (!path.isAbsolute(bindSource)) {
|
||||
const match = bindSource.match(/[0-9]+_(.+)/);
|
||||
if (match == null) {
|
||||
console.log('Error: There was an error parsing a volume bind source, ignoring.');
|
||||
console.log(' bind source: ', bindSource);
|
||||
return null;
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
return _.reject(validVolumes, _.isNil);
|
||||
}
|
||||
|
||||
private getBindsAndVolumes(): {
|
||||
binds: string[],
|
||||
volumes: { [volName: string]: { } }
|
||||
} {
|
||||
const binds: string[] = [ ];
|
||||
const volumes: { [volName: string]: { } } = { };
|
||||
_.each(this.config.volumes, (volume) => {
|
||||
if (_.includes(volume, ':')) {
|
||||
binds.push(volume);
|
||||
} else {
|
||||
volumes[volume] = { };
|
||||
}
|
||||
});
|
||||
|
||||
return { binds, volumes };
|
||||
}
|
||||
|
||||
private generateExposeAndPorts(): DockerPortOptions {
|
||||
const exposed: DockerPortOptions['exposedPorts'] = { };
|
||||
const ports: DockerPortOptions['portBindings'] = { };
|
||||
|
||||
_.each(this.config.portMaps, (pmap) => {
|
||||
const { exposedPorts, portBindings } = pmap.toDockerOpts();
|
||||
_.merge(exposed, exposedPorts);
|
||||
_.merge(ports, portBindings);
|
||||
});
|
||||
|
||||
// We also want to merge the compose and image exposedPorts
|
||||
// into the list of exposedPorts
|
||||
const composeExposed: DockerPortOptions['exposedPorts'] = { };
|
||||
_.each(this.config.expose, (port) => {
|
||||
composeExposed[port] = { };
|
||||
});
|
||||
_.merge(exposed, composeExposed);
|
||||
|
||||
return { exposedPorts: exposed, portBindings: ports };
|
||||
}
|
||||
|
||||
private static extendEnvVars(
|
||||
environment: { [envVarName: string]: string } | null | undefined,
|
||||
options: DeviceMetadata,
|
||||
appId: number,
|
||||
serviceName: string,
|
||||
): { [envVarName: string]: string } {
|
||||
let env = _.defaults(environment, {
|
||||
RESIN_APP_ID: appId.toString(),
|
||||
RESIN_APP_NAME: options.appName,
|
||||
RESIN_SERVICE_NAME: serviceName,
|
||||
RESIN_DEVICE_UUID: options.uuid,
|
||||
RESIN_DEVICE_TYPE: options.deviceType,
|
||||
RESIN_HOST_OS_VERSION: options.osVersion,
|
||||
RESIN_SUPERVISOR_VERSION: options.version,
|
||||
RESIN_APP_LOCK_PATH: '/tmp/resin/resin-updates.lock',
|
||||
RESIN_SERVICE_KILL_ME_PATH: '/tmp/resin/resin-kill-me',
|
||||
RESIN: '1',
|
||||
USER: 'root',
|
||||
});
|
||||
|
||||
const imageInfoEnv = _.get(options.imageInfo, 'Config.Env', []);
|
||||
env = _.defaults(env, conversions.envArrayToObject(imageInfoEnv));
|
||||
return env;
|
||||
}
|
||||
|
||||
private isSameNetwork(
|
||||
current: ServiceConfig['networks'][0],
|
||||
target: ServiceConfig['networks'][0],
|
||||
): boolean {
|
||||
let sameNetwork = true;
|
||||
// Compare only the values which are defined in the target, as the current
|
||||
// values get set to defaults by docker
|
||||
if (target.aliases != null) {
|
||||
if (current.aliases == null) {
|
||||
sameNetwork = false;
|
||||
} else {
|
||||
// Remove the auto-added docker container id
|
||||
const currentAliases = _.filter(current.aliases, (alias: string) => {
|
||||
return !_.startsWith(this.containerId!, alias);
|
||||
});
|
||||
const targetAliases = _.filter(current.aliases, (alias: string) => {
|
||||
return !_.startsWith(this.containerId!, alias);
|
||||
});
|
||||
|
||||
// Docker adds container ids to the alias list, directly after
|
||||
// the service name, to detect this, check for both target having
|
||||
// exactly half of the amount of entries as the current, and check
|
||||
// that every second entry (starting from 0) is equal
|
||||
if (currentAliases.length === targetAliases.length * 2) {
|
||||
sameNetwork = _(currentAliases)
|
||||
.filter((_v, k) => k % 2 === 0)
|
||||
.isEqual(targetAliases);
|
||||
} else {
|
||||
// Otherwise compare them literally
|
||||
sameNetwork = _.isEmpty(_.xorWith(currentAliases, targetAliases, _.isEqual));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (target.ipv4Address != null) {
|
||||
sameNetwork = sameNetwork && _.isEqual(current.ipv4Address, target.ipv4Address);
|
||||
}
|
||||
if (target.ipv6Address != null) {
|
||||
sameNetwork = sameNetwork && _.isEqual(current.ipv6Address, target.ipv6Address);
|
||||
}
|
||||
if (target.linkLocalIps != null) {
|
||||
sameNetwork = sameNetwork && _.isEqual(current.linkLocalIps, target.linkLocalIps);
|
||||
}
|
||||
return sameNetwork;
|
||||
}
|
||||
|
||||
private static extendLabels(
|
||||
labels: { [labelName: string]: string } | null | undefined,
|
||||
{ imageInfo }: DeviceMetadata,
|
||||
appId: number,
|
||||
serviceId: number,
|
||||
serviceName: string,
|
||||
): { [labelName: string]: string } {
|
||||
let newLabels = _.defaults(labels, {
|
||||
'io.resin.supervised': 'true',
|
||||
'io.resin.app-id': appId.toString(),
|
||||
'io.resin.service-id': serviceId.toString(),
|
||||
'io.resin.service-name': serviceName,
|
||||
});
|
||||
|
||||
const imageLabels = _.get(imageInfo, 'Config.Labels', { });
|
||||
newLabels = _.defaults(newLabels, imageLabels);
|
||||
return newLabels;
|
||||
}
|
||||
|
||||
private static extendAndSanitiseVolumes(
|
||||
composeVolumes: ServiceComposeConfig['volumes'],
|
||||
imageInfo: Dockerode.ImageInspectInfo | undefined,
|
||||
appId: number,
|
||||
serviceName: string,
|
||||
): ServiceConfig['volumes'] {
|
||||
let volumes: ServiceConfig['volumes'] = [];
|
||||
|
||||
_.each(composeVolumes, (volume) => {
|
||||
const isBind = _.includes(volume, ':');
|
||||
if (isBind) {
|
||||
const [ bindSource, bindDest, mode ] = volume.split(':');
|
||||
if (!path.isAbsolute(bindSource)) {
|
||||
// namespace our volumes by appId
|
||||
let volumeDef = `${appId}_${bindSource}:${bindDest}`;
|
||||
if (mode != null) {
|
||||
volumeDef = `${volumeDef}:${mode}`;
|
||||
}
|
||||
volumes.push(volumeDef);
|
||||
} else {
|
||||
console.log(`Ignoring invalid bind mount ${volume}`);
|
||||
}
|
||||
} else {
|
||||
volumes.push(volume);
|
||||
}
|
||||
});
|
||||
|
||||
// Now add the default and image binds
|
||||
volumes = volumes.concat(Service.defaultBinds(appId, serviceName));
|
||||
volumes = _.union(_.keys(_.get(imageInfo, 'Config.Volumes')), volumes);
|
||||
|
||||
return volumes;
|
||||
}
|
||||
|
||||
private static defaultBinds(appId: number, serviceName: string): string[] {
|
||||
return [
|
||||
`${updateLock.lockPath(appId, serviceName)}:/tmp/resin`,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
204
src/compose/types/service.ts
Normal file
204
src/compose/types/service.ts
Normal file
@ -0,0 +1,204 @@
|
||||
import * as Dockerode from 'dockerode';
|
||||
|
||||
import { PortMap } from '../ports';
|
||||
|
||||
export interface ComposeHealthcheck {
|
||||
test: string | string[];
|
||||
interval?: string;
|
||||
timeout?: string;
|
||||
startPeriod?: string;
|
||||
retries?: number;
|
||||
disable?: boolean;
|
||||
}
|
||||
|
||||
export interface ServiceHealthcheck {
|
||||
test: string[];
|
||||
interval?: number;
|
||||
timeout?: number;
|
||||
startPeriod?: number;
|
||||
retries?: number;
|
||||
}
|
||||
|
||||
// This is the config directly from the compose file (after running it
|
||||
// through _.camelCase)
|
||||
export interface ServiceComposeConfig {
|
||||
// Used for converting these fields in a programmatic fashion
|
||||
// Unfortunately even keys known at compiler time don't work
|
||||
// with the type system, as least at the moment
|
||||
[key: string]: any;
|
||||
|
||||
capAdd?: string[];
|
||||
capDrop?: string[];
|
||||
command?: string[] | string;
|
||||
cgroupParent?: string;
|
||||
devices?: string[];
|
||||
dns?: string | string[];
|
||||
dnsOpt?: string[];
|
||||
dnsSearch?: string | string[];
|
||||
tmpfs?: string | string[];
|
||||
entrypoint?: string | string[];
|
||||
environment?: { [envVarName: string]: string };
|
||||
expose?: string[];
|
||||
extraHosts?: string[];
|
||||
groupAdd?: string[];
|
||||
healthcheck?: ComposeHealthcheck;
|
||||
image: string;
|
||||
init?: string | boolean;
|
||||
labels?: { [labelName: string]: string };
|
||||
running: boolean;
|
||||
networkMode?: string;
|
||||
networks?: string[] | {
|
||||
[networkName: string]: {
|
||||
aliases?: string[];
|
||||
ipv4Address?: string;
|
||||
ipv6Address?: string;
|
||||
linkLocalIps?: string[];
|
||||
}
|
||||
};
|
||||
pid?: string;
|
||||
pidsLimit?: number;
|
||||
ports?: string[];
|
||||
securityOpt?: string[];
|
||||
stopGracePeriod?: string;
|
||||
stopSignal?: string;
|
||||
storageOpt?: { [opt: string]: string };
|
||||
sysctls?: { [name: string]: string };
|
||||
ulimits?: {
|
||||
[ulimitName: string]: number | { soft: number, hard: number };
|
||||
};
|
||||
usernsMode?: string;
|
||||
volumes?: string[];
|
||||
restart?: string;
|
||||
cpuShares?: number;
|
||||
cpuQuota?: number;
|
||||
cpus?: number;
|
||||
cpuset?: string;
|
||||
domainname?: string;
|
||||
hostname?: string;
|
||||
ipc?: string;
|
||||
macAddress?: string;
|
||||
memLimit?: string;
|
||||
memReservation?: string;
|
||||
oomKillDisable?: boolean;
|
||||
oomScoreAdj?: number;
|
||||
privileged?: boolean;
|
||||
readOnly?: boolean;
|
||||
shmSize?: string;
|
||||
user?: string;
|
||||
workingDir?: string;
|
||||
}
|
||||
|
||||
// This is identical to ServiceComposeConfig, except for the
|
||||
// cases where these values are represented by higher level types.
|
||||
export interface ServiceConfig {
|
||||
portMaps: PortMap[];
|
||||
|
||||
capAdd: string[];
|
||||
capDrop: string[];
|
||||
command: string[];
|
||||
cgroupParent: string;
|
||||
devices: DockerDevice[];
|
||||
dns: string | string[];
|
||||
dnsOpt: string[];
|
||||
dnsSearch: string | string[];
|
||||
tmpfs: string[];
|
||||
entrypoint: string | string[];
|
||||
environment: { [envVarName: string]: string };
|
||||
expose: string[];
|
||||
extraHosts: string[];
|
||||
groupAdd: string[];
|
||||
healthcheck: ServiceHealthcheck;
|
||||
image: string;
|
||||
labels: { [labelName: string]: string };
|
||||
running: boolean;
|
||||
networkMode: string;
|
||||
networks: {
|
||||
[networkName: string]: {
|
||||
aliases?: string[];
|
||||
ipv4Address?: string;
|
||||
ipv6Address?: string;
|
||||
linkLocalIps?: string[];
|
||||
}
|
||||
};
|
||||
pid: string;
|
||||
pidsLimit: number;
|
||||
securityOpt: string[];
|
||||
stopGracePeriod: number;
|
||||
stopSignal: string;
|
||||
storageOpt: { [opt: string]: string };
|
||||
sysctls: { [name: string]: string };
|
||||
ulimits: {
|
||||
[ulimitName: string]: { soft: number, hard: number };
|
||||
};
|
||||
usernsMode: string;
|
||||
volumes: string[];
|
||||
restart: string;
|
||||
cpuShares: number;
|
||||
cpuQuota: number;
|
||||
cpus: number;
|
||||
cpuset: string;
|
||||
domainname: string;
|
||||
hostname: string;
|
||||
ipc: string;
|
||||
macAddress: string;
|
||||
memLimit: number;
|
||||
memReservation: number;
|
||||
oomKillDisable: boolean;
|
||||
oomScoreAdj: number;
|
||||
privileged: boolean;
|
||||
readOnly: boolean;
|
||||
shmSize: number;
|
||||
user: string;
|
||||
workingDir: string;
|
||||
}
|
||||
|
||||
export type ServiceConfigArrayField = 'volumes' |
|
||||
'devices' |
|
||||
'capAdd' |
|
||||
'capDrop' |
|
||||
'dns' |
|
||||
'dnsSearch' |
|
||||
'dnsOpt' |
|
||||
'expose' |
|
||||
'tmpfs' |
|
||||
'extraHosts' |
|
||||
'ulimitsArray' |
|
||||
'groupAdd' |
|
||||
'securityOpt';
|
||||
|
||||
// The config directly from the application manager, which contains
|
||||
// application information, plus the compose data
|
||||
export interface ConfigMap {
|
||||
[name: string]: any;
|
||||
}
|
||||
|
||||
// When creating a service from the compose data, we need to extend the labels
|
||||
// and environment variables which more information, defined below. Note
|
||||
// that these do not need to be provided when create the service object from
|
||||
// the docker inspect call, because they would have already been set by then.
|
||||
// TODO: Move these to a more appropriate location once more of the supervisor
|
||||
// is typescript
|
||||
export interface DeviceMetadata {
|
||||
imageInfo?: Dockerode.ImageInspectInfo;
|
||||
uuid: string;
|
||||
appName: string;
|
||||
version: string;
|
||||
deviceType: string;
|
||||
deviceApiKey: string;
|
||||
listenPort: number;
|
||||
apiSecret: string;
|
||||
supervisorApiHost: string;
|
||||
osVersion: string;
|
||||
hostnameOnHost: string;
|
||||
hostPathExists: {
|
||||
modules: boolean;
|
||||
firmware: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DockerDevice {
|
||||
PathOnHost: string;
|
||||
PathInContainer: string;
|
||||
CgroupPermissions: string;
|
||||
}
|
||||
|
436
src/compose/utils.ts
Normal file
436
src/compose/utils.ts
Normal file
@ -0,0 +1,436 @@
|
||||
import * as Dockerode from 'dockerode';
|
||||
import Duration = require('duration-js');
|
||||
import * as _ from 'lodash';
|
||||
import { parse as parseCommand } from 'shell-quote';
|
||||
|
||||
import * as constants from '../lib/constants';
|
||||
import { checkTruthy } from '../lib/validation';
|
||||
import { Service } from './service';
|
||||
import {
|
||||
ComposeHealthcheck,
|
||||
ConfigMap,
|
||||
DeviceMetadata,
|
||||
DockerDevice,
|
||||
ServiceComposeConfig,
|
||||
ServiceConfig,
|
||||
ServiceHealthcheck,
|
||||
} from './types/service';
|
||||
|
||||
export function camelCaseConfig(literalConfig: ConfigMap): ServiceComposeConfig {
|
||||
const config = _.mapKeys(literalConfig, (_v, k) => _.camelCase(k));
|
||||
|
||||
if (_.isObject(config.networks)) {
|
||||
const networksTmp = _.cloneDeep(config.networks);
|
||||
_.each(networksTmp, (v, k) => {
|
||||
config.networks[k] = _.mapKeys(v, (_v, k) => _.camelCase(k));
|
||||
});
|
||||
}
|
||||
|
||||
return config as ServiceComposeConfig;
|
||||
}
|
||||
|
||||
export function parseMemoryNumber(valueAsString: string | null | undefined, defaultValue?: string): number {
|
||||
if (valueAsString == null) {
|
||||
if (defaultValue != null) {
|
||||
return parseMemoryNumber(defaultValue);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
const match = valueAsString.toString().match(/^([0-9]+)([bkmg]?)b?$/i);
|
||||
if (match == null) {
|
||||
if (defaultValue != null) {
|
||||
return parseMemoryNumber(defaultValue);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
const num = match[1];
|
||||
const pow: { [key: string]: number } = { '': 0, b: 0, B: 0, K: 1, k: 1, m: 2, M: 2, g: 3, G: 3 };
|
||||
return parseInt(num, 10) * 1024 ** pow[match[2]];
|
||||
}
|
||||
|
||||
export const validRestartPolicies = [
|
||||
'no',
|
||||
'always',
|
||||
'on-failure',
|
||||
'unless-stopped',
|
||||
];
|
||||
|
||||
export function createRestartPolicy(
|
||||
name?: string,
|
||||
): string {
|
||||
if (name == null) {
|
||||
return 'always';
|
||||
}
|
||||
|
||||
// Ensure that name is a string, otherwise the below could
|
||||
// throw
|
||||
if (!_.isString(name)) {
|
||||
console.log(`Warning: Non-string argument for restart field: ${name} - ignoring.`);
|
||||
return 'always';
|
||||
}
|
||||
|
||||
name = name.toLowerCase().trim();
|
||||
if(!_.includes(validRestartPolicies, name)) {
|
||||
return 'always';
|
||||
}
|
||||
|
||||
if (name === 'no') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
function processCommandString(command: string): string {
|
||||
return command.replace(/(\$)/g, '\\$1');
|
||||
}
|
||||
|
||||
function processCommandParsedArrayElement(arg: string | { [key: string]: string}): string {
|
||||
if (_.isString(arg)) {
|
||||
return arg;
|
||||
}
|
||||
if (arg.op === 'glob') {
|
||||
return arg.pattern;
|
||||
}
|
||||
return arg.op;
|
||||
}
|
||||
|
||||
function commandAsArray(command: string | string[]): string[] {
|
||||
if (_.isString(command)) {
|
||||
return _.map(
|
||||
parseCommand(processCommandString(command)),
|
||||
processCommandParsedArrayElement,
|
||||
);
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
export function getCommand(
|
||||
composeCommand: string | string[] | null | undefined,
|
||||
imageInfo?: Dockerode.ImageInspectInfo,
|
||||
): string[] {
|
||||
if (composeCommand != null) {
|
||||
return commandAsArray(composeCommand);
|
||||
}
|
||||
const imgCommand = _.get(imageInfo, 'Config.Cmd', []);
|
||||
return commandAsArray(imgCommand);
|
||||
}
|
||||
|
||||
export function getEntryPoint(
|
||||
composeEntry: string | string[] | null | undefined,
|
||||
imageInfo?: Dockerode.ImageInspectInfo,
|
||||
): string[] {
|
||||
if (composeEntry != null) {
|
||||
return commandAsArray(composeEntry);
|
||||
}
|
||||
const imgEntry = _.get(imageInfo, 'Config.Entrypoint', []);
|
||||
return commandAsArray(imgEntry);
|
||||
}
|
||||
|
||||
// Note that the typings for the compose file stop signal
|
||||
// say that this can only be a string, but the yaml parser
|
||||
// could pass it through as a number, so support that here
|
||||
export function getStopSignal(
|
||||
composeStop: string | number | null | undefined,
|
||||
imageInfo?: Dockerode.ImageInspectInfo,
|
||||
): string {
|
||||
if (composeStop != null) {
|
||||
if (!_.isString(composeStop)) {
|
||||
return composeStop.toString();
|
||||
}
|
||||
return composeStop;
|
||||
}
|
||||
return _.get(imageInfo, 'Config.StopSignal', '');
|
||||
}
|
||||
|
||||
// TODO: Move healthcheck stuff into seperate module
|
||||
export function dockerHealthcheckToServiceHealthcheck(
|
||||
healthcheck?: Dockerode.DockerHealthcheck,
|
||||
): ServiceHealthcheck {
|
||||
if (healthcheck == null || _.isEmpty(healthcheck)) {
|
||||
return { test: [ 'NONE' ] };
|
||||
}
|
||||
const serviceHC: ServiceHealthcheck = {
|
||||
test: healthcheck.Test,
|
||||
};
|
||||
|
||||
if (healthcheck.Interval != null) {
|
||||
serviceHC.interval = healthcheck.Interval;
|
||||
}
|
||||
|
||||
if (healthcheck.Timeout != null) {
|
||||
serviceHC.timeout = healthcheck.Timeout;
|
||||
}
|
||||
|
||||
if (healthcheck.StartPeriod != null) {
|
||||
serviceHC.startPeriod = healthcheck.StartPeriod;
|
||||
}
|
||||
|
||||
if (healthcheck.Retries != null) {
|
||||
serviceHC.retries = healthcheck.Retries;
|
||||
}
|
||||
|
||||
return serviceHC;
|
||||
}
|
||||
|
||||
function buildHealthcheckTest(
|
||||
test: string | string[],
|
||||
): string[] {
|
||||
if (_.isString(test)) {
|
||||
return [ 'CMD-SHELL', test];
|
||||
}
|
||||
return test;
|
||||
}
|
||||
|
||||
function getNanoseconds(timeStr: string): number {
|
||||
return new Duration(timeStr).nanoseconds();
|
||||
}
|
||||
|
||||
export function composeHealthcheckToServiceHealthcheck(
|
||||
healthcheck: ComposeHealthcheck | null | undefined,
|
||||
): ServiceHealthcheck {
|
||||
if (healthcheck == null || healthcheck.disable) {
|
||||
return { test: [ 'NONE' ] };
|
||||
}
|
||||
|
||||
const serviceHC: ServiceHealthcheck = {
|
||||
test: buildHealthcheckTest(healthcheck.test),
|
||||
};
|
||||
|
||||
if (healthcheck.interval != null) {
|
||||
serviceHC.interval = getNanoseconds(healthcheck.interval);
|
||||
}
|
||||
|
||||
if (healthcheck.timeout != null) {
|
||||
serviceHC.timeout = getNanoseconds(healthcheck.timeout);
|
||||
}
|
||||
|
||||
if (healthcheck.startPeriod != null) {
|
||||
serviceHC.startPeriod = getNanoseconds(healthcheck.startPeriod);
|
||||
}
|
||||
|
||||
if (healthcheck.retries != null) {
|
||||
serviceHC.retries = healthcheck.retries;
|
||||
}
|
||||
|
||||
return serviceHC;
|
||||
}
|
||||
|
||||
export function getHealthcheck(
|
||||
composeHealthcheck: ComposeHealthcheck | null | undefined,
|
||||
imageInfo?: Dockerode.ImageInspectInfo,
|
||||
): ServiceHealthcheck {
|
||||
// get the image info healtcheck
|
||||
const imageServiceHealthcheck = dockerHealthcheckToServiceHealthcheck(
|
||||
_.get(imageInfo, 'Config.Healthcheck', null),
|
||||
);
|
||||
const composeServiceHealthcheck = composeHealthcheckToServiceHealthcheck(
|
||||
composeHealthcheck,
|
||||
);
|
||||
|
||||
return _.defaults(composeServiceHealthcheck, imageServiceHealthcheck);
|
||||
}
|
||||
|
||||
export function serviceHealthcheckToDockerHealthcheck(
|
||||
healthcheck: ServiceHealthcheck,
|
||||
): Dockerode.DockerHealthcheck {
|
||||
return {
|
||||
Test: healthcheck.test,
|
||||
Interval: healthcheck.interval,
|
||||
Retries: healthcheck.retries,
|
||||
StartPeriod: healthcheck.startPeriod,
|
||||
Timeout: healthcheck.timeout,
|
||||
};
|
||||
}
|
||||
|
||||
export function getWorkingDir(
|
||||
workingDir: string | null | undefined,
|
||||
imageInfo?: Dockerode.ImageInspectInfo,
|
||||
): string {
|
||||
return (workingDir != null ? workingDir : _.get(imageInfo, 'Config.WorkingDir', ''))
|
||||
.replace(/(^.+)\/$/, '$1');
|
||||
}
|
||||
|
||||
export function getUser(
|
||||
user: string | null | undefined,
|
||||
imageInfo?: Dockerode.ImageInspectInfo,
|
||||
): string {
|
||||
return user != null ? user : _.get(imageInfo, 'Config.User', '');
|
||||
}
|
||||
|
||||
export function sanitiseExposeFromCompose(
|
||||
portStr: string,
|
||||
): string {
|
||||
if (/^[0-9]*$/.test(portStr)) {
|
||||
return `${portStr}/tcp`;
|
||||
}
|
||||
return portStr;
|
||||
}
|
||||
|
||||
export function formatDevice(
|
||||
deviceStr: string,
|
||||
): DockerDevice {
|
||||
const [ pathOnHost, ...parts ] = deviceStr.split(':');
|
||||
let [ pathInContainer, cgroup ] = parts;
|
||||
if (pathInContainer == null) {
|
||||
pathInContainer = pathOnHost;
|
||||
}
|
||||
if (cgroup == null) {
|
||||
cgroup = 'rwm';
|
||||
}
|
||||
return {
|
||||
PathOnHost: pathOnHost,
|
||||
PathInContainer: pathInContainer,
|
||||
CgroupPermissions: cgroup,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Export these strings to a constant lib, to
|
||||
// enable changing them easily
|
||||
// Mutates service
|
||||
export function addFeaturesFromLabels(
|
||||
service: Service,
|
||||
options: DeviceMetadata,
|
||||
): void {
|
||||
if (checkTruthy(service.config.labels['io.resin.features.dbus'])) {
|
||||
service.config.volumes.push('/run/dbus:/host/run/dbus');
|
||||
}
|
||||
|
||||
if (
|
||||
checkTruthy(service.config.labels['io.resin.features.kernel-modules']) &&
|
||||
options.hostPathExists.modules
|
||||
) {
|
||||
service.config.volumes.push('/lib/modules:/lib/modules');
|
||||
}
|
||||
|
||||
if (
|
||||
checkTruthy(service.config.labels['io.resin.features.firmware']) &&
|
||||
options.hostPathExists.firmware
|
||||
) {
|
||||
service.config.volumes.push('/lib/firmware:/lib/firmware');
|
||||
}
|
||||
|
||||
if (checkTruthy(service.config.labels['io.resin.features.balena-socket'])) {
|
||||
service.config.volumes.push('/var/run/balena.sock:/var/run/balena.sock');
|
||||
if (service.config.environment['DOCKER_HOST'] == null) {
|
||||
service.config.environment['DOCKER_HOST'] = 'unix:///var/run/balena.sock';
|
||||
}
|
||||
}
|
||||
|
||||
if (checkTruthy('io.resin.features.resin-api')) {
|
||||
service.config.environment['RESIN_API_KEY'] = options.deviceApiKey;
|
||||
}
|
||||
|
||||
if (checkTruthy(service.config.labels['io.resin.features.supervisor-api'])) {
|
||||
service.config.environment['RESIN_SUPERVISOR_PORT'] = options.listenPort.toString();
|
||||
service.config.environment['RESIN_SUPERVISOR_API_KEY'] = options.apiSecret;
|
||||
if (service.config.networkMode === 'host') {
|
||||
service.config.environment['RESIN_SUPERVISOR_HOST'] = '127.0.0.1';
|
||||
service.config.environment['RESIN_SUPERVISOR_ADDRESS'] = `http://127.0.0.1:${options.listenPort}`;
|
||||
} else {
|
||||
service.config.environment['RESIN_SUPERVISOR_HOST'] = options.supervisorApiHost;
|
||||
service.config.environment['RESIN_SUPERVISOR_ADDRESS'] =
|
||||
`http://${options.supervisorApiHost}:${options.listenPort}`;
|
||||
service.config.networks[constants.supervisorNetworkInterface] = { };
|
||||
}
|
||||
} else {
|
||||
// Ensure that the user hasn't added 'supervisor0' to the service's list
|
||||
// of networks
|
||||
delete service.config.networks[constants.supervisorNetworkInterface];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function serviceUlimitsToDockerUlimits(
|
||||
ulimits: ServiceConfig['ulimits'] | null | undefined,
|
||||
): Array<{ Name: string, Soft: number, Hard: number }> {
|
||||
|
||||
const ret: Array<{ Name: string, Soft: number, Hard: number }> = [];
|
||||
_.each(ulimits, ({ soft, hard }, name) => {
|
||||
ret.push({ Name: name, Soft: soft, Hard: hard });
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function serviceRestartToDockerRestartPolicy(restart: string): { Name: string, MaximumRetryCount: number } {
|
||||
return {
|
||||
Name: restart,
|
||||
MaximumRetryCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function serviceNetworksToDockerNetworks(networks: ServiceConfig['networks'])
|
||||
: Dockerode.ContainerCreateOptions['NetworkingConfig'] {
|
||||
|
||||
const dockerNetworks: Dockerode.ContainerCreateOptions['NetworkingConfig'] = {
|
||||
EndpointsConfig: { },
|
||||
};
|
||||
|
||||
_.each(networks, (net, name) => {
|
||||
// WHY??? This shouldn't be necessary, as we define it above...
|
||||
if (dockerNetworks.EndpointsConfig != null) {
|
||||
dockerNetworks.EndpointsConfig[name] = { };
|
||||
const conf = dockerNetworks.EndpointsConfig[name];
|
||||
conf.IPAMConfig = { };
|
||||
conf.Aliases = [ ];
|
||||
_.each(net, (v, k) => {
|
||||
switch(k) {
|
||||
case 'ipv4Address':
|
||||
conf.IPAMConfig.IPV4Address = v;
|
||||
break;
|
||||
case 'ipv6Address':
|
||||
conf.IPAMConfig.IPV6Address = v;
|
||||
break;
|
||||
case 'linkLocalIps':
|
||||
conf.IPAMConfig.LinkLocalIps = v;
|
||||
break;
|
||||
case 'aliases':
|
||||
conf.Aliases = v;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return dockerNetworks;
|
||||
}
|
||||
|
||||
export function dockerNetworkToServiceNetwork(
|
||||
dockerNetworks: Dockerode.ContainerInspectInfo['NetworkSettings']['Networks'],
|
||||
): ServiceConfig['networks'] {
|
||||
// Take the input network object, filter out any nullish fields, extract things to
|
||||
// the correct level and return
|
||||
const networks: ServiceConfig['networks'] = { };
|
||||
|
||||
_.each(dockerNetworks, (net, name) => {
|
||||
networks[name] = { };
|
||||
if (net.Aliases != null && !_.isEmpty(net.Aliases)) {
|
||||
networks[name].aliases = net.Aliases;
|
||||
}
|
||||
if (net.IPAMConfig != null) {
|
||||
const ipam = net.IPAMConfig;
|
||||
if (ipam.IPv4Address != null && !_.isEmpty(ipam.IPv4Address)) {
|
||||
networks[name].ipv4Address = ipam.IPv4Address;
|
||||
}
|
||||
if (ipam.IPv6Address != null && !_.isEmpty(ipam.IPv6Address)) {
|
||||
networks[name].ipv6Address = ipam.IPv6Address;
|
||||
}
|
||||
if (ipam.LinkLocalIps != null && !_.isEmpty(ipam.LinkLocalIps)) {
|
||||
networks[name].linkLocalIps = ipam.LinkLocalIps;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return networks;
|
||||
}
|
||||
|
||||
// Mutates obj
|
||||
export function normalizeNullValues(obj: Dictionary<any>): void {
|
||||
_.each(obj, (v, k) => {
|
||||
if (v == null) {
|
||||
obj[k] = undefined;
|
||||
} else if(_.isObject(v)) {
|
||||
normalizeNullValues(v);
|
||||
}
|
||||
});
|
||||
}
|
@ -19,3 +19,7 @@ export function envArrayToObject(env: string[]): EnvVarObject {
|
||||
.fromPairs()
|
||||
.value();
|
||||
}
|
||||
|
||||
export function envObjectToArray(env: EnvVarObject): string[] {
|
||||
return _.map(env, (v, k) => `${k}=${v}`);
|
||||
}
|
||||
|
@ -32,3 +32,9 @@ export function UnitNotLoadedError(err: string[]): boolean {
|
||||
export class InvalidNetGatewayError extends TypedError { }
|
||||
|
||||
export class DeltaStillProcessingError extends TypedError { }
|
||||
|
||||
export class InvalidAppIdError extends TypedError {
|
||||
public constructor(public appId: any) {
|
||||
super(`Invalid appId: ${appId}`);
|
||||
}
|
||||
}
|
||||
|
11
src/lib/update-lock.d.ts
vendored
Normal file
11
src/lib/update-lock.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
import TypedError = require('typed-error');
|
||||
|
||||
export interface LockCallback {
|
||||
(appId: number, opts: { force: boolean }, fn: () => void): Promise<void>;
|
||||
}
|
||||
|
||||
export class UpdatesLockedError extends TypedError {
|
||||
}
|
||||
|
||||
export function lock(): LockCallback;
|
||||
export function lockPath(appId: number, serviceName: string): string;
|
@ -11,6 +11,7 @@ const ENV_VAR_KEY_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
||||
const LABEL_NAME_REGEX = /^[a-zA-Z][a-zA-Z0-9\.\-]*$/;
|
||||
|
||||
type NullableString = string | undefined | null;
|
||||
type NullableLiteral = number | NullableString;
|
||||
|
||||
/**
|
||||
* checkInt
|
||||
@ -18,12 +19,14 @@ type NullableString = string | undefined | null;
|
||||
* Check an input string as a number, optionally specifying a requirement
|
||||
* to be positive
|
||||
*/
|
||||
export function checkInt(s: NullableString, options: CheckIntOptions = {}): number | void {
|
||||
export function checkInt(s: NullableLiteral, options: CheckIntOptions = {}): number | void {
|
||||
if (s == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const i = parseInt(s, 10);
|
||||
// parseInt will happily take a number, but the typings won't accept it,
|
||||
// simply cast it here
|
||||
const i = parseInt(s as string, 10);
|
||||
|
||||
if (isNaN(i)) {
|
||||
return;
|
||||
@ -41,7 +44,7 @@ export function checkInt(s: NullableString, options: CheckIntOptions = {}): numb
|
||||
*
|
||||
* Check that a string exists, and is not an empty string, 'null', or 'undefined'
|
||||
*/
|
||||
export function checkString(s: NullableString): string | void {
|
||||
export function checkString(s: NullableLiteral): string | void {
|
||||
if (s == null || !_.isString(s) || _.includes([ 'null', 'undefined', '' ], s)) {
|
||||
return;
|
||||
}
|
||||
|
@ -198,10 +198,6 @@ class ResinLogBackend extends LogBackend {
|
||||
}
|
||||
}
|
||||
|
||||
interface LoggerConstructOptions {
|
||||
eventTracker: EventTracker;
|
||||
}
|
||||
|
||||
interface LoggerSetupOptions {
|
||||
apiEndpoint: string;
|
||||
uuid: string;
|
||||
@ -217,6 +213,10 @@ enum OutputStream {
|
||||
Stderr,
|
||||
}
|
||||
|
||||
interface LoggerConstructOptions {
|
||||
eventTracker: EventTracker;
|
||||
}
|
||||
|
||||
export class Logger {
|
||||
private writeLock: (key: string) => Bluebird<() => void> = Bluebird.promisify(
|
||||
new Lock().async.writeLock,
|
||||
@ -410,9 +410,9 @@ export class Logger {
|
||||
}
|
||||
if (eventObj.service != null &&
|
||||
eventObj.service.serviceName != null &&
|
||||
eventObj.service.image != null
|
||||
eventObj.service.config.image != null
|
||||
) {
|
||||
return `${eventObj.service.serviceName} ${eventObj.service.image}`;
|
||||
return `${eventObj.service.serviceName} ${eventObj.service.config.image}`;
|
||||
}
|
||||
|
||||
if (eventObj.image != null) {
|
||||
@ -427,6 +427,10 @@ export class Logger {
|
||||
return eventObj.volume.name;
|
||||
}
|
||||
|
||||
if (eventObj.fields != null) {
|
||||
return eventObj.fields.join(',');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,24 @@
|
||||
m = require 'mochainon'
|
||||
{ expect } = m.chai
|
||||
Service = require '../src/compose/service'
|
||||
|
||||
describe 'compose/service.cofee', ->
|
||||
_ = require 'lodash'
|
||||
|
||||
{ Service } = require '../src/compose/service'
|
||||
|
||||
configs = {
|
||||
simple: {
|
||||
compose: require('./data/docker-states/simple/compose.json');
|
||||
imageInfo: require('./data/docker-states/simple/imageInfo.json');
|
||||
inspect: require('./data/docker-states/simple/inspect.json');
|
||||
}
|
||||
entrypoint: {
|
||||
compose: require('./data/docker-states/entrypoint/compose.json');
|
||||
imageInfo: require('./data/docker-states/entrypoint/imageInfo.json');
|
||||
inspect: require('./data/docker-states/entrypoint/inspect.json');
|
||||
}
|
||||
}
|
||||
|
||||
describe 'compose/service.coffee', ->
|
||||
|
||||
it 'extends environment variables properly', ->
|
||||
extendEnvVarsOpts = {
|
||||
@ -24,9 +40,9 @@ describe 'compose/service.cofee', ->
|
||||
FOO: 'bar'
|
||||
A_VARIABLE: 'ITS_VALUE'
|
||||
}
|
||||
s = new Service(service, extendEnvVarsOpts)
|
||||
s = Service.fromComposeObject(service, extendEnvVarsOpts)
|
||||
|
||||
expect(s.environment).to.deep.equal({
|
||||
expect(s.config.environment).to.deep.equal({
|
||||
FOO: 'bar'
|
||||
A_VARIABLE: 'ITS_VALUE'
|
||||
RESIN_APP_ID: '23'
|
||||
@ -43,20 +59,20 @@ describe 'compose/service.cofee', ->
|
||||
})
|
||||
|
||||
it 'returns the correct default bind mounts', ->
|
||||
s = new Service({
|
||||
s = Service.fromComposeObject({
|
||||
appId: '1234'
|
||||
serviceName: 'foo'
|
||||
releaseId: 2
|
||||
serviceId: 3
|
||||
imageId: 4
|
||||
})
|
||||
binds = s.defaultBinds()
|
||||
}, { appName: 'foo' })
|
||||
binds = Service.defaultBinds(s.appId, s.serviceName)
|
||||
expect(binds).to.deep.equal([
|
||||
'/tmp/resin-supervisor/services/1234/foo:/tmp/resin'
|
||||
])
|
||||
|
||||
it 'produces the correct port bindings and exposed ports', ->
|
||||
s = new Service({
|
||||
s = Service.fromComposeObject({
|
||||
appId: '1234'
|
||||
serviceName: 'foo'
|
||||
releaseId: 2
|
||||
@ -80,7 +96,7 @@ describe 'compose/service.cofee', ->
|
||||
}
|
||||
}
|
||||
})
|
||||
ports = s.generatePortBindings()
|
||||
ports = s.generateExposeAndPorts()
|
||||
expect(ports.portBindings).to.deep.equal({
|
||||
'2344/tcp': [{
|
||||
HostIp: '',
|
||||
@ -106,7 +122,7 @@ describe 'compose/service.cofee', ->
|
||||
})
|
||||
|
||||
it 'correctly handles port ranges', ->
|
||||
s = new Service({
|
||||
s = Service.fromComposeObject({
|
||||
appId: '1234'
|
||||
serviceName: 'foo'
|
||||
releaseId: 2
|
||||
@ -119,9 +135,9 @@ describe 'compose/service.cofee', ->
|
||||
ports: [
|
||||
'1000-1003:2000-2003'
|
||||
]
|
||||
})
|
||||
}, { appName: 'test' })
|
||||
|
||||
ports = s.generatePortBindings()
|
||||
ports = s.generateExposeAndPorts()
|
||||
expect(ports.portBindings).to.deep.equal({
|
||||
'2000/tcp': [
|
||||
HostIp: ''
|
||||
@ -152,7 +168,7 @@ describe 'compose/service.cofee', ->
|
||||
|
||||
it 'should correctly handle large port ranges', ->
|
||||
@timeout(60000)
|
||||
s = new Service({
|
||||
s = Service.fromComposeObject({
|
||||
appId: '1234'
|
||||
serviceName: 'foo'
|
||||
releaseId: 2
|
||||
@ -162,65 +178,188 @@ describe 'compose/service.cofee', ->
|
||||
'5-65536:5-65536/tcp'
|
||||
'5-65536:5-65536/udp'
|
||||
]
|
||||
})
|
||||
}, { appName: 'test' })
|
||||
|
||||
expect(s.generatePortBindings()).to.not.throw
|
||||
expect(s.generateExposeAndPorts()).to.not.throw
|
||||
|
||||
it 'should correctly report implied exposed ports from portMappings', ->
|
||||
service = Service.fromComposeObject({
|
||||
appId: 123456,
|
||||
serviceId: 123456,
|
||||
serviceName: 'test',
|
||||
ports: [
|
||||
"80:80"
|
||||
"100:100"
|
||||
]
|
||||
}, { appName: 'test' })
|
||||
|
||||
expect(service.config).to.have.property('expose').that.deep.equals(['80/tcp', '100/tcp'])
|
||||
|
||||
describe 'parseMemoryNumber()', ->
|
||||
makeComposeServiceWithLimit = (memLimit) ->
|
||||
new Service(
|
||||
Service.fromComposeObject({
|
||||
appId: 123456
|
||||
serviceId: 123456
|
||||
serviceName: 'foobar'
|
||||
memLimit: memLimit
|
||||
)
|
||||
mem_limit: memLimit
|
||||
}, { appName: 'test' })
|
||||
|
||||
it 'should correctly parse memory number strings without a unit', ->
|
||||
expect(makeComposeServiceWithLimit('64').memLimit).to.equal(64)
|
||||
expect(makeComposeServiceWithLimit('64').config.memLimit).to.equal(64)
|
||||
|
||||
it 'should correctly apply the default value', ->
|
||||
expect(makeComposeServiceWithLimit(undefined).memLimit).to.equal(0)
|
||||
expect(makeComposeServiceWithLimit(undefined).config.memLimit).to.equal(0)
|
||||
|
||||
it 'should correctly support parsing numbers as memory limits', ->
|
||||
expect(makeComposeServiceWithLimit(64).memLimit).to.equal(64)
|
||||
expect(makeComposeServiceWithLimit(64).config.memLimit).to.equal(64)
|
||||
|
||||
it 'should correctly parse memory number strings that use a byte unit', ->
|
||||
expect(makeComposeServiceWithLimit('64b').memLimit).to.equal(64)
|
||||
expect(makeComposeServiceWithLimit('64B').memLimit).to.equal(64)
|
||||
expect(makeComposeServiceWithLimit('64b').config.memLimit).to.equal(64)
|
||||
expect(makeComposeServiceWithLimit('64B').config.memLimit).to.equal(64)
|
||||
|
||||
it 'should correctly parse memory number strings that use a kilobyte unit', ->
|
||||
expect(makeComposeServiceWithLimit('64k').memLimit).to.equal(65536)
|
||||
expect(makeComposeServiceWithLimit('64K').memLimit).to.equal(65536)
|
||||
expect(makeComposeServiceWithLimit('64k').config.memLimit).to.equal(65536)
|
||||
expect(makeComposeServiceWithLimit('64K').config.memLimit).to.equal(65536)
|
||||
|
||||
expect(makeComposeServiceWithLimit('64kb').memLimit).to.equal(65536)
|
||||
expect(makeComposeServiceWithLimit('64Kb').memLimit).to.equal(65536)
|
||||
expect(makeComposeServiceWithLimit('64kb').config.memLimit).to.equal(65536)
|
||||
expect(makeComposeServiceWithLimit('64Kb').config.memLimit).to.equal(65536)
|
||||
|
||||
it 'should correctly parse memory number strings that use a megabyte unit', ->
|
||||
expect(makeComposeServiceWithLimit('64m').memLimit).to.equal(67108864)
|
||||
expect(makeComposeServiceWithLimit('64M').memLimit).to.equal(67108864)
|
||||
expect(makeComposeServiceWithLimit('64m').config.memLimit).to.equal(67108864)
|
||||
expect(makeComposeServiceWithLimit('64M').config.memLimit).to.equal(67108864)
|
||||
|
||||
expect(makeComposeServiceWithLimit('64mb').memLimit).to.equal(67108864)
|
||||
expect(makeComposeServiceWithLimit('64Mb').memLimit).to.equal(67108864)
|
||||
expect(makeComposeServiceWithLimit('64mb').config.memLimit).to.equal(67108864)
|
||||
expect(makeComposeServiceWithLimit('64Mb').config.memLimit).to.equal(67108864)
|
||||
|
||||
it 'should correctly parse memory number strings that use a gigabyte unit', ->
|
||||
expect(makeComposeServiceWithLimit('64g').memLimit).to.equal(68719476736)
|
||||
expect(makeComposeServiceWithLimit('64G').memLimit).to.equal(68719476736)
|
||||
expect(makeComposeServiceWithLimit('64g').config.memLimit).to.equal(68719476736)
|
||||
expect(makeComposeServiceWithLimit('64G').config.memLimit).to.equal(68719476736)
|
||||
|
||||
expect(makeComposeServiceWithLimit('64gb').memLimit).to.equal(68719476736)
|
||||
expect(makeComposeServiceWithLimit('64Gb').memLimit).to.equal(68719476736)
|
||||
expect(makeComposeServiceWithLimit('64gb').config.memLimit).to.equal(68719476736)
|
||||
expect(makeComposeServiceWithLimit('64Gb').config.memLimit).to.equal(68719476736)
|
||||
|
||||
describe 'getWorkingDir', ->
|
||||
makeComposeServiceWithWorkdir = (workdir) ->
|
||||
new Service(
|
||||
Service.fromComposeObject({
|
||||
appId: 123456,
|
||||
serviceId: 123456,
|
||||
serviceName: 'foobar'
|
||||
workingDir: workdir
|
||||
)
|
||||
}, { appName: 'test' })
|
||||
|
||||
it 'should remove a trailing slash', ->
|
||||
expect(makeComposeServiceWithWorkdir('/usr/src/app/').workingDir).to.equal('/usr/src/app')
|
||||
expect(makeComposeServiceWithWorkdir('/').workingDir).to.equal('/')
|
||||
expect(makeComposeServiceWithWorkdir('/usr/src/app').workingDir).to.equal('/usr/src/app')
|
||||
expect(makeComposeServiceWithWorkdir('').workingDir).to.equal('')
|
||||
expect(makeComposeServiceWithWorkdir('/usr/src/app/').config.workingDir).to.equal('/usr/src/app')
|
||||
expect(makeComposeServiceWithWorkdir('/').config.workingDir).to.equal('/')
|
||||
expect(makeComposeServiceWithWorkdir('/usr/src/app').config.workingDir).to.equal('/usr/src/app')
|
||||
expect(makeComposeServiceWithWorkdir('').config.workingDir).to.equal('')
|
||||
|
||||
describe 'Docker <-> Compose config', ->
|
||||
|
||||
omitConfigForComparison = (config) ->
|
||||
return _.omit(config, ['running', 'networks'])
|
||||
|
||||
it 'should be identical when converting a simple service', ->
|
||||
composeSvc = Service.fromComposeObject(configs.simple.compose, configs.simple.imageInfo)
|
||||
dockerSvc = Service.fromDockerContainer(configs.simple.inspect)
|
||||
|
||||
composeConfig = omitConfigForComparison(composeSvc.config)
|
||||
dockerConfig = omitConfigForComparison(dockerSvc.config)
|
||||
expect(composeConfig).to.deep.equal(dockerConfig)
|
||||
|
||||
expect(dockerSvc.isEqualConfig(composeSvc)).to.be.true
|
||||
|
||||
|
||||
it 'should correct convert formats with a null entrypoint', ->
|
||||
composeSvc = Service.fromComposeObject(configs.entrypoint.compose, configs.entrypoint.imageInfo)
|
||||
dockerSvc = Service.fromDockerContainer(configs.entrypoint.inspect)
|
||||
|
||||
composeConfig = omitConfigForComparison(composeSvc.config)
|
||||
dockerConfig = omitConfigForComparison(dockerSvc.config)
|
||||
expect(composeConfig).to.deep.equal(dockerConfig)
|
||||
|
||||
expect(dockerSvc.isEqualConfig(composeSvc)).to.equals(true)
|
||||
|
||||
describe 'Networks', ->
|
||||
|
||||
it 'should correctly convert networks from compose to docker format', ->
|
||||
makeComposeServiceWithNetwork = (networks) ->
|
||||
Service.fromComposeObject({
|
||||
appId: 123456,
|
||||
serviceId: 123456,
|
||||
serviceName: 'test',
|
||||
networks
|
||||
}, { appName: 'test' })
|
||||
|
||||
expect(makeComposeServiceWithNetwork({
|
||||
"balena": {
|
||||
"ipv4Address": "1.2.3.4"
|
||||
}
|
||||
}).toDockerContainer().NetworkingConfig).to.deep.equal({
|
||||
EndpointsConfig: {
|
||||
"123456_balena": {
|
||||
IPAMConfig: {
|
||||
IPV4Address: "1.2.3.4"
|
||||
},
|
||||
Aliases: []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect(makeComposeServiceWithNetwork({
|
||||
balena: {
|
||||
aliases: [ 'test', '1123']
|
||||
ipv4Address: '1.2.3.4'
|
||||
ipv6Address: '5.6.7.8'
|
||||
linkLocalIps: [ '123.123.123' ]
|
||||
}
|
||||
}).toDockerContainer().NetworkingConfig).to.deep.equal({
|
||||
EndpointsConfig: {
|
||||
"123456_balena": {
|
||||
IPAMConfig: {
|
||||
IPV4Address: '1.2.3.4'
|
||||
IPV6Address: '5.6.7.8'
|
||||
LinkLocalIps: [ '123.123.123' ]
|
||||
}
|
||||
Aliases: [ 'test', '1123' ]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it 'should correctly convert Docker format to service format', ->
|
||||
dockerCfg = require('./data/docker-states/simple/inspect.json');
|
||||
makeServiceFromDockerWithNetwork = (networks) ->
|
||||
Service.fromDockerContainer(
|
||||
newConfig = _.cloneDeep(dockerCfg);
|
||||
newConfig.NetworkSettings = { Networks: networks }
|
||||
)
|
||||
|
||||
expect(makeServiceFromDockerWithNetwork({
|
||||
'123456_balena': {
|
||||
IPAMConfig: {
|
||||
IPv4Address: "1.2.3.4"
|
||||
},
|
||||
Aliases: []
|
||||
}
|
||||
}).config.networks).to.deep.equal({
|
||||
'123456_balena': {
|
||||
"ipv4Address": "1.2.3.4"
|
||||
}
|
||||
})
|
||||
|
||||
expect(makeServiceFromDockerWithNetwork({
|
||||
'123456_balena': {
|
||||
IPAMConfig: {
|
||||
IPv4Address: '1.2.3.4'
|
||||
IPv6Address: '5.6.7.8'
|
||||
LinkLocalIps: [ '123.123.123' ]
|
||||
}
|
||||
Aliases: [ 'test', '1123' ]
|
||||
}
|
||||
}).config.networks).to.deep.equal({
|
||||
'123456_balena': {
|
||||
ipv4Address: '1.2.3.4'
|
||||
ipv6Address: '5.6.7.8'
|
||||
linkLocalIps: [ '123.123.123' ]
|
||||
aliases: [ 'test', '1123' ]
|
||||
}
|
||||
})
|
||||
|
@ -11,7 +11,7 @@ DB = require('../src/db')
|
||||
Config = require('../src/config')
|
||||
{ RPiConfigBackend } = require('../src/config/backend')
|
||||
|
||||
Service = require '../src/compose/service'
|
||||
{ Service } = require '../src/compose/service'
|
||||
|
||||
mockedInitialConfig = {
|
||||
'RESIN_SUPERVISOR_CONNECTIVITY_CHECK': 'true'
|
||||
@ -199,9 +199,9 @@ describe 'deviceState', ->
|
||||
eventTracker = {
|
||||
track: console.log
|
||||
}
|
||||
stub(Service.prototype, 'extendEnvVars').callsFake (env) ->
|
||||
@environment['ADDITIONAL_ENV_VAR'] = 'foo'
|
||||
return @environment
|
||||
stub(Service, 'extendEnvVars').callsFake (env) ->
|
||||
env['ADDITIONAL_ENV_VAR'] = 'foo'
|
||||
return env
|
||||
@deviceState = new DeviceState({ @db, @config, eventTracker })
|
||||
stub(@deviceState.applications.docker, 'getNetworkGateway').returns(Promise.resolve('172.17.0.1'))
|
||||
stub(@deviceState.applications.images, 'inspectByName').callsFake ->
|
||||
@ -215,7 +215,7 @@ describe 'deviceState', ->
|
||||
@config.init()
|
||||
|
||||
after ->
|
||||
Service.prototype.extendEnvVars.restore()
|
||||
Service.extendEnvVars.restore()
|
||||
@deviceState.applications.docker.getNetworkGateway.restore()
|
||||
@deviceState.applications.images.inspectByName.restore()
|
||||
|
||||
@ -229,7 +229,7 @@ describe 'deviceState', ->
|
||||
testTarget = _.cloneDeep(testTarget1)
|
||||
testTarget.local.apps['1234'].services = _.map testTarget.local.apps['1234'].services, (s) ->
|
||||
s.imageName = s.image
|
||||
return new Service(s)
|
||||
return Service.fromComposeObject(s, { appName: 'superapp' })
|
||||
# We serialize and parse JSON to avoid checking fields that are functions or undefined
|
||||
expect(JSON.parse(JSON.stringify(targetState))).to.deep.equal(JSON.parse(JSON.stringify(testTarget)))
|
||||
@deviceState.applications.images.save.restore()
|
||||
@ -261,7 +261,7 @@ describe 'deviceState', ->
|
||||
.then (imageName) ->
|
||||
s.image = imageName
|
||||
s.imageName = imageName
|
||||
new Service(s)
|
||||
Service.fromComposeObject(s, { appName: 'supertest' })
|
||||
.then (services) =>
|
||||
testTarget.local.apps['1234'].services = services
|
||||
@deviceState.setTarget(testTarget2)
|
||||
|
@ -10,9 +10,7 @@ prepare = require './lib/prepare'
|
||||
DeviceState = require '../src/device-state'
|
||||
DB = require('../src/db')
|
||||
Config = require('../src/config')
|
||||
Service = require '../src/compose/service'
|
||||
|
||||
{ currentState, targetState, availableImages } = require './lib/application-manager-test-states'
|
||||
{ Service } = require '../src/compose/service'
|
||||
|
||||
appDBFormatNormalised = {
|
||||
appId: 1234
|
||||
@ -103,6 +101,8 @@ dependentStateFormatNormalised = {
|
||||
imageId: 45
|
||||
}
|
||||
|
||||
currentState = targetState = availableImages = null
|
||||
|
||||
dependentDBFormat = {
|
||||
appId: 1234
|
||||
image: 'foo/bar:latest'
|
||||
@ -137,19 +137,19 @@ describe 'ApplicationManager', ->
|
||||
}
|
||||
})
|
||||
stub(@applications.docker, 'getNetworkGateway').returns(Promise.resolve('172.17.0.1'))
|
||||
stub(Service.prototype, 'extendEnvVars').callsFake (env) ->
|
||||
@environment['ADDITIONAL_ENV_VAR'] = 'foo'
|
||||
return @environment
|
||||
stub(Service, 'extendEnvVars').callsFake (env) ->
|
||||
env['ADDITIONAL_ENV_VAR'] = 'foo'
|
||||
return env
|
||||
@normaliseCurrent = (current) ->
|
||||
Promise.map current.local.apps, (app) ->
|
||||
Promise.map app.services, (service) ->
|
||||
new Service(service)
|
||||
Service.fromComposeObject(service, { appName: 'test' })
|
||||
.then (normalisedServices) ->
|
||||
appCloned = _.clone(app)
|
||||
appCloned = _.cloneDeep(app)
|
||||
appCloned.services = normalisedServices
|
||||
return appCloned
|
||||
.then (normalisedApps) ->
|
||||
currentCloned = _.clone(current)
|
||||
currentCloned = _.cloneDeep(current)
|
||||
currentCloned.local.apps = normalisedApps
|
||||
return currentCloned
|
||||
|
||||
@ -163,9 +163,9 @@ describe 'ApplicationManager', ->
|
||||
# We mock what createTargetService does when an image is available
|
||||
targetCloned.local.apps = _.map apps, (app) ->
|
||||
app.services = _.map app.services, (service) ->
|
||||
img = _.find(available, (i) -> i.name == service.image)
|
||||
img = _.find(available, (i) -> i.name == service.config.image)
|
||||
if img?
|
||||
service.image = img.dockerImageId
|
||||
service.config.image = img.dockerImageId
|
||||
return service
|
||||
return app
|
||||
return targetCloned
|
||||
@ -173,10 +173,13 @@ describe 'ApplicationManager', ->
|
||||
.then =>
|
||||
@config.init()
|
||||
|
||||
beforeEach ->
|
||||
{ currentState, targetState, availableImages } = require './lib/application-manager-test-states'
|
||||
|
||||
after ->
|
||||
@applications.images.inspectByName.restore()
|
||||
@applications.docker.getNetworkGateway.restore()
|
||||
Service.prototype.extendEnvVars.restore()
|
||||
Service.extendEnvVars.restore()
|
||||
|
||||
it 'infers a start step when all that changes is a running state', ->
|
||||
Promise.join(
|
||||
@ -366,7 +369,7 @@ describe 'ApplicationManager', ->
|
||||
opts = { imageInfo: { Config: { Cmd: [ 'someCommand' ], Entrypoint: [ 'theEntrypoint' ] } } }
|
||||
appStateFormatWithDefaults.services = _.map appStateFormatWithDefaults.services, (service) ->
|
||||
service.imageName = service.image
|
||||
return new Service(service, opts)
|
||||
return Service.fromComposeObject(service, opts)
|
||||
expect(JSON.parse(JSON.stringify(app))).to.deep.equal(JSON.parse(JSON.stringify(appStateFormatWithDefaults)))
|
||||
|
||||
it 'converts a dependent app in DB format into state format', ->
|
||||
|
13
test/data/docker-states/entrypoint/compose.json
Normal file
13
test/data/docker-states/entrypoint/compose.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"imageId": 478890,
|
||||
"serviceName": "main",
|
||||
"image": "sha256:7f54fa690ce19a1f625b04479ae1f12f44d36112a74be7edfefa777ecfdb194b",
|
||||
"running": true,
|
||||
"environment": {},
|
||||
"labels": {},
|
||||
"appId": 1011165,
|
||||
"releaseId": 597007,
|
||||
"serviceId": 43697,
|
||||
"commit": "ff300a701054ac15281de1f9c0e84b8c",
|
||||
"imageName": "registry2.resin.io/v2/bf9c649a5ac2fe147bbe350875042388@sha256:3a5c17b715b4f8265539c1a006dd1abdd2ff3b758aa23df99f77c792f40c3d43"
|
||||
}
|
102
test/data/docker-states/entrypoint/imageInfo.json
Normal file
102
test/data/docker-states/entrypoint/imageInfo.json
Normal file
@ -0,0 +1,102 @@
|
||||
{
|
||||
"serviceName": "main",
|
||||
"imageInfo": {
|
||||
"Id": "sha256:7f54fa690ce19a1f625b04479ae1f12f44d36112a74be7edfefa777ecfdb194b",
|
||||
"RepoTags": [],
|
||||
"RepoDigests": [
|
||||
"registry2.resin.io/v2/90e3bf4c3dc1e59221b7b3e659a327f6@sha256:3a5c17b715b4f8265539c1a006dd1abdd2ff3b758aa23df99f77c792f40c3d43"
|
||||
],
|
||||
"Parent": "",
|
||||
"Comment": "",
|
||||
"Created": "2018-09-12T13:00:43.974720835Z",
|
||||
"Container": "07cb0400e218ae235e67cf1fd283dc09559f57fbe2a36f5cc89302388371781c",
|
||||
"ContainerConfig": {
|
||||
"Hostname": "137f767087a2",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"#(nop) ",
|
||||
"CMD [\"/bin/sh\" \"-c\" \"while true; do echo 'hello'; sleep 5; done;\"]"
|
||||
],
|
||||
"ArgsEscaped": true,
|
||||
"Image": "sha256:8d68949dbddcb3ab1a61caeffa0aa1a6e27425ecc4f7665d04d8d0e5bfa03298",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": null,
|
||||
"OnBuild": [],
|
||||
"Labels": {}
|
||||
},
|
||||
"DockerVersion": "17.05.0-ce",
|
||||
"Author": "",
|
||||
"Config": {
|
||||
"Hostname": "137f767087a2",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"while true; do echo 'hello'; sleep 5; done;"
|
||||
],
|
||||
"ArgsEscaped": true,
|
||||
"Image": "sha256:8d68949dbddcb3ab1a61caeffa0aa1a6e27425ecc4f7665d04d8d0e5bfa03298",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": null,
|
||||
"OnBuild": [],
|
||||
"Labels": {}
|
||||
},
|
||||
"Architecture": "arm64",
|
||||
"Os": "linux",
|
||||
"Size": 104966431,
|
||||
"VirtualSize": 104966431,
|
||||
"GraphDriver": {
|
||||
"Data": null,
|
||||
"Name": "aufs"
|
||||
},
|
||||
"RootFS": {
|
||||
"Type": "layers",
|
||||
"Layers": [
|
||||
"sha256:a3075e9def48840598abcfe08c1ee564c989d1014d847082d950dca2c94098ec",
|
||||
"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
||||
]
|
||||
},
|
||||
"Metadata": {
|
||||
"LastTagTime": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
},
|
||||
"appName": "supervisortest",
|
||||
"supervisorApiHost": "172.17.0.1",
|
||||
"hostPathExists": {
|
||||
"firmware": true,
|
||||
"modules": true
|
||||
},
|
||||
"hostnameOnHost": "7dadabd",
|
||||
"uuid": "a7feb967fac7f559ccf2a006a36bcf5d",
|
||||
"listenPort": "48484",
|
||||
"name": "Office",
|
||||
"apiSecret": "d4bf8369519c32adaa5dd1f84367aa817403f2a3ce976be9c9bacd4d344fdd",
|
||||
"deviceApiKey": "ff89e1d8db58a7ca52a435f2adea319a",
|
||||
"version": "7.18.0",
|
||||
"deviceType": "raspberrypi3",
|
||||
"osVersion": "Resin OS 2.13.6+rev1"
|
||||
}
|
210
test/data/docker-states/entrypoint/inspect.json
Normal file
210
test/data/docker-states/entrypoint/inspect.json
Normal file
@ -0,0 +1,210 @@
|
||||
{
|
||||
"Id": "52cfd7a64d50236376741dd2578c4fbb0178d90e2e4fae55f3e14cd905e9ac9e",
|
||||
"Created": "2018-09-12T14:38:42.696028995Z",
|
||||
"Path": "/bin/sh",
|
||||
"Args": [
|
||||
"-c",
|
||||
"while true; do echo 'hello'; sleep 5; done;"
|
||||
],
|
||||
"State": {
|
||||
"Status": "exited",
|
||||
"Running": false,
|
||||
"Paused": false,
|
||||
"Restarting": false,
|
||||
"OOMKilled": false,
|
||||
"Dead": false,
|
||||
"Pid": 0,
|
||||
"ExitCode": 137,
|
||||
"Error": "",
|
||||
"StartedAt": "2018-09-12T14:38:45.408574694Z",
|
||||
"FinishedAt": "2018-09-12T14:38:46.462783621Z"
|
||||
},
|
||||
"Image": "sha256:7f54fa690ce19a1f625b04479ae1f12f44d36112a74be7edfefa777ecfdb194b",
|
||||
"ResolvConfPath": "/var/lib/docker/containers/52cfd7a64d50236376741dd2578c4fbb0178d90e2e4fae55f3e14cd905e9ac9e/resolv.conf",
|
||||
"HostnamePath": "/var/lib/docker/containers/52cfd7a64d50236376741dd2578c4fbb0178d90e2e4fae55f3e14cd905e9ac9e/hostname",
|
||||
"HostsPath": "/var/lib/docker/containers/52cfd7a64d50236376741dd2578c4fbb0178d90e2e4fae55f3e14cd905e9ac9e/hosts",
|
||||
"LogPath": "",
|
||||
"Name": "/nifty_swartz",
|
||||
"RestartCount": 0,
|
||||
"Driver": "aufs",
|
||||
"Platform": "linux",
|
||||
"MountLabel": "",
|
||||
"ProcessLabel": "",
|
||||
"AppArmorProfile": "",
|
||||
"ExecIDs": null,
|
||||
"HostConfig": {
|
||||
"Binds": [
|
||||
"/tmp/resin-supervisor/services/1011165/main:/tmp/resin"
|
||||
],
|
||||
"ContainerIDFile": "",
|
||||
"LogConfig": {
|
||||
"Type": "journald",
|
||||
"Config": {}
|
||||
},
|
||||
"NetworkMode": "1011165_default",
|
||||
"PortBindings": {},
|
||||
"RestartPolicy": {
|
||||
"Name": "always",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
"AutoRemove": false,
|
||||
"VolumeDriver": "",
|
||||
"VolumesFrom": null,
|
||||
"CapAdd": [],
|
||||
"CapDrop": [],
|
||||
"Dns": [],
|
||||
"DnsOptions": [],
|
||||
"DnsSearch": [],
|
||||
"ExtraHosts": [],
|
||||
"GroupAdd": [],
|
||||
"IpcMode": "shareable",
|
||||
"Cgroup": "",
|
||||
"Links": null,
|
||||
"OomScoreAdj": 0,
|
||||
"PidMode": "",
|
||||
"Privileged": false,
|
||||
"PublishAllPorts": false,
|
||||
"ReadonlyRootfs": false,
|
||||
"SecurityOpt": [],
|
||||
"UTSMode": "",
|
||||
"UsernsMode": "",
|
||||
"ShmSize": 67108864,
|
||||
"Runtime": "runc",
|
||||
"ConsoleSize": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"Isolation": "",
|
||||
"CpuShares": 0,
|
||||
"Memory": 0,
|
||||
"NanoCpus": 0,
|
||||
"CgroupParent": "",
|
||||
"BlkioWeight": 0,
|
||||
"BlkioWeightDevice": null,
|
||||
"BlkioDeviceReadBps": null,
|
||||
"BlkioDeviceWriteBps": null,
|
||||
"BlkioDeviceReadIOps": null,
|
||||
"BlkioDeviceWriteIOps": null,
|
||||
"CpuPeriod": 0,
|
||||
"CpuQuota": 0,
|
||||
"CpuRealtimePeriod": 0,
|
||||
"CpuRealtimeRuntime": 0,
|
||||
"CpusetCpus": "",
|
||||
"CpusetMems": "",
|
||||
"Devices": [],
|
||||
"DeviceCgroupRules": null,
|
||||
"DiskQuota": 0,
|
||||
"KernelMemory": 0,
|
||||
"MemoryReservation": 0,
|
||||
"MemorySwap": 0,
|
||||
"MemorySwappiness": -1,
|
||||
"OomKillDisable": false,
|
||||
"PidsLimit": 0,
|
||||
"Ulimits": [],
|
||||
"CpuCount": 0,
|
||||
"CpuPercent": 0,
|
||||
"IOMaximumIOps": 0,
|
||||
"IOMaximumBandwidth": 0,
|
||||
"Init": false
|
||||
},
|
||||
"GraphDriver": {
|
||||
"Data": null,
|
||||
"Name": "aufs"
|
||||
},
|
||||
"Mounts": [
|
||||
{
|
||||
"Type": "bind",
|
||||
"Source": "/tmp/resin-supervisor/services/1011165/main",
|
||||
"Destination": "/tmp/resin",
|
||||
"Mode": "",
|
||||
"RW": true,
|
||||
"Propagation": "rprivate"
|
||||
}
|
||||
],
|
||||
"Config": {
|
||||
"Hostname": "52cfd7a64d50",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"Tty": true,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"RESIN_APP_ID=1011165",
|
||||
"RESIN_APP_NAME=supervisortest",
|
||||
"RESIN_SERVICE_NAME=main",
|
||||
"RESIN_DEVICE_UUID=a7feb967fac7f559ccf2a006a36bcf5d",
|
||||
"RESIN_DEVICE_TYPE=raspberrypi3",
|
||||
"RESIN_HOST_OS_VERSION=Resin OS 2.13.6+rev1",
|
||||
"RESIN_SUPERVISOR_VERSION=7.18.0",
|
||||
"RESIN_APP_LOCK_PATH=/tmp/resin/resin-updates.lock",
|
||||
"RESIN_SERVICE_KILL_ME_PATH=/tmp/resin/resin-kill-me",
|
||||
"RESIN=1",
|
||||
"USER=root",
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"while true; do echo 'hello'; sleep 5; done;"
|
||||
],
|
||||
"Healthcheck": {
|
||||
"Test": [
|
||||
"NONE"
|
||||
]
|
||||
},
|
||||
"Image": "sha256:7f54fa690ce19a1f625b04479ae1f12f44d36112a74be7edfefa777ecfdb194b",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": null,
|
||||
"OnBuild": null,
|
||||
"Labels": {
|
||||
"io.resin.app-id": "1011165",
|
||||
"io.resin.service-id": "43697",
|
||||
"io.resin.service-name": "main",
|
||||
"io.resin.supervised": "true"
|
||||
},
|
||||
"StopTimeout": 0
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Bridge": "",
|
||||
"SandboxID": "bf4952b7f6695a8f05da1807946723b37e1041b8f41588678d6dece310270990",
|
||||
"HairpinMode": false,
|
||||
"LinkLocalIPv6Address": "",
|
||||
"LinkLocalIPv6PrefixLen": 0,
|
||||
"Ports": {},
|
||||
"SandboxKey": "/var/run/balena/netns/bf4952b7f669",
|
||||
"SecondaryIPAddresses": null,
|
||||
"SecondaryIPv6Addresses": null,
|
||||
"EndpointID": "",
|
||||
"Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"IPAddress": "",
|
||||
"IPPrefixLen": 0,
|
||||
"IPv6Gateway": "",
|
||||
"MacAddress": "",
|
||||
"Networks": {
|
||||
"1011165_default": {
|
||||
"IPAMConfig": {},
|
||||
"Links": null,
|
||||
"Aliases": [
|
||||
"main",
|
||||
"52cfd7a64d50"
|
||||
],
|
||||
"NetworkID": "f88716ed3d340f1b9aa61df22d92ce6ad8aa752d8bf8e4aa6e74142dea677465",
|
||||
"EndpointID": "",
|
||||
"Gateway": "",
|
||||
"IPAddress": "",
|
||||
"IPPrefixLen": 0,
|
||||
"IPv6Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"MacAddress": "",
|
||||
"DriverOpts": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
test/data/docker-states/simple/compose.json
Normal file
11
test/data/docker-states/simple/compose.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"imageId": 431889,
|
||||
"serviceName": "main",
|
||||
"image": "sha256:f9e0fa6e3e68caedbcbb4ef35d5a8dce2a8d33e39cc94115d567800f25d826f4",
|
||||
"running": true,
|
||||
"appId": 1011165,
|
||||
"releaseId": 572579,
|
||||
"serviceId": 43697,
|
||||
"commit": "b14730d691467ab0f448a308af6bf839",
|
||||
"imageName": "registry2.resin.io/v2/8ddbe4a22e881f06def0f31400bfb6de@sha256:09b0db9e71cead5f91107fc9254b1af7088444cc6da55afa2da595940f72a34a"
|
||||
}
|
128
test/data/docker-states/simple/imageInfo.json
Normal file
128
test/data/docker-states/simple/imageInfo.json
Normal file
@ -0,0 +1,128 @@
|
||||
{
|
||||
"serviceName": "main",
|
||||
"imageInfo": {
|
||||
"Id": "sha256:f9e0fa6e3e68caedbcbb4ef35d5a8dce2a8d33e39cc94115d567800f25d826f4",
|
||||
"RepoTags": [],
|
||||
"RepoDigests": [
|
||||
"registry2.resin.io/v2/8ddbe4a22e881f06def0f31400bfb6de@sha256:09b0db9e71cead5f91107fc9254b1af7088444cc6da55afa2da595940f72a34a"
|
||||
],
|
||||
"Parent": "",
|
||||
"Comment": "",
|
||||
"Created": "2018-08-15T12:43:06.43392045Z",
|
||||
"Container": "b6cc9227f272b905512a58926b6d515b38de34b604386031aa3c21e94d9dbb4a",
|
||||
"ContainerConfig": {
|
||||
"Hostname": "f15babe8256c",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"UDEV=on"
|
||||
],
|
||||
"Cmd": [
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"#(nop) ",
|
||||
"CMD [\"/bin/sh\" \"-c\" \"./call-supervisor.sh\"]"
|
||||
],
|
||||
"ArgsEscaped": true,
|
||||
"Image": "sha256:828d725f5e6d09ee9abc214f6c11fadf69192ba4871b050984cc9c4cec37b208",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "/usr/src/app",
|
||||
"Entrypoint": [
|
||||
"/usr/bin/entry.sh"
|
||||
],
|
||||
"OnBuild": [],
|
||||
"Labels": {
|
||||
"io.resin.architecture": "armv7hf",
|
||||
"io.resin.device-type": "raspberry-pi2",
|
||||
"io.resin.qemu.version": "2.9.0.resin1-arm"
|
||||
}
|
||||
},
|
||||
"DockerVersion": "17.05.0-ce",
|
||||
"Author": "",
|
||||
"Config": {
|
||||
"Hostname": "f15babe8256c",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"UDEV=on"
|
||||
],
|
||||
"Cmd": [
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"./call-supervisor.sh"
|
||||
],
|
||||
"ArgsEscaped": true,
|
||||
"Image": "sha256:828d725f5e6d09ee9abc214f6c11fadf69192ba4871b050984cc9c4cec37b208",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "/usr/src/app",
|
||||
"Entrypoint": [
|
||||
"/usr/bin/entry.sh"
|
||||
],
|
||||
"OnBuild": [],
|
||||
"Labels": {
|
||||
"io.resin.architecture": "armv7hf",
|
||||
"io.resin.device-type": "raspberry-pi2",
|
||||
"io.resin.qemu.version": "2.9.0.resin1-arm"
|
||||
}
|
||||
},
|
||||
"Architecture": "arm64",
|
||||
"Os": "linux",
|
||||
"Size": 38692178,
|
||||
"VirtualSize": 38692178,
|
||||
"GraphDriver": {
|
||||
"Data": null,
|
||||
"Name": "aufs"
|
||||
},
|
||||
"RootFS": {
|
||||
"Type": "layers",
|
||||
"Layers": [
|
||||
"sha256:7c30ac6ce381873d5388b7d23b346af7d1e5f6af000a84b97e6203ed9e6dcab2",
|
||||
"sha256:450b73019ae79e6a99774fcd37c18769f95065c8b271be936dfb3f93afadc4a8",
|
||||
"sha256:54742c4169d9ff56328f60aea070a6c67f507d4b82389f3432a2c5d92742223c",
|
||||
"sha256:1f609e3b26a8772335a4658ee1980e9b34019d55ac8ee5dcb281f1d4cd5e8e9c",
|
||||
"sha256:c062d099c615146d6dc095254c11babbe120edf06d66419aeef955b88d8543ce",
|
||||
"sha256:2b57e2af57a24bcbafc5bfa04d928ab11695232df7942c294a7c1ca115ba42ca",
|
||||
"sha256:6eb88b69d374abd577336ddc8ab01b25b970020537bb6605676496dcb041b462",
|
||||
"sha256:e410a938934a7ad4f44334cceca97084df7405a5654eefc30cede0aa5bbe8394",
|
||||
"sha256:201b3de34ff5e12e1ada0331d0ce4d0b041059ff9350cb26d8ee15c7be50fe57",
|
||||
"sha256:115ca022a36d9de6fb7a4ba3917545711a0c20564dacf3b189567f68e381e73e",
|
||||
"sha256:4fdd323f81af620a5f19a544a1caa21093f2567d83671ebf24fbde77cefde67c",
|
||||
"sha256:e9a758756b9b5537fe624f87d02f16dd7a523c27a1688de9820f5d2157e5d37d",
|
||||
"sha256:2d082f247d32fd789dbd46fc50054e16810c79a64fa9ea47e0e4845226a0e011",
|
||||
"sha256:6ab67aaf666bfb7001ab93deffe785f24775f4e0da3d6d421ad6096ba869fd0d"
|
||||
]
|
||||
},
|
||||
"Metadata": {
|
||||
"LastTagTime": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
},
|
||||
"appName": "supervisortest",
|
||||
"supervisorApiHost": "172.17.0.1",
|
||||
"hostPathExists": {
|
||||
"firmware": true,
|
||||
"modules": true
|
||||
},
|
||||
"hostnameOnHost": "7dadabd",
|
||||
"uuid": "7dadabd4edec3067948d5952c2f2f26f",
|
||||
"listenPort": "48484",
|
||||
"name": "Office",
|
||||
"apiSecret": "d4bf8369519c32adaa5dd1f84367aa817403f2a3ce976be9c9bacd4d344fdd",
|
||||
"deviceApiKey": "ff89e1d8db58a7ca52a435f2adea319a",
|
||||
"version": "7.16.6",
|
||||
"deviceType": "raspberrypi3",
|
||||
"osVersion": "Resin OS 2.13.6+rev1"
|
||||
}
|
227
test/data/docker-states/simple/inspect.json
Normal file
227
test/data/docker-states/simple/inspect.json
Normal file
@ -0,0 +1,227 @@
|
||||
{
|
||||
"Id": "5bff0c9e6ef8ae4fdc02b7ba05050078c8599775e8773ce039982e987107a4a4",
|
||||
"Created": "2018-08-16T13:00:47.100056946Z",
|
||||
"Path": "/usr/bin/entry.sh",
|
||||
"Args": [
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"./call-supervisor.sh"
|
||||
],
|
||||
"State": {
|
||||
"Status": "restarting",
|
||||
"Running": true,
|
||||
"Paused": false,
|
||||
"Restarting": true,
|
||||
"OOMKilled": false,
|
||||
"Dead": false,
|
||||
"Pid": 0,
|
||||
"ExitCode": 0,
|
||||
"Error": "",
|
||||
"StartedAt": "2018-08-16T13:22:17.991455639Z",
|
||||
"FinishedAt": "2018-08-16T13:22:18.845432218Z"
|
||||
},
|
||||
"Image": "sha256:f9e0fa6e3e68caedbcbb4ef35d5a8dce2a8d33e39cc94115d567800f25d826f4",
|
||||
"ResolvConfPath": "/var/lib/docker/containers/5bff0c9e6ef8ae4fdc02b7ba05050078c8599775e8773ce039982e987107a4a4/resolv.conf",
|
||||
"HostnamePath": "/var/lib/docker/containers/5bff0c9e6ef8ae4fdc02b7ba05050078c8599775e8773ce039982e987107a4a4/hostname",
|
||||
"HostsPath": "/var/lib/docker/containers/5bff0c9e6ef8ae4fdc02b7ba05050078c8599775e8773ce039982e987107a4a4/hosts",
|
||||
"LogPath": "",
|
||||
"Name": "/main_431889_572579",
|
||||
"RestartCount": 29,
|
||||
"Driver": "aufs",
|
||||
"Platform": "linux",
|
||||
"MountLabel": "",
|
||||
"ProcessLabel": "",
|
||||
"AppArmorProfile": "",
|
||||
"ExecIDs": null,
|
||||
"HostConfig": {
|
||||
"Binds": [
|
||||
"/tmp/resin-supervisor/services/1011165/main:/tmp/resin"
|
||||
],
|
||||
"ContainerIDFile": "",
|
||||
"LogConfig": {
|
||||
"Type": "journald",
|
||||
"Config": {}
|
||||
},
|
||||
"NetworkMode": "1011165_default",
|
||||
"PortBindings": {},
|
||||
"RestartPolicy": {
|
||||
"Name": "always",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
"AutoRemove": false,
|
||||
"VolumeDriver": "",
|
||||
"VolumesFrom": null,
|
||||
"CapAdd": [],
|
||||
"CapDrop": [],
|
||||
"Dns": [],
|
||||
"DnsOptions": null,
|
||||
"DnsSearch": [],
|
||||
"ExtraHosts": [],
|
||||
"GroupAdd": [],
|
||||
"IpcMode": "shareable",
|
||||
"Cgroup": "",
|
||||
"Links": null,
|
||||
"OomScoreAdj": 0,
|
||||
"PidMode": "",
|
||||
"Privileged": false,
|
||||
"PublishAllPorts": false,
|
||||
"ReadonlyRootfs": false,
|
||||
"SecurityOpt": [],
|
||||
"UTSMode": "",
|
||||
"UsernsMode": "",
|
||||
"ShmSize": 67108864,
|
||||
"Runtime": "runc",
|
||||
"ConsoleSize": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"Isolation": "",
|
||||
"CpuShares": 0,
|
||||
"Memory": 0,
|
||||
"NanoCpus": 0,
|
||||
"CgroupParent": "",
|
||||
"BlkioWeight": 0,
|
||||
"BlkioWeightDevice": null,
|
||||
"BlkioDeviceReadBps": null,
|
||||
"BlkioDeviceWriteBps": null,
|
||||
"BlkioDeviceReadIOps": null,
|
||||
"BlkioDeviceWriteIOps": null,
|
||||
"CpuPeriod": 0,
|
||||
"CpuQuota": 0,
|
||||
"CpuRealtimePeriod": 0,
|
||||
"CpuRealtimeRuntime": 0,
|
||||
"CpusetCpus": "",
|
||||
"CpusetMems": "",
|
||||
"Devices": [],
|
||||
"DeviceCgroupRules": null,
|
||||
"DiskQuota": 0,
|
||||
"KernelMemory": 0,
|
||||
"MemoryReservation": 0,
|
||||
"MemorySwap": 0,
|
||||
"MemorySwappiness": -1,
|
||||
"OomKillDisable": false,
|
||||
"PidsLimit": 0,
|
||||
"Ulimits": [],
|
||||
"CpuCount": 0,
|
||||
"CpuPercent": 0,
|
||||
"IOMaximumIOps": 0,
|
||||
"IOMaximumBandwidth": 0
|
||||
},
|
||||
"GraphDriver": {
|
||||
"Data": null,
|
||||
"Name": "aufs"
|
||||
},
|
||||
"Mounts": [
|
||||
{
|
||||
"Type": "bind",
|
||||
"Source": "/tmp/resin-supervisor/services/1011165/main",
|
||||
"Destination": "/tmp/resin",
|
||||
"Mode": "",
|
||||
"RW": true,
|
||||
"Propagation": "rprivate"
|
||||
}
|
||||
],
|
||||
"Config": {
|
||||
"Hostname": "5bff0c9e6ef8",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"Tty": true,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"RESIN_APP_ID=1011165",
|
||||
"RESIN_APP_NAME=supervisortest",
|
||||
"RESIN_SERVICE_NAME=main",
|
||||
"RESIN_DEVICE_UUID=7dadabd4edec3067948d5952c2f2f26f",
|
||||
"RESIN_DEVICE_TYPE=raspberrypi3",
|
||||
"RESIN_HOST_OS_VERSION=Resin OS 2.13.6+rev1",
|
||||
"RESIN_SUPERVISOR_VERSION=7.16.6",
|
||||
"RESIN_APP_LOCK_PATH=/tmp/resin/resin-updates.lock",
|
||||
"RESIN_SERVICE_KILL_ME_PATH=/tmp/resin/resin-kill-me",
|
||||
"RESIN=1",
|
||||
"USER=root",
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"UDEV=on"
|
||||
],
|
||||
"Cmd": [
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"./call-supervisor.sh"
|
||||
],
|
||||
"Image": "sha256:f9e0fa6e3e68caedbcbb4ef35d5a8dce2a8d33e39cc94115d567800f25d826f4",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "/usr/src/app",
|
||||
"Entrypoint": [
|
||||
"/usr/bin/entry.sh"
|
||||
],
|
||||
"OnBuild": null,
|
||||
"Labels": {
|
||||
"io.resin.app-id": "1011165",
|
||||
"io.resin.architecture": "armv7hf",
|
||||
"io.resin.device-type": "raspberry-pi2",
|
||||
"io.resin.qemu.version": "2.9.0.resin1-arm",
|
||||
"io.resin.service-id": "43697",
|
||||
"io.resin.service-name": "main",
|
||||
"io.resin.supervised": "true"
|
||||
}
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Bridge": "",
|
||||
"SandboxID": "9124030ea083331f46529d0ffb1549f780b5ca913e912d438726e31fc5c716da",
|
||||
"HairpinMode": false,
|
||||
"LinkLocalIPv6Address": "",
|
||||
"LinkLocalIPv6PrefixLen": 0,
|
||||
"Ports": {},
|
||||
"SandboxKey": "/var/run/balena/netns/9124030ea083",
|
||||
"SecondaryIPAddresses": null,
|
||||
"SecondaryIPv6Addresses": null,
|
||||
"EndpointID": "",
|
||||
"Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"IPAddress": "",
|
||||
"IPPrefixLen": 0,
|
||||
"IPv6Gateway": "",
|
||||
"MacAddress": "",
|
||||
"Networks": {
|
||||
"1011165_default": {
|
||||
"IPAMConfig": null,
|
||||
"Links": null,
|
||||
"Aliases": [
|
||||
"main",
|
||||
"5bff0c9e6ef8"
|
||||
],
|
||||
"NetworkID": "4afe52d663d8de16ea7cbd3c3faaff0705109d2f246e4f18cbed3b2789ce0a7a",
|
||||
"EndpointID": "",
|
||||
"Gateway": "",
|
||||
"IPAddress": "",
|
||||
"IPPrefixLen": 0,
|
||||
"IPv6Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"MacAddress": "",
|
||||
"DriverOpts": null
|
||||
},
|
||||
"supervisor0": {
|
||||
"IPAMConfig": null,
|
||||
"Links": null,
|
||||
"Aliases": [
|
||||
"5bff0c9e6ef8"
|
||||
],
|
||||
"NetworkID": "2ad3a9f0a52d912fff9990837167cd7a3d9f1133e73a40cbed6438be81a96126",
|
||||
"EndpointID": "",
|
||||
"Gateway": "",
|
||||
"IPAddress": "",
|
||||
"IPPrefixLen": 0,
|
||||
"IPv6Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"MacAddress": "",
|
||||
"DriverOpts": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -334,9 +334,7 @@ currentState[0] = {
|
||||
|
||||
}
|
||||
privileged: false
|
||||
restartPolicy:
|
||||
Name: 'always'
|
||||
MaximumRetryCount: 0
|
||||
restart: 'always'
|
||||
volumes: [
|
||||
'/tmp/resin-supervisor/services/1234/aservice:/tmp/resin'
|
||||
]
|
||||
@ -349,8 +347,8 @@ currentState[0] = {
|
||||
running: true
|
||||
createdAt: new Date()
|
||||
containerId: '1'
|
||||
networkMode: '1234_default'
|
||||
networks: { '1234_default': {} }
|
||||
networkMode: 'default'
|
||||
networks: { 'default': { aliases: [ 'aservice' ] } }
|
||||
command: [ 'someCommand' ]
|
||||
entrypoint: [ 'theEntrypoint' ]
|
||||
},
|
||||
@ -370,9 +368,7 @@ currentState[0] = {
|
||||
'/tmp/resin-supervisor/services/1234/anotherService:/tmp/resin'
|
||||
]
|
||||
privileged: false
|
||||
restartPolicy:
|
||||
Name: 'always'
|
||||
MaximumRetryCount: 0
|
||||
restart: 'always'
|
||||
labels: {
|
||||
'io.resin.app-id': '1234'
|
||||
'io.resin.service-id': '24'
|
||||
@ -382,8 +378,8 @@ currentState[0] = {
|
||||
running: false
|
||||
createdAt: new Date()
|
||||
containerId: '2'
|
||||
networkMode: '1234_default'
|
||||
networks: { '1234_default': {} }
|
||||
networkMode: 'default'
|
||||
networks: { 'default': { aliases: [ 'anotherService' ] } }
|
||||
command: [ 'someCommand' ]
|
||||
entrypoint: [ 'theEntrypoint' ]
|
||||
}
|
||||
@ -447,9 +443,7 @@ currentState[2] = {
|
||||
'ADDITIONAL_ENV_VAR': 'foo'
|
||||
}
|
||||
privileged: false
|
||||
restartPolicy:
|
||||
Name: 'always'
|
||||
MaximumRetryCount: 0
|
||||
restart: 'always'
|
||||
volumes: [
|
||||
'/tmp/resin-supervisor/services/1234/aservice:/tmp/resin'
|
||||
]
|
||||
@ -462,8 +456,8 @@ currentState[2] = {
|
||||
running: true
|
||||
createdAt: new Date()
|
||||
containerId: '1'
|
||||
networkMode: '1234_default'
|
||||
networks: { '1234_default': {} }
|
||||
networkMode: 'default'
|
||||
networks: { 'default': { aliases: [ 'aservice' ] } }
|
||||
command: [ 'someCommand' ]
|
||||
entrypoint: [ 'theEntrypoint' ]
|
||||
}
|
||||
@ -505,9 +499,7 @@ currentState[3] = {
|
||||
'ADDITIONAL_ENV_VAR': 'foo'
|
||||
}
|
||||
privileged: false
|
||||
restartPolicy:
|
||||
Name: 'always'
|
||||
MaximumRetryCount: 0
|
||||
restart: 'always'
|
||||
volumes: [
|
||||
'/tmp/resin-supervisor/services/1234/aservice:/tmp/resin'
|
||||
]
|
||||
@ -520,8 +512,8 @@ currentState[3] = {
|
||||
running: true
|
||||
createdAt: new Date(0)
|
||||
containerId: '1'
|
||||
networkMode: '1234_default'
|
||||
networks: { '1234_default': {} }
|
||||
networkMode: 'default'
|
||||
networks: { 'default': { aliases: [ 'aservice' ] } }
|
||||
command: [ 'someCommand' ]
|
||||
entrypoint: [ 'theEntrypoint' ]
|
||||
},
|
||||
@ -540,9 +532,7 @@ currentState[3] = {
|
||||
'ADDITIONAL_ENV_VAR': 'foo'
|
||||
}
|
||||
privileged: false
|
||||
restartPolicy:
|
||||
Name: 'always'
|
||||
MaximumRetryCount: 0
|
||||
restart: 'always'
|
||||
volumes: [
|
||||
'/tmp/resin-supervisor/services/1234/aservice:/tmp/resin'
|
||||
]
|
||||
@ -555,8 +545,8 @@ currentState[3] = {
|
||||
running: true
|
||||
createdAt: new Date(1)
|
||||
containerId: '2'
|
||||
networkMode: '1234_default'
|
||||
networks: { '1234_default': {} }
|
||||
networkMode: 'default'
|
||||
networks: { 'default': { aliases: [ 'aservice' ] } }
|
||||
command: [ 'someCommand' ]
|
||||
entrypoint: [ 'theEntrypoint' ]
|
||||
}
|
||||
@ -599,9 +589,7 @@ currentState[4] = {
|
||||
'/tmp/resin-supervisor/services/1234/anotherService:/tmp/resin'
|
||||
]
|
||||
privileged: false
|
||||
restartPolicy:
|
||||
Name: 'always'
|
||||
MaximumRetryCount: 0
|
||||
restart: 'always'
|
||||
labels: {
|
||||
'io.resin.app-id': '1234'
|
||||
'io.resin.service-id': '24'
|
||||
@ -611,8 +599,8 @@ currentState[4] = {
|
||||
running: false
|
||||
createdAt: new Date()
|
||||
containerId: '2'
|
||||
networkMode: '1234_default'
|
||||
networks: { '1234_default': {} }
|
||||
networkMode: 'default'
|
||||
networks: { 'default': { aliases: [ 'aservice' ] } }
|
||||
command: [ 'someCommand' ]
|
||||
entrypoint: [ 'theEntrypoint' ]
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
"strictNullChecks": true,
|
||||
"outDir": "./build/src/",
|
||||
"lib": [
|
||||
"es2015",
|
||||
"es6"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
|
26
typings/dockerode-ext.d.ts
vendored
Normal file
26
typings/dockerode-ext.d.ts
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
import { ContainerInspectInfo } from 'dockerode';
|
||||
|
||||
declare module 'dockerode' {
|
||||
|
||||
// Extend the HostConfig interface with the missing fields.
|
||||
// TODO: Add these upstream to DefinitelyTyped
|
||||
interface HostConfig {
|
||||
Sysctls: { [sysctlsOpt: string]: string };
|
||||
GroupAdd: string[];
|
||||
UsernsMode: string;
|
||||
}
|
||||
|
||||
export interface DockerHealthcheck {
|
||||
Test: string[];
|
||||
Interval?: number;
|
||||
Timeout?: number;
|
||||
Retries?: number;
|
||||
StartPeriod?: number;
|
||||
}
|
||||
|
||||
interface ContainerCreateOptions {
|
||||
Healthcheck?: DockerHealthcheck;
|
||||
StopTimeout?: number;
|
||||
}
|
||||
|
||||
}
|
62
typings/duration-js.d.ts
vendored
Normal file
62
typings/duration-js.d.ts
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
// From: https://github.com/icholy/Duration.js/pull/15
|
||||
// Once the above is merged, use the inbuilt module types
|
||||
declare module "duration-js" {
|
||||
type DurationLike = Duration | string | number;
|
||||
type DateLike = Date | number;
|
||||
|
||||
class Duration {
|
||||
|
||||
private _milliseconds: number;
|
||||
|
||||
constructor(value?: DurationLike);
|
||||
|
||||
static millisecond: Duration;
|
||||
static second: Duration;
|
||||
static minute: Duration;
|
||||
static hour: Duration;
|
||||
static day: Duration;
|
||||
static week: Duration;
|
||||
|
||||
static milliseconds(milliseconds: number): Duration;
|
||||
static seconds(seconds: number): Duration;
|
||||
static minutes(minutes: number): Duration;
|
||||
static hours(hours: number): Duration;
|
||||
static days(days: number): Duration;
|
||||
static weeks(weeks: number): Duration;
|
||||
|
||||
nanoseconds(): number;
|
||||
microseconds(): number;
|
||||
milliseconds(): number;
|
||||
seconds(): number;
|
||||
minutes(): number;
|
||||
hours(): number;
|
||||
days(): number;
|
||||
weeks(): number;
|
||||
|
||||
toString(): string;
|
||||
valueOf(): number;
|
||||
|
||||
isGreaterThan(duration: DurationLike): boolean;
|
||||
isLessThan(duration: DurationLike): boolean;
|
||||
isEqualTo(duration: DurationLike): boolean;
|
||||
|
||||
roundTo(duration: DurationLike): void;
|
||||
|
||||
after(date: DateLike): Date;
|
||||
|
||||
static since(date: DateLike): Duration;
|
||||
static until(date: DateLike): Duration;
|
||||
static between(a: DateLike, b: DateLike): Duration;
|
||||
static parse(duration: string): Duration;
|
||||
static fromMicroseconds(us: number): Duration;
|
||||
static fromNanoseconds(ns: number): Duration;
|
||||
|
||||
static add(a: Duration, b: Duration): Duration;
|
||||
static subtract(a: Duration, b: Duration): Duration;
|
||||
static multiply(a: Duration, b: number): Duration;
|
||||
static multiply(a: number, b: Duration): Duration;
|
||||
static divide(a: Duration, b: Duration): number;
|
||||
static abs(d: DurationLike): Duration;
|
||||
}
|
||||
export = Duration;
|
||||
}
|
Loading…
Reference in New Issue
Block a user