diff --git a/src/application-manager.coffee b/src/application-manager.coffee index 7219165a..b650a5d4 100644 --- a/src/application-manager.coffee +++ b/src/application-manager.coffee @@ -317,7 +317,7 @@ module.exports = class ApplicationManager extends EventEmitter # Compares current and target services and returns a list of service pairs to be updated/removed/installed. # The returned list is an array of objects where the "current" and "target" properties define the update pair, and either can be null # (in the case of an install or removal). - compareServicesForUpdate: (currentServices, targetServices) => + compareServicesForUpdate: (currentServices, targetServices, containerIds) => removePairs = [] installPairs = [] updatePairs = [] @@ -366,13 +366,13 @@ module.exports = class ApplicationManager extends EventEmitter # already started it before. In this case it means it just exited so we don't want to start it again. alreadyStarted = (serviceId) => return ( - currentServicesPerId[serviceId].isEqualExceptForRunningState(targetServicesPerId[serviceId]) and + currentServicesPerId[serviceId].isEqualExceptForRunningState(targetServicesPerId[serviceId], containerIds) and targetServicesPerId[serviceId].config.running and @_containerStarted[currentServicesPerId[serviceId].containerId] ) needUpdate = _.filter toBeMaybeUpdated, (serviceId) -> - !currentServicesPerId[serviceId].isEqual(targetServicesPerId[serviceId]) and !alreadyStarted(serviceId) + !currentServicesPerId[serviceId].isEqual(targetServicesPerId[serviceId], containerIds) and !alreadyStarted(serviceId) for serviceId in needUpdate updatePairs.push({ @@ -535,7 +535,7 @@ module.exports = class ApplicationManager extends EventEmitter return { action: 'noop' } } - _nextStepForService: ({ current, target }, updateContext, localMode) => + _nextStepForService: ({ current, target }, updateContext, localMode, containerIds) => { targetApp, networkPairs, volumePairs, installPairs, updatePairs, availableImages, downloading } = updateContext if current?.status == 'Stopping' # There is already a kill step in progress for this service, so we wait @@ -564,7 +564,7 @@ module.exports = class ApplicationManager extends EventEmitter # even if its strategy is handover needsSpecialKill = @_hasCurrentNetworksOrVolumes(current, networkPairs, volumePairs) - if current?.isEqualConfig(target) + if current?.isEqualConfig(target, containerIds) # We're only stopping/starting it return @_updateContainerStep(current, target) else if !current? @@ -578,7 +578,7 @@ module.exports = class ApplicationManager extends EventEmitter timeout = checkInt(target.config.labels['io.balena.update.handover-timeout']) return @_strategySteps[strategy](current, target, needsDownload, dependenciesMetForStart, dependenciesMetForKill, needsSpecialKill, timeout) - _nextStepsForAppUpdate: (currentApp, targetApp, localMode, availableImages = [], downloading = []) => + _nextStepsForAppUpdate: (currentApp, targetApp, localMode, containerIds, availableImages = [], downloading = []) => emptyApp = { services: [], volumes: {}, networks: {} } if !targetApp? targetApp = emptyApp @@ -599,7 +599,7 @@ module.exports = class ApplicationManager extends EventEmitter appId = targetApp.appId ? currentApp.appId networkPairs = @compareNetworksForUpdate({ current: currentApp.networks, target: targetApp.networks }, appId) volumePairs = @compareVolumesForUpdate({ current: currentApp.volumes, target: targetApp.volumes }, appId) - { removePairs, installPairs, updatePairs } = @compareServicesForUpdate(currentApp.services, targetApp.services) + { removePairs, installPairs, updatePairs } = @compareServicesForUpdate(currentApp.services, targetApp.services, containerIds) steps = [] # All removePairs get a 'kill' action for pair in removePairs @@ -611,7 +611,7 @@ module.exports = class ApplicationManager extends EventEmitter # 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) - step = @_nextStepForService(pair, { targetApp, networkPairs, volumePairs, installPairs, updatePairs, availableImages, downloading }, localMode) + step = @_nextStepForService(pair, { targetApp, networkPairs, volumePairs, installPairs, updatePairs, availableImages, downloading }, localMode, containerIds) if step? steps.push(step) # next step for network pairs - remove requires services killed, create kill if no pairs or steps affect that service @@ -858,7 +858,7 @@ module.exports = class ApplicationManager extends EventEmitter return notUsedForDelta and notUsedByProxyvisor return { imagesToSave, imagesToRemove } - _inferNextSteps: (cleanupNeeded, availableImages, downloading, supervisorNetworkReady, current, target, ignoreImages, { localMode, delta }) => + _inferNextSteps: (cleanupNeeded, availableImages, downloading, supervisorNetworkReady, current, target, ignoreImages, { localMode, delta }, containerIds) => volumePromises = [] Promise.try => if localMode @@ -919,7 +919,7 @@ module.exports = class ApplicationManager extends EventEmitter if _.isEmpty(nextSteps) allAppIds = _.union(_.keys(currentByAppId), _.keys(targetByAppId)) for appId in allAppIds - nextSteps = nextSteps.concat(@_nextStepsForAppUpdate(currentByAppId[appId], targetByAppId[appId], localMode, availableImages, downloading)) + nextSteps = nextSteps.concat(@_nextStepsForAppUpdate(currentByAppId[appId], targetByAppId[appId], localMode, containerIds[appId], availableImages, downloading)) if oldApps != null and _.includes(oldApps, checkInt(appId)) # We check if everything else has been done for # the old app to be removed. If it has, we then @@ -965,7 +965,16 @@ module.exports = class ApplicationManager extends EventEmitter return Promise.reject(new Error("Invalid action #{step.action}")) @actionExecutors[step.action](step, { force, skipLock }) - getExtraStateForComparison: => + getExtraStateForComparison: (currentState, targetState) => + containerIdsByAppId = {} + _(currentState.local.apps) + .keys() + .concat(_.keys(targetState.local.apps)) + .uniq() + .each (id) => + intId = checkInt(id) + containerIdsByAppId[intId] = @services.getContainerIdMap(intId) + @config.get('localMode').then (localMode) => Promise.props({ cleanupNeeded: @images.isCleanupNeeded() @@ -973,15 +982,16 @@ module.exports = class ApplicationManager extends EventEmitter downloading: @images.getDownloadingImageIds() supervisorNetworkReady: @networks.supervisorNetworkReady() delta: @config.get('delta') + containerIds: Promise.props(containerIdsByAppId) localMode }) getRequiredSteps: (currentState, targetState, extraState, ignoreImages = false) => - { cleanupNeeded, availableImages, downloading, supervisorNetworkReady, delta, localMode } = extraState + { cleanupNeeded, availableImages, downloading, supervisorNetworkReady, delta, localMode, containerIds } = extraState conf = { delta, localMode } if conf.localMode cleanupNeeded = false - @_inferNextSteps(cleanupNeeded, availableImages, downloading, supervisorNetworkReady, currentState, targetState, ignoreImages, conf) + @_inferNextSteps(cleanupNeeded, availableImages, downloading, supervisorNetworkReady, currentState, targetState, ignoreImages, conf, containerIds) .then (nextSteps) => if ignoreImages and _.some(nextSteps, action: 'fetch') throw new Error('Cannot fetch images while executing an API action') diff --git a/src/compose/service-manager.ts b/src/compose/service-manager.ts index 1014d6d8..3ed30f58 100644 --- a/src/compose/service-manager.ts +++ b/src/compose/service-manager.ts @@ -93,9 +93,13 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve } public async get(service: Service) { + // Get the container ids for special network handling + const containerIds = await this.getContainerIdMap(service.appId!); const services = (await this.getAll( `service-id=${service.serviceId}`, - )).filter(currentService => currentService.isEqualConfig(service)); + )).filter(currentService => + currentService.isEqualConfig(service, containerIds), + ); if (services.length === 0) { const e: StatusCodeError = new Error( @@ -266,7 +270,17 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve ); } - const conf = service.toDockerContainer({ deviceName }); + // Get all created services so far + if (service.appId == null) { + throw new InternalInconsistencyError( + 'Attempt to start a service without an existing application ID', + ); + } + const serviceContainerIds = await this.getContainerIdMap(service.appId); + const conf = service.toDockerContainer({ + deviceName, + containerIds: serviceContainerIds, + }); const nets = serviceNetworksToDockerNetworks( service.extraNetworksToJoin(), ); @@ -494,6 +508,13 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve } } + public async getContainerIdMap(appId: number): Promise> { + return _(await this.getAllByAppId(appId)) + .keyBy('serviceName') + .mapValues('containerId') + .value() as Dictionary; + } + private reportChange(containerId?: string, status?: Partial) { if (containerId != null) { if (status != null) { diff --git a/src/compose/service.ts b/src/compose/service.ts index 2c8ed321..ee3c3c67 100644 --- a/src/compose/service.ts +++ b/src/compose/service.ts @@ -23,6 +23,9 @@ import { sanitiseComposeConfig } from './sanitise'; import log from '../lib/supervisor-console'; +const SERVICE_NETWORK_MODE_REGEX = /service:\s*(.+)/; +const CONTAINER_NETWORK_MODE_REGEX = /container:\s*(.+)/; + export class Service { public appId: number | null; public imageId: number | null; @@ -33,7 +36,7 @@ export class Service { public imageName: string | null; public containerId: string | null; - public dependsOn: string | null; + public dependsOn: string[] | null; public status: string; public createdAt: Date | null; @@ -63,6 +66,9 @@ export class Service { // reported on a container inspect, so we cannot use it // to compare containers 'cpus', + // These fields are special case, due to network_mode:service: + 'networkMode', + 'hostname', ].concat(Service.configArrayFields); private constructor() {} @@ -137,21 +143,6 @@ export class Service { }); delete config.networks; - // Check for unsupported networkMode entries - if (config.networkMode != null) { - if (/service:(\s*)?.+/.test(config.networkMode)) { - log.warn( - 'A network_mode referencing a service is not yet supported. Ignoring.', - ); - delete config.networkMode; - } else if (/container:(\s*)?.+/.test(config.networkMode)) { - log.warn( - '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( @@ -190,8 +181,27 @@ export class Service { config.dnsSearch = [config.dnsSearch]; } - // Assign network_mode to a default value if necessary - if (!config.networkMode) { + // Special case network modes + let serviceNetworkMode = false; + if (config.networkMode != null) { + const match = config.networkMode.match(SERVICE_NETWORK_MODE_REGEX); + if (match != null) { + // We need to add a depends on here to ensure that + // the needed container has started up by the time + // we try to start this service + if (service.dependsOn == null) { + service.dependsOn = []; + } + service.dependsOn.push(match[1]); + serviceNetworkMode = true; + } else if (CONTAINER_NETWORK_MODE_REGEX.test(config.networkMode)) { + log.warn( + 'A network_mode referencing a container is not supported. Ignoring.', + ); + delete config.networkMode; + } + } else { + // Assign network_mode to a default value if necessary if (!_.isEmpty(networks)) { config.networkMode = _.keys(networks)[0]; } else { @@ -203,7 +213,7 @@ export class Service { config.networkMode !== 'bridge' && config.networkMode !== 'none' ) { - if (networks[config.networkMode] == null) { + if (networks[config.networkMode!] == null && !serviceNetworkMode) { // The network mode has not been set explicitly config.networkMode = `${service.appId}_${config.networkMode}`; // If we don't have any networks, we need to @@ -551,6 +561,7 @@ export class Service { public toDockerContainer(opts: { deviceName: string; + containerIds: Dictionary; }): Dockerode.ContainerCreateOptions { const { binds, volumes } = this.getBindsAndVolumes(); const { exposedPorts, portBindings } = this.generateExposeAndPorts(); @@ -565,6 +576,16 @@ export class Service { (_v, k) => k === this.config.networkMode, ) as ServiceConfig['networks']; + const match = this.config.networkMode.match(SERVICE_NETWORK_MODE_REGEX); + if (match != null) { + const containerId = opts.containerIds[match[1]]; + if (!containerId) { + throw new Error( + `No container for network_mode: 'service: ${match[1]}'`, + ); + } + this.config.networkMode = `container:${containerId}`; + } return { name: `${this.serviceName}_${this.imageId}_${this.releaseId}`, Tty: this.config.tty, @@ -642,7 +663,10 @@ export class Service { }; } - public isEqualConfig(service: Service): boolean { + public isEqualConfig( + service: Service, + currentContainerIds: Dictionary, + ): boolean { // Check all of the networks for any changes let sameNetworks = true; _.each(service.config.networks, (network, name) => { @@ -702,24 +726,73 @@ export class Service { log.debug(' Network changes detected'); } } - return sameNetworks && sameConfig; + + // Check the network mode separetely, as if it is a + // service: network mode, the container id needs to be + // checked against the running containers + + // When this function is called, it's with the current + // state as a parameter and the target as the instance. + // We shouldn't rely on that because it's not enforced + // anywhere. For that reason we need to consider both + // network_modes in the correct way + let sameNetworkMode = false; + for (const [a, b] of [ + [this.config.networkMode, service.config.networkMode], + [service.config.networkMode, this.config.networkMode], + ]) { + const aMatch = a.match(SERVICE_NETWORK_MODE_REGEX); + const bMatch = b.match(SERVICE_NETWORK_MODE_REGEX); + + if (aMatch !== null) { + if (bMatch === null) { + const containerMatch = b.match(CONTAINER_NETWORK_MODE_REGEX); + if ( + containerMatch !== null && + currentContainerIds[aMatch[1]] === containerMatch[1] + ) { + sameNetworkMode = true; + break; + } + } else { + // They're both service entries, we shouldn't get here + // but it's technically an equal configuration + if (a === b) { + sameNetworkMode = true; + break; + } + } + } else if (a === b && this.config.hostname === service.config.hostname) { + // We consider the hostname when it's not a service: entry + sameNetworkMode = true; + break; + } + } + + return sameNetworks && sameConfig && sameNetworkMode; } public extraNetworksToJoin(): ServiceConfig['networks'] { return _.omit(this.config.networks, this.config.networkMode); } - public isEqualExceptForRunningState(service: Service): boolean { + public isEqualExceptForRunningState( + service: Service, + currentContainerIds: Dictionary, + ): boolean { return ( - this.isEqualConfig(service) && + this.isEqualConfig(service, currentContainerIds) && this.releaseId === service.releaseId && this.imageId === service.imageId ); } - public isEqual(service: Service): boolean { + public isEqual( + service: Service, + currentContainerIds: Dictionary, + ): boolean { return ( - this.isEqualExceptForRunningState(service) && + this.isEqualExceptForRunningState(service, currentContainerIds) && this.config.running === service.config.running ); } diff --git a/src/device-state.coffee b/src/device-state.coffee index 5245cca3..b70aef28 100644 --- a/src/device-state.coffee +++ b/src/device-state.coffee @@ -605,13 +605,13 @@ module.exports = class DeviceState extends EventEmitter @applications.localModeSwitchCompletion() .then => @usingInferStepsLock => - @applications.getExtraStateForComparison() - .then (extraState) => - Promise.all([ - @getCurrentForComparison() - @getTarget({ initial, intermediate }) - ]) - .then ([ currentState, targetState ]) => + Promise.all([ + @getCurrentForComparison() + @getTarget({ initial, intermediate }) + ]) + .then ([ currentState, targetState ]) => + @applications.getExtraStateForComparison(currentState, targetState) + .then (extraState) => @deviceConfig.getRequiredSteps(currentState, targetState) .then (deviceConfigSteps) => noConfigSteps = _.every(deviceConfigSteps, ({ action }) -> action is 'noop') diff --git a/test/04-service.spec.coffee b/test/04-service.spec.coffee index 58e92682..997a6446 100644 --- a/test/04-service.spec.coffee +++ b/test/04-service.spec.coffee @@ -14,6 +14,11 @@ configs = { compose: require('./data/docker-states/entrypoint/compose.json') imageInfo: require('./data/docker-states/entrypoint/imageInfo.json') inspect: require('./data/docker-states/entrypoint/inspect.json') + }, + networkModeService: { + compose: require('./data/docker-states/network-mode-service/compose.json') + imageInfo: require('./data/docker-states/network-mode-service/imageInfo.json') + inspect: require('./data/docker-states/network-mode-service/inspect.json') } } @@ -375,3 +380,74 @@ describe 'compose/service', -> aliases: [ 'test', '1123' ] } }) + + describe 'Network mode=service:', -> + it 'should correctly add a depends_on entry for the service', -> + s = Service.fromComposeObject({ + appId: '1234' + serviceName: 'foo' + releaseId: 2 + serviceId: 3 + imageId: 4 + network_mode: 'service: test' + }, { appName: 'test' }) + + expect(s.dependsOn).to.deep.equal(['test']) + + s = Service.fromComposeObject({ + appId: '1234' + serviceName: 'foo' + releaseId: 2 + serviceId: 3 + imageId: 4 + depends_on: [ + 'another_service' + ] + network_mode: 'service: test' + }, { appName: 'test' }) + + expect(s.dependsOn).to.deep.equal([ + 'another_service', + 'test' + ]) + + it 'should correctly convert a network_mode service: to a container:', -> + s = Service.fromComposeObject({ + appId: '1234' + serviceName: 'foo' + releaseId: 2 + serviceId: 3 + imageId: 4 + network_mode: 'service: test' + }, { appName: 'test' }) + expect(s.toDockerContainer({ deviceName: '', containerIds: { test: 'abcdef' } })) + .to.have.property('HostConfig') + .that.has.property('NetworkMode') + .that.equals('container:abcdef') + + it 'should not cause a container restart if a service: container has not changed', -> + composeSvc = Service.fromComposeObject(configs.networkModeService.compose, configs.networkModeService.imageInfo) + dockerSvc = Service.fromDockerContainer(configs.networkModeService.inspect) + + composeConfig = omitConfigForComparison(composeSvc.config) + dockerConfig = omitConfigForComparison(dockerSvc.config) + expect(composeConfig).to.not.deep.equal(dockerConfig) + + expect(dockerSvc.isEqualConfig( + composeSvc, + { test: 'abcdef' } + )).to.be.true + + it 'should restart a container when its dependent network mode container changes', -> + composeSvc = Service.fromComposeObject(configs.networkModeService.compose, configs.networkModeService.imageInfo) + dockerSvc = Service.fromDockerContainer(configs.networkModeService.inspect) + + composeConfig = omitConfigForComparison(composeSvc.config) + dockerConfig = omitConfigForComparison(dockerSvc.config) + expect(composeConfig).to.not.deep.equal(dockerConfig) + + expect(dockerSvc.isEqualConfig( + composeSvc, + { test: 'qwerty' } + )).to.be.false + diff --git a/test/14-application-manager.spec.coffee b/test/14-application-manager.spec.coffee index 4cf6107f..ba437497 100644 --- a/test/14-application-manager.spec.coffee +++ b/test/14-application-manager.spec.coffee @@ -141,6 +141,7 @@ describe 'ApplicationManager', -> } }) stub(@applications.docker, 'getNetworkGateway').returns(Promise.resolve('172.17.0.1')) + stub(@applications.docker, 'listContainers').returns(Promise.resolve([])) stub(Service, 'extendEnvVars').callsFake (env) -> env['ADDITIONAL_ENV_VAR'] = 'foo' return env @@ -191,6 +192,7 @@ describe 'ApplicationManager', -> after -> @applications.images.inspectByName.restore() @applications.docker.getNetworkGateway.restore() + @applications.docker.listContainers.restore() Service.extendEnvVars.restore() it 'infers a start step when all that changes is a running state', -> @@ -198,7 +200,7 @@ describe 'ApplicationManager', -> @normaliseCurrent(currentState[0]) @normaliseTarget(targetState[0], availableImages[0]) (current, target) => - steps = @applications._inferNextSteps(false, availableImages[0], [], true, current, target, false, {}) + steps = @applications._inferNextSteps(false, availableImages[0], [], true, current, target, false, {}, {}) expect(steps).to.eventually.deep.equal([{ action: 'start' current: current.local.apps['1234'].services[1] @@ -214,7 +216,7 @@ describe 'ApplicationManager', -> @normaliseCurrent(currentState[0]) @normaliseTarget(targetState[1], availableImages[0]) (current, target) => - steps = @applications._inferNextSteps(false, availableImages[0], [], true, current, target, false, {}) + steps = @applications._inferNextSteps(false, availableImages[0], [], true, current, target, false, {}, {}) expect(steps).to.eventually.deep.equal([{ action: 'kill' current: current.local.apps['1234'].services[1] @@ -230,7 +232,7 @@ describe 'ApplicationManager', -> @normaliseCurrent(currentState[0]) @normaliseTarget(targetState[2], availableImages[0]) (current, target) => - steps = @applications._inferNextSteps(false, availableImages[0], [], true, current, target, false, {}) + steps = @applications._inferNextSteps(false, availableImages[0], [], true, current, target, false, {}, {}) expect(steps).to.eventually.deep.equal([{ action: 'fetch' image: @applications.imageForService(target.local.apps['1234'].services[1]) @@ -245,7 +247,7 @@ describe 'ApplicationManager', -> @normaliseCurrent(currentState[0]) @normaliseTarget(targetState[2], availableImages[0]) (current, target) => - steps = @applications._inferNextSteps(false, availableImages[0], [ target.local.apps['1234'].services[1].imageId ], true, current, target, false, {}) + steps = @applications._inferNextSteps(false, availableImages[0], [ target.local.apps['1234'].services[1].imageId ], true, current, target, false, {}, {}) expect(steps).to.eventually.deep.equal([{ action: 'noop', appId: 1234 }]) ) @@ -254,7 +256,7 @@ describe 'ApplicationManager', -> @normaliseCurrent(currentState[0]) @normaliseTarget(targetState[3], availableImages[0]) (current, target) => - steps = @applications._inferNextSteps(false, availableImages[0], [], true, current, target, false, {}) + steps = @applications._inferNextSteps(false, availableImages[0], [], true, current, target, false, {}, {}) expect(steps).to.eventually.deep.equal([{ action: 'kill' current: current.local.apps['1234'].services[1] @@ -270,7 +272,7 @@ describe 'ApplicationManager', -> @normaliseCurrent(currentState[4]) @normaliseTarget(targetState[4], availableImages[2]) (current, target) => - steps = @applications._inferNextSteps(false, availableImages[2], [], true, current, target, false, {}) + steps = @applications._inferNextSteps(false, availableImages[2], [], true, current, target, false, {}, {}) expect(steps).to.eventually.have.deep.members([{ action: 'fetch' image: @applications.imageForService(target.local.apps['1234'].services[0]) @@ -285,7 +287,7 @@ describe 'ApplicationManager', -> @normaliseCurrent(currentState[0]) @normaliseTarget(targetState[5], availableImages[1]) (current, target) => - steps = @applications._inferNextSteps(false, availableImages[1], [], true, current, target, false, {}) + steps = @applications._inferNextSteps(false, availableImages[1], [], true, current, target, false, {}, {}) expect(steps).to.eventually.have.deep.members([ { action: 'kill' @@ -311,7 +313,7 @@ describe 'ApplicationManager', -> @normaliseCurrent(currentState[1]) @normaliseTarget(targetState[4], availableImages[1]) (current, target) => - steps = @applications._inferNextSteps(false, availableImages[1], [], true, current, target, false, {}) + steps = @applications._inferNextSteps(false, availableImages[1], [], true, current, target, false, {}, {}) expect(steps).to.eventually.have.deep.members([ { action: 'start' @@ -329,7 +331,7 @@ describe 'ApplicationManager', -> @normaliseCurrent(currentState[2]) @normaliseTarget(targetState[4], availableImages[1]) (current, target) => - steps = @applications._inferNextSteps(false, availableImages[1], [], true, current, target, false, {}) + steps = @applications._inferNextSteps(false, availableImages[1], [], true, current, target, false, {}, {}, {}) expect(steps).to.eventually.have.deep.members([ { action: 'start' @@ -347,7 +349,7 @@ describe 'ApplicationManager', -> @normaliseCurrent(currentState[3]) @normaliseTarget(targetState[4], availableImages[1]) (current, target) => - steps = @applications._inferNextSteps(false, availableImages[1], [], true, current, target, false, {}) + steps = @applications._inferNextSteps(false, availableImages[1], [], true, current, target, false, {}, {}) expect(steps).to.eventually.have.deep.members([ { action: 'kill' @@ -406,7 +408,7 @@ describe 'ApplicationManager', -> @normaliseCurrent(currentState[6]), @normaliseTarget(targetState[0], availableImages[0]) (current, target) => - @applications._inferNextSteps(false, availableImages[0], [], true, current, target, false, {}).then (steps) -> + @applications._inferNextSteps(false, availableImages[0], [], true, current, target, false, {}, {}).then (steps) -> expect( _.every(steps, (s) -> s.action != 'removeVolume'), 'Volumes from current app should not be removed' @@ -418,7 +420,7 @@ describe 'ApplicationManager', -> @normaliseCurrent(currentState[5]) @normaliseTarget(targetState[6], []) (current, target) => - @applications._inferNextSteps(false, [], [], true, current, target, false, {}).then (steps) -> + @applications._inferNextSteps(false, [], [], true, current, target, false, {}, {}).then (steps) -> expect(steps).to.have.length(1) expect(steps[0]).to.have.property('action').that.equals('removeVolume') expect(steps[0].current).to.have.property('appId').that.equals(12) diff --git a/test/21-device-api.ts b/test/21-device-api.ts new file mode 100644 index 00000000..e69de29b diff --git a/test/data/docker-states/network-mode-service/compose.json b/test/data/docker-states/network-mode-service/compose.json new file mode 100644 index 00000000..2afee5e9 --- /dev/null +++ b/test/data/docker-states/network-mode-service/compose.json @@ -0,0 +1,13 @@ +{ + "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", + "tty": true, + "network_mode": "service: test" +} diff --git a/test/data/docker-states/network-mode-service/imageInfo.json b/test/data/docker-states/network-mode-service/imageInfo.json new file mode 100644 index 00000000..7bb981e1 --- /dev/null +++ b/test/data/docker-states/network-mode-service/imageInfo.json @@ -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" +} \ No newline at end of file diff --git a/test/data/docker-states/network-mode-service/inspect.json b/test/data/docker-states/network-mode-service/inspect.json new file mode 100644 index 00000000..e6559c32 --- /dev/null +++ b/test/data/docker-states/network-mode-service/inspect.json @@ -0,0 +1,246 @@ +{ + "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/balena-supervisor/services/1011165/main:/tmp/resin", + "/tmp/balena-supervisor/services/1011165/main:/tmp/balena" + ], + "ContainerIDFile": "", + "LogConfig": { + "Type": "journald", + "Config": {} + }, + "NetworkMode": "container:abcdef", + "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/balena-supervisor/services/1011165/main", + "Destination": "/tmp/resin", + "Mode": "", + "RW": true, + "Propagation": "rprivate" + }, + { + "Type": "bind", + "Source": "/tmp/balena-supervisor/services/1011165/main", + "Destination": "/tmp/balena", + "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/balena/updates.lock", + "RESIN_SERVICE_KILL_ME_PATH=/tmp/balena/handover-complete", + "RESIN=1", + "BALENA_APP_ID=1011165", + "BALENA_APP_NAME=supervisortest", + "BALENA_SERVICE_NAME=main", + "BALENA_DEVICE_UUID=7dadabd4edec3067948d5952c2f2f26f", + "BALENA_DEVICE_TYPE=raspberrypi3", + "BALENA_HOST_OS_VERSION=Resin OS 2.13.6+rev1", + "BALENA_SUPERVISOR_VERSION=7.16.6", + "BALENA_APP_LOCK_PATH=/tmp/balena/updates.lock", + "BALENA_SERVICE_HANDOVER_COMPLETE_PATH=/tmp/balena/handover-complete", + "BALENA=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 + } + } + } +}