From 5d93f358aa2c80fb318c8f988adde2a976a19a3b Mon Sep 17 00:00:00 2001 From: Felipe Lalanne Date: Mon, 24 Jun 2024 14:49:06 -0400 Subject: [PATCH] Create replicating test This adds a test to check the case where a service image changes along with networks being removed. --- test/integration/.mocharc.js | 2 +- test/integration/state-engine.spec.ts | 111 ++++++++++++++++++++++++++ test/unit/compose/app.spec.ts | 71 ++++++++++++++++ 3 files changed, 183 insertions(+), 1 deletion(-) diff --git a/test/integration/.mocharc.js b/test/integration/.mocharc.js index 683453ed..0e8f6ffc 100644 --- a/test/integration/.mocharc.js +++ b/test/integration/.mocharc.js @@ -10,5 +10,5 @@ module.exports = { 'test/lib/chai.ts', 'test/lib/mocha-hooks.ts', ], - timeout: '30000', + timeout: '60000', }; diff --git a/test/integration/state-engine.spec.ts b/test/integration/state-engine.spec.ts index a4a795f9..3d5b5af2 100644 --- a/test/integration/state-engine.spec.ts +++ b/test/integration/state-engine.spec.ts @@ -380,4 +380,115 @@ describe('state engine', () => { Options: {}, }); }); + + it('updates an app with two services with a network removal', async () => { + await setTargetState({ + config: {}, + apps: { + '123': { + name: 'test-app', + commit: 'deadbeef', + releaseId: 1, + services: { + '1': { + image: 'alpine:3.18', + imageId: 11, + serviceName: 'one', + restart: 'unless-stopped', + running: true, + command: 'sleep infinity', + stop_signal: 'SIGKILL', + labels: {}, + environment: {}, + networks: ['balena'], + }, + '2': { + image: 'ubuntu:focal', + imageId: 12, + serviceName: 'two', + restart: 'unless-stopped', + running: true, + command: 'sleep infinity', + labels: {}, + environment: {}, + network_mode: 'host', + }, + }, + networks: { + balena: {}, + }, + volumes: {}, + }, + }, + }); + + const state = await getCurrentState(); + expect( + state.apps['123'].services.map((s: any) => s.serviceName), + ).to.deep.equal(['one', 'two']); + + const containers = await docker.listContainers(); + expect( + containers.map(({ Names, State }) => ({ Name: Names[0], State })), + ).to.have.deep.members([ + { Name: '/one_11_1_deadbeef', State: 'running' }, + { Name: '/two_12_1_deadbeef', State: 'running' }, + ]); + const containerIds = containers.map(({ Id }) => Id); + await expect(docker.getNetwork('123_balena').inspect()).to.not.be.rejected; + + 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: 'ubuntu:latest', + imageId: 22, + serviceName: 'two', + restart: 'unless-stopped', + running: true, + command: 'sh -c "echo two && sleep infinity"', + stop_signal: 'SIGKILL', + network_mode: 'host', + labels: {}, + environment: {}, + }, + }, + networks: {}, + 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, + ); + + await expect(docker.getNetwork('123_balena').inspect()).to.be.rejected; + }); }); diff --git a/test/unit/compose/app.spec.ts b/test/unit/compose/app.spec.ts index fdb5e2c6..38397249 100644 --- a/test/unit/compose/app.spec.ts +++ b/test/unit/compose/app.spec.ts @@ -692,6 +692,77 @@ describe('compose/app', () => { expectNoStep('removeNetwork', steps2); }); + it('should perform fetch steps first even for services that reference deleted networks', async () => { + const current = createApp({ + appUuid: 'deadbeef', + services: [ + await createService({ + appId: 1, + appUuid: 'deadbeef', + image: 'test-image', + serviceName: 'test', + composition: { networks: ['test-network'] }, + }), + await createService({ + appId: 1, + appUuid: 'deadbeef', + image: 'other-image', + serviceName: 'other', + composition: { network_mode: 'host' }, + }), + ], + networks: [ + Network.fromComposeObject('test-network', 1, 'deadbeef', {}), + DEFAULT_NETWORK, + ], + }); + const target = createApp({ + appUuid: 'deadbeef', + services: [ + await createService({ + appId: 1, + appUuid: 'deadbeef', + serviceName: 'test', + image: 'new-test-image', + commit: 'new-commit', + }), + await createService({ + appId: 1, + appUuid: 'deadbeef', + serviceName: 'other', + image: 'new-other-image', + commit: 'new-commit', + composition: { network_mode: 'host' }, + }), + ], + networks: [], + isTarget: true, + }); + + const availableImages = [ + createImage({ + appUuid: 'deadbeef', + name: 'test-image', + serviceName: 'test', + }), + createImage({ + appUuid: 'deadbeef', + name: 'other-image', + serviceName: 'other', + }), + ]; + // Take lock first + const steps = current.nextStepsForAppUpdate( + { ...defaultContext, availableImages }, + target, + ); + + const fetchSteps = expectSteps('fetch', steps, 2); + expect(fetchSteps.map((s) => (s as any).image.name)).to.have.deep.members( + ['new-test-image', 'new-other-image'], + ); + }); + it('should kill dependencies of networks before changing config', async () => { const current = createApp({ services: [