Merge pull request #1036 from balena-io/851-service-network-mode

Support network_modes of service:<servicename>
This commit is contained in:
CameronDiver 2019-07-23 06:33:48 -07:00 committed by GitHub
commit 5e73d4d9a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 628 additions and 59 deletions

View File

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

View File

@ -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<Dictionary<string>> {
return _(await this.getAllByAppId(appId))
.keyBy('serviceName')
.mapValues('containerId')
.value() as Dictionary<string>;
}
private reportChange(containerId?: string, status?: Partial<Service>) {
if (containerId != null) {
if (status != null) {

View File

@ -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:<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<string>;
}): 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<string>,
): 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<string>,
): 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<string>,
): boolean {
return (
this.isEqualExceptForRunningState(service) &&
this.isEqualExceptForRunningState(service, currentContainerIds) &&
this.config.running === service.config.running
);
}

View File

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

View File

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

View File

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

0
test/21-device-api.ts Normal file
View File

View File

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

View 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"
}

View File

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