Create tests with recovery from #1576

Devices affected by the bug described in 1576, are also stuck with some
services in the `Downloaded` state, because the state engine does not
detect that the running services should be killed on a network change
even if they belong to a new release. This is a bug, which can be
replicated by the tests in this commit

Change-type: patch
This commit is contained in:
Felipe Lalanne 2023-04-25 14:43:15 -04:00
parent 7aecaae8b0
commit 7b8b187c74
2 changed files with 176 additions and 1 deletions

View File

@ -134,6 +134,118 @@ describe('state engine', () => {
]);
});
// This test recovery from issue #1576, where a device running a service from the target release
// would not stop the service even if there were still network and container changes to be applied
it('always stops running services depending on a network being changed', async () => {
// Install part of the target release
await setTargetState({
config: {},
apps: {
'123': {
name: 'test-app',
commit: 'deadca1f',
releaseId: 2,
services: {
'1': {
image: 'alpine:latest',
imageId: 21,
serviceName: 'one',
restart: 'unless-stopped',
running: true,
command: 'sleep infinity',
stop_signal: 'SIGKILL',
labels: {},
environment: {},
},
},
networks: {},
volumes: {},
},
},
});
const state = await getCurrentState();
expect(
state.apps['123'].services.map((s: any) => s.serviceName),
).to.deep.equal(['one']);
const containers = await docker.listContainers();
expect(
containers.map(({ Names, State }) => ({ Name: Names[0], State })),
).to.have.deep.members([{ Name: '/one_21_2_deadca1f', State: 'running' }]);
const containerIds = containers.map(({ Id }) => Id);
await setTargetState({
config: {},
apps: {
'123': {
name: 'test-app',
commit: 'deadca1f',
releaseId: 2,
services: {
'1': {
image: 'alpine:latest',
imageId: 21,
serviceName: 'one',
restart: 'unless-stopped',
running: true,
command: 'sleep infinity',
stop_signal: 'SIGKILL',
networks: ['default'],
labels: {},
environment: {},
},
'2': {
image: 'alpine:latest',
imageId: 22,
serviceName: 'two',
restart: 'unless-stopped',
running: true,
command: 'sh -c "echo two && sleep infinity"',
stop_signal: 'SIGKILL',
networks: ['default'],
labels: {},
environment: {},
},
},
networks: {
default: {
driver: 'bridge',
ipam: {
config: [
{ gateway: '192.168.91.1', subnet: '192.168.91.0/24' },
],
driver: 'default',
},
},
},
volumes: {},
},
},
});
const updatedContainers = await docker.listContainers();
expect(
updatedContainers.map(({ Names, State }) => ({ Name: Names[0], State })),
).to.have.deep.members([
{ Name: '/one_21_2_deadca1f', State: 'running' },
{ Name: '/two_22_2_deadca1f', State: 'running' },
]);
// Container ids must have changed
expect(updatedContainers.map(({ Id }) => Id)).to.not.have.members(
containerIds,
);
expect(await docker.getNetwork('123_default').inspect())
.to.have.property('IPAM')
.to.deep.equal({
Config: [{ Gateway: '192.168.91.1', Subnet: '192.168.91.0/24' }],
Driver: 'default',
Options: {},
});
});
it('updates an app with two services with a network change', async () => {
await setTargetState({
config: {},

View File

@ -639,8 +639,12 @@ describe('compose/app', () => {
isTarget: true,
});
const steps = current.nextStepsForAppUpdate(defaultContext, target);
const availableImages = [createImage({ appUuid: 'deadbeef' })];
const steps = current.nextStepsForAppUpdate(
{ ...defaultContext, availableImages },
target,
);
const [killStep] = expectSteps('kill', steps);
expect(killStep)
.to.have.property('current')
@ -681,6 +685,65 @@ describe('compose/app', () => {
expectNoStep('removeNetwork', steps);
});
it('should always kill dependencies of networks before removing', async () => {
const current = createApp({
services: [
// The device for some reason is already running some services
// of the new release, but we need to kill it anyways
await createService({
image: 'alpine',
serviceName: 'one',
commit: 'deadca1f',
composition: { command: 'sleep infinity', networks: ['default'] },
}),
],
networks: [Network.fromComposeObject('default', 1, 'appuuid', {})],
});
const target = createApp({
services: [
await createService({
image: 'alpine',
serviceName: 'one',
commit: 'deadca1f',
composition: { command: 'sleep infinity', networks: ['default'] },
}),
await createService({
image: 'alpine',
serviceName: 'two',
commit: 'deadca1f',
composition: {
command: 'sh -c "echo two && sleep infinity"',
networks: ['default'],
},
}),
],
networks: [
Network.fromComposeObject('default', 1, 'appuuid', {
labels: { test: 'test' },
}),
],
isTarget: true,
});
const availableImages = [
createImage({ appId: 1, serviceName: 'one', name: 'alpine' }),
createImage({ appId: 1, serviceName: 'two', name: 'alpine' }),
];
const steps = current.nextStepsForAppUpdate(
{ ...defaultContext, availableImages },
target,
);
const [killStep] = expectSteps('kill', steps);
expect(killStep)
.to.have.property('current')
.that.deep.includes({ serviceName: 'one' });
// We shouldn't try to remove the network until we have gotten rid of the dependencies
expectNoStep('removeNetwork', steps);
});
it('should kill dependencies of networks before updating between releases', async () => {
const current = createApp({
services: [