mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-06-16 22:38:14 +00:00
Simplify lock interface to prep for adding takeLock to state funnel
This commit changes a few things: * Pass `force` to `takeLock` step directly. This allows us to remove the `lockFn` used by app manager's action executors, setting takeLock as the main interface to interact with the update lock module. Note that this commit by itself will not pass tests, as no update locking occurs where it once did. This will be amended in the next commit. * Remove locking functions from doRestart & doPurge, as this is the only area where skipLock is required. * Remove `skipLock` interface, as it's redundant with the functionality of `force`. The only time `skipLock` is true is in doRestart/doPurge, as those API methods are already run within a lock function. We removed the lock function which removes the need for skipLock, and in the next commit we'll add locking as a composition step to replace the functionality removed here. * Remove some methods not in use, such as app manager's `stopAll`. Signed-off-by: Christina Ying Wang <christina@balena.io>
This commit is contained in:
@ -4,6 +4,7 @@ import { stub } from 'sinon';
|
||||
import Docker from 'dockerode';
|
||||
import request from 'supertest';
|
||||
import { setTimeout } from 'timers/promises';
|
||||
import { testfs } from 'mocha-pod';
|
||||
|
||||
import * as deviceState from '~/src/device-state';
|
||||
import * as config from '~/src/config';
|
||||
@ -11,10 +12,12 @@ import * as hostConfig from '~/src/host-config';
|
||||
import * as deviceApi from '~/src/device-api';
|
||||
import * as actions from '~/src/device-api/actions';
|
||||
import * as TargetState from '~/src/device-state/target-state';
|
||||
import * as updateLock from '~/lib/update-lock';
|
||||
import { pathOnRoot } from '~/lib/host-utils';
|
||||
import { exec } from '~/lib/fs-utils';
|
||||
import * as lockfile from '~/lib/lockfile';
|
||||
import { cleanupDocker } from '~/test-lib/docker-helper';
|
||||
|
||||
import { exec } from '~/src/lib/fs-utils';
|
||||
|
||||
export async function dbusSend(
|
||||
dest: string,
|
||||
path: string,
|
||||
@ -79,6 +82,7 @@ describe('manages application lifecycle', () => {
|
||||
const BALENA_SUPERVISOR_ADDRESS =
|
||||
process.env.BALENA_SUPERVISOR_ADDRESS || 'http://balena-supervisor:48484';
|
||||
const APP_ID = 1;
|
||||
const lockdir = pathOnRoot(updateLock.BASE_LOCK_DIR);
|
||||
const docker = new Docker();
|
||||
|
||||
const getSupervisorTarget = async () =>
|
||||
@ -218,6 +222,11 @@ describe('manages application lifecycle', () => {
|
||||
ctns.every(({ State }) => !startedAt.includes(State.StartedAt));
|
||||
};
|
||||
|
||||
const mockFs = testfs(
|
||||
{ [`${lockdir}/${APP_ID}`]: {} },
|
||||
{ cleanup: [`${lockdir}/${APP_ID}/**/*.lock`] },
|
||||
);
|
||||
|
||||
before(async () => {
|
||||
// Images are ignored in local mode so we need to pull the base image
|
||||
await docker.pull(BASE_IMAGE);
|
||||
@ -251,10 +260,16 @@ describe('manages application lifecycle', () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await mockFs.enable();
|
||||
|
||||
// Create a single-container application in local mode
|
||||
await setSupervisorTarget(targetState);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await mockFs.restore();
|
||||
});
|
||||
|
||||
// Make sure the app is running and correct before testing more assertions
|
||||
it('should setup a single container app (sanity check)', async () => {
|
||||
containers = await waitForSetup(targetState);
|
||||
@ -292,6 +307,69 @@ describe('manages application lifecycle', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should not restart an application when user locks are present', async () => {
|
||||
containers = await waitForSetup(targetState);
|
||||
|
||||
// Create a lock
|
||||
await lockfile.lock(
|
||||
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||
);
|
||||
|
||||
await request(BALENA_SUPERVISOR_ADDRESS)
|
||||
.post(`/v1/restart`)
|
||||
.set('Content-Type', 'application/json')
|
||||
.send(JSON.stringify({ appId: APP_ID }))
|
||||
.expect(423);
|
||||
|
||||
// Containers should not have been restarted
|
||||
const containersAfterRestart = await waitForSetup(targetState);
|
||||
expect(
|
||||
containersAfterRestart.map((ctn) => ctn.State.StartedAt),
|
||||
).to.deep.include.members(containers.map((ctn) => ctn.State.StartedAt));
|
||||
|
||||
// Remove the lock
|
||||
await lockfile.unlock(
|
||||
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should restart an application when user locks are present if force is specified', async () => {
|
||||
containers = await waitForSetup(targetState);
|
||||
const isRestartSuccessful = startTimesChanged(
|
||||
containers.map((ctn) => ctn.State.StartedAt),
|
||||
);
|
||||
|
||||
// Create a lock
|
||||
await lockfile.lock(
|
||||
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||
);
|
||||
|
||||
await request(BALENA_SUPERVISOR_ADDRESS)
|
||||
.post(`/v1/restart`)
|
||||
.set('Content-Type', 'application/json')
|
||||
.send(JSON.stringify({ appId: APP_ID, force: true }));
|
||||
|
||||
const restartedContainers = await waitForSetup(
|
||||
targetState,
|
||||
isRestartSuccessful,
|
||||
);
|
||||
|
||||
// Technically the wait function above should already verify that the two
|
||||
// containers have been restarted, but verify explcitly with an assertion
|
||||
expect(isRestartSuccessful(restartedContainers)).to.be.true;
|
||||
|
||||
// Containers should have different Ids since they're recreated
|
||||
expect(restartedContainers.map(({ Id }) => Id)).to.not.have.members(
|
||||
containers.map((ctn) => ctn.Id),
|
||||
);
|
||||
|
||||
// Wait briefly for state to settle which includes releasing locks
|
||||
await setTimeout(1000);
|
||||
|
||||
// User lock should be overridden
|
||||
expect(await updateLock.getLocksTaken()).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it('should restart service by removing and recreating corresponding container', async () => {
|
||||
containers = await waitForSetup(targetState);
|
||||
const isRestartSuccessful = startTimesChanged(
|
||||
@ -321,6 +399,73 @@ describe('manages application lifecycle', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// Since restart-service follows the same code paths as start|stop-service,
|
||||
// these lock test cases should be sufficient to cover all three service actions.
|
||||
it('should not restart service when user locks are present', async () => {
|
||||
containers = await waitForSetup(targetState);
|
||||
|
||||
// Create a lock
|
||||
await lockfile.lock(
|
||||
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||
);
|
||||
|
||||
await request(BALENA_SUPERVISOR_ADDRESS)
|
||||
.post('/v2/applications/1/restart-service')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send(JSON.stringify({ serviceName: serviceNames[0] }))
|
||||
.expect(423);
|
||||
|
||||
// Containers should not have been restarted
|
||||
const containersAfterRestart = await waitForSetup(targetState);
|
||||
expect(
|
||||
containersAfterRestart.map((ctn) => ctn.State.StartedAt),
|
||||
).to.deep.include.members(containers.map((ctn) => ctn.State.StartedAt));
|
||||
|
||||
// Remove the lock
|
||||
await lockfile.unlock(
|
||||
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should restart service when user locks are present if force is specified', async () => {
|
||||
containers = await waitForSetup(targetState);
|
||||
const isRestartSuccessful = startTimesChanged(
|
||||
containers
|
||||
.filter((ctn) => ctn.Name.includes(serviceNames[0]))
|
||||
.map((ctn) => ctn.State.StartedAt),
|
||||
);
|
||||
|
||||
// Create a lock
|
||||
await lockfile.lock(
|
||||
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||
);
|
||||
|
||||
await request(BALENA_SUPERVISOR_ADDRESS)
|
||||
.post('/v2/applications/1/restart-service')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send(JSON.stringify({ serviceName: serviceNames[0], force: true }));
|
||||
|
||||
const restartedContainers = await waitForSetup(
|
||||
targetState,
|
||||
isRestartSuccessful,
|
||||
);
|
||||
|
||||
// Technically the wait function above should already verify that the two
|
||||
// containers have been restarted, but verify explcitly with an assertion
|
||||
expect(isRestartSuccessful(restartedContainers)).to.be.true;
|
||||
|
||||
// Containers should have different Ids since they're recreated
|
||||
expect(restartedContainers.map(({ Id }) => Id)).to.not.have.members(
|
||||
containers.map((ctn) => ctn.Id),
|
||||
);
|
||||
|
||||
// Wait briefly for state to settle which includes releasing locks
|
||||
await setTimeout(1000);
|
||||
|
||||
// User lock should be overridden
|
||||
expect(await updateLock.getLocksTaken()).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it('should stop a running service', async () => {
|
||||
containers = await waitForSetup(targetState);
|
||||
|
||||
@ -520,10 +665,15 @@ describe('manages application lifecycle', () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await mockFs.enable();
|
||||
// Create a multi-container application in local mode
|
||||
await setSupervisorTarget(targetState);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await mockFs.restore();
|
||||
});
|
||||
|
||||
// Make sure the app is running and correct before testing more assertions
|
||||
it('should setup a multi-container app (sanity check)', async () => {
|
||||
containers = await waitForSetup(targetState);
|
||||
@ -560,6 +710,69 @@ describe('manages application lifecycle', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should not restart an application when user locks are present', async () => {
|
||||
containers = await waitForSetup(targetState);
|
||||
|
||||
// Create a lock
|
||||
await lockfile.lock(
|
||||
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||
);
|
||||
|
||||
await request(BALENA_SUPERVISOR_ADDRESS)
|
||||
.post(`/v1/restart`)
|
||||
.set('Content-Type', 'application/json')
|
||||
.send(JSON.stringify({ appId: APP_ID }))
|
||||
.expect(423);
|
||||
|
||||
// Containers should not have been restarted
|
||||
const containersAfterRestart = await waitForSetup(targetState);
|
||||
expect(
|
||||
containersAfterRestart.map((ctn) => ctn.State.StartedAt),
|
||||
).to.deep.include.members(containers.map((ctn) => ctn.State.StartedAt));
|
||||
|
||||
// Remove the lock
|
||||
await lockfile.unlock(
|
||||
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should restart an application when user locks are present if force is specified', async () => {
|
||||
containers = await waitForSetup(targetState);
|
||||
const isRestartSuccessful = startTimesChanged(
|
||||
containers.map((ctn) => ctn.State.StartedAt),
|
||||
);
|
||||
|
||||
// Create a lock
|
||||
await lockfile.lock(
|
||||
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||
);
|
||||
|
||||
await request(BALENA_SUPERVISOR_ADDRESS)
|
||||
.post(`/v1/restart`)
|
||||
.set('Content-Type', 'application/json')
|
||||
.send(JSON.stringify({ appId: APP_ID, force: true }));
|
||||
|
||||
const restartedContainers = await waitForSetup(
|
||||
targetState,
|
||||
isRestartSuccessful,
|
||||
);
|
||||
|
||||
// Technically the wait function above should already verify that the two
|
||||
// containers have been restarted, but verify explcitly with an assertion
|
||||
expect(isRestartSuccessful(restartedContainers)).to.be.true;
|
||||
|
||||
// Containers should have different Ids since they're recreated
|
||||
expect(restartedContainers.map(({ Id }) => Id)).to.not.have.members(
|
||||
containers.map((ctn) => ctn.Id),
|
||||
);
|
||||
|
||||
// Wait briefly for state to settle which includes releasing locks
|
||||
await setTimeout(500);
|
||||
|
||||
// User lock should be overridden
|
||||
expect(await updateLock.getLocksTaken()).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it('should restart service by removing and recreating corresponding container', async () => {
|
||||
containers = await waitForSetup(targetState);
|
||||
const serviceName = serviceNames[0];
|
||||
@ -601,6 +814,73 @@ describe('manages application lifecycle', () => {
|
||||
expect(sharedIds.length).to.equal(1);
|
||||
});
|
||||
|
||||
// Since restart-service follows the same code paths as start|stop-service,
|
||||
// these lock test cases should be sufficient to cover all three service actions.
|
||||
it('should not restart service when user locks are present', async () => {
|
||||
containers = await waitForSetup(targetState);
|
||||
|
||||
// Create a lock
|
||||
await lockfile.lock(
|
||||
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||
);
|
||||
|
||||
await request(BALENA_SUPERVISOR_ADDRESS)
|
||||
.post('/v2/applications/1/restart-service')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send(JSON.stringify({ serviceName: serviceNames[0] }))
|
||||
.expect(423);
|
||||
|
||||
// Containers should not have been restarted
|
||||
const containersAfterRestart = await waitForSetup(targetState);
|
||||
expect(
|
||||
containersAfterRestart.map((ctn) => ctn.State.StartedAt),
|
||||
).to.deep.include.members(containers.map((ctn) => ctn.State.StartedAt));
|
||||
|
||||
// Remove the lock
|
||||
await lockfile.unlock(
|
||||
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should restart service when user locks are present if force is specified', async () => {
|
||||
containers = await waitForSetup(targetState);
|
||||
const isRestartSuccessful = startTimesChanged(
|
||||
containers
|
||||
.filter((ctn) => ctn.Name.includes(serviceNames[0]))
|
||||
.map((ctn) => ctn.State.StartedAt),
|
||||
);
|
||||
|
||||
// Create a lock
|
||||
await lockfile.lock(
|
||||
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||
);
|
||||
|
||||
await request(BALENA_SUPERVISOR_ADDRESS)
|
||||
.post('/v2/applications/1/restart-service')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send(JSON.stringify({ serviceName: serviceNames[0], force: true }));
|
||||
|
||||
const restartedContainers = await waitForSetup(
|
||||
targetState,
|
||||
isRestartSuccessful,
|
||||
);
|
||||
|
||||
// Technically the wait function above should already verify that the two
|
||||
// containers have been restarted, but verify explcitly with an assertion
|
||||
expect(isRestartSuccessful(restartedContainers)).to.be.true;
|
||||
|
||||
// Containers should have different Ids since they're recreated
|
||||
expect(restartedContainers.map(({ Id }) => Id)).to.not.have.members(
|
||||
containers.map((ctn) => ctn.Id),
|
||||
);
|
||||
|
||||
// Wait briefly for state to settle which includes releasing locks
|
||||
await setTimeout(500);
|
||||
|
||||
// User lock should be overridden
|
||||
expect(await updateLock.getLocksTaken()).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it('should stop a running service', async () => {
|
||||
containers = await waitForSetup(targetState);
|
||||
|
||||
|
Reference in New Issue
Block a user