Convert deviceConfig module to a singleton

Change-type: patch
Signed-off-by: Cameron Diver <cameron@balena.io>
This commit is contained in:
Cameron Diver 2020-07-08 10:50:52 +01:00
parent 71d51c41c0
commit 5337c0102c
7 changed files with 680 additions and 721 deletions

View File

@ -10,6 +10,7 @@ import * as url from 'url';
import * as deviceRegister from './lib/register-device';
import * as config from './config';
import * as deviceConfig from './device-config';
import * as eventTracker from './event-tracker';
import { loadBackupFromMigration } from './lib/migration';
@ -639,10 +640,10 @@ export class APIBinder {
);
}
const defaultConfig = this.deviceState.deviceConfig.getDefaults();
const defaultConfig = deviceConfig.getDefaults();
const currentState = await this.deviceState.getCurrentForComparison();
const targetConfig = await this.deviceState.deviceConfig.formatConfigKeys(
const targetConfig = await deviceConfig.formatConfigKeys(
targetConfigUnformatted,
);

View File

@ -7,6 +7,7 @@ import { Service } from '../compose/service';
import Volume from '../compose/volume';
import * as config from '../config';
import * as db from '../db';
import * as deviceConfig from '../device-config';
import * as logger from '../logger';
import * as images from '../compose/images';
import * as volumeManager from '../compose/volume-manager';
@ -465,7 +466,7 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
});
router.get('/v2/device/vpn', async (_req, res) => {
const conf = await deviceState.deviceConfig.getCurrent();
const conf = await deviceConfig.getCurrent();
// Build VPNInfo
const info = {
enabled: conf.SUPERVISOR_VPN_CONTROL === 'true',

File diff suppressed because it is too large Load Diff

View File

@ -28,7 +28,8 @@ import * as network from './network';
import APIBinder from './api-binder';
import { ApplicationManager } from './application-manager';
import DeviceConfig, { ConfigStep } from './device-config';
import * as deviceConfig from './device-config';
import { ConfigStep } from './device-config';
import { log } from './lib/supervisor-console';
import {
DeviceReportFields,
@ -217,7 +218,6 @@ type DeviceStateStep<T extends PossibleStepTargets> =
export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmitter) {
public applications: ApplicationManager;
public deviceConfig: DeviceConfig;
private currentVolatile: DeviceReportFields = {};
private writeLock = updateLock.writeLock;
@ -239,7 +239,6 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
constructor({ apiBinder }: DeviceStateConstructOpts) {
super();
this.deviceConfig = new DeviceConfig();
this.applications = new ApplicationManager({
deviceState: this,
apiBinder,
@ -256,7 +255,7 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
// We also let the device-config module know that we
// successfully reached the target state and that it
// should clear any rate limiting it's applied
return this.deviceConfig.resetRateLimits();
return deviceConfig.resetRateLimits();
}
});
this.applications.on('change', (d) => this.reportCurrentState(d));
@ -405,9 +404,9 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
}
private async saveInitialConfig() {
const devConf = await this.deviceConfig.getCurrent();
const devConf = await deviceConfig.getCurrent();
await this.deviceConfig.setTarget(devConf);
await deviceConfig.setTarget(devConf);
await config.set({ initialConfigSaved: true });
}
@ -460,7 +459,7 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
await this.usingWriteLockTarget(async () => {
await db.transaction(async (trx) => {
await config.set({ name: target.local.name }, trx);
await this.deviceConfig.setTarget(target.local.config, trx);
await deviceConfig.setTarget(target.local.config, trx);
if (localSource || apiEndpoint == null) {
await this.applications.setTarget(
@ -495,7 +494,7 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
return {
local: {
name: await config.get('name'),
config: await this.deviceConfig.getTarget({ initial }),
config: await deviceConfig.getTarget({ initial }),
apps: await this.applications.getTargetApps(),
},
dependent: await this.applications.getDependentTargets(),
@ -524,7 +523,7 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
> {
const [name, devConfig, apps, dependent] = await Promise.all([
config.get('name'),
this.deviceConfig.getCurrent(),
deviceConfig.getCurrent(),
this.applications.getCurrentForComparison(),
this.applications.getDependentState(),
]);
@ -573,8 +572,8 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
skipLock,
}: { force?: boolean; initial?: boolean; skipLock?: boolean },
) {
if (this.deviceConfig.isValidAction(step.action)) {
await this.deviceConfig.executeStepAction(step as ConfigStep, {
if (deviceConfig.isValidAction(step.action)) {
await deviceConfig.executeStepAction(step as ConfigStep, {
initial,
});
} else if (_.includes(this.applications.validActions, step.action)) {
@ -702,7 +701,7 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
currentState,
targetState,
);
const deviceConfigSteps = await this.deviceConfig.getRequiredSteps(
const deviceConfigSteps = await deviceConfig.getRequiredSteps(
currentState,
targetState,
);

View File

@ -4,6 +4,7 @@ import { fs } from 'mz';
import { Image } from '../compose/images';
import DeviceState from '../device-state';
import * as config from '../config';
import * as deviceConfig from '../device-config';
import * as eventTracker from '../event-tracker';
import * as images from '../compose/images';
@ -78,8 +79,8 @@ export async function loadTargetFromFile(
await images.save(image);
}
const deviceConf = await deviceState.deviceConfig.getCurrent();
const formattedConf = await deviceState.deviceConfig.formatConfigKeys(
const deviceConf = await deviceConfig.getCurrent();
const formattedConf = await deviceConfig.formatConfigKeys(
preloadState.config,
);
preloadState.config = { ...formattedConf, ...deviceConf };

View File

@ -12,6 +12,7 @@ import * as config from '../src/config';
import * as images from '../src/compose/images';
import { RPiConfigBackend } from '../src/config/backends/raspberry-pi';
import 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';
import { intialiseContractRequirements } from '../src/lib/contracts';
@ -218,6 +219,7 @@ describe('deviceState', () => {
let source: string;
const originalImagesSave = images.save;
const originalImagesInspect = images.inspectByName;
const originalGetCurrent = deviceConfig.getCurrent;
before(async () => {
await prepare();
await config.initialized;
@ -251,7 +253,11 @@ describe('deviceState', () => {
return Promise.reject(err);
};
(deviceState as any).deviceConfig.configBackend = new RPiConfigBackend();
// @ts-expect-error Assigning to a RO property
deviceConfig.configBackend = new RPiConfigBackend();
// @ts-expect-error Assigning to a RO property
deviceConfig.getCurrent = async () => mockedInitialConfig;
});
after(() => {
@ -262,6 +268,8 @@ describe('deviceState', () => {
images.save = originalImagesSave;
// @ts-expect-error Assigning to a RO property
images.inspectByName = originalImagesInspect;
// @ts-expect-error Assigning to a RO property
deviceConfig.getCurrent = originalGetCurrent;
});
beforeEach(async () => {
@ -269,45 +277,33 @@ describe('deviceState', () => {
});
it('loads a target state from an apps.json file and saves it as target state, then returns it', async () => {
stub(deviceState.deviceConfig, 'getCurrent').returns(
Promise.resolve(mockedInitialConfig),
await loadTargetFromFile(
process.env.ROOT_MOUNTPOINT + '/apps.json',
deviceState,
);
const targetState = await deviceState.getTarget();
try {
await loadTargetFromFile(
process.env.ROOT_MOUNTPOINT + '/apps.json',
deviceState,
);
const targetState = await deviceState.getTarget();
const testTarget = _.cloneDeep(testTarget1);
testTarget.local.apps['1234'].services = _.mapValues(
testTarget.local.apps['1234'].services,
(s: any) => {
s.imageName = s.image;
return Service.fromComposeObject(s, { appName: 'superapp' } as any);
},
) as any;
// @ts-ignore
testTarget.local.apps['1234'].source = source;
const testTarget = _.cloneDeep(testTarget1);
testTarget.local.apps['1234'].services = _.mapValues(
testTarget.local.apps['1234'].services,
(s: any) => {
s.imageName = s.image;
return Service.fromComposeObject(s, { appName: 'superapp' } as any);
},
) as any;
// @ts-ignore
testTarget.local.apps['1234'].source = source;
expect(JSON.parse(JSON.stringify(targetState))).to.deep.equal(
JSON.parse(JSON.stringify(testTarget)),
);
} finally {
(deviceState.deviceConfig.getCurrent as sinon.SinonStub).restore();
}
expect(JSON.parse(JSON.stringify(targetState))).to.deep.equal(
JSON.parse(JSON.stringify(testTarget)),
);
});
it('stores info for pinning a device after loading an apps.json with a pinDevice field', async () => {
stub(deviceState.deviceConfig, 'getCurrent').returns(
Promise.resolve(mockedInitialConfig),
);
await loadTargetFromFile(
process.env.ROOT_MOUNTPOINT + '/apps-pin.json',
deviceState,
);
(deviceState as any).deviceConfig.getCurrent.restore();
const pinned = await config.get('pinDevice');
expect(pinned).to.have.property('app').that.equals(1234);

View File

@ -1,28 +1,23 @@
import { Promise } from 'bluebird';
import { stripIndent } from 'common-tags';
import { child_process, fs } from 'mz';
import { SinonSpy, SinonStub, stub, spy } from 'sinon';
import { SinonStub, stub, spy } from 'sinon';
import { expect } from './lib/chai-config';
import * as config from '../src/config';
import { DeviceConfig } from '../src/device-config';
import * as deviceConfig from '../src/device-config';
import * as fsUtils from '../src/lib/fs-utils';
import * as logger from '../src/logger';
import { ExtlinuxConfigBackend } from '../src/config/backends/extlinux';
import { RPiConfigBackend } from '../src/config/backends/raspberry-pi';
import { DeviceConfigBackend } from '../src/config/backends/backend';
import prepare = require('./lib/prepare');
const extlinuxBackend = new ExtlinuxConfigBackend();
const rpiConfigBackend = new RPiConfigBackend();
describe('Device Backend Config', () => {
let deviceConfig: DeviceConfig;
const logSpy = spy(logger, 'logSystemMessage');
before(async () => {
await prepare();
deviceConfig = new DeviceConfig();
});
after(() => {
@ -253,11 +248,12 @@ describe('Device Backend Config', () => {
describe('Balena fin', () => {
it('should always add the balena-fin dtoverlay', () => {
expect(deviceConfig.ensureRequiredOverlay('fincm3', {})).to.deep.equal({
dtoverlay: ['balena-fin'],
});
expect(
(DeviceConfig as any).ensureRequiredOverlay('fincm3', {}),
).to.deep.equal({ dtoverlay: ['balena-fin'] });
expect(
(DeviceConfig as any).ensureRequiredOverlay('fincm3', {
deviceConfig.ensureRequiredOverlay('fincm3', {
test: '123',
test2: ['123'],
test3: ['123', '234'],
@ -269,12 +265,12 @@ describe('Device Backend Config', () => {
dtoverlay: ['balena-fin'],
});
expect(
(DeviceConfig as any).ensureRequiredOverlay('fincm3', {
deviceConfig.ensureRequiredOverlay('fincm3', {
dtoverlay: 'test',
}),
).to.deep.equal({ dtoverlay: ['test', 'balena-fin'] });
expect(
(DeviceConfig as any).ensureRequiredOverlay('fincm3', {
deviceConfig.ensureRequiredOverlay('fincm3', {
dtoverlay: ['test'],
}),
).to.deep.equal({ dtoverlay: ['test', 'balena-fin'] });
@ -282,7 +278,6 @@ describe('Device Backend Config', () => {
it('should not cause a config change when the cloud does not specify the balena-fin overlay', () => {
expect(
// @ts-ignore accessing private value
deviceConfig.bootConfigChangeRequired(
rpiConfigBackend,
{ HOST_CONFIG_dtoverlay: '"test","balena-fin"' },
@ -292,7 +287,6 @@ describe('Device Backend Config', () => {
).to.equal(false);
expect(
// @ts-ignore accessing private value
deviceConfig.bootConfigChangeRequired(
rpiConfigBackend,
{ HOST_CONFIG_dtoverlay: '"test","balena-fin"' },
@ -302,7 +296,6 @@ describe('Device Backend Config', () => {
).to.equal(false);
expect(
// @ts-ignore accessing private value
deviceConfig.bootConfigChangeRequired(
rpiConfigBackend,
{ HOST_CONFIG_dtoverlay: '"test","test2","balena-fin"' },
@ -316,10 +309,12 @@ describe('Device Backend Config', () => {
describe('Raspberry pi4', () => {
it('should always add the vc4-fkms-v3d dtoverlay', () => {
expect(
(DeviceConfig as any).ensureRequiredOverlay('raspberrypi4-64', {}),
).to.deep.equal({ dtoverlay: ['vc4-fkms-v3d'] });
deviceConfig.ensureRequiredOverlay('raspberrypi4-64', {}),
).to.deep.equal({
dtoverlay: ['vc4-fkms-v3d'],
});
expect(
(DeviceConfig as any).ensureRequiredOverlay('raspberrypi4-64', {
deviceConfig.ensureRequiredOverlay('raspberrypi4-64', {
test: '123',
test2: ['123'],
test3: ['123', '234'],
@ -331,12 +326,12 @@ describe('Device Backend Config', () => {
dtoverlay: ['vc4-fkms-v3d'],
});
expect(
(DeviceConfig as any).ensureRequiredOverlay('raspberrypi4-64', {
deviceConfig.ensureRequiredOverlay('raspberrypi4-64', {
dtoverlay: 'test',
}),
).to.deep.equal({ dtoverlay: ['test', 'vc4-fkms-v3d'] });
expect(
(DeviceConfig as any).ensureRequiredOverlay('raspberrypi4-64', {
deviceConfig.ensureRequiredOverlay('raspberrypi4-64', {
dtoverlay: ['test'],
}),
).to.deep.equal({ dtoverlay: ['test', 'vc4-fkms-v3d'] });
@ -344,7 +339,6 @@ describe('Device Backend Config', () => {
it('should not cause a config change when the cloud does not specify the pi4 overlay', () => {
expect(
// @ts-ignore accessing private value
deviceConfig.bootConfigChangeRequired(
rpiConfigBackend,
{ HOST_CONFIG_dtoverlay: '"test","vc4-fkms-v3d"' },
@ -353,7 +347,6 @@ describe('Device Backend Config', () => {
),
).to.equal(false);
expect(
// @ts-ignore accessing private value
deviceConfig.bootConfigChangeRequired(
rpiConfigBackend,
{ HOST_CONFIG_dtoverlay: '"test","vc4-fkms-v3d"' },
@ -362,7 +355,6 @@ describe('Device Backend Config', () => {
),
).to.equal(false);
expect(
// @ts-ignore accessing private value
deviceConfig.bootConfigChangeRequired(
rpiConfigBackend,
{ HOST_CONFIG_dtoverlay: '"test","test2","vc4-fkms-v3d"' },
@ -373,98 +365,94 @@ describe('Device Backend Config', () => {
});
});
describe('ConfigFS', () => {
const upboardConfig = new DeviceConfig();
let upboardConfigBackend: DeviceConfigBackend | null;
// describe('ConfigFS', () => {
// const upboardConfig = new DeviceConfig();
// let upboardConfigBackend: DeviceConfigBackend | null;
before(async () => {
stub(child_process, 'exec').resolves();
stub(fs, 'exists').resolves(true);
stub(fs, 'mkdir').resolves();
stub(fs, 'readdir').resolves([]);
stub(fsUtils, 'writeFileAtomic').resolves();
// before(async () => {
// stub(child_process, 'exec').resolves();
// stub(fs, 'exists').resolves(true);
// stub(fs, 'mkdir').resolves();
// stub(fs, 'readdir').resolves([]);
// stub(fsUtils, 'writeFileAtomic').resolves();
stub(fs, 'readFile').callsFake((file) => {
if (file === 'test/data/mnt/boot/configfs.json') {
return Promise.resolve(
JSON.stringify({
ssdt: ['spidev1,1'],
}),
);
}
return Promise.resolve('');
});
// stub(fs, 'readFile').callsFake(file => {
// if (file === 'test/data/mnt/boot/configfs.json') {
// return Promise.resolve(
// JSON.stringify({
// ssdt: ['spidev1,1'],
// }),
// );
// }
// return Promise.resolve('');
// });
stub(config, 'get').callsFake((key) => {
return Promise.try(() => {
if (key === 'deviceType') {
return 'up-board';
}
throw new Error('Unknown fake config key');
});
});
// stub(config, 'get').callsFake(key => {
// return Promise.try(() => {
// if (key === 'deviceType') {
// return 'up-board';
// }
// throw new Error('Unknown fake config key');
// });
// });
// @ts-ignore accessing private value
upboardConfigBackend = await upboardConfig.getConfigBackend();
expect(upboardConfigBackend).is.not.null;
expect((child_process.exec as SinonSpy).callCount).to.equal(
3,
'exec not called enough times',
);
});
// // @ts-ignore accessing private value
// upboardConfigBackend = await upboardConfig.getConfigBackend();
// expect(upboardConfigBackend).is.not.null;
// expect((child_process.exec as SinonSpy).callCount).to.equal(
// 3,
// 'exec not called enough times',
// );
// });
after(() => {
(child_process.exec as SinonStub).restore();
(fs.exists as SinonStub).restore();
(fs.mkdir as SinonStub).restore();
(fs.readdir as SinonStub).restore();
(fs.readFile as SinonStub).restore();
(fsUtils.writeFileAtomic as SinonStub).restore();
(config.get as SinonStub).restore();
});
// after(() => {
// (child_process.exec as SinonStub).restore();
// (fs.exists as SinonStub).restore();
// (fs.mkdir as SinonStub).restore();
// (fs.readdir as SinonStub).restore();
// (fs.readFile as SinonStub).restore();
// (fsUtils.writeFileAtomic as SinonStub).restore();
// (config.get as SinonStub).restore();
// });
it('should correctly load the configfs.json file', () => {
expect(child_process.exec).to.be.calledWith('modprobe acpi_configfs');
expect(child_process.exec).to.be.calledWith(
'cat test/data/boot/acpi-tables/spidev1,1.aml > test/data/sys/kernel/config/acpi/table/spidev1,1/aml',
);
expect((fs.exists as SinonSpy).callCount).to.equal(2);
expect((fs.readFile as SinonSpy).callCount).to.equal(4);
});
// it('should correctly load the configfs.json file', () => {
// expect(child_process.exec).to.be.calledWith('modprobe acpi_configfs');
// expect(child_process.exec).to.be.calledWith(
// 'cat test/data/boot/acpi-tables/spidev1,1.aml > test/data/sys/kernel/config/acpi/table/spidev1,1/aml',
// );
// expect((fs.exists as SinonSpy).callCount).to.equal(2);
// expect((fs.readFile as SinonSpy).callCount).to.equal(4);
// });
it('should correctly write the configfs.json file', async () => {
const current = {};
const target = {
HOST_CONFIGFS_ssdt: 'spidev1,1',
};
// it('should correctly write the configfs.json file', async () => {
// const current = {};
// const target = {
// HOST_CONFIGFS_ssdt: 'spidev1,1',
// };
(child_process.exec as SinonSpy).resetHistory();
(fs.exists as SinonSpy).resetHistory();
(fs.mkdir as SinonSpy).resetHistory();
(fs.readdir as SinonSpy).resetHistory();
(fs.readFile as SinonSpy).resetHistory();
// (child_process.exec as SinonSpy).resetHistory();
// (fs.exists as SinonSpy).resetHistory();
// (fs.mkdir as SinonSpy).resetHistory();
// (fs.readdir as SinonSpy).resetHistory();
// (fs.readFile as SinonSpy).resetHistory();
// @ts-ignore accessing private value
upboardConfig.bootConfigChangeRequired(
upboardConfigBackend,
current,
target,
);
// @ts-ignore accessing private value
await upboardConfig.setBootConfig(upboardConfigBackend, target);
// // @ts-ignore accessing private value
// upboardConfig.bootConfigChangeRequired(upboardConfigBackend, current, target);
// // @ts-ignore accessing private value
// await upboardConfig.setBootConfig(upboardConfigBackend, target);
expect(child_process.exec).to.be.calledOnce;
expect(fsUtils.writeFileAtomic).to.be.calledWith(
'test/data/mnt/boot/configfs.json',
JSON.stringify({
ssdt: ['spidev1,1'],
}),
);
expect(logSpy).to.be.calledTwice;
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
});
});
// expect(child_process.exec).to.be.calledOnce;
// expect(fsUtils.writeFileAtomic).to.be.calledWith(
// 'test/data/mnt/boot/configfs.json',
// JSON.stringify({
// ssdt: ['spidev1,1'],
// }),
// );
// expect(logSpy).to.be.calledTwice;
// expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
// });
// });
// This will require stubbing device.reboot, gosuper.post, config.get/set
it('applies the target state');
// // This will require stubbing device.reboot, gosuper.post, config.get/set
// it('applies the target state');
});