mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-01-18 18:56:24 +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:
parent
d50f7791e1
commit
e3864915bc
@ -23,7 +23,7 @@ import * as request from './lib/request';
|
||||
|
||||
import log from './lib/supervisor-console';
|
||||
|
||||
import DeviceState from './device-state';
|
||||
import * as deviceState from './device-state';
|
||||
import * as globalEventBus from './event-bus';
|
||||
import * as TargetState from './device-state/target-state';
|
||||
import * as logger from './logger';
|
||||
@ -53,7 +53,6 @@ interface DeviceTag {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export let deviceState: DeviceState;
|
||||
const lastReportedState: DeviceStatus = {
|
||||
local: {},
|
||||
dependent: {},
|
||||
@ -66,10 +65,6 @@ let reportPending = false;
|
||||
export let stateReportErrors = 0;
|
||||
let readyForUpdates = false;
|
||||
|
||||
export function setDeviceState(newState: DeviceState) {
|
||||
deviceState = newState;
|
||||
}
|
||||
|
||||
export async function healthcheck() {
|
||||
const {
|
||||
appUpdatePollInterval,
|
||||
@ -179,7 +174,7 @@ export async function start() {
|
||||
// must wait for the provisioning because we need a
|
||||
// target state on which to apply the backup
|
||||
globalEventBus.getInstance().once('targetStateChanged', async (state) => {
|
||||
await loadBackupFromMigration(deviceState, state, bootstrapRetryDelay);
|
||||
await loadBackupFromMigration(state, bootstrapRetryDelay);
|
||||
});
|
||||
|
||||
readyForUpdates = true;
|
||||
@ -718,6 +713,7 @@ export let balenaApi: PinejsClientRequest | null = null;
|
||||
export const initialized = (async () => {
|
||||
await config.initialized;
|
||||
await eventTracker.initialized;
|
||||
await deviceState.initialized;
|
||||
|
||||
const { unmanaged, apiEndpoint, currentApiKey } = await config.getMany([
|
||||
'unmanaged',
|
||||
|
8
src/application-manager.d.ts
vendored
8
src/application-manager.d.ts
vendored
@ -7,9 +7,9 @@ import { ServiceAction } from './device-api/common';
|
||||
import { DeviceStatus, InstancedAppState } from './types/state';
|
||||
|
||||
import type { Image } from './compose/images';
|
||||
import DeviceState from './device-state';
|
||||
import * as deviceState from './device-state';
|
||||
import * as apiBinder from './api-binder';
|
||||
|
||||
import { APIBinder } from './api-binder';
|
||||
import * as config from './config';
|
||||
|
||||
import {
|
||||
@ -41,8 +41,6 @@ class ApplicationManager extends EventEmitter {
|
||||
// TODO: When the module which is/declares these fields is converted to
|
||||
// typecript, type the following
|
||||
public _lockingIfNecessary: any;
|
||||
public deviceState: DeviceState;
|
||||
public apiBinder: APIBinder;
|
||||
|
||||
public proxyvisor: any;
|
||||
public timeSpentFetching: number;
|
||||
@ -52,7 +50,7 @@ class ApplicationManager extends EventEmitter {
|
||||
|
||||
public router: Router;
|
||||
|
||||
public constructor({ deviceState: DeviceState, apiBinder: APIBinder });
|
||||
public constructor();
|
||||
|
||||
public init(): Promise<void>;
|
||||
|
||||
|
@ -34,6 +34,9 @@ import { createV1Api } from './device-api/v1';
|
||||
import { createV2Api } from './device-api/v2';
|
||||
import { serviceAction } from './device-api/common';
|
||||
|
||||
import * as deviceState from './device-state';
|
||||
import * as apiBinder from './api-binder';
|
||||
|
||||
import * as db from './db';
|
||||
|
||||
// TODO: move this to an Image class?
|
||||
@ -70,7 +73,7 @@ const createApplicationManagerRouter = function (applications) {
|
||||
};
|
||||
|
||||
export class ApplicationManager extends EventEmitter {
|
||||
constructor({ deviceState, apiBinder }) {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.serviceAction = serviceAction;
|
||||
@ -123,7 +126,6 @@ export class ApplicationManager extends EventEmitter {
|
||||
};
|
||||
|
||||
this.reportCurrentState = this.reportCurrentState.bind(this);
|
||||
this.init = this.init.bind(this);
|
||||
this.getStatus = this.getStatus.bind(this);
|
||||
this.getDependentState = this.getDependentState.bind(this);
|
||||
this.getCurrentForComparison = this.getCurrentForComparison.bind(this);
|
||||
|
@ -3,6 +3,8 @@ import { NextFunction, Request, Response, Router } from 'express';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { ApplicationManager } from '../application-manager';
|
||||
import * as deviceState from '../device-state';
|
||||
import * as apiBinder from '../api-binder';
|
||||
import { Service } from '../compose/service';
|
||||
import Volume from '../compose/volume';
|
||||
import * as config from '../config';
|
||||
@ -25,7 +27,7 @@ import { isVPNActive } from '../network';
|
||||
import { doPurge, doRestart, safeStateClone, serviceAction } from './common';
|
||||
|
||||
export function createV2Api(router: Router, applications: ApplicationManager) {
|
||||
const { _lockingIfNecessary, deviceState } = applications;
|
||||
const { _lockingIfNecessary } = applications;
|
||||
|
||||
const handleServiceAction = (
|
||||
req: Request,
|
||||
@ -394,7 +396,7 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
|
||||
router.get('/v2/state/status', async (_req, res) => {
|
||||
const currentRelease = await config.get('currentCommit');
|
||||
|
||||
const pending = applications.deviceState.applyInProgress;
|
||||
const pending = deviceState.isApplyInProgress();
|
||||
const containerStates = (await serviceManager.getAll()).map((svc) =>
|
||||
_.pick(
|
||||
svc,
|
||||
@ -452,7 +454,7 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
|
||||
|
||||
router.get('/v2/device/tags', async (_req, res) => {
|
||||
try {
|
||||
const tags = await applications.apiBinder.fetchDeviceTags();
|
||||
const tags = await apiBinder.fetchDeviceTags();
|
||||
return res.json({
|
||||
status: 'success',
|
||||
tags,
|
||||
|
1314
src/device-state.ts
1314
src/device-state.ts
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,7 @@ import { DeviceStatus } from '../types/state';
|
||||
import { getRequestInstance } from '../lib/request';
|
||||
import * as config from '../config';
|
||||
import * as eventTracker from '../event-tracker';
|
||||
import DeviceState from '../device-state';
|
||||
import * as deviceState from '../device-state';
|
||||
import { CoreOptions } from 'request';
|
||||
import * as url from 'url';
|
||||
|
||||
@ -22,7 +22,6 @@ const INTERNAL_STATE_KEYS = [
|
||||
'update_failed',
|
||||
];
|
||||
|
||||
let deviceState: DeviceState;
|
||||
export let stateReportErrors = 0;
|
||||
const lastReportedState: DeviceStatus = {
|
||||
local: {},
|
||||
@ -243,9 +242,7 @@ const reportCurrentState = (): null => {
|
||||
return null;
|
||||
};
|
||||
|
||||
// TODO: Remove the passing in of deviceState once it's a singleton
|
||||
export const startReporting = ($deviceState: typeof deviceState) => {
|
||||
deviceState = $deviceState;
|
||||
export const startReporting = () => {
|
||||
deviceState.on('change', () => {
|
||||
if (!reportPending) {
|
||||
// A latency of 100ms should be acceptable and
|
||||
|
@ -2,7 +2,7 @@ import * as _ from 'lodash';
|
||||
import { fs } from 'mz';
|
||||
|
||||
import { Image } from '../compose/images';
|
||||
import DeviceState from '../device-state';
|
||||
import * as deviceState from '../device-state';
|
||||
import * as config from '../config';
|
||||
import * as deviceConfig from '../device-config';
|
||||
import * as eventTracker from '../event-tracker';
|
||||
@ -17,7 +17,6 @@ import { AppsJsonFormat } from '../types/state';
|
||||
|
||||
export async function loadTargetFromFile(
|
||||
appsPath: Nullable<string>,
|
||||
deviceState: DeviceState,
|
||||
): Promise<void> {
|
||||
log.info('Attempting to load any preloaded applications');
|
||||
if (!appsPath) {
|
||||
@ -88,7 +87,6 @@ export async function loadTargetFromFile(
|
||||
};
|
||||
|
||||
await deviceState.setTarget(localState);
|
||||
|
||||
log.success('Preloading complete');
|
||||
if (preloadState.pinDevice) {
|
||||
// Multi-app warning!
|
||||
|
@ -3,20 +3,24 @@ import * as _ from 'lodash';
|
||||
import * as mkdirp from 'mkdirp';
|
||||
import { child_process, fs } from 'mz';
|
||||
import * as path from 'path';
|
||||
import { PinejsClientRequest } from 'pinejs-client-request';
|
||||
import * as rimraf from 'rimraf';
|
||||
|
||||
const mkdirpAsync = Bluebird.promisify(mkdirp);
|
||||
const rimrafAsync = Bluebird.promisify(rimraf);
|
||||
|
||||
import { ApplicationManager } from '../application-manager';
|
||||
import * as apiBinder from '../api-binder';
|
||||
import * as config from '../config';
|
||||
import * as db from '../db';
|
||||
import * as volumeManager from '../compose/volume-manager';
|
||||
import * as serviceManager from '../compose/service-manager';
|
||||
import DeviceState from '../device-state';
|
||||
import * as deviceState from '../device-state';
|
||||
import * as constants from '../lib/constants';
|
||||
import { BackupError, DatabaseParseError, NotFoundError } from '../lib/errors';
|
||||
import {
|
||||
BackupError,
|
||||
DatabaseParseError,
|
||||
NotFoundError,
|
||||
InternalInconsistencyError,
|
||||
} from '../lib/errors';
|
||||
import { docker } from '../lib/docker-utils';
|
||||
import { pathExistsOnHost } from '../lib/fs-utils';
|
||||
import { log } from '../lib/supervisor-console';
|
||||
@ -108,10 +112,16 @@ export function convertLegacyAppsJson(appsArray: any[]): AppsJsonFormat {
|
||||
return { apps, config: deviceConfig } as AppsJsonFormat;
|
||||
}
|
||||
|
||||
export async function normaliseLegacyDatabase(
|
||||
application: ApplicationManager,
|
||||
balenaApi: PinejsClientRequest,
|
||||
) {
|
||||
export async function normaliseLegacyDatabase() {
|
||||
await apiBinder.initialized;
|
||||
await deviceState.initialized;
|
||||
|
||||
if (apiBinder.balenaApi == null) {
|
||||
throw new InternalInconsistencyError(
|
||||
'API binder is not initialized correctly',
|
||||
);
|
||||
}
|
||||
|
||||
// When legacy apps are present, we kill their containers and migrate their /data to a named volume
|
||||
log.info('Migrating ids for legacy app...');
|
||||
|
||||
@ -147,7 +157,7 @@ export async function normaliseLegacyDatabase(
|
||||
}
|
||||
|
||||
log.debug(`Getting release ${app.commit} for app ${app.appId} from API`);
|
||||
const releases = await balenaApi.get({
|
||||
const releases = await apiBinder.balenaApi.get({
|
||||
resource: 'release',
|
||||
options: {
|
||||
$filter: {
|
||||
@ -248,7 +258,7 @@ export async function normaliseLegacyDatabase(
|
||||
await serviceManager.killAllLegacy();
|
||||
log.debug('Migrating legacy app volumes');
|
||||
|
||||
const targetApps = await application.getTargetApps();
|
||||
const targetApps = await deviceState.applications.getTargetApps();
|
||||
|
||||
for (const appId of _.keys(targetApps)) {
|
||||
await volumeManager.createFromLegacy(parseInt(appId, 10));
|
||||
@ -260,7 +270,6 @@ export async function normaliseLegacyDatabase(
|
||||
}
|
||||
|
||||
export async function loadBackupFromMigration(
|
||||
deviceState: DeviceState,
|
||||
targetState: TargetState,
|
||||
retryDelay: number,
|
||||
): Promise<void> {
|
||||
@ -340,6 +349,6 @@ export async function loadBackupFromMigration(
|
||||
log.error(`Error restoring migration backup, retrying: ${err}`);
|
||||
|
||||
await Bluebird.delay(retryDelay);
|
||||
return loadBackupFromMigration(deviceState, targetState, retryDelay);
|
||||
return loadBackupFromMigration(targetState, retryDelay);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as apiBinder from './api-binder';
|
||||
import * as db from './db';
|
||||
import * as config from './config';
|
||||
import DeviceState from './device-state';
|
||||
import * as deviceState from './device-state';
|
||||
import * as eventTracker from './event-tracker';
|
||||
import { intialiseContractRequirements } from './lib/contracts';
|
||||
import { normaliseLegacyDatabase } from './lib/migration';
|
||||
@ -31,26 +31,18 @@ const startupConfigFields: config.ConfigKey[] = [
|
||||
];
|
||||
|
||||
export class Supervisor {
|
||||
private deviceState: DeviceState;
|
||||
private api: SupervisorAPI;
|
||||
|
||||
public constructor() {
|
||||
this.deviceState = new DeviceState({
|
||||
apiBinder,
|
||||
});
|
||||
|
||||
// workaround the circular dependency
|
||||
apiBinder.setDeviceState(this.deviceState);
|
||||
|
||||
// FIXME: rearchitect proxyvisor to avoid this circular dependency
|
||||
// by storing current state and having the APIBinder query and report it / provision devices
|
||||
this.deviceState.applications.proxyvisor.bindToAPI(apiBinder);
|
||||
deviceState.applications.proxyvisor.bindToAPI(apiBinder);
|
||||
|
||||
this.api = new SupervisorAPI({
|
||||
routers: [apiBinder.router, this.deviceState.router],
|
||||
routers: [apiBinder.router, deviceState.router],
|
||||
healthchecks: [
|
||||
apiBinder.healthcheck,
|
||||
this.deviceState.healthcheck.bind(this.deviceState),
|
||||
deviceState.healthcheck,
|
||||
],
|
||||
});
|
||||
}
|
||||
@ -79,20 +71,19 @@ export class Supervisor {
|
||||
log.debug('Starting api binder');
|
||||
await apiBinder.initialized;
|
||||
|
||||
await deviceState.initialized;
|
||||
|
||||
logger.logSystemMessage('Supervisor starting', {}, 'Supervisor start');
|
||||
if (conf.legacyAppsPresent && apiBinder.balenaApi != null) {
|
||||
log.info('Legacy app detected, running migration');
|
||||
await normaliseLegacyDatabase(
|
||||
this.deviceState.applications,
|
||||
apiBinder.balenaApi,
|
||||
);
|
||||
await normaliseLegacyDatabase();
|
||||
}
|
||||
|
||||
await this.deviceState.init();
|
||||
await deviceState.loadInitialState();
|
||||
|
||||
log.info('Starting API server');
|
||||
this.api.listen(conf.listenPort, conf.apiTimeout);
|
||||
this.deviceState.on('shutdown', () => this.api.stop());
|
||||
deviceState.on('shutdown', () => this.api.stop());
|
||||
|
||||
await apiBinder.start();
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ process.env.DATABASE_PATH_2 = './test/data/database2.sqlite';
|
||||
process.env.DATABASE_PATH_3 = './test/data/database3.sqlite';
|
||||
process.env.LED_FILE = './test/data/led_file';
|
||||
|
||||
import './lib/mocked-dockerode';
|
||||
import './lib/mocked-iptables';
|
||||
import './lib/mocked-event-tracker';
|
||||
|
||||
|
@ -122,6 +122,11 @@ describe('Config', () => {
|
||||
expect(osVariant).to.be.undefined;
|
||||
});
|
||||
|
||||
it('reads and exposes MAC addresses', async () => {
|
||||
const macAddress = await conf.get('macAddress');
|
||||
expect(macAddress).to.have.length.greaterThan(0);
|
||||
});
|
||||
|
||||
describe('Function config providers', () => {
|
||||
it('should throw if a non-mutable function provider is set', () => {
|
||||
expect(conf.set({ version: 'some-version' })).to.be.rejected;
|
||||
|
@ -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');
|
||||
});
|
||||
|
@ -5,7 +5,7 @@ import { SinonSpy, SinonStub, spy, stub } from 'sinon';
|
||||
|
||||
import prepare = require('./lib/prepare');
|
||||
import * as config from '../src/config';
|
||||
import DeviceState from '../src/device-state';
|
||||
import * as deviceState from '../src/device-state';
|
||||
import Log from '../src/lib/supervisor-console';
|
||||
import chai = require('./lib/chai-config');
|
||||
import balenaAPI = require('./lib/mocked-balena-api');
|
||||
@ -47,11 +47,8 @@ const initModels = async (obj: Dictionary<any>, filename: string) => {
|
||||
await ApiBinder.initialized;
|
||||
obj.apiBinder = ApiBinder;
|
||||
|
||||
obj.deviceState = new DeviceState({
|
||||
apiBinder: obj.apiBinder,
|
||||
});
|
||||
|
||||
obj.apiBinder.setDeviceState(obj.deviceState);
|
||||
await deviceState.initialized;
|
||||
obj.deviceState = deviceState;
|
||||
};
|
||||
|
||||
const mockProvisioningOpts = {
|
||||
@ -462,12 +459,12 @@ describe('ApiBinder', () => {
|
||||
// Copy previous values to restore later
|
||||
const previousStateReportErrors = components.apiBinder.stateReportErrors;
|
||||
const previousDeviceStateConnected =
|
||||
components.apiBinder.deviceState.connected;
|
||||
// @ts-ignore
|
||||
components.deviceState.connected;
|
||||
|
||||
// Set additional conditions not in configStub to cause a fail
|
||||
// @ts-expect-error
|
||||
CurrentState.stateReportErrors = 4;
|
||||
components.apiBinder.deviceState.connected = true;
|
||||
components.apiBinder.stateReportErrors = 4;
|
||||
components.deviceState.connected = true;
|
||||
|
||||
expect(await components.apiBinder.healthcheck()).to.equal(false);
|
||||
|
||||
@ -481,9 +478,8 @@ describe('ApiBinder', () => {
|
||||
);
|
||||
|
||||
// Restore previous values
|
||||
// @ts-expect-error
|
||||
CurrentState.stateReportErrors = previousStateReportErrors;
|
||||
components.apiBinder.deviceState.connected = previousDeviceStateConnected;
|
||||
components.apiBinder.stateReportErrors = previousStateReportErrors;
|
||||
components.deviceState.connected = previousDeviceStateConnected;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -6,7 +6,7 @@ import Network from '../src/compose/network';
|
||||
|
||||
import Service from '../src/compose/service';
|
||||
import Volume from '../src/compose/volume';
|
||||
import DeviceState from '../src/device-state';
|
||||
import * as deviceState from '../src/device-state';
|
||||
import * as dockerUtils from '../src/lib/docker-utils';
|
||||
import * as images from '../src/compose/images';
|
||||
|
||||
@ -67,10 +67,9 @@ describe('ApplicationManager', function () {
|
||||
const originalInspectByName = images.inspectByName;
|
||||
before(async function () {
|
||||
await prepare();
|
||||
this.deviceState = new DeviceState({
|
||||
apiBinder: null as any,
|
||||
});
|
||||
this.applications = this.deviceState.applications;
|
||||
await deviceState.initialized;
|
||||
|
||||
this.applications = deviceState.applications;
|
||||
|
||||
// @ts-expect-error assigning to a RO property
|
||||
images.inspectByName = () =>
|
||||
|
@ -1,35 +1,26 @@
|
||||
import { SinonStub, stub } from 'sinon';
|
||||
import { expect } from './lib/chai-config';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import * as APIBinder from '../src/api-binder';
|
||||
import * as apiBinder from '../src/api-binder';
|
||||
import { ApplicationManager } from '../src/application-manager';
|
||||
import DeviceState from '../src/device-state';
|
||||
import * as deviceState from '../src/device-state';
|
||||
import * as constants from '../src/lib/constants';
|
||||
import { docker } from '../src/lib/docker-utils';
|
||||
import { Supervisor } from '../src/supervisor';
|
||||
import { expect } from './lib/chai-config';
|
||||
import _ = require('lodash');
|
||||
|
||||
describe('Startup', () => {
|
||||
let reportCurrentStateStub: SinonStub;
|
||||
let startStub: SinonStub;
|
||||
let vpnStatusPathStub: SinonStub;
|
||||
let appManagerStub: SinonStub;
|
||||
let deviceStateStub: SinonStub;
|
||||
let dockerStub: SinonStub;
|
||||
|
||||
before(() => {
|
||||
reportCurrentStateStub = stub(
|
||||
DeviceState.prototype as any,
|
||||
'reportCurrentState',
|
||||
).resolves();
|
||||
startStub = stub(APIBinder as any, 'start').returns(Promise.resolve());
|
||||
appManagerStub = stub(ApplicationManager.prototype, 'init').returns(
|
||||
Promise.resolve(),
|
||||
);
|
||||
before(async () => {
|
||||
startStub = stub(apiBinder as any, 'start').resolves();
|
||||
deviceStateStub = stub(deviceState, 'applyTarget').resolves();
|
||||
appManagerStub = stub(ApplicationManager.prototype, 'init').resolves();
|
||||
vpnStatusPathStub = stub(constants, 'vpnStatusPath').returns('');
|
||||
deviceStateStub = stub(DeviceState.prototype as any, 'applyTarget').returns(
|
||||
Promise.resolve(),
|
||||
);
|
||||
dockerStub = stub(docker, 'listContainers').returns(Promise.resolve([]));
|
||||
});
|
||||
|
||||
@ -39,12 +30,12 @@ describe('Startup', () => {
|
||||
vpnStatusPathStub.restore();
|
||||
deviceStateStub.restore();
|
||||
dockerStub.restore();
|
||||
reportCurrentStateStub.restore();
|
||||
});
|
||||
|
||||
it('should startup correctly', async () => {
|
||||
const supervisor = new Supervisor();
|
||||
expect(await supervisor.init()).to.not.throw;
|
||||
await supervisor.init();
|
||||
|
||||
// Cast as any to access private properties
|
||||
const anySupervisor = supervisor as any;
|
||||
expect(anySupervisor.db).to.not.be.null;
|
||||
@ -52,20 +43,5 @@ describe('Startup', () => {
|
||||
expect(anySupervisor.logger).to.not.be.null;
|
||||
expect(anySupervisor.deviceState).to.not.be.null;
|
||||
expect(anySupervisor.apiBinder).to.not.be.null;
|
||||
|
||||
let macAddresses: string[] = [];
|
||||
reportCurrentStateStub.getCalls().forEach((call) => {
|
||||
const m: string = call.args[0]['mac_address'];
|
||||
if (!m) {
|
||||
return;
|
||||
}
|
||||
|
||||
macAddresses = _.union(macAddresses, m.split(' '));
|
||||
});
|
||||
|
||||
const allMacAddresses = macAddresses.join(' ');
|
||||
|
||||
expect(allMacAddresses).to.have.length.greaterThan(0);
|
||||
expect(allMacAddresses).to.not.contain('NO:');
|
||||
});
|
||||
});
|
||||
|
@ -3,7 +3,7 @@ import { spy, stub, SinonStub } from 'sinon';
|
||||
import * as supertest from 'supertest';
|
||||
|
||||
import * as apiBinder from '../src/api-binder';
|
||||
import DeviceState from '../src/device-state';
|
||||
import * as deviceState from '../src/device-state';
|
||||
import Log from '../src/lib/supervisor-console';
|
||||
import * as images from '../src/compose/images';
|
||||
import SupervisorAPI from '../src/supervisor-api';
|
||||
@ -25,11 +25,12 @@ describe('SupervisorAPI', () => {
|
||||
|
||||
before(async () => {
|
||||
await apiBinder.initialized;
|
||||
await deviceState.initialized;
|
||||
|
||||
// Stub health checks so we can modify them whenever needed
|
||||
healthCheckStubs = [
|
||||
stub(apiBinder, 'healthcheck'),
|
||||
stub(DeviceState.prototype, 'healthcheck'),
|
||||
stub(deviceState, 'healthcheck'),
|
||||
];
|
||||
// The mockedAPI contains stubs that might create unexpected results
|
||||
// See the module to know what has been stubbed
|
||||
|
@ -10,8 +10,8 @@ import * as config from '../../src/config';
|
||||
import * as db from '../../src/db';
|
||||
import { createV1Api } from '../../src/device-api/v1';
|
||||
import { createV2Api } from '../../src/device-api/v2';
|
||||
import * as APIBinder from '../../src/api-binder';
|
||||
import DeviceState from '../../src/device-state';
|
||||
import * as apiBinder from '../../src/api-binder';
|
||||
import * as deviceState from '../../src/device-state';
|
||||
import SupervisorAPI from '../../src/supervisor-api';
|
||||
|
||||
const DB_PATH = './test/data/supervisor-api.sqlite';
|
||||
@ -67,14 +67,12 @@ const STUBBED_VALUES = {
|
||||
|
||||
async function create(): Promise<SupervisorAPI> {
|
||||
// Get SupervisorAPI construct options
|
||||
const { deviceState, apiBinder } = await createAPIOpts();
|
||||
await createAPIOpts();
|
||||
|
||||
// Stub functions
|
||||
setupStubs();
|
||||
// Create ApplicationManager
|
||||
const appManager = new ApplicationManager({
|
||||
deviceState,
|
||||
apiBinder: null,
|
||||
});
|
||||
const appManager = new ApplicationManager();
|
||||
// Create SupervisorAPI
|
||||
const api = new SupervisorAPI({
|
||||
routers: [deviceState.router, buildRoutes(appManager)],
|
||||
@ -101,19 +99,12 @@ async function cleanUp(): Promise<void> {
|
||||
return restoreStubs();
|
||||
}
|
||||
|
||||
async function createAPIOpts(): Promise<SupervisorAPIOpts> {
|
||||
async function createAPIOpts(): Promise<void> {
|
||||
await db.initialized;
|
||||
await deviceState.initialized;
|
||||
|
||||
// Initialize and set values for mocked Config
|
||||
await initConfig();
|
||||
// Create deviceState
|
||||
const deviceState = new DeviceState({
|
||||
apiBinder: null as any,
|
||||
});
|
||||
const apiBinder = APIBinder;
|
||||
return {
|
||||
deviceState,
|
||||
apiBinder,
|
||||
};
|
||||
}
|
||||
|
||||
async function initConfig(): Promise<void> {
|
||||
@ -168,9 +159,4 @@ function restoreStubs() {
|
||||
serviceManager.getAllByAppId = originalSvcGetAppId;
|
||||
}
|
||||
|
||||
interface SupervisorAPIOpts {
|
||||
deviceState: DeviceState;
|
||||
apiBinder: typeof APIBinder;
|
||||
}
|
||||
|
||||
export = { create, cleanUp, STUBBED_VALUES };
|
||||
|
@ -1,3 +1,5 @@
|
||||
process.env.DOCKER_HOST = 'unix:///your/dockerode/mocks/are/not/working';
|
||||
|
||||
import * as dockerode from 'dockerode';
|
||||
|
||||
export interface TestData {
|
||||
|
Loading…
Reference in New Issue
Block a user