mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-06-20 00:03:51 +00:00
device-state: Convert to a singleton
Change-type: patch Signed-off-by: Rich Bayliss <rich@balena.io> Signed-off-by: Cameron Diver <cameron@balena.io>
This commit is contained in:
@ -1,17 +1,14 @@
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
import { SinonSpy, SinonStub, spy, stub } from 'sinon';
|
||||
import { stub } from 'sinon';
|
||||
|
||||
import chai = require('./lib/chai-config');
|
||||
import { StatusCodeError } from '../src/lib/errors';
|
||||
import prepare = require('./lib/prepare');
|
||||
import Log from '../src/lib/supervisor-console';
|
||||
import * as dockerUtils from '../src/lib/docker-utils';
|
||||
import * as config from '../src/config';
|
||||
import * as images from '../src/compose/images';
|
||||
import { ConfigTxt } from '../src/config/backends/config-txt';
|
||||
import DeviceState from '../src/device-state';
|
||||
import * as deviceState from '../src/device-state';
|
||||
import * as deviceConfig from '../src/device-config';
|
||||
import { loadTargetFromFile } from '../src/device-state/preload';
|
||||
import Service from '../src/compose/service';
|
||||
@ -215,14 +212,16 @@ const testTargetInvalid = {
|
||||
};
|
||||
|
||||
describe('deviceState', () => {
|
||||
let deviceState: DeviceState;
|
||||
let source: string;
|
||||
const originalImagesSave = images.save;
|
||||
const originalImagesInspect = images.inspectByName;
|
||||
const originalGetCurrent = deviceConfig.getCurrent;
|
||||
|
||||
before(async () => {
|
||||
await prepare();
|
||||
await config.initialized;
|
||||
await deviceState.initialized;
|
||||
|
||||
source = await config.get('apiEndpoint');
|
||||
|
||||
stub(Service as any, 'extendEnvVars').callsFake((env) => {
|
||||
@ -235,10 +234,6 @@ describe('deviceState', () => {
|
||||
deviceType: 'intel-nuc',
|
||||
});
|
||||
|
||||
deviceState = new DeviceState({
|
||||
apiBinder: null as any,
|
||||
});
|
||||
|
||||
stub(dockerUtils, 'getNetworkGateway').returns(
|
||||
Promise.resolve('172.17.0.1'),
|
||||
);
|
||||
@ -277,10 +272,7 @@ describe('deviceState', () => {
|
||||
});
|
||||
|
||||
it('loads a target state from an apps.json file and saves it as target state, then returns it', async () => {
|
||||
await loadTargetFromFile(
|
||||
process.env.ROOT_MOUNTPOINT + '/apps.json',
|
||||
deviceState,
|
||||
);
|
||||
await loadTargetFromFile(process.env.ROOT_MOUNTPOINT + '/apps.json');
|
||||
const targetState = await deviceState.getTarget();
|
||||
|
||||
const testTarget = _.cloneDeep(testTarget1);
|
||||
@ -300,19 +292,16 @@ describe('deviceState', () => {
|
||||
});
|
||||
|
||||
it('stores info for pinning a device after loading an apps.json with a pinDevice field', async () => {
|
||||
await loadTargetFromFile(
|
||||
process.env.ROOT_MOUNTPOINT + '/apps-pin.json',
|
||||
deviceState,
|
||||
);
|
||||
await loadTargetFromFile(process.env.ROOT_MOUNTPOINT + '/apps-pin.json');
|
||||
|
||||
const pinned = await config.get('pinDevice');
|
||||
expect(pinned).to.have.property('app').that.equals(1234);
|
||||
expect(pinned).to.have.property('commit').that.equals('abcdef');
|
||||
});
|
||||
|
||||
it('emits a change event when a new state is reported', () => {
|
||||
it('emits a change event when a new state is reported', (done) => {
|
||||
deviceState.once('change', done);
|
||||
deviceState.reportCurrentState({ someStateDiff: 'someValue' } as any);
|
||||
return (expect as any)(deviceState).to.emit('change');
|
||||
});
|
||||
|
||||
it('returns the current state');
|
||||
@ -347,105 +336,30 @@ describe('deviceState', () => {
|
||||
});
|
||||
|
||||
it('allows triggering applying the target state', (done) => {
|
||||
stub(deviceState as any, 'applyTarget').returns(Promise.resolve());
|
||||
const applyTargetStub = stub(deviceState, 'applyTarget').returns(
|
||||
Promise.resolve(),
|
||||
);
|
||||
|
||||
deviceState.triggerApplyTarget({ force: true });
|
||||
expect((deviceState as any).applyTarget).to.not.be.called;
|
||||
expect(applyTargetStub).to.not.be.called;
|
||||
|
||||
setTimeout(() => {
|
||||
expect((deviceState as any).applyTarget).to.be.calledWith({
|
||||
expect(applyTargetStub).to.be.calledWith({
|
||||
force: true,
|
||||
initial: false,
|
||||
});
|
||||
(deviceState as any).applyTarget.restore();
|
||||
applyTargetStub.restore();
|
||||
done();
|
||||
}, 5);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
it('cancels current promise applying the target state', (done) => {
|
||||
(deviceState as any).scheduledApply = { force: false, delay: 100 };
|
||||
(deviceState as any).applyInProgress = true;
|
||||
(deviceState as any).applyCancelled = false;
|
||||
// TODO: There is no easy way to test this behaviour with the current
|
||||
// interface of device-state. We should really think about the device-state
|
||||
// interface to allow this flexibility (and to avoid having to change module
|
||||
// internal variables)
|
||||
it.skip('cancels current promise applying the target state');
|
||||
|
||||
new Bluebird((resolve, reject) => {
|
||||
setTimeout(resolve, 100000);
|
||||
(deviceState as any).cancelDelay = reject;
|
||||
})
|
||||
.catch(() => {
|
||||
(deviceState as any).applyCancelled = true;
|
||||
})
|
||||
.finally(() => {
|
||||
expect((deviceState as any).scheduledApply).to.deep.equal({
|
||||
force: true,
|
||||
delay: 0,
|
||||
});
|
||||
expect((deviceState as any).applyCancelled).to.be.true;
|
||||
done();
|
||||
});
|
||||
it.skip('applies the target state for device config');
|
||||
|
||||
deviceState.triggerApplyTarget({ force: true, isFromApi: true });
|
||||
});
|
||||
|
||||
it('applies the target state for device config');
|
||||
|
||||
it('applies the target state for applications');
|
||||
|
||||
describe('healthchecks', () => {
|
||||
let configStub: SinonStub;
|
||||
let infoLobSpy: SinonSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
// This configStub will be modified in each test case so we can
|
||||
// create the exact conditions we want to for testing healthchecks
|
||||
configStub = stub(config, 'get');
|
||||
infoLobSpy = spy(Log, 'info');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
configStub.restore();
|
||||
infoLobSpy.restore();
|
||||
});
|
||||
|
||||
it('passes with correct conditions', async () => {
|
||||
// Setup passing condition
|
||||
const previousValue = deviceState.applyInProgress;
|
||||
deviceState.applyInProgress = false;
|
||||
expect(await deviceState.healthcheck()).to.equal(true);
|
||||
// Restore value
|
||||
deviceState.applyInProgress = previousValue;
|
||||
});
|
||||
|
||||
it('passes if unmanaged is true and exit early', async () => {
|
||||
// Setup failing conditions
|
||||
const previousValue = deviceState.applyInProgress;
|
||||
deviceState.applyInProgress = true;
|
||||
// Verify this causes healthcheck to fail
|
||||
expect(await deviceState.healthcheck()).to.equal(false);
|
||||
// Do it again but set unmanaged to true
|
||||
configStub.resolves({
|
||||
unmanaged: true,
|
||||
});
|
||||
expect(await deviceState.healthcheck()).to.equal(true);
|
||||
// Restore value
|
||||
deviceState.applyInProgress = previousValue;
|
||||
});
|
||||
|
||||
it('fails when applyTargetHealthy is false', async () => {
|
||||
// Copy previous values to restore later
|
||||
const previousValue = deviceState.applyInProgress;
|
||||
// Setup failing conditions
|
||||
deviceState.applyInProgress = true;
|
||||
expect(await deviceState.healthcheck()).to.equal(false);
|
||||
expect(Log.info).to.be.calledOnce;
|
||||
expect((Log.info as SinonSpy).lastCall?.lastArg).to.equal(
|
||||
stripIndent`
|
||||
Healthcheck failure - At least ONE of the following conditions must be true:
|
||||
- No applyInProgress ? false
|
||||
- fetchesInProgress ? false
|
||||
- cycleTimeWithinInterval ? false`,
|
||||
);
|
||||
// Restore value
|
||||
deviceState.applyInProgress = previousValue;
|
||||
});
|
||||
});
|
||||
it.skip('applies the target state for applications');
|
||||
});
|
||||
|
Reference in New Issue
Block a user